feat: Add custom attributes components (#10467)
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Switch from 'dashboard/components-next/switch/Switch.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
attribute: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isEditingView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'delete']);
|
||||||
|
|
||||||
|
const attributeValue = ref(Boolean(props.attribute.value));
|
||||||
|
|
||||||
|
const handleChange = value => {
|
||||||
|
emit('update', value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center w-full gap-2"
|
||||||
|
:class="{
|
||||||
|
'justify-start': isEditingView,
|
||||||
|
'justify-end': !isEditingView,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Switch v-model="attributeValue" @change="handleChange" />
|
||||||
|
<Button
|
||||||
|
v-if="isEditingView"
|
||||||
|
variant="faded"
|
||||||
|
color="ruby"
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="emit('delete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required } from '@vuelidate/validators';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
attribute: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isEditingView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'delete']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const isEditingValue = ref(false);
|
||||||
|
const editedValue = ref(props.attribute.value || '');
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
editedValue: {
|
||||||
|
required,
|
||||||
|
isDate: value => new Date(value).toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, { editedValue });
|
||||||
|
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
return props.attribute.value
|
||||||
|
? new Date(props.attribute.value).toLocaleDateString()
|
||||||
|
: t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.TRIGGER.INPUT');
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasError = computed(() => v$.value.$errors.length > 0);
|
||||||
|
|
||||||
|
const defaultDateValue = computed({
|
||||||
|
get() {
|
||||||
|
const existingDate = editedValue.value ?? props.attribute.value;
|
||||||
|
if (existingDate) return new Date(existingDate).toISOString().slice(0, 10);
|
||||||
|
return isEditingValue.value && !hasError.value
|
||||||
|
? new Date().toISOString().slice(0, 10)
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
editedValue.value = value ? new Date(value).toISOString() : value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleEditValue = value => {
|
||||||
|
isEditingValue.value =
|
||||||
|
typeof value === 'boolean' ? value : !isEditingValue.value;
|
||||||
|
|
||||||
|
if (isEditingValue.value && !editedValue.value) {
|
||||||
|
v$.value.$reset();
|
||||||
|
editedValue.value = new Date().toISOString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputUpdate = async () => {
|
||||||
|
const isValid = await v$.value.$validate();
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
emit('update', parseISO(editedValue.value));
|
||||||
|
isEditingValue.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center w-full min-w-0 gap-2"
|
||||||
|
:class="{
|
||||||
|
'justify-start': isEditingView,
|
||||||
|
'justify-end': !isEditingView,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!isEditingValue"
|
||||||
|
class="min-w-0 text-sm"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||||
|
!isEditingView,
|
||||||
|
'text-n-slate-12 truncate flex-1': isEditingView,
|
||||||
|
}"
|
||||||
|
@click="toggleEditValue(!isEditingView)"
|
||||||
|
>
|
||||||
|
{{ formattedDate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isEditingView && !isEditingValue"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
icon="i-lucide-pencil"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="toggleEditValue(true)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="ruby"
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="emit('delete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isEditingValue"
|
||||||
|
v-on-clickaway="() => toggleEditValue(false)"
|
||||||
|
class="flex items-center w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model="defaultDateValue"
|
||||||
|
type="date"
|
||||||
|
class="w-full [&>p]:absolute [&>p]:mt-0.5 [&>p]:top-8 ltr:[&>p]:left-0 rtl:[&>p]:right-0"
|
||||||
|
:message="
|
||||||
|
hasError
|
||||||
|
? t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.VALIDATIONS.INVALID_DATE')
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
:message-type="hasError ? 'error' : 'info'"
|
||||||
|
autofocus
|
||||||
|
custom-input-class="h-8 ltr:rounded-r-none rtl:rounded-l-none"
|
||||||
|
@keyup.enter="handleInputUpdate"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-check"
|
||||||
|
:color="hasError ? 'ruby' : 'blue'"
|
||||||
|
size="sm"
|
||||||
|
class="flex-shrink-0 ltr:rounded-l-none rtl:rounded-r-none"
|
||||||
|
@click="handleInputUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
attribute: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isEditingView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'delete']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const [showAttributeListDropdown, toggleAttributeListDropdown] = useToggle();
|
||||||
|
|
||||||
|
const attributeListMenuItems = computed(() => {
|
||||||
|
return (
|
||||||
|
props.attribute.attributeValues?.map(value => ({
|
||||||
|
label: value,
|
||||||
|
value,
|
||||||
|
action: 'select',
|
||||||
|
isSelected: value === props.attribute.value,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAttributeAction = async action => {
|
||||||
|
emit('update', action.value);
|
||||||
|
toggleAttributeListDropdown(false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center w-full min-w-0 gap-2"
|
||||||
|
:class="{
|
||||||
|
'justify-start': isEditingView,
|
||||||
|
'justify-end': !isEditingView,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-on-clickaway="() => toggleAttributeListDropdown(false)"
|
||||||
|
class="relative flex items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="min-w-0 text-sm"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||||
|
!isEditingView,
|
||||||
|
'text-n-slate-12 truncate flex-1': isEditingView,
|
||||||
|
}"
|
||||||
|
@click="toggleAttributeListDropdown(!props.isEditingView)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
attribute.value ||
|
||||||
|
t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.TRIGGER.SELECT')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<DropdownMenu
|
||||||
|
v-if="showAttributeListDropdown"
|
||||||
|
:menu-items="attributeListMenuItems"
|
||||||
|
show-search
|
||||||
|
class="w-48 mt-2 top-full"
|
||||||
|
:class="{
|
||||||
|
'ltr:right-0 rtl:left-0': !isEditingView,
|
||||||
|
'ltr:left-0 rtl:right-0': isEditingView,
|
||||||
|
}"
|
||||||
|
@action="handleAttributeAction($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isEditingView" class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
icon="i-lucide-pencil"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="toggleAttributeListDropdown()"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="ruby"
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="emit('delete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
<!-- Attribute type "Text, URL, Number" -->
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required } from '@vuelidate/validators';
|
||||||
|
import { isValidURL } from 'dashboard/helper/URLHelper.js';
|
||||||
|
import { getRegexp } from 'shared/helpers/Validators';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
attribute: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isEditingView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'delete']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const isEditingValue = ref(false);
|
||||||
|
const editedValue = ref(props.attribute.value || '');
|
||||||
|
|
||||||
|
const isAttributeTypeLink = computed(
|
||||||
|
() => props.attribute.attributeDisplayType === 'link'
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAttributeTypeText = computed(
|
||||||
|
() => props.attribute.attributeDisplayType === 'text'
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAttributeTypeNumber = computed(
|
||||||
|
() => props.attribute.attributeDisplayType === 'number'
|
||||||
|
);
|
||||||
|
|
||||||
|
const rules = computed(() => ({
|
||||||
|
editedValue: {
|
||||||
|
required,
|
||||||
|
...(isAttributeTypeLink.value && {
|
||||||
|
url: value => !value || isValidURL(value),
|
||||||
|
}),
|
||||||
|
...(isAttributeTypeText.value &&
|
||||||
|
props.attribute.regexPattern && {
|
||||||
|
regexValidation: value => {
|
||||||
|
if (!value) return true;
|
||||||
|
return getRegexp(props.attribute.regexPattern).test(value);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const v$ = useVuelidate(rules, { editedValue });
|
||||||
|
|
||||||
|
const hasError = computed(() => v$.value.$error);
|
||||||
|
|
||||||
|
const attributeErrorMessage = computed(() => {
|
||||||
|
if (!hasError.value) return '';
|
||||||
|
|
||||||
|
if (isAttributeTypeLink.value && v$.value.editedValue.url?.$invalid) {
|
||||||
|
return t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.VALIDATIONS.INVALID_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isAttributeTypeText.value &&
|
||||||
|
props.attribute.regexPattern &&
|
||||||
|
v$.value.editedValue.regexValidation?.$invalid
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
props.attribute.regexCue ||
|
||||||
|
t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.VALIDATIONS.INVALID_INPUT')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAttributeTypeNumber.value && v$.value.editedValue.required?.$invalid) {
|
||||||
|
return t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.VALIDATIONS.INVALID_NUMBER');
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||||
|
});
|
||||||
|
|
||||||
|
const getInputType = computed(() => {
|
||||||
|
switch (props.attribute.attributeDisplayType) {
|
||||||
|
case 'link':
|
||||||
|
return 'url';
|
||||||
|
case 'number':
|
||||||
|
return 'number';
|
||||||
|
default:
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleEditValue = value => {
|
||||||
|
isEditingValue.value =
|
||||||
|
typeof value === 'boolean' ? value : !isEditingValue.value;
|
||||||
|
if (isEditingValue.value) {
|
||||||
|
v$.value.$reset();
|
||||||
|
editedValue.value = props.attribute.value || '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputUpdate = async () => {
|
||||||
|
const isValid = await v$.value.$validate();
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
emit('update', editedValue.value);
|
||||||
|
toggleEditValue(false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center w-full min-w-0 gap-2"
|
||||||
|
:class="{
|
||||||
|
'justify-start': isEditingView,
|
||||||
|
'justify-end': !isEditingView,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!isEditingValue"
|
||||||
|
class="min-w-0 text-sm"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer text-n-slate-11 hover:text-n-slate-12 py-2 select-none font-medium':
|
||||||
|
!isEditingView,
|
||||||
|
'text-n-slate-12 truncate flex-1':
|
||||||
|
isEditingView && !isAttributeTypeLink,
|
||||||
|
'truncate flex-1 hover:text-n-brand text-n-blue-text':
|
||||||
|
isEditingView && isAttributeTypeLink,
|
||||||
|
}"
|
||||||
|
@click="toggleEditValue(!isEditingView)"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="isAttributeTypeLink && attribute.value && isEditingView"
|
||||||
|
:href="attribute.value"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="hover:underline"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ attribute.value }}
|
||||||
|
</a>
|
||||||
|
<template v-else>
|
||||||
|
{{
|
||||||
|
attribute.value ||
|
||||||
|
t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.TRIGGER.INPUT')
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isEditingView && !isEditingValue"
|
||||||
|
class="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
icon="i-lucide-pencil"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="toggleEditValue(true)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="ruby"
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
size="xs"
|
||||||
|
class="flex-shrink-0 opacity-0 group-hover/attribute:opacity-100 hover:no-underline"
|
||||||
|
@click="emit('delete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isEditingValue"
|
||||||
|
v-on-clickaway="() => toggleEditValue(false)"
|
||||||
|
class="flex items-center w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model="editedValue"
|
||||||
|
:placeholder="t('CONTACTS_LAYOUT.SIDEBAR.ATTRIBUTES.TRIGGER.INPUT')"
|
||||||
|
:type="getInputType"
|
||||||
|
class="w-full [&>p]:absolute [&>p]:mt-0.5 [&>p]:top-8 ltr:[&>p]:left-0 rtl:[&>p]:right-0"
|
||||||
|
autofocus
|
||||||
|
:message="attributeErrorMessage"
|
||||||
|
:message-type="hasError ? 'error' : 'info'"
|
||||||
|
custom-input-class="h-8 ltr:rounded-r-none rtl:rounded-l-none"
|
||||||
|
@keyup.enter="handleInputUpdate"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-check"
|
||||||
|
:color="hasError ? 'ruby' : 'blue'"
|
||||||
|
size="sm"
|
||||||
|
class="flex-shrink-0 ltr:rounded-l-none rtl:rounded-r-none"
|
||||||
|
@click="handleInputUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup>
|
||||||
|
import Attributes from './fixtures';
|
||||||
|
|
||||||
|
import OtherAttribute from '../OtherAttribute.vue';
|
||||||
|
import ListAttribute from '../ListAttribute.vue';
|
||||||
|
import DateAttribute from '../DateAttribute.vue';
|
||||||
|
import CheckboxAttribute from '../CheckboxAttribute.vue';
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
list: ListAttribute,
|
||||||
|
checkbox: CheckboxAttribute,
|
||||||
|
date: DateAttribute,
|
||||||
|
default: OtherAttribute,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentComponent = type => {
|
||||||
|
return componentMap[type] || componentMap.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (type, value) => {
|
||||||
|
console.log(`${type} updated:`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = type => {
|
||||||
|
console.log(`${type} deleted`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/CustomAttributes"
|
||||||
|
:layout="{ type: 'grid', width: '600px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Create View">
|
||||||
|
<div class="flex flex-col gap-4 p-4 border rounded-lg border-n-strong">
|
||||||
|
<div
|
||||||
|
v-for="attribute in Attributes"
|
||||||
|
:key="attribute.attributeKey"
|
||||||
|
class="grid grid-cols-[140px,1fr] group-hover/attribute items-center gap-1 min-h-10"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between truncate">
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ attribute.attributeDisplayName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<component
|
||||||
|
:is="getCurrentComponent(attribute.attributeDisplayType)"
|
||||||
|
:attribute="attribute"
|
||||||
|
@update="
|
||||||
|
value => handleUpdate(attribute.attributeDisplayType, value)
|
||||||
|
"
|
||||||
|
@delete="() => handleDelete(attribute.attributeDisplayType)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Saved View">
|
||||||
|
<div class="flex flex-col gap-4 p-4 border rounded-lg border-n-strong">
|
||||||
|
<div
|
||||||
|
v-for="attribute in Attributes"
|
||||||
|
:key="attribute.attributeKey"
|
||||||
|
class="grid grid-cols-[140px,1fr] group-hover/attribute items-center gap-1 min-h-10"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between truncate">
|
||||||
|
<span class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ attribute.attributeDisplayName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<component
|
||||||
|
:is="getCurrentComponent(attribute.attributeDisplayType)"
|
||||||
|
:attribute="attribute"
|
||||||
|
is-editing-view
|
||||||
|
@update="
|
||||||
|
value => handleUpdate(attribute.attributeDisplayType, value)
|
||||||
|
"
|
||||||
|
@delete="() => handleDelete(attribute.attributeDisplayType)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
attributeKey: 'textContact',
|
||||||
|
attributeDisplayName: 'Text Input',
|
||||||
|
attributeDisplayType: 'text',
|
||||||
|
value: 'Sample text value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeKey: 'linkContact',
|
||||||
|
attributeDisplayName: 'URL Input',
|
||||||
|
attributeDisplayType: 'link',
|
||||||
|
value: 'https://www.chatwoot.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeKey: 'numberContact',
|
||||||
|
attributeDisplayName: 'Number Input',
|
||||||
|
attributeDisplayType: 'number',
|
||||||
|
value: '42',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeKey: 'listContact',
|
||||||
|
attributeDisplayName: 'List Input',
|
||||||
|
attributeDisplayType: 'list',
|
||||||
|
value: 'Option 2',
|
||||||
|
attributeValues: ['Option 1', 'Option 2', 'Option 3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeKey: 'dateContact',
|
||||||
|
attributeDisplayName: 'Date Input',
|
||||||
|
attributeDisplayType: 'date',
|
||||||
|
value: '2024-03-25T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeKey: 'checkboxContact',
|
||||||
|
attributeDisplayName: 'Checkbox Input',
|
||||||
|
attributeDisplayType: 'checkbox',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
import Switch from './Switch.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// Default varian
|
||||||
|
const isEnabled = ref(false);
|
||||||
|
|
||||||
|
// States variant
|
||||||
|
const defaultValue = ref(false);
|
||||||
|
const checkedValue = ref(true);
|
||||||
|
|
||||||
|
// Events variant
|
||||||
|
const eventValue = ref(false);
|
||||||
|
const lastChange = ref('No changes yet');
|
||||||
|
|
||||||
|
const onChange = value => {
|
||||||
|
lastChange.value = `Changed to: ${value} at ${new Date().toLocaleTimeString()}`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story title="Components/Switch" :layout="{ type: 'grid', width: '200px' }">
|
||||||
|
<Variant title="Default">
|
||||||
|
<div class="p-2">
|
||||||
|
<Switch v-model="isEnabled" />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="States">
|
||||||
|
<div class="p-2 space-y-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="w-20">Default:</span>
|
||||||
|
<Switch v-model="defaultValue" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="w-20">Checked:</span>
|
||||||
|
<Switch v-model="checkedValue" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Events">
|
||||||
|
<div class="p-2 space-y-4">
|
||||||
|
<Switch v-model="eventValue" @change="onChange" />
|
||||||
|
<div class="text-sm text-gray-600">Last change: {{ lastChange }}</div>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Disabled">
|
||||||
|
<div class="p-2">
|
||||||
|
<Switch v-model="isEnabled" disabled />
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
38
app/javascript/dashboard/components-next/switch/Switch.vue
Normal file
38
app/javascript/dashboard/components-next/switch/Switch.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const modelValue = defineModel({
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateValue = () => {
|
||||||
|
modelValue.value = !modelValue.value;
|
||||||
|
emit('change', !modelValue.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:ring-offset-n-slate-2 focus:ring-offset-2"
|
||||||
|
:class="modelValue ? 'bg-n-brand' : 'bg-n-alpha-1 dark:bg-n-alpha-2'"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="modelValue"
|
||||||
|
@click="updateValue"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
|
||||||
|
<span
|
||||||
|
class="absolute top-px left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-in-out"
|
||||||
|
:class="
|
||||||
|
modelValue
|
||||||
|
? 'translate-x-2.5 bg-white'
|
||||||
|
: 'translate-x-0 bg-white dark:bg-n-black'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
"BREADCRUMB": {
|
"BREADCRUMB": {
|
||||||
"ARIA_LABEL": "Breadcrumb"
|
"ARIA_LABEL": "Breadcrumb"
|
||||||
},
|
},
|
||||||
|
"SWITCH": {
|
||||||
|
"TOGGLE": "Toggle switch"
|
||||||
|
},
|
||||||
"LABEL": {
|
"LABEL": {
|
||||||
"TAG_BUTTON": "tag"
|
"TAG_BUTTON": "tag"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,6 +442,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
|
"ATTRIBUTES": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search for attributes",
|
||||||
|
"UNUSED_ATTRIBUTES": "{count} Used attribute | {count} Unused attributes",
|
||||||
|
"EMPTY_STATE": "There are no contact custom attributes available in this account. You can create a custom attribute in settings.",
|
||||||
|
"YES": "Yes",
|
||||||
|
"NO": "No",
|
||||||
|
"TRIGGER": {
|
||||||
|
"SELECT": "Select value",
|
||||||
|
"INPUT": "Enter value"
|
||||||
|
},
|
||||||
|
"VALIDATIONS": {
|
||||||
|
"INVALID_NUMBER": "Invalid number",
|
||||||
|
"REQUIRED": "Valid value is required",
|
||||||
|
"INVALID_INPUT": "Invalid input",
|
||||||
|
"INVALID_URL": "Invalid URL",
|
||||||
|
"INVALID_DATE": "Invalid date"
|
||||||
|
},
|
||||||
|
"NO_ATTRIBUTES": "No attributes found",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Attribute updated successfully",
|
||||||
|
"DELETE_SUCCESS_MESSAGE": "Attribute deleted successfully",
|
||||||
|
"UPDATE_ERROR": "Unable to update attribute. Please try again later",
|
||||||
|
"DELETE_ERROR": "Unable to delete attribute. Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
"MERGE": {
|
"MERGE": {
|
||||||
"TITLE": "Merge contact",
|
"TITLE": "Merge contact",
|
||||||
"DESCRIPTION": "Combine two profiles into one, including all attributes and conversations. In case of conflict, the primary contact’s attributes will take precedence.",
|
"DESCRIPTION": "Combine two profiles into one, including all attributes and conversations. In case of conflict, the primary contact’s attributes will take precedence.",
|
||||||
|
|||||||
Reference in New Issue
Block a user