From abd08b38108108ec1bc24b83d9e98ac2c7f2ca01 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 29 Jan 2026 11:15:16 +0100 Subject: [PATCH 1/4] Refactor: settings locales page refactored to shadcn components --- pnpm-lock.yaml | 24 ++ services/console/package.json | 1 + .../console/src/views/settings/Locales.tsx | 372 ++++++++++++++---- 3 files changed, 327 insertions(+), 70 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bc3f40b..b90e400c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.15 version: 4.1.15(vite@7.1.11(@types/node@16.18.126)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@textea/json-viewer': specifier: 2.16.2 version: 2.16.2(@types/react@19.2.8)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2254,6 +2257,7 @@ packages: '@measured/puck@0.21.0-canary.72e4fcca': resolution: {integrity: sha512-dw0YOtV8ZtZn14aLPysfTXG+hJxqkrpIH3oPJNPleavbMRsWbcN0lICBssU4kw8PxfdNApNpD3Yzcz+04127FA==} + deprecated: 'Puck has moved. Please use @puckeditor/core instead: https://www.npmjs.com/package/@puckeditor/core' peerDependencies: react: ^18.0.0 || ^19.0.0 @@ -3863,12 +3867,23 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.12': resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.12': resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} @@ -5990,6 +6005,7 @@ packages: deep-diff@1.0.2: resolution: {integrity: sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. deep-equal@1.0.1: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} @@ -15958,12 +15974,20 @@ snapshots: tailwindcss: 4.1.15 vite: 7.1.11(@types/node@16.18.126)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1) + '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/virtual-core': 3.13.12 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-core@3.13.12': {} '@testing-library/dom@8.20.1': diff --git a/services/console/package.json b/services/console/package.json index 678b0107..88dd3b7c 100644 --- a/services/console/package.json +++ b/services/console/package.json @@ -29,6 +29,7 @@ "@react-router/serve": "^7.9.2", "@rehookify/datepicker": "^3.2.0", "@tailwindcss/vite": "^4.1.15", + "@tanstack/react-table": "^8.21.3", "@textea/json-viewer": "2.16.2", "@vitejs/plugin-react-swc": "^4.1.0", "axios": "0.27.2", diff --git a/services/console/src/views/settings/Locales.tsx b/services/console/src/views/settings/Locales.tsx index ad30a626..0c119311 100644 --- a/services/console/src/views/settings/Locales.tsx +++ b/services/console/src/views/settings/Locales.tsx @@ -1,102 +1,334 @@ +import * as React from 'react' import { useCallback, useContext, useState } from 'react' +import { useForm } from 'react-hook-form' import api from '../../api' import { ProjectContext } from '../../contexts' -import FormWrapper from '../../ui/form/FormWrapper' -import Modal from '../../ui/Modal' -import { SearchTable, useSearchTableState } from '../../ui/SearchTable' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' import type { Locale, LocaleOption, FieldProps } from '../../types' -import TextInput, { type TextInputProps } from '../../ui/form/TextInput' +import type { TextInputProps } from '../../ui/form/TextInput' import type { FieldPath, FieldValues } from 'react-hook-form' import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + useReactTable, + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, +} from '@tanstack/react-table' +import { MoreHorizontal } from 'lucide-react' +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Form } from '@/components/ui/form' import { PlusIcon } from '../../components/icons' import { languageName } from '../../utils' import { useTranslation } from 'react-i18next' export const LocaleTextField = >(params: TextInputProps

& FieldProps) => { + const { t } = useTranslation() + const { + form, + name, + required, + subtitle, + disabled, + readOnly, + onFocus, + onChange, + type = 'text', + } = params + + const initialLocale = form.getValues(name) as string | undefined + const [language, setLanguage] = useState(initialLocale ? languageName(initialLocale) : undefined) - const [language, setLanguage] = useState(languageName(params.form.getValues()[params.name])) const handlePreviewLanguage = (locale: string) => { + if (!locale) { + setLanguage(undefined) + return + } setLanguage(languageName(locale)) } - return <> - - + + return ( + ( + + + + {t('locale.singular')} + {required && *} + + + {subtitle && {subtitle}} + +

+ { + field.onChange(event) + handlePreviewLanguage(event.target.value) + onChange?.(event.target.value as any) + }} + /> + {language && ( + + {language} + + )} +
+ + + + )} + /> + ) } export default function Locales() { const { t } = useTranslation() const [project] = useContext(ProjectContext) - const state = useSearchTableState(useCallback(async params => await api.locales.search(project.id, params), [project])) const [open, setOpen] = useState(false) + const [locales, setLocales] = useState([]) + const [loading, setLoading] = useState(true) + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const form = useForm>({ + defaultValues: { + key: '' + } + }) + + const loadLocales = useCallback(async () => { + setLoading(true) + try { + const response = await api.locales.search(project.id, { limit: 100 }) + setLocales(response.results) + } finally { + setLoading(false) + } + }, [project.id]) + + React.useEffect(() => { + loadLocales() + }, [loadLocales]) + const handleDeleteLocale = async (locale: Locale) => { - if (!confirm(t('locale_delete_confirmation'))) return + if (!confirm(t('locale.delete_confirmation'))) return await api.locales.delete(project.id, locale.id) - await state.reload() + await loadLocales() } + const columns: ColumnDef[] = [ + { + id: "key", + accessorKey: "key", + header: t('key'), + cell: ({ row }) =>
{row.getValue("key")}
, + }, + { + id: "label", + accessorKey: "label", + header: t('label'), + cell: ({ row }) =>
{row.getValue("label")}
, + }, + { + id: "actions", + accessorKey: "actions", + header: t('action'), + cell: ({ row }) => { + const locale = row.original + return ( + + + + + + + {t('action')} + await handleDeleteLocale(locale)} + > + {t('delete')} + + + + + ) + }, + }, + ] + + const table = useReactTable({ + data: locales, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + return ( <> - ( - + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {loading ? ( + + + {t('loading')} + + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t('')} + + + )} + +
+
+ + + + + {t('create_locale')} + +
+ { + await api.locales.create(project.id, { key, label: languageName(key) ?? key }) + await loadLocales() + setOpen(false) + })}> + ( + + + {t('locale.singular')} + * + + + {t('locale.select.create_new')} + + + + + + + )} + /> + - ), - }, - ]} - itemKey={({ item }) => item.key} - title={t('locale.singular')} - actions={ - <> - - - } - /> - setOpen(false)} - > - > - onSubmit={async ({ key }) => { - await api.locales.create(project.id, { key, label: languageName(key) ?? key }) - await state.reload() - setOpen(false) - }} - > - { - form => ( - <> - - - ) - } - - + + +
+
) } From 77a30d791ee3d8e850af3af284bec7d1c4de9a75 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 29 Jan 2026 11:49:56 +0100 Subject: [PATCH 2/4] Fix: added copilot suggestions --- .../console/src/views/settings/Locales.tsx | 99 ++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/services/console/src/views/settings/Locales.tsx b/services/console/src/views/settings/Locales.tsx index 0c119311..5a5663e9 100644 --- a/services/console/src/views/settings/Locales.tsx +++ b/services/console/src/views/settings/Locales.tsx @@ -48,7 +48,13 @@ import { PlusIcon } from '../../components/icons' import { languageName } from '../../utils' import { useTranslation } from 'react-i18next' -export const LocaleTextField = >(params: TextInputProps

& FieldProps) => { +type LocaleFieldProps> = TextInputProps & + FieldProps & { + form: any + name: P + } + +export const LocaleTextField = >(params: LocaleFieldProps) => { const { t } = useTranslation() const { form, @@ -73,49 +79,50 @@ export const LocaleTextField = >(p setLanguage(languageName(locale)) } - return ( - ( - - - - {t('locale.singular')} - {required && *} - - - {subtitle && {subtitle}} - -

- { - field.onChange(event) - handlePreviewLanguage(event.target.value) - onChange?.(event.target.value as any) - }} - /> - {language && ( - - {language} - - )} -
- +
+ ( + + + + {params.label ?? t('locale.singular')} + {required && *} + + + {subtitle && {subtitle}} + +
+ { + field.onChange(event) + handlePreviewLanguage(event.target.value) + onChange?.(event.target.value) + }} + /> + {language && ( + + {language} + + )} +
+
- )} - /> + )} + /> + ) } @@ -151,7 +158,7 @@ export default function Locales() { }, [loadLocales]) const handleDeleteLocale = async (locale: Locale) => { - if (!confirm(t('locale.delete_confirmation'))) return + if (!confirm(t('locale_delete_confirmation'))) return await api.locales.delete(project.id, locale.id) await loadLocales() } @@ -178,7 +185,8 @@ export default function Locales() { return ( - @@ -283,7 +291,7 @@ export default function Locales() { colSpan={columns.length} className="h-24 text-center" > - {t('')} + No locales found. )} @@ -301,6 +309,7 @@ export default function Locales() { await api.locales.create(project.id, { key, label: languageName(key) ?? key }) await loadLocales() setOpen(false) + form.reset({ key: ''}) })}> * - {t('locale.select.create_new')} + {t('create_locale')} From 5157859b252b8396fcbafb2b06334d2d8f9e7e87 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 29 Jan 2026 12:18:19 +0100 Subject: [PATCH 3/4] fix: Added Copilot suggestions --- .../console/src/views/settings/Locales.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/services/console/src/views/settings/Locales.tsx b/services/console/src/views/settings/Locales.tsx index 5a5663e9..e52e975a 100644 --- a/services/console/src/views/settings/Locales.tsx +++ b/services/console/src/views/settings/Locales.tsx @@ -49,10 +49,7 @@ import { languageName } from '../../utils' import { useTranslation } from 'react-i18next' type LocaleFieldProps> = TextInputProps & - FieldProps & { - form: any - name: P - } + FieldProps export const LocaleTextField = >(params: LocaleFieldProps) => { const { t } = useTranslation() @@ -136,6 +133,7 @@ export default function Locales() { const [columnFilters, setColumnFilters] = React.useState([]) const [columnVisibility, setColumnVisibility] = React.useState({}) const [rowSelection, setRowSelection] = React.useState({}) + const [localePreview, setLocalePreview] = useState() const form = useForm>({ defaultValues: { @@ -185,8 +183,11 @@ export default function Locales() { return ( - @@ -291,7 +292,6 @@ export default function Locales() { colSpan={columns.length} className="h-24 text-center" > - No locales found. )} @@ -322,11 +322,26 @@ export default function Locales() { * - {t('create_locale')} + {t('locale.field_subtitle')} - - - +
+ + { + field.onChange(e) + const value = e.target.value + setLocalePreview(value ? languageName(value) : undefined) + }} + /> + + {localePreview && ( + + {localePreview} + + )} +
)} From 8e284d7b41737d20c1508a46e767d4d6b240bad1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 29 Jan 2026 12:43:36 +0100 Subject: [PATCH 4/4] fix: Dropdown aligned and added better translation for delete locale --- services/console/src/views/settings/Locales.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/console/src/views/settings/Locales.tsx b/services/console/src/views/settings/Locales.tsx index e52e975a..9211f687 100644 --- a/services/console/src/views/settings/Locales.tsx +++ b/services/console/src/views/settings/Locales.tsx @@ -156,7 +156,7 @@ export default function Locales() { }, [loadLocales]) const handleDeleteLocale = async (locale: Locale) => { - if (!confirm(t('locale_delete_confirmation'))) return + if (!confirm(t('locale.delete_confirmation'))) return await api.locales.delete(project.id, locale.id) await loadLocales() } @@ -191,7 +191,7 @@ export default function Locales() { - + {t('action')}