mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 12:37:56 +00:00
feat: Update the design for label management page (#9932)
This PR is part of the settings design update series. It updates the design for the label management page. I've made a few changes to the SettingsLayout page to reduce boilerplate code.
This commit is contained in:
@@ -3,13 +3,18 @@
|
|||||||
"HEADER": "Labels",
|
"HEADER": "Labels",
|
||||||
"HEADER_BTN_TXT": "Add label",
|
"HEADER_BTN_TXT": "Add label",
|
||||||
"LOADING": "Fetching labels",
|
"LOADING": "Fetching labels",
|
||||||
|
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
|
||||||
|
"LEARN_MORE": "Learn more about labels",
|
||||||
"SEARCH_404": "There are no items matching this query",
|
"SEARCH_404": "There are no items matching this query",
|
||||||
"SIDEBAR_TXT": "<p><b>Labels</b> <p>Labels help you to categorize conversations and prioritize them. You can assign label to a conversation from the sidepanel. <br /><br />Labels are tied to the account and can be used to create custom workflows in your organization. You can assign custom color to a label, it makes it easier to identify the label. You will be able to display the label on the sidebar to filter the conversations easily.</p>",
|
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "There are no labels available in this account.",
|
"404": "There are no labels available in this account.",
|
||||||
"TITLE": "Manage labels",
|
"TITLE": "Manage labels",
|
||||||
"DESC": "Labels let you group the conversations together.",
|
"DESC": "Labels let you group the conversations together.",
|
||||||
"TABLE_HEADER": ["Name", "Description", "Color"]
|
"TABLE_HEADER": [
|
||||||
|
"Name",
|
||||||
|
"Description",
|
||||||
|
"Color"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"NAME": {
|
"NAME": {
|
||||||
|
|||||||
@@ -4,21 +4,35 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
noRecordsFound: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
loadingMessage: {
|
loadingMessage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
noRecordsMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col w-full h-full gap-10 font-inter">
|
<div class="flex flex-col w-full h-full gap-10 font-inter">
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
<div>
|
|
||||||
<slot v-if="isLoading" name="loading">
|
<slot v-if="isLoading" name="loading">
|
||||||
<woot-loading-state :message="loadingMessage" />
|
<woot-loading-state :message="loadingMessage" />
|
||||||
</slot>
|
</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" />
|
<slot v-else name="body" />
|
||||||
</div>
|
<!-- Do not delete the slot below. It is required to render anything that is not defined in the above slots. -->
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,138 +1,137 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { computed, onBeforeMount, ref } from 'vue';
|
||||||
|
import { useI18n } from 'dashboard/composables/useI18n';
|
||||||
|
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||||
|
|
||||||
import AddLabel from './AddLabel.vue';
|
import AddLabel from './AddLabel.vue';
|
||||||
import EditLabel from './EditLabel.vue';
|
import EditLabel from './EditLabel.vue';
|
||||||
|
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||||
|
import SettingsLayout from '../SettingsLayout.vue';
|
||||||
|
|
||||||
export default {
|
const getters = useStoreGetters();
|
||||||
components: {
|
const store = useStore();
|
||||||
AddLabel,
|
const { t } = useI18n();
|
||||||
EditLabel,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: {},
|
|
||||||
showAddPopup: false,
|
|
||||||
showEditPopup: false,
|
|
||||||
showDeleteConfirmationPopup: false,
|
|
||||||
selectedResponse: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
records: 'labels/getLabels',
|
|
||||||
uiFlags: 'labels/getUIFlags',
|
|
||||||
}),
|
|
||||||
// Delete Modal
|
|
||||||
deleteConfirmText() {
|
|
||||||
return this.$t('LABEL_MGMT.DELETE.CONFIRM.YES');
|
|
||||||
},
|
|
||||||
deleteRejectText() {
|
|
||||||
return this.$t('LABEL_MGMT.DELETE.CONFIRM.NO');
|
|
||||||
},
|
|
||||||
deleteMessage() {
|
|
||||||
return ` ${this.selectedResponse.title}?`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$store.dispatch('labels/get');
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openAddPopup() {
|
|
||||||
this.showAddPopup = true;
|
|
||||||
},
|
|
||||||
hideAddPopup() {
|
|
||||||
this.showAddPopup = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
openEditPopup(response) {
|
const loading = ref({});
|
||||||
this.showEditPopup = true;
|
const showAddPopup = ref(false);
|
||||||
this.selectedResponse = response;
|
const showEditPopup = ref(false);
|
||||||
},
|
const showDeleteConfirmationPopup = ref(false);
|
||||||
hideEditPopup() {
|
const selectedLabel = ref({});
|
||||||
this.showEditPopup = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
openDeletePopup(response) {
|
const records = computed(() => getters['labels/getLabels'].value);
|
||||||
this.showDeleteConfirmationPopup = true;
|
const uiFlags = computed(() => getters['labels/getUIFlags'].value);
|
||||||
this.selectedResponse = response;
|
|
||||||
},
|
|
||||||
closeDeletePopup() {
|
|
||||||
this.showDeleteConfirmationPopup = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmDeletion() {
|
const deleteMessage = computed(() => ` ${selectedLabel.value.title}?`);
|
||||||
this.loading[this.selectedResponse.id] = true;
|
|
||||||
this.closeDeletePopup();
|
const openAddPopup = () => {
|
||||||
this.deleteLabel(this.selectedResponse.id);
|
showAddPopup.value = true;
|
||||||
},
|
|
||||||
deleteLabel(id) {
|
|
||||||
this.$store
|
|
||||||
.dispatch('labels/delete', id)
|
|
||||||
.then(() => {
|
|
||||||
useAlert(this.$t('LABEL_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
useAlert(this.$t('LABEL_MGMT.DELETE.API.ERROR_MESSAGE'));
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.loading[this.selectedResponse.id] = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
const hideAddPopup = () => {
|
||||||
|
showAddPopup.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditPopup = response => {
|
||||||
|
showEditPopup.value = true;
|
||||||
|
selectedLabel.value = response;
|
||||||
|
};
|
||||||
|
const hideEditPopup = () => {
|
||||||
|
showEditPopup.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeletePopup = response => {
|
||||||
|
showDeleteConfirmationPopup.value = true;
|
||||||
|
selectedLabel.value = response;
|
||||||
|
};
|
||||||
|
const closeDeletePopup = () => {
|
||||||
|
showDeleteConfirmationPopup.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLabel = async id => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('labels/delete', id);
|
||||||
|
useAlert(t('LABEL_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.message || t('LABEL_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||||
|
useAlert(errorMessage);
|
||||||
|
} finally {
|
||||||
|
loading.value[selectedLabel.value.id] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeletion = () => {
|
||||||
|
loading.value[selectedLabel.value.id] = true;
|
||||||
|
closeDeletePopup();
|
||||||
|
deleteLabel(selectedLabel.value.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
store.dispatch('labels/get');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 overflow-auto">
|
<SettingsLayout
|
||||||
|
:is-loading="uiFlags.isFetching"
|
||||||
|
:loading-message="$t('LABEL_MGMT.LOADING')"
|
||||||
|
:no-records-found="!records.length"
|
||||||
|
:no-records-message="$t('LABEL_MGMT.LIST.404')"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<BaseSettingsHeader
|
||||||
|
:title="$t('LABEL_MGMT.HEADER')"
|
||||||
|
:description="$t('LABEL_MGMT.DESCRIPTION')"
|
||||||
|
:link-text="$t('LABEL_MGMT.LEARN_MORE')"
|
||||||
|
feature-name="labels"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="success"
|
class="button nice rounded-md"
|
||||||
class-names="button--fixed-top"
|
|
||||||
icon="add-circle"
|
icon="add-circle"
|
||||||
@click="openAddPopup"
|
@click="openAddPopup"
|
||||||
>
|
>
|
||||||
{{ $t('LABEL_MGMT.HEADER_BTN_TXT') }}
|
{{ $t('LABEL_MGMT.HEADER_BTN_TXT') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<div class="flex flex-row gap-4 p-8">
|
</template>
|
||||||
<div class="w-full xl:w-3/5">
|
</BaseSettingsHeader>
|
||||||
<p
|
</template>
|
||||||
v-if="!uiFlags.isFetching && !records.length"
|
<template #body>
|
||||||
class="flex flex-col items-center justify-center h-full"
|
<table
|
||||||
|
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
|
||||||
>
|
>
|
||||||
{{ $t('LABEL_MGMT.LIST.404') }}
|
|
||||||
</p>
|
|
||||||
<woot-loading-state
|
|
||||||
v-if="uiFlags.isFetching"
|
|
||||||
:message="$t('LABEL_MGMT.LOADING')"
|
|
||||||
/>
|
|
||||||
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<th
|
<th
|
||||||
v-for="thHeader in $t('LABEL_MGMT.LIST.TABLE_HEADER')"
|
v-for="thHeader in $t('LABEL_MGMT.LIST.TABLE_HEADER')"
|
||||||
:key="thHeader"
|
:key="thHeader"
|
||||||
|
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-slate-700 dark:text-slate-300"
|
||||||
>
|
>
|
||||||
{{ thHeader }}
|
{{ thHeader }}
|
||||||
</th>
|
</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody
|
||||||
|
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
|
||||||
|
>
|
||||||
<tr v-for="(label, index) in records" :key="label.title">
|
<tr v-for="(label, index) in records" :key="label.title">
|
||||||
<td class="label-title">
|
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||||
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
|
|
||||||
label.title
|
|
||||||
}}</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ label.description }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="label-color--container">
|
|
||||||
<span
|
<span
|
||||||
class="label-color--display"
|
class="font-medium break-words text-slate-700 dark:text-slate-100 mb-1"
|
||||||
|
>
|
||||||
|
{{ label.title }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ label.description }}</td>
|
||||||
|
<td class="leading-6 py-4 ltr:pr-4 rtl:pl-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="rounded h-4 w-4 mr-1 rtl:mr-0 rtl:ml-1 border border-solid border-slate-50 dark:border-slate-700"
|
||||||
:style="{ backgroundColor: label.color }"
|
:style="{ backgroundColor: label.color }"
|
||||||
/>
|
/>
|
||||||
{{ label.color }}
|
{{ label.color }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="button-wrapper">
|
<td class="py-4 min-w-xs">
|
||||||
|
<div class="flex gap-1">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-tooltip.top="$t('LABEL_MGMT.FORM.EDIT')"
|
v-tooltip.top="$t('LABEL_MGMT.FORM.EDIT')"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
@@ -153,22 +152,19 @@ export default {
|
|||||||
:is-loading="loading[label.id]"
|
:is-loading="loading[label.id]"
|
||||||
@click="openDeletePopup(label, index)"
|
@click="openDeletePopup(label, index)"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div class="hidden w-1/3 xl:block">
|
|
||||||
<span v-dompurify-html="$t('LABEL_MGMT.SIDEBAR_TXT')" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||||
<AddLabel @close="hideAddPopup" />
|
<AddLabel @close="hideAddPopup" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
|
||||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||||
<EditLabel :selected-response="selectedResponse" @close="hideEditPopup" />
|
<EditLabel :selected-response="selectedLabel" @close="hideEditPopup" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
|
||||||
<woot-delete-modal
|
<woot-delete-modal
|
||||||
@@ -178,26 +174,8 @@ export default {
|
|||||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||||
:message="$t('LABEL_MGMT.DELETE.CONFIRM.MESSAGE')"
|
:message="$t('LABEL_MGMT.DELETE.CONFIRM.MESSAGE')"
|
||||||
:message-value="deleteMessage"
|
:message-value="deleteMessage"
|
||||||
:confirm-text="deleteConfirmText"
|
:confirm-text="$t('LABEL_MGMT.DELETE.CONFIRM.YES')"
|
||||||
:reject-text="deleteRejectText"
|
:reject-text="$t('LABEL_MGMT.DELETE.CONFIRM.NO')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</SettingsLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import '~dashboard/assets/scss/variables';
|
|
||||||
|
|
||||||
.label-color--container {
|
|
||||||
@apply flex items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-color--display {
|
|
||||||
@apply rounded h-4 w-4 mr-1 rtl:mr-0 rtl:ml-1 border border-solid border-slate-50 dark:border-slate-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-title {
|
|
||||||
span {
|
|
||||||
@apply w-60 inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const SettingsContent = () => import('../Wrapper.vue');
|
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||||
const Index = () => import('./Index.vue');
|
const Index = () => import('./Index.vue');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/settings/labels'),
|
path: frontendURL('accounts/:accountId/settings/labels'),
|
||||||
component: SettingsContent,
|
component: SettingsWrapper,
|
||||||
props: {
|
|
||||||
headerTitle: 'LABEL_MGMT.HEADER',
|
|
||||||
icon: 'tag',
|
|
||||||
showNewButton: false,
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user