mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
feat(design): Update the design for the custom attribute console (#10049)
This PR continues the design update series, updates the design for the custom attributes management page. This PR improves the interaction in the Add Custom Attribute feature. Now, the attribute model in the add attribute form will default to the currently selected tab. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -22,17 +22,21 @@ defineProps({
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full gap-10 font-inter">
|
||||
<slot name="header" />
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
</slot>
|
||||
<p
|
||||
v-else-if="noRecordsFound"
|
||||
class="flex-1 text-slate-700 dark:text-slate-100 flex items-center justify-center text-base"
|
||||
>
|
||||
{{ noRecordsMessage }}
|
||||
</p>
|
||||
<slot v-else name="body" />
|
||||
<!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
|
||||
<slot />
|
||||
<!-- Added to render any templates that should be rendered before body -->
|
||||
<div>
|
||||
<slot name="preBody" />
|
||||
<slot v-if="isLoading" name="loading">
|
||||
<woot-loading-state :message="loadingMessage" />
|
||||
</slot>
|
||||
<p
|
||||
v-else-if="noRecordsFound"
|
||||
class="flex-1 text-slate-700 dark:text-slate-100 flex items-center justify-center text-base"
|
||||
>
|
||||
{{ noRecordsMessage }}
|
||||
</p>
|
||||
<slot v-else name="body" />
|
||||
<!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,6 +12,12 @@ export default {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
// Passes 0 or 1 based on the selected AttributeModel tab selected in the UI
|
||||
// Needs a better data type, todo: refactor this component later
|
||||
selectedAttributeModelTab: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
@@ -20,7 +26,10 @@ export default {
|
||||
return {
|
||||
displayName: '',
|
||||
description: '',
|
||||
attributeModel: 0,
|
||||
// Using the prop as default. There is no side effect here as the component
|
||||
// is destroyed completely when the modal is closed. The prop doesn't change
|
||||
// dynamically when the modal is active.
|
||||
attributeModel: this.selectedAttributeModelTab || 0,
|
||||
attributeType: 0,
|
||||
attributeKey: '',
|
||||
regexPattern: null,
|
||||
@@ -280,13 +289,16 @@ export default {
|
||||
padding: 0 var(--space-small) var(--space-small) 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.multiselect--wrap {
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
.error-message {
|
||||
color: var(--r-400);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
::v-deep {
|
||||
.multiselect__tags {
|
||||
@@ -295,13 +307,16 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.multiselect {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.multiselect--active .multiselect__tags {
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
|
||||
@@ -1,195 +1,141 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EditAttribute from './EditAttribute.vue';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
const props = defineProps({
|
||||
attributeModel: {
|
||||
type: String,
|
||||
default: 'conversation_attribute',
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditAttribute,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTabIndex: 0,
|
||||
showEditPopup: false,
|
||||
showDeletePopup: false,
|
||||
selectedAttribute: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'attributes/getUIFlags',
|
||||
}),
|
||||
attributes() {
|
||||
const attributeModel = this.selectedTabIndex
|
||||
? 'contact_attribute'
|
||||
: 'conversation_attribute';
|
||||
const { t } = useI18n();
|
||||
|
||||
return this.$store.getters['attributes/getAttributesByModel'](
|
||||
attributeModel
|
||||
);
|
||||
},
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
key: 0,
|
||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONTACT'),
|
||||
},
|
||||
];
|
||||
},
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.YES')} ${
|
||||
this.selectedAttribute.attribute_display_name
|
||||
}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.NO');
|
||||
},
|
||||
confirmDeleteTitle() {
|
||||
return this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.TITLE', {
|
||||
attributeName: this.selectedAttribute.attribute_display_name,
|
||||
});
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t('ATTRIBUTES_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
attributeName: this.selectedAttribute.attribute_display_name,
|
||||
})}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAttributes(this.selectedTabIndex);
|
||||
},
|
||||
methods: {
|
||||
onClickTabChange(index) {
|
||||
this.selectedTabIndex = index;
|
||||
this.fetchAttributes(index);
|
||||
},
|
||||
fetchAttributes(index) {
|
||||
this.$store.dispatch('attributes/get', index);
|
||||
},
|
||||
async deleteAttributes({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('attributes/delete', id);
|
||||
useAlert(this.$t('ATTRIBUTES_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('ATTRIBUTES_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
openEditPopup(response) {
|
||||
this.showEditPopup = true;
|
||||
this.selectedAttribute = response;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.deleteAttributes(this.selectedAttribute);
|
||||
this.closeDelete();
|
||||
},
|
||||
openDelete(value) {
|
||||
this.showDeletePopup = true;
|
||||
this.selectedAttribute = value;
|
||||
},
|
||||
closeDelete() {
|
||||
this.showDeletePopup = false;
|
||||
this.selectedAttribute = {};
|
||||
},
|
||||
},
|
||||
const showEditPopup = ref(false);
|
||||
const showDeletePopup = ref(false);
|
||||
const selectedAttribute = ref({});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
|
||||
const attributes = computed(() =>
|
||||
getters['attributes/getAttributesByModel'].value(props.attributeModel)
|
||||
);
|
||||
const uiFlags = computed(() => getters['attributes/getUIFlags'].value);
|
||||
|
||||
const attributeDisplayName = computed(
|
||||
() => selectedAttribute.value.attribute_display_name
|
||||
);
|
||||
const deleteConfirmText = computed(
|
||||
() =>
|
||||
`${t('ATTRIBUTES_MGMT.DELETE.CONFIRM.YES')} ${attributeDisplayName.value}`
|
||||
);
|
||||
const deleteRejectText = computed(() => t('ATTRIBUTES_MGMT.DELETE.CONFIRM.NO'));
|
||||
const confirmDeleteTitle = computed(() =>
|
||||
t('ATTRIBUTES_MGMT.DELETE.CONFIRM.TITLE', {
|
||||
attributeName: attributeDisplayName.value,
|
||||
})
|
||||
);
|
||||
const confirmPlaceHolderText = computed(
|
||||
() =>
|
||||
`${t('ATTRIBUTES_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
attributeName: attributeDisplayName.value,
|
||||
})}`
|
||||
);
|
||||
|
||||
const deleteAttributes = async ({ id }) => {
|
||||
try {
|
||||
await store.dispatch('attributes/delete', id);
|
||||
useAlert(t('ATTRIBUTES_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message || t('ATTRIBUTES_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
const openEditPopup = response => {
|
||||
showEditPopup.value = true;
|
||||
selectedAttribute.value = response;
|
||||
};
|
||||
const hideEditPopup = () => {
|
||||
showEditPopup.value = false;
|
||||
};
|
||||
|
||||
const closeDelete = () => {
|
||||
showDeletePopup.value = false;
|
||||
selectedAttribute.value = {};
|
||||
};
|
||||
const confirmDeletion = () => {
|
||||
deleteAttributes(selectedAttribute.value);
|
||||
closeDelete();
|
||||
};
|
||||
const openDelete = value => {
|
||||
showDeletePopup.value = true;
|
||||
selectedAttribute.value = value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-4 p-8">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
|
||||
<div class="w-full">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !attributes.length"
|
||||
class="flex items-center justify-center mt-12"
|
||||
<div class="flex flex-col">
|
||||
<table class="min-w-full overflow-x-auto">
|
||||
<thead>
|
||||
<th
|
||||
v-for="tableHeader in $t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="tableHeader"
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.LIST.EMPTY_RESULT.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
/>
|
||||
<table
|
||||
v-if="!uiFlags.isFetching && attributes.length"
|
||||
class="w-full mt-2 table-fixed woot-table"
|
||||
>
|
||||
<thead>
|
||||
<th
|
||||
v-for="tableHeader in $t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="tableHeader"
|
||||
class="pl-0 max-w-[6.25rem] min-w-[5rem]"
|
||||
>
|
||||
{{ tableHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="attribute in attributes" :key="attribute.attribute_key">
|
||||
<td
|
||||
class="pl-0 max-w-[6.25rem] min-w-[5rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_name }}
|
||||
</td>
|
||||
<td
|
||||
class="pl-0 max-w-[10rem] min-w-[6.25rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_description }}
|
||||
</td>
|
||||
<td
|
||||
class="pl-0 max-w-[6.25rem] min-w-[5rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_type }}
|
||||
</td>
|
||||
<td
|
||||
class="attribute-key pl-0 max-w-[6.25rem] min-w-[5rem] overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_key }}
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.EDIT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
@click="openEditPopup(attribute)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.DELETE')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
@click="openDelete(attribute)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<span v-dompurify-html="$t('ATTRIBUTES_MGMT.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
{{ tableHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody
|
||||
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
<tr v-for="attribute in attributes" :key="attribute.attribute_key">
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_name }}
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
{{ attribute.attribute_description }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_display_type }}
|
||||
</td>
|
||||
<td
|
||||
class="py-4 ltr:pr-4 rtl:pl-4 attribute-key overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ attribute.attribute_key }}
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.EDIT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="grey-btn"
|
||||
icon="edit"
|
||||
@click="openEditPopup(attribute)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('ATTRIBUTES_MGMT.LIST.BUTTONS.DELETE')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
class-names="grey-btn"
|
||||
@click="openDelete(attribute)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<EditAttribute
|
||||
:selected-attribute="selectedAttribute"
|
||||
@@ -216,17 +162,4 @@ export default {
|
||||
.attribute-key {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.tabs--container {
|
||||
.tabs {
|
||||
@apply p-0;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-title a {
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,112 @@
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import AddAttribute from './AddAttribute.vue';
|
||||
import CustomAttribute from './CustomAttribute.vue';
|
||||
export default {
|
||||
components: {
|
||||
AddAttribute,
|
||||
CustomAttribute,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAddPopup: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
|
||||
const showAddPopup = ref(false);
|
||||
const selectedTabIndex = ref(0);
|
||||
const uiFlags = computed(() => getters['attributes/getUIFlags'].value);
|
||||
|
||||
const openAddPopup = () => {
|
||||
showAddPopup.value = true;
|
||||
};
|
||||
const hideAddPopup = () => {
|
||||
showAddPopup.value = false;
|
||||
};
|
||||
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 0,
|
||||
name: t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
{
|
||||
key: 1,
|
||||
name: t('ATTRIBUTES_MGMT.TABS.CONTACT'),
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('attributes/get');
|
||||
});
|
||||
|
||||
const attributeModel = computed(() =>
|
||||
selectedTabIndex.value ? 'contact_attribute' : 'conversation_attribute'
|
||||
);
|
||||
|
||||
const attributes = computed(() =>
|
||||
getters['attributes/getAttributesByModel'].value(attributeModel.value)
|
||||
);
|
||||
|
||||
const onClickTabChange = index => {
|
||||
selectedTabIndex.value = index;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup()"
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('ATTRIBUTES_MGMT.LOADING')"
|
||||
:no-records-found="!attributes.length"
|
||||
:no-records-message="$t('ATTRIBUTES_MGMT.LIST.EMPTY_RESULT.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('ATTRIBUTES_MGMT.HEADER')"
|
||||
:description="$t('ATTRIBUTES_MGMT.DESCRIPTION')"
|
||||
:link-text="$t('ATTRIBUTES_MGMT.LEARN_MORE')"
|
||||
feature-name="custom_attributes"
|
||||
>
|
||||
<template #actions>
|
||||
<woot-button
|
||||
class="button nice rounded-md"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #preBody>
|
||||
<woot-tabs
|
||||
class="font-medium [&_.tabs]:p-0 mb-4"
|
||||
:index="selectedTabIndex"
|
||||
@change="onClickTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</template>
|
||||
<template #body>
|
||||
<CustomAttribute
|
||||
:key="attributeModel"
|
||||
:attribute-model="attributeModel"
|
||||
/>
|
||||
</template>
|
||||
<woot-modal
|
||||
v-if="showAddPopup"
|
||||
:show.sync="showAddPopup"
|
||||
:on-close="hideAddPopup"
|
||||
>
|
||||
{{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
<CustomAttribute />
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<AddAttribute :on-close="hideAddPopup" />
|
||||
<AddAttribute
|
||||
:on-close="hideAddPopup"
|
||||
:selected-attribute-model-tab="selectedTabIndex"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
const SettingsContent = () => import('../Wrapper.vue');
|
||||
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||
const AttributesHome = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/custom-attributes'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'ATTRIBUTES_MGMT.HEADER',
|
||||
icon: 'code',
|
||||
showNewButton: false,
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
||||
Reference in New Issue
Block a user