fix(macros): disable public visibility for agents (#14349)
This commit is contained in:
@@ -49,6 +49,9 @@
|
||||
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
|
||||
}
|
||||
},
|
||||
"VIEW": {
|
||||
"TOOLTIP": "View macro"
|
||||
},
|
||||
"EDIT": {
|
||||
"TOOLTIP": "Edit macro",
|
||||
"API": {
|
||||
@@ -66,7 +69,9 @@
|
||||
"LABEL": "Macro Visibility",
|
||||
"GLOBAL": {
|
||||
"LABEL": "Public",
|
||||
"DESCRIPTION": "This macro is available publicly for all agents in this account."
|
||||
"DESCRIPTION": "This macro is available publicly for all agents in this account.",
|
||||
"CREATE_DISABLED_DESCRIPTION": "Only administrators can create public macros.",
|
||||
"EDIT_DISABLED_DESCRIPTION": "Only administrators can edit public macros."
|
||||
},
|
||||
"PERSONAL": {
|
||||
"LABEL": "Private",
|
||||
|
||||
@@ -9,10 +9,12 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { BaseTable } from 'dashboard/components-next/table';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const showDeleteConfirmationPopup = ref(false);
|
||||
const selectedMacro = ref({});
|
||||
@@ -109,6 +111,7 @@ const tableHeaders = computed(() => {
|
||||
v-for="macro in items"
|
||||
:key="macro.id"
|
||||
:macro="macro"
|
||||
:can-manage-public-macros="isAdmin"
|
||||
@delete="openDeletePopup(macro)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MACRO_ACTION_TYPES } from './constants';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import actionQueryGenerator from 'dashboard/helper/actionQueryGenerator.js';
|
||||
import { useMacros } from 'dashboard/composables/useMacros';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
@@ -18,6 +19,7 @@ const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { getMacroDropdownValues } = useMacros();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const macro = ref(null);
|
||||
const mode = ref('CREATE');
|
||||
@@ -33,6 +35,9 @@ provide('macroActionTypes', macroActionTypes);
|
||||
|
||||
const uiFlags = computed(() => getters['macros/getUIFlags'].value);
|
||||
const macroId = computed(() => route.params.macroId);
|
||||
const isPublicMacroReadOnly = computed(
|
||||
() => macro.value?.visibility === 'global' && !isAdmin.value
|
||||
);
|
||||
|
||||
const fetchDropdownData = () => {
|
||||
store.dispatch('agents/get');
|
||||
@@ -92,7 +97,7 @@ const initNewMacro = () => {
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
visibility: isAdmin.value ? 'global' : 'personal',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -110,6 +115,8 @@ watch(
|
||||
);
|
||||
|
||||
const saveMacro = async macroData => {
|
||||
if (isPublicMacroReadOnly.value) return;
|
||||
|
||||
try {
|
||||
const action = mode.value === 'EDIT' ? 'macros/update' : 'macros/create';
|
||||
const successMessage =
|
||||
@@ -136,6 +143,8 @@ const saveMacro = async macroData => {
|
||||
<MacroForm
|
||||
v-if="macro && !uiFlags.isFetchingItem"
|
||||
:macro-data="macro"
|
||||
:can-manage-public-macros="isAdmin"
|
||||
:read-only="isPublicMacroReadOnly"
|
||||
@update:macro-data="macro = $event"
|
||||
@submit="saveMacro"
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,14 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
canManagePublicMacros: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['submit'],
|
||||
setup() {
|
||||
@@ -112,19 +120,23 @@ export default {
|
||||
<div
|
||||
class="flex-1 w-full h-full max-h-full ltr:pl-12 ltr:pr-6 rtl:pl-6 rtl:pr-12 py-4 overflow-y-auto lg:w-auto macro-gradient-radial dark:macro-dark-gradient-radial macro-gradient-radial-size"
|
||||
>
|
||||
<MacroNodes
|
||||
v-model="macro.actions"
|
||||
:files="files"
|
||||
:errors="errors"
|
||||
@add-new-node="appendNode"
|
||||
@delete-node="deleteNode"
|
||||
@reset-action="resetNode"
|
||||
/>
|
||||
<div :inert="readOnly" :class="{ 'opacity-75': readOnly }">
|
||||
<MacroNodes
|
||||
v-model="macro.actions"
|
||||
:files="files"
|
||||
:errors="errors"
|
||||
@add-new-node="appendNode"
|
||||
@delete-node="deleteNode"
|
||||
@reset-action="resetNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/3 pb-4">
|
||||
<MacroProperties
|
||||
:macro-name="macro.name"
|
||||
:macro-visibility="macro.visibility"
|
||||
:can-manage-public-macros="canManagePublicMacros"
|
||||
:read-only="readOnly"
|
||||
@update:name="updateName"
|
||||
@update:visibility="updateVisibility"
|
||||
@submit="submit"
|
||||
|
||||
@@ -17,8 +17,36 @@ export default {
|
||||
type: String,
|
||||
default: 'global',
|
||||
},
|
||||
canManagePublicMacros: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:name', 'update:visibility', 'submit'],
|
||||
computed: {
|
||||
isPublicVisibilityDisabled() {
|
||||
return !this.canManagePublicMacros;
|
||||
},
|
||||
publicVisibilityDescription() {
|
||||
if (this.readOnly) {
|
||||
return this.$t(
|
||||
'MACROS.EDITOR.VISIBILITY.GLOBAL.EDIT_DISABLED_DESCRIPTION'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isPublicVisibilityDisabled) {
|
||||
return this.$t(
|
||||
'MACROS.EDITOR.VISIBILITY.GLOBAL.CREATE_DISABLED_DESCRIPTION'
|
||||
);
|
||||
}
|
||||
|
||||
return this.$t('MACROS.EDITOR.VISIBILITY.GLOBAL.DESCRIPTION');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(key) {
|
||||
return this.macroVisibility === key
|
||||
@@ -26,9 +54,14 @@ export default {
|
||||
: 'bg-white dark:bg-n-solid-2 border-n-weak dark:border-n-strong';
|
||||
},
|
||||
onUpdateName(value) {
|
||||
if (this.readOnly) return;
|
||||
|
||||
this.$emit('update:name', value);
|
||||
},
|
||||
onUpdateVisibility(value) {
|
||||
if (this.readOnly) return;
|
||||
if (value === 'global' && this.isPublicVisibilityDisabled) return;
|
||||
|
||||
this.$emit('update:visibility', value);
|
||||
},
|
||||
},
|
||||
@@ -46,6 +79,7 @@ export default {
|
||||
:placeholder="$t('MACROS.ADD.FORM.NAME.PLACEHOLDER')"
|
||||
:error="v$.macro.name.$error ? $t('MACROS.ADD.FORM.NAME.ERROR') : null"
|
||||
:class="{ error: v$.macro.name.$error }"
|
||||
:readonly="readOnly"
|
||||
@update:model-value="onUpdateName"
|
||||
/>
|
||||
</div>
|
||||
@@ -55,8 +89,13 @@ export default {
|
||||
</p>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<button
|
||||
class="p-2 relative rounded-md border border-solid justify-between items-start gap-2 flex flex-col text-start cursor-default"
|
||||
type="button"
|
||||
class="p-2 relative rounded-md border border-solid justify-between items-start gap-2 flex flex-col text-start"
|
||||
:class="isActive('global')"
|
||||
:disabled="isPublicVisibilityDisabled || readOnly"
|
||||
:aria-describedby="
|
||||
isPublicVisibilityDisabled ? 'macro-public-visibility-help' : null
|
||||
"
|
||||
@click="onUpdateVisibility('global')"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0 justify-between w-full">
|
||||
@@ -69,13 +108,18 @@ export default {
|
||||
class="text-n-brand size-4"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-n-slate-11 text-label-small">
|
||||
{{ $t('MACROS.EDITOR.VISIBILITY.GLOBAL.DESCRIPTION') }}
|
||||
<p
|
||||
id="macro-public-visibility-help"
|
||||
class="text-n-slate-11 text-label-small"
|
||||
>
|
||||
{{ publicVisibilityDescription }}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
class="p-2 relative rounded-md border border-solid justify-between items-start gap-2 flex flex-col text-start cursor-default"
|
||||
type="button"
|
||||
class="p-2 relative rounded-md border border-solid justify-between items-start gap-2 flex flex-col text-start"
|
||||
:class="isActive('personal')"
|
||||
:disabled="readOnly"
|
||||
@click="onUpdateVisibility('personal')"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0 justify-between w-full">
|
||||
@@ -111,6 +155,7 @@ export default {
|
||||
solid
|
||||
:label="$t('MACROS.HEADER_BTN_TXT_SAVE')"
|
||||
class="w-full"
|
||||
:disabled="readOnly"
|
||||
@click="$emit('submit')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
canManagePublicMacros: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
defineEmits(['delete']);
|
||||
const { t } = useI18n();
|
||||
@@ -32,6 +36,14 @@ const visibilityLabel = computed(() => {
|
||||
: 'MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL';
|
||||
return t(i18nKey);
|
||||
});
|
||||
|
||||
const canManageMacro = computed(
|
||||
() => props.canManagePublicMacros || props.macro.visibility !== 'global'
|
||||
);
|
||||
|
||||
const editTooltip = computed(() =>
|
||||
canManageMacro.value ? t('MACROS.EDIT.TOOLTIP') : t('MACROS.VIEW.TOOLTIP')
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,13 +97,14 @@ const visibilityLabel = computed(() => {
|
||||
:to="{ name: 'macros_edit', params: { macroId: macro.id } }"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
||||
v-tooltip.top="editTooltip"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
/>
|
||||
</router-link>
|
||||
<Button
|
||||
v-if="canManageMacro"
|
||||
v-tooltip.top="$t('MACROS.DELETE.TOOLTIP')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MacroProperties from '../MacroProperties.vue';
|
||||
|
||||
const mountComponent = props =>
|
||||
shallowMount(MacroProperties, {
|
||||
props: {
|
||||
macroName: 'Close conversation',
|
||||
macroVisibility: 'personal',
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
v$: {
|
||||
macro: {
|
||||
name: {
|
||||
$error: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
WootInput: true,
|
||||
NextButton: true,
|
||||
Icon: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('MacroProperties.vue', () => {
|
||||
it('allows administrators to select public visibility', async () => {
|
||||
const wrapper = mountComponent({ canManagePublicMacros: true });
|
||||
const publicButton = wrapper.findAll('button')[0];
|
||||
|
||||
await publicButton.trigger('click');
|
||||
|
||||
expect(publicButton.attributes('disabled')).toBeUndefined();
|
||||
expect(wrapper.emitted('update:visibility')?.[0]).toEqual(['global']);
|
||||
});
|
||||
|
||||
it('disables public visibility for agents with helper copy', async () => {
|
||||
const wrapper = mountComponent({ canManagePublicMacros: false });
|
||||
const publicButton = wrapper.findAll('button')[0];
|
||||
|
||||
await publicButton.trigger('click');
|
||||
|
||||
expect(publicButton.attributes('disabled')).toBeDefined();
|
||||
expect(wrapper.emitted('update:visibility')).toBeUndefined();
|
||||
expect(wrapper.text()).toContain(
|
||||
'Only administrators can create public macros.'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps existing public macros visibly selected when public is disabled', () => {
|
||||
const wrapper = mountComponent({
|
||||
canManagePublicMacros: false,
|
||||
macroVisibility: 'global',
|
||||
});
|
||||
|
||||
expect(wrapper.findComponent({ name: 'Icon' }).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows existing public macros as read-only for agents', async () => {
|
||||
const wrapper = mountComponent({
|
||||
canManagePublicMacros: false,
|
||||
macroVisibility: 'global',
|
||||
readOnly: true,
|
||||
});
|
||||
const [publicButton, privateButton] = wrapper.findAll('button');
|
||||
|
||||
await privateButton.trigger('click');
|
||||
|
||||
expect(publicButton.attributes('disabled')).toBeDefined();
|
||||
expect(privateButton.attributes('disabled')).toBeDefined();
|
||||
expect(wrapper.emitted('update:visibility')).toBeUndefined();
|
||||
expect(wrapper.text()).toContain(
|
||||
'Only administrators can edit public macros.'
|
||||
);
|
||||
});
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MacrosTableRow from '../MacrosTableRow.vue';
|
||||
|
||||
const macro = visibility => ({
|
||||
id: 1,
|
||||
name: 'Close conversation',
|
||||
visibility,
|
||||
created_by: {
|
||||
available_name: 'Maya Chen',
|
||||
email: 'maya.chen@example.com',
|
||||
},
|
||||
updated_by: {
|
||||
available_name: 'Maya Chen',
|
||||
email: 'maya.chen@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
const mountComponent = props =>
|
||||
shallowMount(MacrosTableRow, {
|
||||
props: {
|
||||
macro: macro('global'),
|
||||
canManagePublicMacros: true,
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Avatar: true,
|
||||
BaseTableRow: {
|
||||
template: '<div><slot /></div>',
|
||||
},
|
||||
BaseTableCell: {
|
||||
template: '<div><slot /></div>',
|
||||
},
|
||||
Button: true,
|
||||
RouterLink: {
|
||||
template: '<a><slot /></a>',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('MacrosTableRow.vue', () => {
|
||||
it('shows actions for public macros when public macros can be managed', () => {
|
||||
const wrapper = mountComponent();
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'Button' })).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps public macros viewable without delete actions when public macros cannot be managed', () => {
|
||||
const wrapper = mountComponent({ canManagePublicMacros: false });
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'Button' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps actions available for personal macros when public macros cannot be managed', () => {
|
||||
const wrapper = mountComponent({
|
||||
macro: macro('personal'),
|
||||
canManagePublicMacros: false,
|
||||
});
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'Button' })).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user