feat: translate macros

This commit is contained in:
Abhinav Raut
2025-03-31 12:15:52 +05:30
parent 9aa9a5e1b2
commit 88e4a55952
9 changed files with 117 additions and 68 deletions

View File

@@ -77,7 +77,7 @@
</Avatar>
<span>{{ selected.label }}</span>
</div>
<span v-else>Select user</span>
<span v-else>{{ $t('admin.macro.visibility.selectUser') }}</span>
</div>
</div>
<div v-else-if="action.type === 'assign_team'">
@@ -86,13 +86,15 @@
{{ selected.emoji }}
<span>{{ selected.label }}</span>
</span>
<span v-else>Select team</span>
<span v-else>
{{ $t('admin.macro.visibility.selectTeam') }}
</span>
</div>
</div>
<div v-else-if="selected">
{{ selected.label }}
</div>
<div v-else>Select</div>
<div v-else>{{ $t('form.field.select') }}</div>
</template>
</ComboBox>
</div>
@@ -131,7 +133,7 @@ import { SelectTag } from '@/components/ui/select'
import { useTagStore } from '@/stores/tag'
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
const model = defineModel("actions", {
const model = defineModel('actions', {
type: Array,
required: true,
default: () => []

View File

@@ -3,9 +3,9 @@
<form @submit="onSubmit" class="space-y-6 w-full" :class="{ 'opacity-50': formLoading }">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{{ t('form.field.name') }} </FormLabel>
<FormControl>
<Input type="text" placeholder="Macro name" v-bind="componentField" />
<Input type="text" placeholder="" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
@@ -13,13 +13,13 @@
<FormField v-slot="{ componentField }" name="message_content">
<FormItem>
<FormLabel>Response to be sent when macro is used (optional)</FormLabel>
<FormLabel>{{ t('admin.macro.message_content') }}</FormLabel>
<FormControl>
<div class="box p-2 h-96 min-h-96">
<Editor
v-model:htmlContent="componentField.modelValue"
@update:htmlContent="(value) => componentField.onChange(value)"
:placeholder="'Shift + Enter to add new line'"
:placeholder="t('admin.macro.message_content.placeholder')"
/>
</div>
</FormControl>
@@ -29,9 +29,13 @@
<FormField v-slot="{ componentField }" name="actions">
<FormItem>
<FormLabel> Actions (optional)</FormLabel>
<FormLabel> {{ t('admin.macro.actions') }}</FormLabel>
<FormControl>
<ActionBuilder v-model:actions="componentField.modelValue" :config="actionConfig" @update:actions="(value) => componentField.onChange(value)" />
<ActionBuilder
v-model:actions="componentField.modelValue"
:config="actionConfig"
@update:actions="(value) => componentField.onChange(value)"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -39,7 +43,7 @@
<FormField v-slot="{ componentField }" name="visibility">
<FormItem>
<FormLabel>Visibility</FormLabel>
<FormLabel>{{ t('admin.macro.visibility') }}</FormLabel>
<FormControl>
<Select v-bind="componentField">
<SelectTrigger>
@@ -47,9 +51,9 @@
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All</SelectItem>
<SelectItem value="team">Team</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="all">{{ t('admin.macro.visibility.all') }}</SelectItem>
<SelectItem value="team">{{ t('admin.macro.visibility.team') }}</SelectItem>
<SelectItem value="user">{{ t('admin.macro.visibility.user') }}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
@@ -60,9 +64,13 @@
<FormField v-if="form.values.visibility === 'team'" v-slot="{ componentField }" name="team_id">
<FormItem>
<FormLabel>Team</FormLabel>
<FormLabel>{{ t('admin.macro.visibility.user') }}</FormLabel>
<FormControl>
<ComboBox v-bind="componentField" :items="tStore.options" placeholder="Select team">
<ComboBox
v-bind="componentField"
:items="tStore.options"
:placeholder="t('admin.macro.visibility.selectTeam')"
>
<template #item="{ item }">
<div class="flex items-center gap-2 ml-2">
<span>{{ item.emoji }}</span>
@@ -75,7 +83,7 @@
{{ selected.emoji }}
<span>{{ selected.label }}</span>
</span>
<span v-else>Select team</span>
<span v-else>{{ t('admin.macro.visibility.selectTeam') }}</span>
</div>
</template>
</ComboBox>
@@ -86,9 +94,13 @@
<FormField v-if="form.values.visibility === 'user'" v-slot="{ componentField }" name="user_id">
<FormItem>
<FormLabel>User</FormLabel>
<FormLabel>{{ t('admin.macro.visibility.user') }}</FormLabel>
<FormControl>
<ComboBox v-bind="componentField" :items="uStore.options" placeholder="Select user">
<ComboBox
v-bind="componentField"
:items="uStore.options"
:placeholder="t('admin.macro.visibility.selectUser')"
>
<template #item="{ item }">
<div class="flex items-center gap-2 ml-2">
<Avatar class="w-7 h-7">
@@ -107,7 +119,7 @@
</Avatar>
<span>{{ selected.label }}</span>
</div>
<span v-else>Select user</span>
<span v-else>{{ t('admin.macro.visibility.selectUser') }}</span>
</div>
</template>
</ComboBox>
@@ -120,7 +132,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, computed } from 'vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { Button } from '@/components/ui/button'
@@ -133,7 +145,7 @@ import { useConversationFilters } from '@/composables/useConversationFilters'
import { useUsersStore } from '@/stores/users'
import { useTeamStore } from '@/stores/team'
import { getTextFromHTML } from '@/utils/strings.js'
import { formSchema } from './formSchema.js'
import { createFormSchema } from './formSchema.js'
import {
Select,
SelectContent,
@@ -143,9 +155,11 @@ import {
SelectValue
} from '@/components/ui/select'
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
import { useI18n } from 'vue-i18n'
import Editor from '@/features/conversation/ConversationTextEditor.vue'
const { macroActions } = useConversationFilters()
const { t } = useI18n()
const formLoading = ref(false)
const uStore = useUsersStore()
const tStore = useTeamStore()
@@ -160,7 +174,7 @@ const props = defineProps({
},
submitLabel: {
type: String,
default: 'Submit'
default: ''
},
isLoading: {
type: Boolean,
@@ -168,15 +182,21 @@ const props = defineProps({
}
})
const submitLabel = computed(() => {
return (
props.submitLabel ||
(props.initialValues.id ? t('globals.buttons.update') : t('globals.buttons.create'))
)
})
const form = useForm({
validationSchema: toTypedSchema(formSchema)
validationSchema: toTypedSchema(createFormSchema(t))
})
const actionConfig = ref({
actions: macroActions,
typePlaceholder: 'Select action type',
valuePlaceholder: 'Select value',
addButtonText: 'Add new action'
typePlaceholder: t('admin.macro.visibility.selectActionType'),
valuePlaceholder: t('admin.macro.visibility.selectValue'),
addButtonText: t('admin.macro.visibility.addNewAction')
})
const onSubmit = form.handleSubmit(async (values) => {

View File

@@ -2,11 +2,11 @@ import { h } from 'vue'
import dropdown from './dataTableDropdown.vue'
import { format } from 'date-fns'
export const columns = [
export const createColumns = (t) => [
{
accessorKey: 'name',
header: function () {
return h('div', { class: 'text-center' }, 'Name')
return h('div', { class: 'text-center' }, t('form.field.name'))
},
cell: function ({ row }) {
return h('div', { class: 'text-center font-medium' }, row.getValue('name'))
@@ -15,7 +15,7 @@ export const columns = [
{
accessorKey: 'visibility',
header: function () {
return h('div', { class: 'text-center' }, 'Visibility')
return h('div', { class: 'text-center' }, t('admin.macro.visibility'))
},
cell: function ({ row }) {
return h('div', { class: 'text-center' }, row.getValue('visibility'))
@@ -24,7 +24,7 @@ export const columns = [
{
accessorKey: 'usage_count',
header: function () {
return h('div', { class: 'text-center' }, 'Usage')
return h('div', { class: 'text-center' }, t('form.field.usage'))
},
cell: function ({ row }) {
return h('div', { class: 'text-center' }, row.getValue('usage_count'))
@@ -33,7 +33,7 @@ export const columns = [
{
accessorKey: 'created_at',
header: function () {
return h('div', { class: 'text-center' }, 'Created at')
return h('div', { class: 'text-center' }, t('form.field.createdAt'))
},
cell: function ({ row }) {
return h('div', { class: 'text-center' }, format(row.getValue('created_at'), 'PPpp'))
@@ -42,7 +42,7 @@ export const columns = [
{
accessorKey: 'updated_at',
header: function () {
return h('div', { class: 'text-center' }, 'Updated at')
return h('div', { class: 'text-center' }, t('form.field.updatedAt'))
},
cell: function ({ row }) {
return h('div', { class: 'text-center' }, format(row.getValue('updated_at'), 'PPpp'))

View File

@@ -2,27 +2,31 @@
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="w-8 h-8 p-0">
<span class="sr-only">Open menu</span>
<span class="sr-only"></span>
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem @click="editMacro">Edit</DropdownMenuItem>
<DropdownMenuItem @click="() => (isDeleteOpen = true)">Delete</DropdownMenuItem>
<DropdownMenuItem @click="editMacro">{{ $t('globals.buttons.edit') }}</DropdownMenuItem>
<DropdownMenuItem @click="() => (isDeleteOpen = true)">
{{ $t('globals.buttons.delete') }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialog :open="isDeleteOpen" @update:open="isDeleteOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Macro</AlertDialogTitle>
<AlertDialogTitle>{{ $t('admin.macro.delete_confirmation_title') }}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this macro? This action cannot be undone.
{{ $t('admin.macro.delete_confirmation') }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction @click="handleDelete">Delete</AlertDialogAction>
<AlertDialogCancel>{{ $t('globals.buttons.cancel') }}</AlertDialogCancel>
<AlertDialogAction @click="handleDelete">{{
$t('globals.buttons.delete')
}}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -1,17 +1,17 @@
import * as z from 'zod'
import { getTextFromHTML } from '@/utils/strings.js'
const actionSchema = z.array(
const actionSchema = (t) => z.array(
z.object({
type: z.string().min(1, 'Action type required'),
value: z.array(z.string().min(1, 'Action value required')).min(1, 'Action value required'),
type: z.string().min(1, t('admin.macro.actionTypeRequired')),
value: z.array(z.string().min(1, t('admin.macro.actionValueRequired'))),
})
)
export const formSchema = z.object({
name: z.string().min(1, 'Macro name is required'),
export const createFormSchema = (t) => z.object({
name: z.string().min(1, t('form.error.name.required')),
message_content: z.string().optional(),
actions: actionSchema.optional().default([]), // Default to empty array if not provided
actions: actionSchema(t).optional().default([]),
visibility: z.enum(['all', 'team', 'user']),
team_id: z.string().nullable().optional(),
user_id: z.string().nullable().optional(),
@@ -26,7 +26,7 @@ export const formSchema = z.object({
return hasMessageContent || hasValidActions
},
{
message: 'Either message content or actions are required',
message: t('admin.macro.messageOrActionRequired'),
// Field path to highlight
path: ['message_content'],
}
@@ -45,7 +45,7 @@ export const formSchema = z.object({
return true
},
{
message: 'team is required when visibility is "team", and user is required when visibility is "user"',
message: t('admin.macro.teamOrUserRequired'),
// Field path to highlight
path: ['visibility'],
}

View File

@@ -2,10 +2,7 @@
<div class="mb-5">
<CustomBreadcrumb :links="breadcrumbLinks" />
</div>
<MacroForm
:submitForm="onSubmit"
:isLoading="formLoading"
/>
<MacroForm :submitForm="onSubmit" :isLoading="formLoading" />
</template>
<script setup>
@@ -16,14 +13,16 @@ import { handleHTTPError } from '@/utils/http'
import { useRouter } from 'vue-router'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import { useI18n } from 'vue-i18n'
import api from '@/api'
const router = useRouter()
const emit = useEmitter()
const { t } = useI18n()
const formLoading = ref(false)
const breadcrumbLinks = [
{ path: 'macro-list', label: 'Macros' },
{ path: '', label: 'New macro' }
{ path: 'macro-list', label: t('admin.macro') },
{ path: '', label: t('admin.macro.new') }
]
const onSubmit = (values) => {
@@ -35,13 +34,11 @@ const createMacro = async (values) => {
formLoading.value = true
await api.createMacro(values)
emit.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Success',
description: 'Macro created successfully'
description: t('admin.macro.created')
})
router.push({ name: 'macro-list' })
} catch (error) {
emit.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Error',
variant: 'destructive',
description: handleHTTPError(error).message
})

View File

@@ -2,7 +2,7 @@
<div class="mb-5">
<CustomBreadcrumb :links="breadcrumbLinks" />
</div>
<Spinner v-if="isLoading"></Spinner>
<Spinner v-if="isLoading"/>
<MacroForm :initialValues="macro" :submitForm="submitForm" :isLoading="formLoading" v-else />
</template>
@@ -14,16 +14,18 @@ import { useEmitter } from '@/composables/useEmitter'
import { handleHTTPError } from '@/utils/http'
import MacroForm from '@/features/admin/macros/MacroForm.vue'
import { CustomBreadcrumb } from '@/components/ui/breadcrumb'
import { useI18n } from 'vue-i18n'
import { Spinner } from '@/components/ui/spinner'
const macro = ref({})
const { t } = useI18n()
const isLoading = ref(false)
const formLoading = ref(false)
const emitter = useEmitter()
const breadcrumbLinks = [
{ path: 'macro-list', label: 'Macros' },
{ path: '', label: 'Edit macro' }
{ path: 'macro-list', label: t('admin.macro') },
{ path: '', label: t('admin.macro.edit') }
]
const submitForm = (values) => {
@@ -35,12 +37,10 @@ const updateMacro = async (payload) => {
formLoading.value = true
await api.updateMacro(macro.value.id, payload)
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Success',
description: 'Macro updated successfully'
description: t('admin.macro.updated')
})
} catch (error) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Could not update macro',
variant: 'destructive',
description: handleHTTPError(error).message
})
@@ -56,7 +56,6 @@ onMounted(async () => {
macro.value = resp.data.data
} catch (error) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Error',
variant: 'destructive',
description: handleHTTPError(error).message
})

View File

@@ -3,11 +3,11 @@
<div :class="{ 'transition-opacity duration-300 opacity-50': formLoading }">
<div class="flex justify-end mb-5">
<router-link :to="{ name: 'new-macro' }">
<Button> New macro </Button>
<Button> {{ $t('admin.macro.new') }} </Button>
</router-link>
</div>
<div>
<DataTable :columns="columns" :data="macros" />
<DataTable :columns="createColumns(t)" :data="macros" />
</div>
</div>
</template>
@@ -15,14 +15,16 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import DataTable from '@/components/datatable/DataTable.vue'
import { columns } from '../../../features/admin/macros/dataTableColumns.js'
import { createColumns } from '@/features/admin/macros/dataTableColumns.js'
import { Spinner } from '@/components/ui/spinner'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import { handleHTTPError } from '@/utils/http'
import { Button } from '@/components/ui/button'
import { useI18n } from 'vue-i18n'
import api from '@/api'
const { t } = useI18n()
const formLoading = ref(false)
const macros = ref([])
const emit = useEmitter()
@@ -47,7 +49,6 @@ const getMacros = async () => {
macros.value = resp.data.data
} catch (error) {
emit.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Error',
variant: 'destructive',
description: handleHTTPError(error).message
})

View File

@@ -197,10 +197,13 @@
"navigation.edit": "Edit",
"navigation.delete": "Delete",
"form.field.name": "Name",
"form.field.select": "Select",
"form.field.date": "Date",
"form.field.description": "Description",
"form.field.email": "Email",
"form.field.password": "Password",
"form.field.visibility": "Visibility",
"form.field.usage": "Usage",
"form.field.createdAt": "Created At",
"form.field.updatedAt": "Updated At",
"form.field.pick_a_date": "Pick a date",
@@ -291,6 +294,29 @@
"admin.conversation_tags.name.valid": "Tag name should at least 3 characters",
"admin.conversation_tags.delete_confirmation_title": "Are you absolutely sure?",
"admin.conversation_tags.delete_confirmation": "This action cannot be undone. This will permanently delete this tag, and remove it from all conversations.",
"admin.macro.message_content": "Response to be sent when macro is used (optional)",
"admin.macro.message_content.placeholder": "Shift + Enter to add a new line",
"admin.macro.actions": "Actions (optional)",
"admin.macro.visibility": "Visibility",
"admin.macro.visibility.all": "All users",
"admin.macro.visibility.team": "Team",
"admin.macro.visibility.selectTeam": "Select team",
"admin.macro.visibility.selectUser": "Select user",
"admin.macro.visibility.user": "User",
"admin.macro.visibility.selectActionType": "Select action type",
"admin.macro.visibility.selectValue": "Select value",
"admin.macro.visibility.addNewAction": "Add new action",
"admin.macro.messageOrActionRequired": "Either message content or actions are required",
"admin.macro.actionTypeRequired": "Action type is required",
"admin.macro.actionValueRequired": "Action value is required",
"admin.macro.teamOrUserRequired": "team is required when visibility is `team` & a user is required when visibility is `user`",
"admin.macro.delete_confirmation_title": "Are you absolutely sure?",
"admin.macro.delete_confirmation": "This action cannot be undone. This will permanently delete this macro.",
"admin.macro.created": "Macro created successfully",
"admin.macro": "Macros",
"admin.macro.new": "New Macro",
"admin.macro.edit": "Edit Macro",
"admin.macro.updated": "Macro updated successfully",
"globals.buttons.save": "Save",
"globals.buttons.save_changes": "Save changes",
"globals.buttons.cancel": "Cancel",