feat: Create category component (#5103)
This commit is contained in:
parent
92bb84127b
commit
bd7a56061e
7 changed files with 264 additions and 9 deletions
|
@ -0,0 +1,30 @@
|
||||||
|
import AddCategoryComponent from './AddCategory.vue';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Help Center',
|
||||||
|
component: AddCategoryComponent,
|
||||||
|
argTypes: {
|
||||||
|
show: {
|
||||||
|
defaultValue: true,
|
||||||
|
control: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: { AddCategoryComponent },
|
||||||
|
template:
|
||||||
|
'<add-category-component v-bind="$props" @create="onCreate" @cancel="onClose" />',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddCategory = Template.bind({});
|
||||||
|
AddCategory.args = {
|
||||||
|
portalName: 'Chatwoot help center',
|
||||||
|
locale: 'En-US',
|
||||||
|
onCreate: action('create'),
|
||||||
|
onClose: action('cancel'),
|
||||||
|
};
|
160
app/javascript/dashboard/components/helpCenter/AddCategory.vue
Normal file
160
app/javascript/dashboard/components/helpCenter/AddCategory.vue
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
<template>
|
||||||
|
<modal :show.sync="show" :on-close="onClose">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('HELP_CENTER.CATEGORY.ADD.TITLE')"
|
||||||
|
:header-content="$t('HELP_CENTER.CATEGORY.ADD.SUB_TITLE')"
|
||||||
|
/>
|
||||||
|
<form class="row" @submit.prevent="onCreate">
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<div class="row article-info">
|
||||||
|
<div class="columns medium-6">
|
||||||
|
<label>
|
||||||
|
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.PORTAL') }}</span>
|
||||||
|
<p class="value">{{ portalName }}</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="columns medium-6">
|
||||||
|
<label>
|
||||||
|
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.LOCALE') }}</span>
|
||||||
|
<p class="value">{{ locale }}</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="name"
|
||||||
|
:class="{ error: $v.name.$error }"
|
||||||
|
class="medium-12 columns"
|
||||||
|
:error="nameError"
|
||||||
|
:label="$t('HELP_CENTER.CATEGORY.ADD.NAME.LABEL')"
|
||||||
|
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.NAME.PLACEHOLDER')"
|
||||||
|
:help-text="$t('HELP_CENTER.CATEGORY.ADD.NAME.HELP_TEXT')"
|
||||||
|
@input="onNameChange"
|
||||||
|
/>
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="slug"
|
||||||
|
:class="{ error: $v.slug.$error }"
|
||||||
|
class="medium-12 columns"
|
||||||
|
:error="slugError"
|
||||||
|
:label="$t('HELP_CENTER.CATEGORY.ADD.SLUG.LABEL')"
|
||||||
|
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.SLUG.PLACEHOLDER')"
|
||||||
|
:help-text="$t('HELP_CENTER.CATEGORY.ADD.SLUG.HELP_TEXT')"
|
||||||
|
@input="$v.slug.$touch"
|
||||||
|
/>
|
||||||
|
<label :class="{ error: $v.description.$error }">
|
||||||
|
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.LABEL') }}
|
||||||
|
<textarea
|
||||||
|
v-model="description"
|
||||||
|
rows="3"
|
||||||
|
type="text"
|
||||||
|
:placeholder="
|
||||||
|
$t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
@blur="$v.description.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.description.$error" class="message">
|
||||||
|
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<div class="modal-footer justify-content-end w-full">
|
||||||
|
<woot-button class="button clear" @click.prevent="onClose">
|
||||||
|
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CANCEL') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button>
|
||||||
|
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CREATE') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from 'dashboard/components/Modal';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
|
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
portalName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
description: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
name: {
|
||||||
|
required,
|
||||||
|
minLength: minLength(2),
|
||||||
|
},
|
||||||
|
slug: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
nameError() {
|
||||||
|
if (this.$v.name.$error) {
|
||||||
|
return this.$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
slugError() {
|
||||||
|
if (this.$v.slug.$error) {
|
||||||
|
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onNameChange() {
|
||||||
|
this.slug = convertToCategorySlug(this.name);
|
||||||
|
},
|
||||||
|
onCreate() {
|
||||||
|
this.$emit('create');
|
||||||
|
this.reset();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.reset();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.name = '';
|
||||||
|
this.slug = '';
|
||||||
|
this.description = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.article-info {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 var(--space-normal);
|
||||||
|
.value {
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<label>
|
<label class="container">
|
||||||
<span v-if="label">{{ label }}</span>
|
<span v-if="label">{{ label }}</span>
|
||||||
<input
|
<input
|
||||||
:value="value"
|
:value="value"
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
@input="onChange"
|
@input="onChange"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
/>
|
/>
|
||||||
<p v-if="helpText" class="help-text" />
|
<p v-if="helpText" class="help-text">{{ helpText }}</p>
|
||||||
<span v-if="error" class="message">
|
<span v-if="error" class="message">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -46,7 +46,7 @@ export default {
|
||||||
},
|
},
|
||||||
readonly: {
|
readonly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
deafaut: false,
|
default: false,
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -63,3 +63,21 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
margin: 0 0 var(--space-normal);
|
||||||
|
input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.page-sub-title {
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
margin-top: var(--space-micro);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
color: var(--s-600);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -62,9 +62,16 @@ export const createPendingMessage = data => {
|
||||||
return pendingMessage;
|
return pendingMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertToSlug = text => {
|
export const convertToAttributeSlug = text => {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^\w ]+/g, '')
|
.replace(/[^\w ]+/g, '')
|
||||||
.replace(/ +/g, '_');
|
.replace(/ +/g, '_');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertToCategorySlug = text => {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w ]+/g, '')
|
||||||
|
.replace(/ +/g, '-');
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import {
|
import {
|
||||||
getTypingUsersText,
|
getTypingUsersText,
|
||||||
createPendingMessage,
|
createPendingMessage,
|
||||||
convertToSlug,
|
convertToAttributeSlug,
|
||||||
|
convertToCategorySlug,
|
||||||
} from '../commons';
|
} from '../commons';
|
||||||
|
|
||||||
describe('#getTypingUsersText', () => {
|
describe('#getTypingUsersText', () => {
|
||||||
|
@ -88,8 +89,18 @@ describe('#createPendingMessage', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('convertToSlug', () => {
|
describe('convertToAttributeSlug', () => {
|
||||||
it('should convert to slug', () => {
|
it('should convert to slug', () => {
|
||||||
expect(convertToSlug('Test@%^&*(){}>.!@`~_ ing')).toBe('test__ing');
|
expect(convertToAttributeSlug('Test@%^&*(){}>.!@`~_ ing')).toBe(
|
||||||
|
'test__ing'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertToCategorySlug', () => {
|
||||||
|
it('should convert to slug', () => {
|
||||||
|
expect(convertToCategorySlug('User profile guide')).toBe(
|
||||||
|
'user-profile-guide'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,35 @@
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"PLACEHOLDER": "Search for articles"
|
"PLACEHOLDER": "Search for articles"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CATEGORY": {
|
||||||
|
"ADD": {
|
||||||
|
"TITLE": "Create a category",
|
||||||
|
"SUB_TITLE": "The category will be used in the public facing portal to categorize articles.",
|
||||||
|
"PORTAL": "Portal",
|
||||||
|
"LOCALE": "Locale",
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Name",
|
||||||
|
"PLACEHOLDER": "Category name",
|
||||||
|
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
|
||||||
|
"ERROR": "Name is required"
|
||||||
|
},
|
||||||
|
"SLUG": {
|
||||||
|
"LABEL": "Slug",
|
||||||
|
"PLACEHOLDER": "Category slug for urls",
|
||||||
|
"HELP_TEXT": "app.chatwoot.com/portal/my-portal/en-US/categories/my-slug",
|
||||||
|
"ERROR": "Slug is required"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Description",
|
||||||
|
"PLACEHOLDER": "Give a short description about the category.",
|
||||||
|
"ERROR": "Description is required"
|
||||||
|
},
|
||||||
|
"BUTTONS": {
|
||||||
|
"CREATE": "Create category",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { convertToSlug } from 'dashboard/helper/commons.js';
|
import { convertToAttributeSlug } from 'dashboard/helper/commons.js';
|
||||||
import { ATTRIBUTE_MODELS, ATTRIBUTE_TYPES } from './constants';
|
import { ATTRIBUTE_MODELS, ATTRIBUTE_TYPES } from './constants';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
|
@ -199,7 +199,7 @@ export default {
|
||||||
this.isTouched = true;
|
this.isTouched = true;
|
||||||
},
|
},
|
||||||
onDisplayNameChange() {
|
onDisplayNameChange() {
|
||||||
this.attributeKey = convertToSlug(this.displayName);
|
this.attributeKey = convertToAttributeSlug(this.displayName);
|
||||||
},
|
},
|
||||||
async addAttributes() {
|
async addAttributes() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
|
|
Loading…
Reference in a new issue