forked from chatwoot/chatwoot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add custom attributes components (chatwoot#10467)
- Loading branch information
Showing
10 changed files
with
740 additions
and
0 deletions.
There are no files selected for viewing
46 changes: 46 additions & 0 deletions
46
app/javascript/dashboard/components-next/CustomAttributes/CheckboxAttribute.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
148 changes: 148 additions & 0 deletions
148
app/javascript/dashboard/components-next/CustomAttributes/DateAttribute.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
100 changes: 100 additions & 0 deletions
100
app/javascript/dashboard/components-next/CustomAttributes/ListAttribute.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Oops, something went wrong.