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:
Pranav
2024-08-29 19:06:11 +05:30
committed by GitHub
parent 6dda1e8c8f
commit 3a47b7e3d1
7 changed files with 274 additions and 247 deletions

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '',