From ade9f02424b92eec088c6ec7d96693c8cb53b013 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 13 Jul 2024 17:31:43 +0200 Subject: [PATCH 01/39] installs shadcn select component --- src/components/ui/select/index.ts | 33 +++++++++++++++ .../ui/select/select-content.svelte | 39 ++++++++++++++++++ src/components/ui/select/select-item.svelte | 40 +++++++++++++++++++ src/components/ui/select/select-label.svelte | 16 ++++++++ .../ui/select/select-separator.svelte | 11 +++++ .../ui/select/select-trigger.svelte | 27 +++++++++++++ 6 files changed, 166 insertions(+) create mode 100644 src/components/ui/select/index.ts create mode 100644 src/components/ui/select/select-content.svelte create mode 100644 src/components/ui/select/select-item.svelte create mode 100644 src/components/ui/select/select-label.svelte create mode 100644 src/components/ui/select/select-separator.svelte create mode 100644 src/components/ui/select/select-trigger.svelte diff --git a/src/components/ui/select/index.ts b/src/components/ui/select/index.ts new file mode 100644 index 00000000..fde69679 --- /dev/null +++ b/src/components/ui/select/index.ts @@ -0,0 +1,33 @@ +import { Select as SelectPrimitive } from 'bits-ui' +import Label from './select-label.svelte' +import Item from './select-item.svelte' +import Content from './select-content.svelte' +import Trigger from './select-trigger.svelte' +import Separator from './select-separator.svelte' + +const Root = SelectPrimitive.Root +const Group = SelectPrimitive.Group +const Input = SelectPrimitive.Input +const Value = SelectPrimitive.Value + +export { + Root, + Group, + Input, + Label, + Item, + Value, + Content, + Trigger, + Separator, + // + Root as Select, + Group as SelectGroup, + Input as SelectInput, + Label as SelectLabel, + Item as SelectItem, + Value as SelectValue, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator +} diff --git a/src/components/ui/select/select-content.svelte b/src/components/ui/select/select-content.svelte new file mode 100644 index 00000000..cd325327 --- /dev/null +++ b/src/components/ui/select/select-content.svelte @@ -0,0 +1,39 @@ + + + +
+ +
+
diff --git a/src/components/ui/select/select-item.svelte b/src/components/ui/select/select-item.svelte new file mode 100644 index 00000000..fbcc1886 --- /dev/null +++ b/src/components/ui/select/select-item.svelte @@ -0,0 +1,40 @@ + + + + + + + + + + {label || value} + + diff --git a/src/components/ui/select/select-label.svelte b/src/components/ui/select/select-label.svelte new file mode 100644 index 00000000..a6093edc --- /dev/null +++ b/src/components/ui/select/select-label.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/components/ui/select/select-separator.svelte b/src/components/ui/select/select-separator.svelte new file mode 100644 index 00000000..cecc030c --- /dev/null +++ b/src/components/ui/select/select-separator.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/components/ui/select/select-trigger.svelte b/src/components/ui/select/select-trigger.svelte new file mode 100644 index 00000000..7b627519 --- /dev/null +++ b/src/components/ui/select/select-trigger.svelte @@ -0,0 +1,27 @@ + + +span]:line-clamp-1 data-[placeholder]:[&>span]:text-muted-foreground', + className + )} + {...$$restProps} + let:builder + on:click + on:keydown +> + +
+ +
+
From 6a40b8ac6997cf17bf67a290f479c065b43bae55 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 13 Jul 2024 18:03:00 +0200 Subject: [PATCH 02/39] adds list of available languages --- .../container/languages/languages.ts | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/components/container/languages/languages.ts diff --git a/src/components/container/languages/languages.ts b/src/components/container/languages/languages.ts new file mode 100644 index 00000000..ca7d6dd1 --- /dev/null +++ b/src/components/container/languages/languages.ts @@ -0,0 +1,186 @@ +type Language = { + label: string + code: string +} + +export const availableLanguages: Language[] = [ + { code: 'af', label: 'Afrikaans' }, + { code: 'sq', label: 'Albanian' }, + { code: 'ar', label: 'Arabic' }, + { code: 'ar-dz', label: 'Arabic (Algeria)' }, + { code: 'ar-bh', label: 'Arabic (Bahrain)' }, + { code: 'ar-eg', label: 'Arabic (Egypt)' }, + { code: 'ar-iq', label: 'Arabic (Iraq)' }, + { code: 'ar-jo', label: 'Arabic (Jordan)' }, + { code: 'ar-kw', label: 'Arabic (Kuwait)' }, + { code: 'ar-lb', label: 'Arabic (Lebanon)' }, + { code: 'ar-ly', label: 'Arabic (Libya)' }, + { code: 'ar-ma', label: 'Arabic (Morocco)' }, + { code: 'ar-om', label: 'Arabic (Oman)' }, + { code: 'ar-qa', label: 'Arabic (Qatar)' }, + { code: 'ar-sa', label: 'Arabic (Saudi Arabia)' }, + { code: 'ar-sy', label: 'Arabic (Syria)' }, + { code: 'ar-tn', label: 'Arabic (Tunisia)' }, + { code: 'ar-ae', label: 'Arabic (U.A.E.)' }, + { code: 'ar-ye', label: 'Arabic (Yemen)' }, + { code: 'an', label: 'Aragonese' }, + { code: 'hy', label: 'Armenian' }, + { code: 'as', label: 'Assamese' }, + { code: 'ast', label: 'Asturian' }, + { code: 'az', label: 'Azerbaijani' }, + { code: 'eu', label: 'Basque' }, + { code: 'be', label: 'Belarusian' }, + { code: 'bn', label: 'Bengali' }, + { code: 'bs', label: 'Bosnian' }, + { code: 'br', label: 'Breton' }, + { code: 'bg', label: 'Bulgarian' }, + { code: 'my', label: 'Burmese' }, + { code: 'ca', label: 'Catalan' }, + { code: 'ch', label: 'Chamorro' }, + { code: 'ce', label: 'Chechen' }, + { code: 'zh', label: 'Chinese' }, + { code: 'zh-hk', label: 'Chinese (Hong Kong)' }, + { code: 'zh-sg', label: 'Chinese (Singapore)' }, + { code: 'zh-tw', label: 'Chinese (Taiwan)' }, + { code: 'cv', label: 'Chuvash' }, + { code: 'co', label: 'Corsican' }, + { code: 'cr', label: 'Cree' }, + { code: 'hr', label: 'Croatian' }, + { code: 'da', label: 'Danish' }, + { code: 'nl', label: 'Dutch' }, + { code: 'nl-be', label: 'Dutch (Belgian)' }, + { code: 'en', label: 'English' }, + { code: 'en-au', label: 'English (Australia)' }, + { code: 'en-bz', label: 'English (Belize)' }, + { code: 'en-ca', label: 'English (Canada)' }, + { code: 'en-ie', label: 'English (Ireland)' }, + { code: 'en-jm', label: 'English (Jamaica)' }, + { code: 'en-nz', label: 'English (New Zealand)' }, + { code: 'en-ph', label: 'English (Philippines)' }, + { code: 'en-za', label: 'English (South Africa)' }, + { code: 'en-tt', label: 'English (Trinidad & Tobago)' }, + { code: 'en-gb', label: 'English (United Kingdom)' }, + { code: 'en-us', label: 'English (United States)' }, + { code: 'en-zw', label: 'English (Zimbabwe)' }, + { code: 'eo', label: 'Esperanto' }, + { code: 'et', label: 'Estonian' }, + { code: 'fo', label: 'Faeroese' }, + { code: 'fa', label: 'Farsi' }, + { code: 'fj', label: 'Fijian' }, + { code: 'fi', label: 'Finnish' }, + { code: 'fr-be', label: 'French (Belgium)' }, + { code: 'fr-ca', label: 'French (Canada)' }, + { code: 'fr-fr', label: 'French (France)' }, + { code: 'fr-lu', label: 'French (Luxembourg)' }, + { code: 'fr-mc', label: 'French (Monaco)' }, + { code: 'fr-ch', label: 'French (Switzerland)' }, + { code: 'fy', label: 'Frisian' }, + { code: 'fur', label: 'Friulian' }, + { code: 'gd', label: 'Gaelic (Scots)' }, + { code: 'gd-ie', label: 'Gaelic (Irish)' }, + { code: 'gl', label: 'Galacian' }, + { code: 'ka', label: 'Georgian' }, + { code: 'de-at', label: 'German (Austria)' }, + { code: 'de-de', label: 'German (Germany)' }, + { code: 'de-li', label: 'German (Liechtenstein)' }, + { code: 'de-lu', label: 'German (Luxembourg)' }, + { code: 'de-ch', label: 'German (Switzerland)' }, + { code: 'gu', label: 'Gujurati' }, + { code: 'ht', label: 'Haitian' }, + { code: 'he', label: 'Hebrew' }, + { code: 'hi', label: 'Hindi' }, + { code: 'is', label: 'Icelandic' }, + { code: 'id', label: 'Indonesian' }, + { code: 'iu', label: 'Inuktitut' }, + { code: 'ga', label: 'Irish' }, + { code: 'it-ch', label: 'Italian (Switzerland)' }, + { code: 'kn', label: 'Kannada' }, + { code: 'ks', label: 'Kashmiri' }, + { code: 'kk', label: 'Kazakh' }, + { code: 'km', label: 'Khmer' }, + { code: 'ky', label: 'Kirghiz' }, + { code: 'tlh', label: 'Klingon' }, + { code: 'ko-kp', label: 'Korean (North Korea)' }, + { code: 'ko-kr', label: 'Korean (South Korea)' }, + { code: 'la', label: 'Latin' }, + { code: 'lv', label: 'Latvian' }, + { code: 'lt', label: 'Lithuanian' }, + { code: 'lb', label: 'Luxembourgish' }, + { code: 'mk', label: 'FYRO Macedonian' }, + { code: 'ms', label: 'Malay' }, + { code: 'ml', label: 'Malayalam' }, + { code: 'mt', label: 'Maltese' }, + { code: 'mi', label: 'Maori' }, + { code: 'mr', label: 'Marathi' }, + { code: 'mo', label: 'Moldavian' }, + { code: 'nv', label: 'Navajo' }, + { code: 'ng', label: 'Ndonga' }, + { code: 'ne', label: 'Nepali' }, + { code: 'no', label: 'Norwegian' }, + { code: 'nb', label: 'Norwegian (Bokmal)' }, + { code: 'nn', label: 'Norwegian (Nynorsk)' }, + { code: 'oc', label: 'Occitan' }, + { code: 'or', label: 'Oriya' }, + { code: 'om', label: 'Oromo' }, + { code: 'fa-ir', label: 'Persian/Iran' }, + { code: 'pa', label: 'Punjabi' }, + { code: 'pa-in', label: 'Punjabi (India)' }, + { code: 'pa-pk', label: 'Punjabi (Pakistan)' }, + { code: 'qu', label: 'Quechua' }, + { code: 'rm', label: 'Rhaeto-Romanic' }, + { code: 'ro-mo', label: 'Romanian (Moldavia)' }, + { code: 'ru-mo', label: 'Russian (Moldavia)' }, + { code: 'sz', label: 'Sami (Lappish)' }, + { code: 'sg', label: 'Sango' }, + { code: 'sa', label: 'Sanskrit' }, + { code: 'sc', label: 'Sardinian' }, + { code: 'sd', label: 'Sindhi' }, + { code: 'si', label: 'Singhalese' }, + { code: 'sr', label: 'Serbian' }, + { code: 'sk', label: 'Slovak' }, + { code: 'sl', label: 'Slovenian' }, + { code: 'so', label: 'Somani' }, + { code: 'sb', label: 'Sorbian' }, + { code: 'es-ar', label: 'Spanish (Argentina)' }, + { code: 'es-bo', label: 'Spanish (Bolivia)' }, + { code: 'es-cl', label: 'Spanish (Chile)' }, + { code: 'es-co', label: 'Spanish (Colombia)' }, + { code: 'es-cr', label: 'Spanish (Costa Rica)' }, + { code: 'es-do', label: 'Spanish (Dominican Republic)' }, + { code: 'es-ec', label: 'Spanish (Ecuador)' }, + { code: 'es-sv', label: 'Spanish (El Salvador)' }, + { code: 'es-gt', label: 'Spanish (Guatemala)' }, + { code: 'es-hn', label: 'Spanish (Honduras)' }, + { code: 'es-mx', label: 'Spanish (Mexico)' }, + { code: 'es-ni', label: 'Spanish (Nicaragua)' }, + { code: 'es-pa', label: 'Spanish (Panama)' }, + { code: 'es-py', label: 'Spanish (Paraguay)' }, + { code: 'es-pe', label: 'Spanish (Peru)' }, + { code: 'es-pr', label: 'Spanish (Puerto Rico)' }, + { code: 'es-uy', label: 'Spanish (Uruguay)' }, + { code: 'es-ve', label: 'Spanish (Venezuela)' }, + { code: 'sx', label: 'Sutu' }, + { code: 'sw', label: 'Swahili' }, + { code: 'sv', label: 'Swedish' }, + { code: 'sv-fi', label: 'Swedish (Finland)' }, + { code: 'sv-sv', label: 'Swedish (Sweden)' }, + { code: 'ta', label: 'Tamil' }, + { code: 'tt', label: 'Tatar' }, + { code: 'te', label: 'Teluga' }, + { code: 'th', label: 'Thai' }, + { code: 'tig', label: 'Tigre' }, + { code: 'ts', label: 'Tsonga' }, + { code: 'tn', label: 'Tswana' }, + { code: 'tk', label: 'Turkmen' }, + { code: 'uk', label: 'Ukrainian' }, + { code: 'hsb', label: 'Upper Sorbian' }, + { code: 'ur', label: 'Urdu' }, + { code: 've', label: 'Venda' }, + { code: 'vi', label: 'Vietnamese' }, + { code: 'vo', label: 'Volapuk' }, + { code: 'wa', label: 'Walloon' }, + { code: 'cy', label: 'Welsh' }, + { code: 'xh', label: 'Xhosa' }, + { code: 'ji', label: 'Yiddish' }, + { code: 'zu', label: 'Zulu' } +] From a942475442ac769b053ebcbb2a5ff0565171d754 Mon Sep 17 00:00:00 2001 From: mledl Date: Sun, 14 Jul 2024 14:12:19 +0200 Subject: [PATCH 03/39] fixes linking user project fk in base migration --- services/src/kysely/migrations/2024-04-28T09_init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index 625f4df4..28e727a5 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -50,7 +50,7 @@ export async function up(db: Kysely): Promise { await createTableMigration(tx, 'projects_users', false, false) .addColumn('project_id', 'integer', (col) => col.references('projects.id').notNull()) - .addColumn('user_id', 'integer', (col) => col.references('user.id').notNull()) + .addColumn('user_id', 'integer', (col) => col.references('users.id').notNull()) .addColumn('permission', 'text', (col) => col.check(sql`permission in ('READONLY', 'WRITE', 'ADMIN')`) ) From 492f8e9e893d09c74329aa15604854603971447f Mon Sep 17 00:00:00 2001 From: mledl Date: Sun, 14 Jul 2024 14:12:50 +0200 Subject: [PATCH 04/39] adds language dropdown to project creation flow for selecting base language --- .../container/auth/signup-form.svelte | 2 +- .../container/languages/LanguageSelect.svelte | 34 ++ .../container/languages/languages.ts | 367 +++++++++--------- .../projects/create-project-schema.ts | 7 +- .../container/projects/create-project.svelte | 7 +- 5 files changed, 226 insertions(+), 191 deletions(-) create mode 100644 src/components/container/languages/LanguageSelect.svelte diff --git a/src/components/container/auth/signup-form.svelte b/src/components/container/auth/signup-form.svelte index 900b6417..8503b47a 100644 --- a/src/components/container/auth/signup-form.svelte +++ b/src/components/container/auth/signup-form.svelte @@ -33,7 +33,7 @@

Register

- Enter you information to create an user account for this Tiny-TMS instance + Enter your information to create an user account for this Tiny-TMS instance

or sign in to an existing account diff --git a/src/components/container/languages/LanguageSelect.svelte b/src/components/container/languages/LanguageSelect.svelte new file mode 100644 index 00000000..35e84b14 --- /dev/null +++ b/src/components/container/languages/LanguageSelect.svelte @@ -0,0 +1,34 @@ + + + { + s && (value = s.value) + }} +> + + + + + + {#each items as language} + + {/each} + + diff --git a/src/components/container/languages/languages.ts b/src/components/container/languages/languages.ts index ca7d6dd1..4123c0fe 100644 --- a/src/components/container/languages/languages.ts +++ b/src/components/container/languages/languages.ts @@ -1,186 +1,183 @@ -type Language = { - label: string - code: string -} +export const availableLanguages = { + af: 'Afrikaans', + sq: 'Albanian', + ar: 'Arabic', + 'ar-dz': 'Arabic (Algeria)', + 'ar-bh': 'Arabic (Bahrain)', + 'ar-eg': 'Arabic (Egypt)', + 'ar-iq': 'Arabic (Iraq)', + 'ar-jo': 'Arabic (Jordan)', + 'ar-kw': 'Arabic (Kuwait)', + 'ar-lb': 'Arabic (Lebanon)', + 'ar-ly': 'Arabic (Libya)', + 'ar-ma': 'Arabic (Morocco)', + 'ar-om': 'Arabic (Oman)', + 'ar-qa': 'Arabic (Qatar)', + 'ar-sa': 'Arabic (Saudi Arabia)', + 'ar-sy': 'Arabic (Syria)', + 'ar-tn': 'Arabic (Tunisia)', + 'ar-ae': 'Arabic (U.A.E.)', + 'ar-ye': 'Arabic (Yemen)', + an: 'Aragonese', + hy: 'Armenian', + as: 'Assamese', + ast: 'Asturian', + az: 'Azerbaijani', + eu: 'Basque', + be: 'Belarusian', + bn: 'Bengali', + bs: 'Bosnian', + br: 'Breton', + bg: 'Bulgarian', + my: 'Burmese', + ca: 'Catalan', + ch: 'Chamorro', + ce: 'Chechen', + zh: 'Chinese', + 'zh-hk': 'Chinese (Hong Kong)', + 'zh-sg': 'Chinese (Singapore)', + 'zh-tw': 'Chinese (Taiwan)', + cv: 'Chuvash', + co: 'Corsican', + cr: 'Cree', + hr: 'Croatian', + da: 'Danish', + nl: 'Dutch', + 'nl-be': 'Dutch (Belgian)', + en: 'English', + 'en-au': 'English (Australia)', + 'en-bz': 'English (Belize)', + 'en-ca': 'English (Canada)', + 'en-ie': 'English (Ireland)', + 'en-jm': 'English (Jamaica)', + 'en-nz': 'English (New Zealand)', + 'en-ph': 'English (Philippines)', + 'en-za': 'English (South Africa)', + 'en-tt': 'English (Trinidad & Tobago)', + 'en-gb': 'English (United Kingdom)', + 'en-us': 'English (United States)', + 'en-zw': 'English (Zimbabwe)', + eo: 'Esperanto', + et: 'Estonian', + fo: 'Faeroese', + fa: 'Farsi', + fj: 'Fijian', + fi: 'Finnish', + 'fr-be': 'French (Belgium)', + 'fr-ca': 'French (Canada)', + 'fr-fr': 'French (France)', + 'fr-lu': 'French (Luxembourg)', + 'fr-mc': 'French (Monaco)', + 'fr-ch': 'French (Switzerland)', + fy: 'Frisian', + fur: 'Friulian', + gd: 'Gaelic (Scots)', + 'gd-ie': 'Gaelic (Irish)', + gl: 'Galacian', + ka: 'Georgian', + 'de-at': 'German (Austria)', + 'de-de': 'German (Germany)', + 'de-li': 'German (Liechtenstein)', + 'de-lu': 'German (Luxembourg)', + 'de-ch': 'German (Switzerland)', + gu: 'Gujurati', + ht: 'Haitian', + he: 'Hebrew', + hi: 'Hindi', + is: 'Icelandic', + id: 'Indonesian', + iu: 'Inuktitut', + ga: 'Irish', + 'it-ch': 'Italian (Switzerland)', + kn: 'Kannada', + ks: 'Kashmiri', + kk: 'Kazakh', + km: 'Khmer', + ky: 'Kirghiz', + tlh: 'Klingon', + 'ko-kp': 'Korean (North Korea)', + 'ko-kr': 'Korean (South Korea)', + la: 'Latin', + lv: 'Latvian', + lt: 'Lithuanian', + lb: 'Luxembourgish', + mk: 'FYRO Macedonian', + ms: 'Malay', + ml: 'Malayalam', + mt: 'Maltese', + mi: 'Maori', + mr: 'Marathi', + mo: 'Moldavian', + nv: 'Navajo', + ng: 'Ndonga', + ne: 'Nepali', + no: 'Norwegian', + nb: 'Norwegian (Bokmal)', + nn: 'Norwegian (Nynorsk)', + oc: 'Occitan', + or: 'Oriya', + om: 'Oromo', + 'fa-ir': 'Persian/Iran', + pa: 'Punjabi', + 'pa-in': 'Punjabi (India)', + 'pa-pk': 'Punjabi (Pakistan)', + qu: 'Quechua', + rm: 'Rhaeto-Romanic', + 'ro-mo': 'Romanian (Moldavia)', + 'ru-mo': 'Russian (Moldavia)', + sz: 'Sami (Lappish)', + sg: 'Sango', + sa: 'Sanskrit', + sc: 'Sardinian', + sd: 'Sindhi', + si: 'Singhalese', + sr: 'Serbian', + sk: 'Slovak', + sl: 'Slovenian', + so: 'Somani', + sb: 'Sorbian', + 'es-ar': 'Spanish (Argentina)', + 'es-bo': 'Spanish (Bolivia)', + 'es-cl': 'Spanish (Chile)', + 'es-co': 'Spanish (Colombia)', + 'es-cr': 'Spanish (Costa Rica)', + 'es-do': 'Spanish (Dominican Republic)', + 'es-ec': 'Spanish (Ecuador)', + 'es-sv': 'Spanish (El Salvador)', + 'es-gt': 'Spanish (Guatemala)', + 'es-hn': 'Spanish (Honduras)', + 'es-mx': 'Spanish (Mexico)', + 'es-ni': 'Spanish (Nicaragua)', + 'es-pa': 'Spanish (Panama)', + 'es-py': 'Spanish (Paraguay)', + 'es-pe': 'Spanish (Peru)', + 'es-pr': 'Spanish (Puerto Rico)', + 'es-uy': 'Spanish (Uruguay)', + 'es-ve': 'Spanish (Venezuela)', + sx: 'Sutu', + sw: 'Swahili', + sv: 'Swedish', + 'sv-fi': 'Swedish (Finland)', + 'sv-sv': 'Swedish (Sweden)', + ta: 'Tamil', + tt: 'Tatar', + te: 'Teluga', + th: 'Thai', + tig: 'Tigre', + ts: 'Tsonga', + tn: 'Tswana', + tk: 'Turkmen', + uk: 'Ukrainian', + hsb: 'Upper Sorbian', + ur: 'Urdu', + ve: 'Venda', + vi: 'Vietnamese', + vo: 'Volapuk', + wa: 'Walloon', + cy: 'Welsh', + xh: 'Xhosa', + ji: 'Yiddish', + zu: 'Zulu' +} as const -export const availableLanguages: Language[] = [ - { code: 'af', label: 'Afrikaans' }, - { code: 'sq', label: 'Albanian' }, - { code: 'ar', label: 'Arabic' }, - { code: 'ar-dz', label: 'Arabic (Algeria)' }, - { code: 'ar-bh', label: 'Arabic (Bahrain)' }, - { code: 'ar-eg', label: 'Arabic (Egypt)' }, - { code: 'ar-iq', label: 'Arabic (Iraq)' }, - { code: 'ar-jo', label: 'Arabic (Jordan)' }, - { code: 'ar-kw', label: 'Arabic (Kuwait)' }, - { code: 'ar-lb', label: 'Arabic (Lebanon)' }, - { code: 'ar-ly', label: 'Arabic (Libya)' }, - { code: 'ar-ma', label: 'Arabic (Morocco)' }, - { code: 'ar-om', label: 'Arabic (Oman)' }, - { code: 'ar-qa', label: 'Arabic (Qatar)' }, - { code: 'ar-sa', label: 'Arabic (Saudi Arabia)' }, - { code: 'ar-sy', label: 'Arabic (Syria)' }, - { code: 'ar-tn', label: 'Arabic (Tunisia)' }, - { code: 'ar-ae', label: 'Arabic (U.A.E.)' }, - { code: 'ar-ye', label: 'Arabic (Yemen)' }, - { code: 'an', label: 'Aragonese' }, - { code: 'hy', label: 'Armenian' }, - { code: 'as', label: 'Assamese' }, - { code: 'ast', label: 'Asturian' }, - { code: 'az', label: 'Azerbaijani' }, - { code: 'eu', label: 'Basque' }, - { code: 'be', label: 'Belarusian' }, - { code: 'bn', label: 'Bengali' }, - { code: 'bs', label: 'Bosnian' }, - { code: 'br', label: 'Breton' }, - { code: 'bg', label: 'Bulgarian' }, - { code: 'my', label: 'Burmese' }, - { code: 'ca', label: 'Catalan' }, - { code: 'ch', label: 'Chamorro' }, - { code: 'ce', label: 'Chechen' }, - { code: 'zh', label: 'Chinese' }, - { code: 'zh-hk', label: 'Chinese (Hong Kong)' }, - { code: 'zh-sg', label: 'Chinese (Singapore)' }, - { code: 'zh-tw', label: 'Chinese (Taiwan)' }, - { code: 'cv', label: 'Chuvash' }, - { code: 'co', label: 'Corsican' }, - { code: 'cr', label: 'Cree' }, - { code: 'hr', label: 'Croatian' }, - { code: 'da', label: 'Danish' }, - { code: 'nl', label: 'Dutch' }, - { code: 'nl-be', label: 'Dutch (Belgian)' }, - { code: 'en', label: 'English' }, - { code: 'en-au', label: 'English (Australia)' }, - { code: 'en-bz', label: 'English (Belize)' }, - { code: 'en-ca', label: 'English (Canada)' }, - { code: 'en-ie', label: 'English (Ireland)' }, - { code: 'en-jm', label: 'English (Jamaica)' }, - { code: 'en-nz', label: 'English (New Zealand)' }, - { code: 'en-ph', label: 'English (Philippines)' }, - { code: 'en-za', label: 'English (South Africa)' }, - { code: 'en-tt', label: 'English (Trinidad & Tobago)' }, - { code: 'en-gb', label: 'English (United Kingdom)' }, - { code: 'en-us', label: 'English (United States)' }, - { code: 'en-zw', label: 'English (Zimbabwe)' }, - { code: 'eo', label: 'Esperanto' }, - { code: 'et', label: 'Estonian' }, - { code: 'fo', label: 'Faeroese' }, - { code: 'fa', label: 'Farsi' }, - { code: 'fj', label: 'Fijian' }, - { code: 'fi', label: 'Finnish' }, - { code: 'fr-be', label: 'French (Belgium)' }, - { code: 'fr-ca', label: 'French (Canada)' }, - { code: 'fr-fr', label: 'French (France)' }, - { code: 'fr-lu', label: 'French (Luxembourg)' }, - { code: 'fr-mc', label: 'French (Monaco)' }, - { code: 'fr-ch', label: 'French (Switzerland)' }, - { code: 'fy', label: 'Frisian' }, - { code: 'fur', label: 'Friulian' }, - { code: 'gd', label: 'Gaelic (Scots)' }, - { code: 'gd-ie', label: 'Gaelic (Irish)' }, - { code: 'gl', label: 'Galacian' }, - { code: 'ka', label: 'Georgian' }, - { code: 'de-at', label: 'German (Austria)' }, - { code: 'de-de', label: 'German (Germany)' }, - { code: 'de-li', label: 'German (Liechtenstein)' }, - { code: 'de-lu', label: 'German (Luxembourg)' }, - { code: 'de-ch', label: 'German (Switzerland)' }, - { code: 'gu', label: 'Gujurati' }, - { code: 'ht', label: 'Haitian' }, - { code: 'he', label: 'Hebrew' }, - { code: 'hi', label: 'Hindi' }, - { code: 'is', label: 'Icelandic' }, - { code: 'id', label: 'Indonesian' }, - { code: 'iu', label: 'Inuktitut' }, - { code: 'ga', label: 'Irish' }, - { code: 'it-ch', label: 'Italian (Switzerland)' }, - { code: 'kn', label: 'Kannada' }, - { code: 'ks', label: 'Kashmiri' }, - { code: 'kk', label: 'Kazakh' }, - { code: 'km', label: 'Khmer' }, - { code: 'ky', label: 'Kirghiz' }, - { code: 'tlh', label: 'Klingon' }, - { code: 'ko-kp', label: 'Korean (North Korea)' }, - { code: 'ko-kr', label: 'Korean (South Korea)' }, - { code: 'la', label: 'Latin' }, - { code: 'lv', label: 'Latvian' }, - { code: 'lt', label: 'Lithuanian' }, - { code: 'lb', label: 'Luxembourgish' }, - { code: 'mk', label: 'FYRO Macedonian' }, - { code: 'ms', label: 'Malay' }, - { code: 'ml', label: 'Malayalam' }, - { code: 'mt', label: 'Maltese' }, - { code: 'mi', label: 'Maori' }, - { code: 'mr', label: 'Marathi' }, - { code: 'mo', label: 'Moldavian' }, - { code: 'nv', label: 'Navajo' }, - { code: 'ng', label: 'Ndonga' }, - { code: 'ne', label: 'Nepali' }, - { code: 'no', label: 'Norwegian' }, - { code: 'nb', label: 'Norwegian (Bokmal)' }, - { code: 'nn', label: 'Norwegian (Nynorsk)' }, - { code: 'oc', label: 'Occitan' }, - { code: 'or', label: 'Oriya' }, - { code: 'om', label: 'Oromo' }, - { code: 'fa-ir', label: 'Persian/Iran' }, - { code: 'pa', label: 'Punjabi' }, - { code: 'pa-in', label: 'Punjabi (India)' }, - { code: 'pa-pk', label: 'Punjabi (Pakistan)' }, - { code: 'qu', label: 'Quechua' }, - { code: 'rm', label: 'Rhaeto-Romanic' }, - { code: 'ro-mo', label: 'Romanian (Moldavia)' }, - { code: 'ru-mo', label: 'Russian (Moldavia)' }, - { code: 'sz', label: 'Sami (Lappish)' }, - { code: 'sg', label: 'Sango' }, - { code: 'sa', label: 'Sanskrit' }, - { code: 'sc', label: 'Sardinian' }, - { code: 'sd', label: 'Sindhi' }, - { code: 'si', label: 'Singhalese' }, - { code: 'sr', label: 'Serbian' }, - { code: 'sk', label: 'Slovak' }, - { code: 'sl', label: 'Slovenian' }, - { code: 'so', label: 'Somani' }, - { code: 'sb', label: 'Sorbian' }, - { code: 'es-ar', label: 'Spanish (Argentina)' }, - { code: 'es-bo', label: 'Spanish (Bolivia)' }, - { code: 'es-cl', label: 'Spanish (Chile)' }, - { code: 'es-co', label: 'Spanish (Colombia)' }, - { code: 'es-cr', label: 'Spanish (Costa Rica)' }, - { code: 'es-do', label: 'Spanish (Dominican Republic)' }, - { code: 'es-ec', label: 'Spanish (Ecuador)' }, - { code: 'es-sv', label: 'Spanish (El Salvador)' }, - { code: 'es-gt', label: 'Spanish (Guatemala)' }, - { code: 'es-hn', label: 'Spanish (Honduras)' }, - { code: 'es-mx', label: 'Spanish (Mexico)' }, - { code: 'es-ni', label: 'Spanish (Nicaragua)' }, - { code: 'es-pa', label: 'Spanish (Panama)' }, - { code: 'es-py', label: 'Spanish (Paraguay)' }, - { code: 'es-pe', label: 'Spanish (Peru)' }, - { code: 'es-pr', label: 'Spanish (Puerto Rico)' }, - { code: 'es-uy', label: 'Spanish (Uruguay)' }, - { code: 'es-ve', label: 'Spanish (Venezuela)' }, - { code: 'sx', label: 'Sutu' }, - { code: 'sw', label: 'Swahili' }, - { code: 'sv', label: 'Swedish' }, - { code: 'sv-fi', label: 'Swedish (Finland)' }, - { code: 'sv-sv', label: 'Swedish (Sweden)' }, - { code: 'ta', label: 'Tamil' }, - { code: 'tt', label: 'Tatar' }, - { code: 'te', label: 'Teluga' }, - { code: 'th', label: 'Thai' }, - { code: 'tig', label: 'Tigre' }, - { code: 'ts', label: 'Tsonga' }, - { code: 'tn', label: 'Tswana' }, - { code: 'tk', label: 'Turkmen' }, - { code: 'uk', label: 'Ukrainian' }, - { code: 'hsb', label: 'Upper Sorbian' }, - { code: 'ur', label: 'Urdu' }, - { code: 've', label: 'Venda' }, - { code: 'vi', label: 'Vietnamese' }, - { code: 'vo', label: 'Volapuk' }, - { code: 'wa', label: 'Walloon' }, - { code: 'cy', label: 'Welsh' }, - { code: 'xh', label: 'Xhosa' }, - { code: 'ji', label: 'Yiddish' }, - { code: 'zu', label: 'Zulu' } -] +export type LanguageCode = keyof typeof availableLanguages diff --git a/src/components/container/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts index b47cf002..1c436f6f 100644 --- a/src/components/container/projects/create-project-schema.ts +++ b/src/components/container/projects/create-project-schema.ts @@ -1,10 +1,13 @@ import { z } from 'zod' +import { type LanguageCode, availableLanguages } from '../languages/languages' export const createProjectSchema = z.object({ name: z .string({ required_error: 'Project name is required' }) .min(1, 'Project name must have at least one character'), base_language: z - .string({ required_error: 'Base language is required' }) - .min(1, 'Base language must have at least one character') + .enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]], { + required_error: 'Base language is required' + }) + .default('en') }) diff --git a/src/components/container/projects/create-project.svelte b/src/components/container/projects/create-project.svelte index 6919e43f..cdd9c959 100644 --- a/src/components/container/projects/create-project.svelte +++ b/src/components/container/projects/create-project.svelte @@ -8,6 +8,7 @@ import * as Form from '$components/ui/form' import { page } from '$app/stores' import { toast } from 'svelte-sonner' + import LanguageSelect from '../languages/LanguageSelect.svelte' export let data: SuperValidated> @@ -61,10 +62,10 @@ Base Language - From a93a4f7b6e793ce804c885b44e19f25052c0193d Mon Sep 17 00:00:00 2001 From: mledl Date: Sun, 14 Jul 2024 16:37:08 +0200 Subject: [PATCH 05/39] fixes typings in select shadcn ui components --- src/components/ui/select/select-content.svelte | 6 +++--- src/components/ui/select/select-item.svelte | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ui/select/select-content.svelte b/src/components/ui/select/select-content.svelte index cd325327..be0a4e9b 100644 --- a/src/components/ui/select/select-content.svelte +++ b/src/components/ui/select/select-content.svelte @@ -6,10 +6,10 @@ type $$Props = SelectPrimitive.ContentProps type $$Events = SelectPrimitive.ContentEvents - export let sideOffset: $$Props['sideOffset'] = 4 - export let inTransition: $$Props['inTransition'] = flyAndScale + export let sideOffset = 4 + export let inTransition = flyAndScale export let inTransitionConfig: $$Props['inTransitionConfig'] = undefined - export let outTransition: $$Props['outTransition'] = scale + export let outTransition = scale export let outTransitionConfig: $$Props['outTransitionConfig'] = { start: 0.95, opacity: 0, diff --git a/src/components/ui/select/select-item.svelte b/src/components/ui/select/select-item.svelte index fbcc1886..d39776cb 100644 --- a/src/components/ui/select/select-item.svelte +++ b/src/components/ui/select/select-item.svelte @@ -15,8 +15,8 @@ Date: Sun, 14 Jul 2024 16:55:07 +0200 Subject: [PATCH 06/39] adds shadcn base table components --- src/components/ui/table/index.ts | 28 ++++++++++++++++++++ src/components/ui/table/table-body.svelte | 13 +++++++++ src/components/ui/table/table-caption.svelte | 13 +++++++++ src/components/ui/table/table-cell.svelte | 18 +++++++++++++ src/components/ui/table/table-footer.svelte | 13 +++++++++ src/components/ui/table/table-head.svelte | 19 +++++++++++++ src/components/ui/table/table-header.svelte | 14 ++++++++++ src/components/ui/table/table-row.svelte | 23 ++++++++++++++++ src/components/ui/table/table.svelte | 15 +++++++++++ 9 files changed, 156 insertions(+) create mode 100644 src/components/ui/table/index.ts create mode 100644 src/components/ui/table/table-body.svelte create mode 100644 src/components/ui/table/table-caption.svelte create mode 100644 src/components/ui/table/table-cell.svelte create mode 100644 src/components/ui/table/table-footer.svelte create mode 100644 src/components/ui/table/table-head.svelte create mode 100644 src/components/ui/table/table-header.svelte create mode 100644 src/components/ui/table/table-row.svelte create mode 100644 src/components/ui/table/table.svelte diff --git a/src/components/ui/table/index.ts b/src/components/ui/table/index.ts new file mode 100644 index 00000000..66026d30 --- /dev/null +++ b/src/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from './table.svelte' +import Body from './table-body.svelte' +import Caption from './table-caption.svelte' +import Cell from './table-cell.svelte' +import Footer from './table-footer.svelte' +import Head from './table-head.svelte' +import Header from './table-header.svelte' +import Row from './table-row.svelte' + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow +} diff --git a/src/components/ui/table/table-body.svelte b/src/components/ui/table/table-body.svelte new file mode 100644 index 00000000..4a2d89f0 --- /dev/null +++ b/src/components/ui/table/table-body.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/components/ui/table/table-caption.svelte b/src/components/ui/table/table-caption.svelte new file mode 100644 index 00000000..e19c0124 --- /dev/null +++ b/src/components/ui/table/table-caption.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/components/ui/table/table-cell.svelte b/src/components/ui/table/table-cell.svelte new file mode 100644 index 00000000..96589181 --- /dev/null +++ b/src/components/ui/table/table-cell.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/components/ui/table/table-footer.svelte b/src/components/ui/table/table-footer.svelte new file mode 100644 index 00000000..cb69ea12 --- /dev/null +++ b/src/components/ui/table/table-footer.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/components/ui/table/table-head.svelte b/src/components/ui/table/table-head.svelte new file mode 100644 index 00000000..2a3ab5a3 --- /dev/null +++ b/src/components/ui/table/table-head.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/components/ui/table/table-header.svelte b/src/components/ui/table/table-header.svelte new file mode 100644 index 00000000..c676e53f --- /dev/null +++ b/src/components/ui/table/table-header.svelte @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/components/ui/table/table-row.svelte b/src/components/ui/table/table-row.svelte new file mode 100644 index 00000000..dcdbd68f --- /dev/null +++ b/src/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/components/ui/table/table.svelte b/src/components/ui/table/table.svelte new file mode 100644 index 00000000..06b9d36d --- /dev/null +++ b/src/components/ui/table/table.svelte @@ -0,0 +1,15 @@ + + +

+ + +
+
From a6eaff25bce63fe20f9c427588ffab9540244bb7 Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 16:49:28 +0200 Subject: [PATCH 07/39] moves language select folder --- .../container/{languages => language}/LanguageSelect.svelte | 0 src/components/container/{languages => language}/languages.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/container/{languages => language}/LanguageSelect.svelte (100%) rename src/components/container/{languages => language}/languages.ts (100%) diff --git a/src/components/container/languages/LanguageSelect.svelte b/src/components/container/language/LanguageSelect.svelte similarity index 100% rename from src/components/container/languages/LanguageSelect.svelte rename to src/components/container/language/LanguageSelect.svelte diff --git a/src/components/container/languages/languages.ts b/src/components/container/language/languages.ts similarity index 100% rename from src/components/container/languages/languages.ts rename to src/components/container/language/languages.ts From ad1c9b025cc1459c60f2b0c1218ab2e7ecdffc29 Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 16:50:07 +0200 Subject: [PATCH 08/39] adds label column to languages table --- services/src/kysely/migrations/2024-04-28T09_init.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index 28e727a5..edc8049f 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -24,6 +24,7 @@ export async function up(db: Kysely): Promise { await createTableMigration(tx, 'languages') .addColumn('code', 'text', (col) => col.notNull()) + .addColumn('label', 'text', (col) => col.notNull()) .addColumn('fallback_language', 'integer', (col) => col.references('languages.id')) .addColumn('project_id', 'integer', (col) => col.references('projects.id').onDelete('cascade').notNull() From 5e82009de84971f9fecc2958535d46cf4ae0ca95 Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 16:57:01 +0200 Subject: [PATCH 09/39] refactor: Update import path for language-related modules --- src/components/container/projects/create-project-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/container/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts index 1c436f6f..192612f8 100644 --- a/src/components/container/projects/create-project-schema.ts +++ b/src/components/container/projects/create-project-schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { type LanguageCode, availableLanguages } from '../languages/languages' +import { type LanguageCode, availableLanguages } from '../language/languages' export const createProjectSchema = z.object({ name: z From ee0d022dc3a9675788f5f9a5761683ef18ceb0d8 Mon Sep 17 00:00:00 2001 From: "mledl (aider)" Date: Wed, 21 Aug 2024 16:57:02 +0200 Subject: [PATCH 10/39] feat: add base_language_label field to createProjectSchema --- src/components/container/projects/create-project-schema.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/container/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts index 192612f8..8b3a6978 100644 --- a/src/components/container/projects/create-project-schema.ts +++ b/src/components/container/projects/create-project-schema.ts @@ -9,5 +9,7 @@ export const createProjectSchema = z.object({ .enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]], { required_error: 'Base language is required' }) - .default('en') + .default('en'), + base_language_label: z + .string({ required_error: 'Base language label is required' }) }) From 9465787c7b05883fee5eb5d731482390d535fc28 Mon Sep 17 00:00:00 2001 From: "mledl (aider)" Date: Wed, 21 Aug 2024 16:58:16 +0200 Subject: [PATCH 11/39] feat: update base_language_label schema with enum and default value --- src/components/container/projects/create-project-schema.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/container/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts index 8b3a6978..6b3e896d 100644 --- a/src/components/container/projects/create-project-schema.ts +++ b/src/components/container/projects/create-project-schema.ts @@ -11,5 +11,8 @@ export const createProjectSchema = z.object({ }) .default('en'), base_language_label: z - .string({ required_error: 'Base language label is required' }) + .enum(Object.values(availableLanguages) as [string, ...string[]], { + required_error: 'Base language label is required' + }) + .default('English') }) From 4f6cb689d96dcb8d8d6667784a8b55def6787e0a Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 17:06:51 +0200 Subject: [PATCH 12/39] makes project creation work with fallback language --- services/src/project/project-repository.ts | 14 +++++++++++++- services/src/project/project-service.ts | 7 ++++++- services/src/project/project.ts | 5 ++++- .../container/projects/create-project-schema.ts | 1 + .../container/projects/create-project.svelte | 2 +- .../(authenticated)/projects/+page.server.ts | 6 +++--- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/services/src/project/project-repository.ts b/services/src/project/project-repository.ts index f7065bb5..f9049a56 100644 --- a/services/src/project/project-repository.ts +++ b/services/src/project/project-repository.ts @@ -11,7 +11,11 @@ export function createProject(project: CreateProjectFormSchema): Promise new Error('Error Creating Base Language')) @@ -29,3 +33,11 @@ export function createProject(project: CreateProjectFormSchema): Promise { return db.selectFrom('projects').selectAll().execute() } + +export function getProjectById(id: number): Promise { + return db + .selectFrom('projects') + .selectAll() + .where('id', '=', id) + .executeTakeFirstOrThrow(() => new Error(`Could not find project with id "${id}".`)) +} diff --git a/services/src/project/project-service.ts b/services/src/project/project-service.ts index f2652f1c..11166399 100644 --- a/services/src/project/project-service.ts +++ b/services/src/project/project-service.ts @@ -7,6 +7,7 @@ export async function createProject(project: CreateProjectFormSchema) { try { return await repository.createProject(project) } catch (e: unknown) { + console.warn(e) if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT_UNIQUE') { throw new CreateProjectNameNotUniqueError() } @@ -18,7 +19,11 @@ export async function createProject(project: CreateProjectFormSchema) { export async function getAllProjects() { try { return await repository.getAllProjects() - } catch (e) { + } catch (e: unknown) { throw new Error('Error Getting Projects') } } + +export async function getProjectById(id: number) { + return await repository.getProjectById(id) +} diff --git a/services/src/project/project.ts b/services/src/project/project.ts index 57b99836..498d59cb 100644 --- a/services/src/project/project.ts +++ b/services/src/project/project.ts @@ -14,7 +14,10 @@ export const createProjectSchema = z.object({ .min(1, 'Project name must have at least one character'), base_language: z .string({ required_error: 'Base language is required' }) - .min(1, 'Base language must have at least one character') + .min(1, 'Base language must have at least one character'), + base_language_label: z + .string({ required_error: 'Base language label is required' }) + .min(1, 'Base language label must have at least one character') }) export type CreateProjectFormSchema = z.infer diff --git a/src/components/container/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts index 6b3e896d..16eb3b03 100644 --- a/src/components/container/projects/create-project-schema.ts +++ b/src/components/container/projects/create-project-schema.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { type LanguageCode, availableLanguages } from '../language/languages' +// TODO: move to a shared and merge with the one in services project.ts export const createProjectSchema = z.object({ name: z .string({ required_error: 'Project name is required' }) diff --git a/src/components/container/projects/create-project.svelte b/src/components/container/projects/create-project.svelte index cdd9c959..d4624e00 100644 --- a/src/components/container/projects/create-project.svelte +++ b/src/components/container/projects/create-project.svelte @@ -8,7 +8,7 @@ import * as Form from '$components/ui/form' import { page } from '$app/stores' import { toast } from 'svelte-sonner' - import LanguageSelect from '../languages/LanguageSelect.svelte' + import LanguageSelect from '../language/LanguageSelect.svelte' export let data: SuperValidated> diff --git a/src/routes/(authenticated)/projects/+page.server.ts b/src/routes/(authenticated)/projects/+page.server.ts index f0f1f90e..31b2889c 100644 --- a/src/routes/(authenticated)/projects/+page.server.ts +++ b/src/routes/(authenticated)/projects/+page.server.ts @@ -1,10 +1,10 @@ import type { Actions, PageServerLoad } from './$types' import { message, setError, superValidate } from 'sveltekit-superforms' import { zod } from 'sveltekit-superforms/adapters' -import { createProjectSchema } from 'services/project/project' -import { createProject } from 'services/project/project-service' -import { getAllProjects } from 'services/project/project-repository' +import { createProject, getAllProjects } from 'services/project/project-service' import { CreateProjectNameNotUniqueError } from 'services/error' +//TODO: shared schema +import { createProjectSchema } from '$components/container/projects/create-project-schema' export const load: PageServerLoad = async () => { return { From 80b0baa13422c31f1105c405a5c694fe0756d2c2 Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 17:08:37 +0200 Subject: [PATCH 13/39] adjusts main content and header component styles --- src/components/layout/main-content/header.svelte | 5 ++++- src/components/layout/main-content/main-content.svelte | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/layout/main-content/header.svelte b/src/components/layout/main-content/header.svelte index 5f2bdf16..d70696c6 100644 --- a/src/components/layout/main-content/header.svelte +++ b/src/components/layout/main-content/header.svelte @@ -3,6 +3,9 @@
-

{title}

+
+

{title}

+ +
diff --git a/src/components/layout/main-content/main-content.svelte b/src/components/layout/main-content/main-content.svelte index 5bd2dbbc..5ce07e84 100644 --- a/src/components/layout/main-content/main-content.svelte +++ b/src/components/layout/main-content/main-content.svelte @@ -1,3 +1,11 @@ -
+ + +
From 1fc90e754bbfa24412b0708a774391820b93ecb6 Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 17:09:02 +0200 Subject: [PATCH 14/39] adds aider specific stuff to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e209d47e..c02bd93a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ vite.config.ts.timestamp-* docs/.vitepress/cache docs/.vitepress/dist - +aider +.aider* From a5f6ad0d7fb60a733926abda0bb38a9894932cc8 Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 17:18:08 +0200 Subject: [PATCH 15/39] adds language path specific code for project --- e2e/specs/create-project-flow.spec.ts | 2 +- services/src/language/language-repository.ts | 11 +++ services/src/language/language-service.ts | 18 +++++ services/src/language/language.model.ts | 6 ++ .../container/language/LanguageTable.svelte | 70 +++++++++++++++++++ src/components/container/language/schema.ts | 18 +++++ src/lib/models/language.model.ts | 8 +++ .../projects/[id]/+layout.server.ts | 8 +++ .../(authenticated)/projects/[id]/+layout.ts | 3 +- .../projects/[id]/languages/+page.server.ts | 13 ++++ .../projects/[id]/languages/+page.svelte | 49 ++++++++++++- 11 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 services/src/language/language-repository.ts create mode 100644 services/src/language/language-service.ts create mode 100644 services/src/language/language.model.ts create mode 100644 src/components/container/language/LanguageTable.svelte create mode 100644 src/components/container/language/schema.ts create mode 100644 src/lib/models/language.model.ts create mode 100644 src/routes/(authenticated)/projects/[id]/+layout.server.ts create mode 100644 src/routes/(authenticated)/projects/[id]/languages/+page.server.ts diff --git a/e2e/specs/create-project-flow.spec.ts b/e2e/specs/create-project-flow.spec.ts index 7025ab3b..063da438 100644 --- a/e2e/specs/create-project-flow.spec.ts +++ b/e2e/specs/create-project-flow.spec.ts @@ -12,7 +12,7 @@ test.describe('create project', () => { await page.getByTestId('create-project-modal-trigger').click() await page.getByTestId('create-project-name-input').fill(projectName) - await page.getByTestId('create-project-base-language-input').fill('en') + await page.getByTestId('create-project-base-language-select').fill('en') await page.getByTestId('create-project-submit-button').click() diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts new file mode 100644 index 00000000..d99dbfad --- /dev/null +++ b/services/src/language/language-repository.ts @@ -0,0 +1,11 @@ +import type { SelectableLanguage } from './language.model' +import { db } from '../db/database' + +export function getLanguagesForProject(id: number): Promise { + return db + .selectFrom('languages as l1') + .innerJoin('languages as l2', 'l1.fallback_language', 'l2.id') + .where('l1.project_id', '=', id) + .select(['l1.id', 'l1.code', 'l1.label', 'l2.code as fallback_language']) + .execute() +} diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts new file mode 100644 index 00000000..1635215e --- /dev/null +++ b/services/src/language/language-service.ts @@ -0,0 +1,18 @@ +import type { LanguageCode } from '$components/container/language/languages' +import type { LanguagesSchema } from '$components/container/language/schema' +import * as repository from './language-repository' + +export async function getLanguagesForProject(id: number): Promise { + console.warn(id) + const languages = await repository.getLanguagesForProject(id) + + console.warn(languages) + + return { + languages: languages.map(({ code, label, fallback_language }) => ({ + code: code as LanguageCode, + label, + fallback: fallback_language as LanguageCode + })) + } +} diff --git a/services/src/language/language.model.ts b/services/src/language/language.model.ts new file mode 100644 index 00000000..037242f9 --- /dev/null +++ b/services/src/language/language.model.ts @@ -0,0 +1,6 @@ +import type { Selectable } from 'kysely' +import type { Languages } from 'kysely-codegen' + +export type SelectableLanguage = Selectable< + Pick & { fallback_language: string } +> diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte new file mode 100644 index 00000000..013aad67 --- /dev/null +++ b/src/components/container/language/LanguageTable.svelte @@ -0,0 +1,70 @@ + + + + + + Code + Label + Fallback + Action + + + + {#each $formData.languages as _, i} + {#if $formData.languages[i]} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/if} + {/each} + + diff --git a/src/components/container/language/schema.ts b/src/components/container/language/schema.ts new file mode 100644 index 00000000..d8e9d794 --- /dev/null +++ b/src/components/container/language/schema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' +import { type LanguageCode, availableLanguages } from './languages' + +export const languageSchema = z.object({ + code: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]], { + required_error: 'Language code is required' + }), + label: z + .string({ + required_error: 'Language label is required' + }) + .min(1, 'Language label must at least consist of a single character'), + fallback: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]]).optional() +}) + +export const languagesSchema = z.object({ languages: z.array(languageSchema) }) + +export type LanguagesSchema = z.infer diff --git a/src/lib/models/language.model.ts b/src/lib/models/language.model.ts new file mode 100644 index 00000000..3a4bb6c7 --- /dev/null +++ b/src/lib/models/language.model.ts @@ -0,0 +1,8 @@ +// TODO pull into models +import type { LanguageCode } from '$components/container/language/languages' + +export type Language = { + code: LanguageCode + label: string + fallback?: LanguageCode +} diff --git a/src/routes/(authenticated)/projects/[id]/+layout.server.ts b/src/routes/(authenticated)/projects/[id]/+layout.server.ts new file mode 100644 index 00000000..200fa59a --- /dev/null +++ b/src/routes/(authenticated)/projects/[id]/+layout.server.ts @@ -0,0 +1,8 @@ +import type { LayoutServerLoad } from './$types' +import { getProjectById } from 'services/project/project-service' + +export const load: LayoutServerLoad = async ({ params }) => { + return { + project: await getProjectById(Number(params.id)) + } +} diff --git a/src/routes/(authenticated)/projects/[id]/+layout.ts b/src/routes/(authenticated)/projects/[id]/+layout.ts index 8c2e6c5b..061e16d9 100644 --- a/src/routes/(authenticated)/projects/[id]/+layout.ts +++ b/src/routes/(authenticated)/projects/[id]/+layout.ts @@ -5,7 +5,7 @@ import Languages from 'lucide-svelte/icons/languages' import Settings from 'lucide-svelte/icons/settings' import type { LayoutLoad } from './$types' -export const load: LayoutLoad = () => { +export const load: LayoutLoad = ({ data }) => { const sidebarElements = [ { name: 'My Projects', @@ -35,6 +35,7 @@ export const load: LayoutLoad = () => { ] return { + ...data, sidebarElements } } diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts new file mode 100644 index 00000000..881e8cca --- /dev/null +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts @@ -0,0 +1,13 @@ +import { getLanguagesForProject } from 'services/language/language-service' +import type { PageServerLoad } from './$types' +import { superValidate } from 'sveltekit-superforms' +import { languagesSchema } from '$components/container/language/schema' +import { zod } from 'sveltekit-superforms/adapters' + +export const load: PageServerLoad = async ({ params }) => { + const existingLanguages = await getLanguagesForProject(Number(params.id)) + + return { + form: await superValidate(existingLanguages, zod(languagesSchema)) + } +} diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte index eab42fea..cfda4a47 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte @@ -1 +1,48 @@ -languages + + + +
+ +
+ Save +
+
+ +
+ +
+ + +
+
From 1c0158261ee0601fcd52f0f1bf7a3f359b6e0c3f Mon Sep 17 00:00:00 2001 From: mledl Date: Wed, 21 Aug 2024 17:36:25 +0200 Subject: [PATCH 16/39] make left join to allow empty fallback language --- services/src/language/language-repository.ts | 2 +- services/src/language/language.model.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index d99dbfad..87347b05 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -4,7 +4,7 @@ import { db } from '../db/database' export function getLanguagesForProject(id: number): Promise { return db .selectFrom('languages as l1') - .innerJoin('languages as l2', 'l1.fallback_language', 'l2.id') + .leftJoin('languages as l2', 'l1.fallback_language', 'l2.id') .where('l1.project_id', '=', id) .select(['l1.id', 'l1.code', 'l1.label', 'l2.code as fallback_language']) .execute() diff --git a/services/src/language/language.model.ts b/services/src/language/language.model.ts index 037242f9..d9074de6 100644 --- a/services/src/language/language.model.ts +++ b/services/src/language/language.model.ts @@ -2,5 +2,5 @@ import type { Selectable } from 'kysely' import type { Languages } from 'kysely-codegen' export type SelectableLanguage = Selectable< - Pick & { fallback_language: string } + Pick & { fallback_language: string | null } > From 21d4d9c5444f2f90be7d6b68c24560c6b9b0bc96 Mon Sep 17 00:00:00 2001 From: mledl Date: Thu, 22 Aug 2024 10:53:47 +0200 Subject: [PATCH 17/39] feat: add console warning for existing languages in project load function --- .../(authenticated)/projects/[id]/languages/+page.server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts index 881e8cca..17544c88 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts @@ -7,6 +7,8 @@ import { zod } from 'sveltekit-superforms/adapters' export const load: PageServerLoad = async ({ params }) => { const existingLanguages = await getLanguagesForProject(Number(params.id)) + console.warn(existingLanguages) + return { form: await superValidate(existingLanguages, zod(languagesSchema)) } From 9b51a63f6a7f15bb347eb02e98d3b1969d22a5c2 Mon Sep 17 00:00:00 2001 From: "mledl (aider)" Date: Thu, 22 Aug 2024 10:53:49 +0200 Subject: [PATCH 18/39] feat: implement individual language updates in LanguageTable --- .../container/language/LanguageTable.svelte | 109 ++++++++++-------- src/components/container/language/schema.ts | 5 +- .../projects/[id]/languages/+page.server.ts | 29 ++++- 3 files changed, 86 insertions(+), 57 deletions(-) diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte index 013aad67..6390ca07 100644 --- a/src/components/container/language/LanguageTable.svelte +++ b/src/components/container/language/LanguageTable.svelte @@ -3,11 +3,20 @@ import * as Form from '$components/ui/form' import type { SuperForm } from 'sveltekit-superforms' import { Input } from '$components/ui/input' - import type { LanguagesSchema } from './schema' + import type { LanguageSchema } from './schema' + import { Button } from '$components/ui/button' + import { superForm } from 'sveltekit-superforms/client' - export let form: SuperForm + export let languages: LanguageSchema[] - const { form: formData } = form + function createLanguageForm(language: LanguageSchema) { + return superForm(language, { + id: `language-${language.id}`, + taintedMessage: null + }) + } + + $: languageForms = languages.map(createLanguageForm) @@ -20,51 +29,55 @@ - {#each $formData.languages as _, i} - {#if $formData.languages[i]} - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/if} + {#each languageForms as languageForm, i} + {@const { form, enhance } = languageForm} + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
{/each}
diff --git a/src/components/container/language/schema.ts b/src/components/container/language/schema.ts index d8e9d794..fa41ae86 100644 --- a/src/components/container/language/schema.ts +++ b/src/components/container/language/schema.ts @@ -2,6 +2,7 @@ import { z } from 'zod' import { type LanguageCode, availableLanguages } from './languages' export const languageSchema = z.object({ + id: z.number().optional(), code: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]], { required_error: 'Language code is required' }), @@ -13,6 +14,4 @@ export const languageSchema = z.object({ fallback: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]]).optional() }) -export const languagesSchema = z.object({ languages: z.array(languageSchema) }) - -export type LanguagesSchema = z.infer +export type LanguageSchema = z.infer diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts index 17544c88..081649b1 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts @@ -1,15 +1,32 @@ -import { getLanguagesForProject } from 'services/language/language-service' -import type { PageServerLoad } from './$types' +import { getLanguagesForProject, updateLanguage } from 'services/language/language-service' +import type { PageServerLoad, Actions } from './$types' import { superValidate } from 'sveltekit-superforms' -import { languagesSchema } from '$components/container/language/schema' +import { languageSchema } from '$components/container/language/schema' import { zod } from 'sveltekit-superforms/adapters' +import { fail } from '@sveltejs/kit' export const load: PageServerLoad = async ({ params }) => { const existingLanguages = await getLanguagesForProject(Number(params.id)) - console.warn(existingLanguages) - return { - form: await superValidate(existingLanguages, zod(languagesSchema)) + languages: existingLanguages + } +} + +export const actions: Actions = { + default: async ({ request }) => { + const formData = await request.formData() + const form = await superValidate(formData, zod(languageSchema)) + + if (!form.valid) { + return fail(400, { form }) + } + + try { + await updateLanguage(form.data) + return { form } + } catch (error) { + return fail(500, { form, error: 'Failed to update language' }) + } } } From 2d14468991a399b34ebdedf6a4b502cf5b8596c4 Mon Sep 17 00:00:00 2001 From: mledl Date: Thu, 22 Aug 2024 11:03:30 +0200 Subject: [PATCH 19/39] refactor: simplify getLanguagesForProject function and update return type --- services/src/language/language-service.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts index 1635215e..4fb210de 100644 --- a/services/src/language/language-service.ts +++ b/services/src/language/language-service.ts @@ -1,18 +1,14 @@ import type { LanguageCode } from '$components/container/language/languages' -import type { LanguagesSchema } from '$components/container/language/schema' +import type { LanguageSchema } from '$components/container/language/schema' import * as repository from './language-repository' -export async function getLanguagesForProject(id: number): Promise { - console.warn(id) +export async function getLanguagesForProject(id: number): Promise { const languages = await repository.getLanguagesForProject(id) - console.warn(languages) - - return { - languages: languages.map(({ code, label, fallback_language }) => ({ - code: code as LanguageCode, - label, - fallback: fallback_language as LanguageCode - })) - } + return languages.map(({ id, code, label, fallback_language }) => ({ + id, + code: code as LanguageCode, + label, + fallback: fallback_language as LanguageCode + })) } From e54256a38d37ff3c5fa4b1e395bc375d7eae3e0d Mon Sep 17 00:00:00 2001 From: mledl Date: Thu, 22 Aug 2024 11:03:34 +0200 Subject: [PATCH 20/39] feat: add updateLanguage function to language repository --- services/src/language/language-repository.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index 87347b05..99fc7b4b 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -8,4 +8,16 @@ export function getLanguagesForProject(id: number): Promise { + return db + .updateTable('languages') + .set({ + code: language.code, + label: language.label, + fallback_language: language.fallback || null + }) + .where('id', '=', language.id!) + .execute() +} } From 321cf6ed8016017cd230631fbb1a60d259b8630c Mon Sep 17 00:00:00 2001 From: "mledl (aider)" Date: Thu, 22 Aug 2024 11:03:35 +0200 Subject: [PATCH 21/39] feat: implement updateLanguage method for updating language entries --- services/src/language/language-repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index 99fc7b4b..8b7dac05 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -1,5 +1,6 @@ import type { SelectableLanguage } from './language.model' import { db } from '../db/database' +import type { LanguageSchema } from '$components/container/language/schema' export function getLanguagesForProject(id: number): Promise { return db From a11993830cb75f0aa70b8503d70b1600362cfb84 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 24 Aug 2024 16:49:22 +0200 Subject: [PATCH 22/39] use json datatype for languages superform due to nested table --- services/src/language/language-repository.ts | 30 ++++- services/src/language/language-service.ts | 29 +++-- .../project-repository.integration.test.ts | 29 ++++- .../src/project/project-service.unit.test.ts | 3 +- .../container/language/LanguageTable.svelte | 111 ++++++++---------- src/components/container/language/schema.ts | 6 + .../projects/[id]/languages/+page.server.ts | 12 +- .../projects/[id]/languages/+page.svelte | 3 +- 8 files changed, 138 insertions(+), 85 deletions(-) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index 8b7dac05..a59b24cf 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -1,6 +1,7 @@ import type { SelectableLanguage } from './language.model' import { db } from '../db/database' import type { LanguageSchema } from '$components/container/language/schema' +import type { LanguageCode } from '$components/container/language/languages' export function getLanguagesForProject(id: number): Promise { return db @@ -9,16 +10,37 @@ export function getLanguagesForProject(id: number): Promise { - return db +export async function updateLanguage(language: LanguageSchema): Promise { + let fallbackId: number | null = null + + if (language.fallback) + fallbackId = ( + await db + .selectFrom('languages') + .where('code', '=', language.fallback) + .select('id') + .executeTakeFirstOrThrow() + ).id + + const updatedLanguages = await db .updateTable('languages') .set({ code: language.code, label: language.label, - fallback_language: language.fallback || null + fallback_language: fallbackId }) .where('id', '=', language.id!) + .returning(['id', 'code', 'label', 'fallback_language']) .execute() -} + + const updatedLanguage = updatedLanguages[0] + + if (!updatedLanguage) throw new Error(`Failed to update language "${language.code}"`) + + return { + ...updatedLanguage, + fallback_language: language.fallback ? (language.fallback as LanguageCode) : null + } } diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts index 4fb210de..1af2a056 100644 --- a/services/src/language/language-service.ts +++ b/services/src/language/language-service.ts @@ -1,14 +1,29 @@ import type { LanguageCode } from '$components/container/language/languages' -import type { LanguageSchema } from '$components/container/language/schema' +import type { LanguageId, LanguageSchema } from '$components/container/language/schema' import * as repository from './language-repository' +import type { SelectableLanguage } from './language.model' + +function mapToLanguage(language: SelectableLanguage): LanguageSchema { + return { + id: language.id as LanguageId, + code: language.code as LanguageCode, + label: language.label, + fallback: language.fallback_language ? (language.fallback_language as LanguageCode) : undefined + } +} export async function getLanguagesForProject(id: number): Promise { const languages = await repository.getLanguagesForProject(id) - return languages.map(({ id, code, label, fallback_language }) => ({ - id, - code: code as LanguageCode, - label, - fallback: fallback_language as LanguageCode - })) + return languages.map(mapToLanguage) +} + +export async function updateLanguage(language: LanguageSchema): Promise { + if (!language.id) { + throw new Error('Language ID is required for updating') + } + + const updatedLanguage = await repository.updateLanguage(language) + + return mapToLanguage(updatedLanguage) } diff --git a/services/src/project/project-repository.integration.test.ts b/services/src/project/project-repository.integration.test.ts index 4145f8a4..6825090a 100644 --- a/services/src/project/project-repository.integration.test.ts +++ b/services/src/project/project-repository.integration.test.ts @@ -8,7 +8,8 @@ import type { Selectable } from 'kysely' const projectCreationObject: CreateProjectFormSchema = { name: 'Test Project', - base_language: 'en' + base_language: 'en', + base_language_label: 'English' } beforeEach(async () => { @@ -71,8 +72,17 @@ describe('Project Repository', () => { }) it('should allow creation of multiple projects with the same base language code', async () => { - const project1 = { name: 'Project 1', base_language: 'en' } - const project2 = { name: 'Project 2', base_language: 'en' } + const project1: CreateProjectFormSchema = { + name: 'Project 1', + base_language: 'en', + base_language_label: 'English' + } + + const project2: CreateProjectFormSchema = { + name: 'Project 2', + base_language: 'en', + base_language_label: 'English' + } await createProject(project1) await createProject(project2) @@ -95,8 +105,17 @@ describe('Project Repository', () => { }) it('should return all created projects', async () => { - const project1 = { name: 'Project 1', base_language: 'en' } - const project2 = { name: 'Project 2', base_language: 'fr' } + const project1: CreateProjectFormSchema = { + name: 'Project 1', + base_language: 'en', + base_language_label: 'English' + } + + const project2: CreateProjectFormSchema = { + name: 'Project 2', + base_language: 'fr', + base_language_label: 'French' + } await createProject(project1) await createProject(project2) diff --git a/services/src/project/project-service.unit.test.ts b/services/src/project/project-service.unit.test.ts index ca629c93..34eb40f7 100644 --- a/services/src/project/project-service.unit.test.ts +++ b/services/src/project/project-service.unit.test.ts @@ -12,7 +12,8 @@ vi.mock('./project-repository', () => ({ const projectCreationObject: CreateProjectFormSchema = { name: 'Test Project', - base_language: 'en' + base_language: 'en', + base_language_label: 'English' } const mockSelectableProject = { diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte index 6390ca07..77f10246 100644 --- a/src/components/container/language/LanguageTable.svelte +++ b/src/components/container/language/LanguageTable.svelte @@ -1,22 +1,14 @@ @@ -29,55 +21,50 @@ - {#each languageForms as languageForm, i} - {@const { form, enhance } = languageForm} - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-
+ {#each $formData.languages as _, i} + {#if $formData.languages[i]} + + + + + + + + + + + + + + + + + + + + + + + + + + + {/if} {/each}
diff --git a/src/components/container/language/schema.ts b/src/components/container/language/schema.ts index fa41ae86..347ec7e1 100644 --- a/src/components/container/language/schema.ts +++ b/src/components/container/language/schema.ts @@ -14,4 +14,10 @@ export const languageSchema = z.object({ fallback: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]]).optional() }) +export const languagesSchema = z.object({ languages: z.array(languageSchema) }) + export type LanguageSchema = z.infer +export type LanguagesSchema = z.infer + +declare const actionPlanningId: unique symbol +export type LanguageId = number & { readonly [actionPlanningId]: never } diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts index 081649b1..d87f33a0 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts @@ -1,21 +1,22 @@ import { getLanguagesForProject, updateLanguage } from 'services/language/language-service' -import type { PageServerLoad, Actions } from './$types' -import { superValidate } from 'sveltekit-superforms' -import { languageSchema } from '$components/container/language/schema' +import type { Actions, PageServerLoad } from './$types' +import { superValidate } from 'sveltekit-superforms/server' +import { languageSchema, languagesSchema } from '$components/container/language/schema' import { zod } from 'sveltekit-superforms/adapters' import { fail } from '@sveltejs/kit' export const load: PageServerLoad = async ({ params }) => { - const existingLanguages = await getLanguagesForProject(Number(params.id)) + const languages = await getLanguagesForProject(Number(params.id)) return { - languages: existingLanguages + form: await superValidate({ languages }, zod(languagesSchema)) } } export const actions: Actions = { default: async ({ request }) => { const formData = await request.formData() + // TODO: check if correct schema used const form = await superValidate(formData, zod(languageSchema)) if (!form.valid) { @@ -24,6 +25,7 @@ export const actions: Actions = { try { await updateLanguage(form.data) + return { form } } catch (error) { return fail(500, { form, error: 'Failed to update language' }) diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte index cfda4a47..324db78f 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte @@ -17,6 +17,7 @@ const form = superForm(data.form, { validators: zodClient(languagesSchema), + dataType: 'json', async onUpdated({ form }) { if (form.message) { if ($page.status >= 400) { @@ -43,6 +44,6 @@
- + From 2b642960fb0d104f6fd1789f8152d57f48ef43e2 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 24 Aug 2024 16:56:58 +0200 Subject: [PATCH 23/39] adds unit tests for language-service --- .../language/language-service.unit.test.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 services/src/language/language-service.unit.test.ts diff --git a/services/src/language/language-service.unit.test.ts b/services/src/language/language-service.unit.test.ts new file mode 100644 index 00000000..783b0c3d --- /dev/null +++ b/services/src/language/language-service.unit.test.ts @@ -0,0 +1,124 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getLanguagesForProject, updateLanguage } from './language-service' +import * as repository from './language-repository' +import type { SelectableLanguage } from './language.model' +import type { LanguageSchema } from '$components/container/language/schema' +import type { LanguageCode } from '$components/container/language/languages' + +vi.mock('./language-repository', () => ({ + getLanguagesForProject: vi.fn(), + updateLanguage: vi.fn() +})) + +const mockSelectableLanguages: SelectableLanguage[] = [ + { + id: 1, + code: 'en', + label: 'English', + fallback_language: null + }, + { + id: 2, + code: 'es', + label: 'Spanish', + fallback_language: 'en' + } +] + +const expectedLanguageSchemas: LanguageSchema[] = [ + { + id: 1, + code: 'en', + label: 'English', + fallback: undefined + }, + { + id: 2, + code: 'es' as LanguageCode, + label: 'Spanish', + fallback: 'en' + } +] + +beforeEach(() => { + vi.resetAllMocks() +}) + +describe('Language Service', () => { + describe('getLanguagesForProject', () => { + it('should call the repository to get languages for a project', async () => { + vi.mocked(repository.getLanguagesForProject).mockResolvedValue(mockSelectableLanguages) + + const languages = await getLanguagesForProject(1) + + expect(repository.getLanguagesForProject).toHaveBeenCalledWith(1) + expect(languages).toEqual(expectedLanguageSchemas) + }) + + it('should return an empty array when there are no languages for the project', async () => { + vi.mocked(repository.getLanguagesForProject).mockResolvedValue([]) + + const languages = await getLanguagesForProject(1) + + expect(repository.getLanguagesForProject).toHaveBeenCalledWith(1) + expect(languages).toEqual([]) + }) + + it('should throw an error if the repository throws an error', async () => { + vi.mocked(repository.getLanguagesForProject).mockRejectedValue(new Error('Repository error')) + + await expect(getLanguagesForProject(1)).rejects.toThrow('Repository error') + }) + }) + + describe('updateLanguage', () => { + const mockLanguageToUpdate: LanguageSchema = { + id: 1, + code: 'fr' as LanguageCode, + label: 'French', + fallback: 'en' + } + + const mockUpdatedSelectableLanguage: SelectableLanguage = { + id: 1, + code: 'fr', + label: 'French', + fallback_language: 'en' + } + + it('should call the repository to update a language and return the updated language', async () => { + vi.mocked(repository.updateLanguage).mockResolvedValue(mockUpdatedSelectableLanguage) + + const updatedLanguage = await updateLanguage(mockLanguageToUpdate) + + expect(repository.updateLanguage).toHaveBeenCalledWith(mockLanguageToUpdate) + expect(updatedLanguage).toEqual(mockLanguageToUpdate) + }) + + it('should throw an error if the language ID is missing', async () => { + const invalidLanguage = { ...mockLanguageToUpdate, id: undefined } + + await expect(updateLanguage(invalidLanguage as LanguageSchema)).rejects.toThrow( + 'Language ID is required for updating' + ) + }) + + it('should handle null fallback_language correctly', async () => { + const languageWithNullFallback: SelectableLanguage = { + ...mockUpdatedSelectableLanguage, + fallback_language: null + } + vi.mocked(repository.updateLanguage).mockResolvedValue(languageWithNullFallback) + + const updatedLanguage = await updateLanguage(mockLanguageToUpdate) + + expect(updatedLanguage.fallback).toBeUndefined() + }) + + it('should throw an error if the repository throws an error', async () => { + vi.mocked(repository.updateLanguage).mockRejectedValue(new Error('Update failed')) + + await expect(updateLanguage(mockLanguageToUpdate)).rejects.toThrow('Update failed') + }) + }) +}) From 60e15375366ac1757f794243fae39545951f68a5 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 24 Aug 2024 17:03:28 +0200 Subject: [PATCH 24/39] adds integration tests for language repository --- .../language-repository.integration.test.ts | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 services/src/language/language-repository.integration.test.ts diff --git a/services/src/language/language-repository.integration.test.ts b/services/src/language/language-repository.integration.test.ts new file mode 100644 index 00000000..8a8358a9 --- /dev/null +++ b/services/src/language/language-repository.integration.test.ts @@ -0,0 +1,181 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { getLanguagesForProject, updateLanguage } from './language-repository' +import { createProject } from '../project/project-repository' +import { runMigration } from '../db/database-migration-util' +import { db } from '../db/database' +import type { CreateProjectFormSchema } from '../project/project' +import type { LanguageSchema } from '$components/container/language/schema' +import type { LanguageCode } from '$components/container/language/languages' + +const projectCreationObject: CreateProjectFormSchema = { + name: 'Test Project', + base_language: 'en', + base_language_label: 'English' +} + +beforeEach(async () => { + db.reset() + await runMigration() +}) + +describe('Language Repository', () => { + describe('getLanguagesForProject', () => { + it('should return an empty array when there are no languages for the project', async () => { + const project = await createProject(projectCreationObject) + const languages = await getLanguagesForProject(project.id) + expect(languages).toHaveLength(1) // Base language is always created + expect(languages[0]?.code).toBe('en') + }) + + it('should return all languages for a project', async () => { + const project = await createProject(projectCreationObject) + + // Add another language + await db + .insertInto('languages') + .values({ + code: 'fr', + label: 'French', + project_id: project.id, + fallback_language: project.base_language + }) + .execute() + + const languages = await getLanguagesForProject(project.id) + expect(languages).toHaveLength(2) + + const languageCodes = languages.map((lang) => lang.code) + expect(languageCodes).toContain('en') + expect(languageCodes).toContain('fr') + }) + + it('should return languages with correct attributes', async () => { + const project = await createProject(projectCreationObject) + const languages = await getLanguagesForProject(project.id) + + expect(languages[0]).toMatchObject({ + id: expect.any(Number), + code: 'en', + label: 'English', + fallback_language: null + }) + }) + + it('should return languages with correct fallback language', async () => { + const project = await createProject(projectCreationObject) + + // Add another language with fallback + await db + .insertInto('languages') + .values({ + code: 'fr', + label: 'French', + project_id: project.id, + fallback_language: project.base_language + }) + .execute() + + const languages = await getLanguagesForProject(project.id) + const frenchLanguage = languages.find((lang) => lang.code === 'fr') + + expect(frenchLanguage).toBeDefined() + expect(frenchLanguage?.fallback_language).toBe('en') + }) + }) + + describe('updateLanguage', () => { + it('should update a language with the correct attributes', async () => { + const project = await createProject(projectCreationObject) + const languages = await getLanguagesForProject(project.id) + const languageToUpdate = languages[0]! + + const updatedLanguageData: LanguageSchema = { + id: languageToUpdate.id, + code: 'fr' as LanguageCode, + label: 'French', + fallback: 'en' + } + + const updatedLanguage = await updateLanguage(updatedLanguageData) + + expect(updatedLanguage).toMatchObject({ + id: languageToUpdate.id, + code: 'fr', + label: 'French', + fallback_language: 'en' + }) + + // Verify the update in the database + const dbLanguage = await db + .selectFrom('languages') + .where('id', '=', languageToUpdate.id) + .selectAll() + .executeTakeFirst() + + expect(dbLanguage).toMatchObject({ + code: 'fr', + label: 'French' + }) + }) + + it('should update a language without changing the fallback if not provided', async () => { + const project = await createProject(projectCreationObject) + const languages = await getLanguagesForProject(project.id) + const languageToUpdate = languages[0]! + + const updatedLanguageData: LanguageSchema = { + id: languageToUpdate.id, + code: 'es' as LanguageCode, + label: 'Spanish' + } + + const updatedLanguage = await updateLanguage(updatedLanguageData) + + expect(updatedLanguage.fallback_language).toBeNull() + }) + + it('should throw an error when updating a non-existent language', async () => { + const nonExistentLanguage: LanguageSchema = { + id: 9999, + code: 'xx' as LanguageCode, + label: 'Non-existent', + fallback: 'en' + } + + await expect(updateLanguage(nonExistentLanguage)).rejects.toThrow() + }) + + it('should update fallback language to null when fallback is undefined', async () => { + const project = await createProject(projectCreationObject) + const languages = await getLanguagesForProject(project.id) + const languageToUpdate = languages[0]! + + // First, set a fallback language + await db + .updateTable('languages') + .set({ fallback_language: project.base_language }) + .where('id', '=', languageToUpdate.id) + .execute() + + const updatedLanguageData: LanguageSchema = { + id: languageToUpdate.id, + code: 'fr' as LanguageCode, + label: 'French' + // fallback is intentionally omitted + } + + const updatedLanguage = await updateLanguage(updatedLanguageData) + + expect(updatedLanguage.fallback_language).toBeNull() + + // Verify the update in the database + const dbLanguage = await db + .selectFrom('languages') + .where('id', '=', languageToUpdate.id) + .selectAll() + .executeTakeFirst() + + expect(dbLanguage?.fallback_language).toBeNull() + }) + }) +}) From 8d38781c98b56d504dacef273b10f74df6a4ba10 Mon Sep 17 00:00:00 2001 From: mledl Date: Mon, 26 Aug 2024 16:03:28 +0200 Subject: [PATCH 25/39] adds add button to add a new language to the table --- .../container/language/LanguageSelect.svelte | 11 ++---- .../projects/[id]/languages/+page.svelte | 35 ++++++++++++++++--- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/components/container/language/LanguageSelect.svelte b/src/components/container/language/LanguageSelect.svelte index 35e84b14..53bef938 100644 --- a/src/components/container/language/LanguageSelect.svelte +++ b/src/components/container/language/LanguageSelect.svelte @@ -3,7 +3,7 @@ import { type LanguageCode, availableLanguages } from './languages' export let name: string - export let value: LanguageCode + export let value: LanguageCode | undefined export let placeholder = 'Select Language' export let disabled = false export let typeahead = true @@ -14,14 +14,7 @@ })) - { - s && (value = s.value) - }} -> + (value = s?.value)}> diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte index 324db78f..aa6af3d3 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte @@ -1,5 +1,5 @@ @@ -40,8 +59,14 @@
-
- +
+
+ +
+
From 29dd710de5d9de95ab9d98544b2c59c04d67ab1a Mon Sep 17 00:00:00 2001 From: mledl Date: Fri, 30 Aug 2024 17:17:33 +0200 Subject: [PATCH 26/39] reflects delete and upsert languages on the client and add unit and integration test cases --- .../language-repository.integration.test.ts | 146 +++++++++++++++++- services/src/language/language-repository.ts | 63 ++++++-- services/src/language/language-service.ts | 15 ++ .../language/language-service.unit.test.ts | 119 +++++++++++++- .../container/language/LanguageTable.svelte | 28 ++++ .../projects/[id]/languages/+page.server.ts | 35 +++-- .../projects/[id]/languages/+page.svelte | 12 +- 7 files changed, 387 insertions(+), 31 deletions(-) diff --git a/services/src/language/language-repository.integration.test.ts b/services/src/language/language-repository.integration.test.ts index 8a8358a9..e66d1637 100644 --- a/services/src/language/language-repository.integration.test.ts +++ b/services/src/language/language-repository.integration.test.ts @@ -1,5 +1,10 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { getLanguagesForProject, updateLanguage } from './language-repository' +import { + deleteLanguage, + getLanguagesForProject, + updateLanguage, + upsertLanguages +} from './language-repository' import { createProject } from '../project/project-repository' import { runMigration } from '../db/database-migration-util' import { db } from '../db/database' @@ -178,4 +183,143 @@ describe('Language Repository', () => { expect(dbLanguage?.fallback_language).toBeNull() }) }) + + describe('upsertLanguages', () => { + it('should insert new languages for a project', async () => { + const project = await createProject(projectCreationObject) + const newLanguages: LanguageSchema[] = [ + { code: 'fr' as LanguageCode, label: 'French', fallback: 'en' }, + { code: 'es' as LanguageCode, label: 'Spanish', fallback: 'en' } + ] + + const upsertedLanguages = await upsertLanguages(project.id, newLanguages) + + expect(upsertedLanguages).toHaveLength(2) + expect(upsertedLanguages[0]).toMatchObject({ + code: 'fr', + label: 'French', + fallback_language: 'en' + }) + + expect(upsertedLanguages[1]).toMatchObject({ + code: 'es', + label: 'Spanish', + fallback_language: 'en' + }) + + // Verify in the database + const dbLanguages = await getLanguagesForProject(project.id) + expect(dbLanguages).toHaveLength(3) // Including the base language + expect(dbLanguages.map((l) => l.code)).toContain('fr') + expect(dbLanguages.map((l) => l.code)).toContain('es') + }) + + it('should update existing languages for a project', async () => { + const project = await createProject(projectCreationObject) + const initialLanguages: LanguageSchema[] = [ + { code: 'fr' as LanguageCode, label: 'French', fallback: 'en' } + ] + await upsertLanguages(project.id, initialLanguages) + + const updatedLanguages: LanguageSchema[] = [ + { code: 'fr' as LanguageCode, label: 'Français', fallback: undefined } + ] + const upsertedLanguages = await upsertLanguages(project.id, updatedLanguages) + + expect(upsertedLanguages).toHaveLength(1) + expect(upsertedLanguages[0]).toMatchObject({ + code: 'fr', + label: 'Français', + fallback_language: null + }) + + // Verify in the database + const dbLanguages = await getLanguagesForProject(project.id) + const frenchLanguage = dbLanguages.find((l) => l.code === 'fr') + expect(frenchLanguage?.label).toBe('Français') + expect(frenchLanguage?.fallback_language).toBeNull() + }) + + it('should handle a mix of insert and update operations', async () => { + const project = await createProject(projectCreationObject) + const initialLanguages: LanguageSchema[] = [ + { code: 'fr' as LanguageCode, label: 'French', fallback: 'en' } + ] + await upsertLanguages(project.id, initialLanguages) + + const mixedLanguages: LanguageSchema[] = [ + { code: 'fr' as LanguageCode, label: 'Français', fallback: undefined }, + { code: 'es' as LanguageCode, label: 'Spanish', fallback: 'en' } + ] + const upsertedLanguages = await upsertLanguages(project.id, mixedLanguages) + + expect(upsertedLanguages).toHaveLength(2) + expect(upsertedLanguages.find((l) => l.code === 'fr')).toMatchObject({ + code: 'fr', + label: 'Français', + fallback_language: null + }) + + expect(upsertedLanguages.find((l) => l.code === 'es')).toMatchObject({ + code: 'es', + label: 'Spanish', + fallback_language: 'en' + }) + + // Verify in the database + const dbLanguages = await getLanguagesForProject(project.id) + expect(dbLanguages).toHaveLength(3) // Including the base language + }) + + it('should throw an error when upserting languages for a non-existent project', async () => { + const nonExistentProjectId = 9999 + const languages: LanguageSchema[] = [ + { code: 'fr' as LanguageCode, label: 'French', fallback: 'en' } + ] + + await expect(upsertLanguages(nonExistentProjectId, languages)).rejects.toThrow() + }) + }) + + describe('deleteLanguage', () => { + it('should delete an existing language', async () => { + const project = await createProject(projectCreationObject) + const newLanguage: LanguageSchema = { + code: 'fr' as LanguageCode, + label: 'French', + fallback: 'en' + } + const [upsertedLanguage] = await upsertLanguages(project.id, [newLanguage]) + + await deleteLanguage(upsertedLanguage?.id as number) + + // Verify the language is deleted + const dbLanguages = await getLanguagesForProject(project.id) + expect(dbLanguages).toHaveLength(1) // Only the base language remains + expect(dbLanguages[0]?.code).not.toBe('fr') + }) + + it('should throw an error when deleting a non-existent language', async () => { + const nonExistentLanguageId = 9999 + + await expect(deleteLanguage(nonExistentLanguageId)).rejects.toThrow() + }) + + it('should not delete the base language of a project', async () => { + const project = await createProject(projectCreationObject) + const languages = await getLanguagesForProject(project.id) + const baseLanguage = languages.find((l) => l.id === project.base_language) + + if (!baseLanguage) { + throw new Error('Base language not found') + } + + await expect(deleteLanguage(baseLanguage.id)).rejects.toThrow() + + // Verify the base language still exists + const dbLanguages = await getLanguagesForProject(project.id) + expect(dbLanguages).toHaveLength(1) + expect(dbLanguages[0]?.id).toBe(project.base_language) + }) + }) }) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index a59b24cf..c4508444 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -3,6 +3,18 @@ import { db } from '../db/database' import type { LanguageSchema } from '$components/container/language/schema' import type { LanguageCode } from '$components/container/language/languages' +async function getFallbackLanguageId(fallback: LanguageCode | undefined) { + if (!fallback) return null + + return ( + await db + .selectFrom('languages') + .where('code', '=', fallback) + .select('id') + .executeTakeFirstOrThrow() + ).id +} + export function getLanguagesForProject(id: number): Promise { return db .selectFrom('languages as l1') @@ -13,23 +25,12 @@ export function getLanguagesForProject(id: number): Promise { - let fallbackId: number | null = null - - if (language.fallback) - fallbackId = ( - await db - .selectFrom('languages') - .where('code', '=', language.fallback) - .select('id') - .executeTakeFirstOrThrow() - ).id - const updatedLanguages = await db .updateTable('languages') .set({ code: language.code, label: language.label, - fallback_language: fallbackId + fallback_language: await getFallbackLanguageId(language.fallback) }) .where('id', '=', language.id!) .returning(['id', 'code', 'label', 'fallback_language']) @@ -44,3 +45,41 @@ export async function updateLanguage(language: LanguageSchema): Promise { + const languagesToUpsert = await Promise.all( + languages.map(async (language) => ({ + project_id: projectId, + code: language.code, + label: language.label, + fallback_language: await getFallbackLanguageId(language.fallback) + })) + ) + + const upsertedLanguages = await db + .insertInto('languages') + .values(languagesToUpsert) + .onConflict((oc) => + oc.columns(['project_id', 'code']).doUpdateSet({ + label: (eb) => eb.ref('excluded.label'), + fallback_language: (eb) => eb.ref('excluded.fallback_language') + }) + ) + .returning(['id', 'code', 'label', 'fallback_language']) + .execute() + + return upsertedLanguages.map((upsertedLanguage) => ({ + ...upsertedLanguage, + fallback_language: + languages.find(({ code }) => upsertedLanguage.code === code)?.fallback ?? null + })) +} + +export async function deleteLanguage(id: number): Promise { + const result = await db.deleteFrom('languages').where('id', '=', id).execute() + + if (result[0]?.numDeletedRows === 0n) throw new Error(`Failed to delete language with id ${id}`) +} diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts index 1af2a056..4395d30d 100644 --- a/services/src/language/language-service.ts +++ b/services/src/language/language-service.ts @@ -27,3 +27,18 @@ export async function updateLanguage(language: LanguageSchema): Promise { + const upsertedLanguages = await repository.upsertLanguages(projectId, languages) + + return upsertedLanguages.map(mapToLanguage) +} + +export async function deleteLanguage(projectId: number, languageId: number) { + await repository.deleteLanguage(languageId) + + return getLanguagesForProject(projectId) +} diff --git a/services/src/language/language-service.unit.test.ts b/services/src/language/language-service.unit.test.ts index 783b0c3d..b03adc1d 100644 --- a/services/src/language/language-service.unit.test.ts +++ b/services/src/language/language-service.unit.test.ts @@ -1,5 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { getLanguagesForProject, updateLanguage } from './language-service' +import { + deleteLanguage, + getLanguagesForProject, + updateLanguage, + upsertLanguagesForProject +} from './language-service' import * as repository from './language-repository' import type { SelectableLanguage } from './language.model' import type { LanguageSchema } from '$components/container/language/schema' @@ -7,7 +12,9 @@ import type { LanguageCode } from '$components/container/language/languages' vi.mock('./language-repository', () => ({ getLanguagesForProject: vi.fn(), - updateLanguage: vi.fn() + updateLanguage: vi.fn(), + upsertLanguages: vi.fn(), + deleteLanguage: vi.fn() })) const mockSelectableLanguages: SelectableLanguage[] = [ @@ -121,4 +128,112 @@ describe('Language Service', () => { await expect(updateLanguage(mockLanguageToUpdate)).rejects.toThrow('Update failed') }) }) + + describe('upsertLanguagesForProject', () => { + const mockProjectId = 1 + const mockLanguagesToUpsert: LanguageSchema[] = [ + { + id: 1, + code: 'en' as LanguageCode, + label: 'English', + fallback: undefined + }, + { + id: 2, + code: 'fr' as LanguageCode, + label: 'French', + fallback: 'en' + } + ] + + const mockUpsertedSelectableLanguages: SelectableLanguage[] = [ + { + id: 1, + code: 'en', + label: 'English', + fallback_language: null + }, + { + id: 2, + code: 'fr', + label: 'French', + fallback_language: 'en' + } + ] + + it('should call the repository to upsert languages and return the upserted languages', async () => { + vi.mocked(repository.upsertLanguages).mockResolvedValue(mockUpsertedSelectableLanguages) + + const upsertedLanguages = await upsertLanguagesForProject( + mockProjectId, + mockLanguagesToUpsert + ) + + expect(repository.upsertLanguages).toHaveBeenCalledWith(mockProjectId, mockLanguagesToUpsert) + expect(upsertedLanguages).toEqual(mockLanguagesToUpsert) + }) + + it('should handle an empty array of languages', async () => { + vi.mocked(repository.upsertLanguages).mockResolvedValue([]) + + const upsertedLanguages = await upsertLanguagesForProject(mockProjectId, []) + + expect(repository.upsertLanguages).toHaveBeenCalledWith(mockProjectId, []) + expect(upsertedLanguages).toEqual([]) + }) + + it('should throw an error if the repository throws an error', async () => { + vi.mocked(repository.upsertLanguages).mockRejectedValue(new Error('Upsert failed')) + + await expect(upsertLanguagesForProject(mockProjectId, mockLanguagesToUpsert)).rejects.toThrow( + 'Upsert failed' + ) + }) + }) + + describe('deleteLanguage', () => { + const mockProjectId = 1 + const mockLanguageId = 2 + + it('should call the repository to delete a language and return updated languages for the project', async () => { + vi.mocked(repository.deleteLanguage).mockResolvedValue() + vi.mocked(repository.getLanguagesForProject).mockResolvedValue([ + mockSelectableLanguages[0] as SelectableLanguage + ]) + + const updatedLanguages = await deleteLanguage(mockProjectId, mockLanguageId) + + expect(repository.deleteLanguage).toHaveBeenCalledWith(mockLanguageId) + expect(repository.getLanguagesForProject).toHaveBeenCalledWith(mockProjectId) + expect(updatedLanguages).toEqual([expectedLanguageSchemas[0]]) + }) + + it('should return an empty array if no languages remain after deletion', async () => { + vi.mocked(repository.deleteLanguage).mockResolvedValue() + vi.mocked(repository.getLanguagesForProject).mockResolvedValue([]) + + const updatedLanguages = await deleteLanguage(mockProjectId, mockLanguageId) + + expect(repository.deleteLanguage).toHaveBeenCalledWith(mockLanguageId) + expect(repository.getLanguagesForProject).toHaveBeenCalledWith(mockProjectId) + expect(updatedLanguages).toEqual([]) + }) + + it('should throw an error if the repository throws an error during deletion', async () => { + vi.mocked(repository.deleteLanguage).mockRejectedValue(new Error('Delete failed')) + + await expect(deleteLanguage(mockProjectId, mockLanguageId)).rejects.toThrow('Delete failed') + }) + + it('should throw an error if getting updated languages fails', async () => { + vi.mocked(repository.deleteLanguage).mockResolvedValue() + vi.mocked(repository.getLanguagesForProject).mockRejectedValue( + new Error('Get languages failed') + ) + + await expect(deleteLanguage(mockProjectId, mockLanguageId)).rejects.toThrow( + 'Get languages failed' + ) + }) + }) }) diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte index 77f10246..af430575 100644 --- a/src/components/container/language/LanguageTable.svelte +++ b/src/components/container/language/LanguageTable.svelte @@ -2,6 +2,7 @@ import * as Table from '$components/ui/table' import * as Form from '$components/ui/form' import { Input } from '$components/ui/input' + import { Trash2 } from 'lucide-svelte' import type { LanguagesSchema } from './schema' import type { SuperForm } from 'sveltekit-superforms' @@ -63,8 +64,35 @@ + + + {/if} {/each} + + diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts index d87f33a0..a2b7b8c5 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.server.ts @@ -1,7 +1,11 @@ -import { getLanguagesForProject, updateLanguage } from 'services/language/language-service' +import { + deleteLanguage, + getLanguagesForProject, + upsertLanguagesForProject +} from 'services/language/language-service' import type { Actions, PageServerLoad } from './$types' import { superValidate } from 'sveltekit-superforms/server' -import { languageSchema, languagesSchema } from '$components/container/language/schema' +import { languagesSchema } from '$components/container/language/schema' import { zod } from 'sveltekit-superforms/adapters' import { fail } from '@sveltejs/kit' @@ -14,21 +18,30 @@ export const load: PageServerLoad = async ({ params }) => { } export const actions: Actions = { - default: async ({ request }) => { - const formData = await request.formData() - // TODO: check if correct schema used - const form = await superValidate(formData, zod(languageSchema)) + upsert: async ({ request, params }) => { + const form = await superValidate(request, zod(languagesSchema)) - if (!form.valid) { - return fail(400, { form }) - } + if (!form.valid) return fail(400, { form }) try { - await updateLanguage(form.data) + const projectId = Number(params.id) + form.data.languages = await upsertLanguagesForProject(projectId, form.data.languages) return { form } } catch (error) { - return fail(500, { form, error: 'Failed to update language' }) + console.error('Failed to update languages:', error) + + return fail(500, { form, error: 'Failed to update languages' }) } + }, + delete: async ({ request, params }) => { + const data = await request.formData() + const languageId = data.get('deleteLanguage') + + if (typeof languageId !== 'string') return fail(400, { message: 'Invalid language to delete.' }) + + const languages = await deleteLanguage(Number(params.id), Number(languageId)) + + return { form: await superValidate({ languages }, zod(languagesSchema)) } } } diff --git a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte index aa6af3d3..91787c73 100644 --- a/src/routes/(authenticated)/projects/[id]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[id]/languages/+page.svelte @@ -17,15 +17,17 @@ let selectedLanguage: LanguageCode | undefined = undefined + // let form: SuperForm const form = superForm(data.form, { validators: zodClient(languagesSchema), dataType: 'json', + resetForm: false, async onUpdated({ form }) { - if (form.message) { + if (form.valid) { + toast.success('Languages updated successfully') + } else { if ($page.status >= 400) { - toast.error(form.message) - } else { - toast.success(form.message.message) + toast.error(form.message ?? 'Failed to update languages') } } } @@ -55,7 +57,7 @@
- Save + Save
From d48e2ea0aaded77bb346ab942243526dd3fd2a1f Mon Sep 17 00:00:00 2001 From: mledl Date: Fri, 30 Aug 2024 18:06:21 +0200 Subject: [PATCH 27/39] fixes language service tests after merge --- services/src/language/language-service.unit.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/src/language/language-service.unit.test.ts b/services/src/language/language-service.unit.test.ts index bba8a8e9..c98cc35e 100644 --- a/services/src/language/language-service.unit.test.ts +++ b/services/src/language/language-service.unit.test.ts @@ -53,28 +53,30 @@ beforeEach(() => { describe('Language Service', () => { describe('getLanguagesForProject', () => { + const mockedProjectSlug = 'test-slug' + it('should call the repository to get languages for a project', async () => { vi.mocked(repository.getLanguagesForProject).mockResolvedValue(mockSelectableLanguages) - const languages = await getLanguagesForProject('test-slug') + const languages = await getLanguagesForProject(mockedProjectSlug) - expect(repository.getLanguagesForProject).toHaveBeenCalledWith(1) + expect(repository.getLanguagesForProject).toHaveBeenCalledWith(mockedProjectSlug) expect(languages).toEqual(expectedLanguageSchemas) }) it('should return an empty array when there are no languages for the project', async () => { vi.mocked(repository.getLanguagesForProject).mockResolvedValue([]) - const languages = await getLanguagesForProject('test-slug') + const languages = await getLanguagesForProject(mockedProjectSlug) - expect(repository.getLanguagesForProject).toHaveBeenCalledWith(1) + expect(repository.getLanguagesForProject).toHaveBeenCalledWith(mockedProjectSlug) expect(languages).toEqual([]) }) it('should throw an error if the repository throws an error', async () => { vi.mocked(repository.getLanguagesForProject).mockRejectedValue(new Error('Repository error')) - await expect(getLanguagesForProject('test-slug')).rejects.toThrow('Repository error') + await expect(getLanguagesForProject(mockedProjectSlug)).rejects.toThrow('Repository error') }) }) From 8574f1b22463d3ee630e0198e54710168300a5e8 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 31 Aug 2024 11:22:09 +0200 Subject: [PATCH 28/39] disallows deletetion of base language from ui and base language cannot have a fallback --- .../language-repository.integration.test.ts | 45 ++++++++++++++ services/src/language/language-repository.ts | 11 ++++ services/src/language/language-service.ts | 7 +++ .../language/language-service.unit.test.ts | 60 +++++++++++++++++++ .../container/language/LanguageTable.svelte | 49 ++++++++------- .../projects/[slug]/languages/+page.server.ts | 5 +- .../projects/[slug]/languages/+page.svelte | 3 +- 7 files changed, 155 insertions(+), 25 deletions(-) diff --git a/services/src/language/language-repository.integration.test.ts b/services/src/language/language-repository.integration.test.ts index bc7fd51d..8709da2f 100644 --- a/services/src/language/language-repository.integration.test.ts +++ b/services/src/language/language-repository.integration.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { deleteLanguage, + getBaseLanguageForProject, getLanguagesForProject, updateLanguage, upsertLanguages @@ -323,4 +324,48 @@ describe('Language Repository', () => { expect(dbLanguages[0]?.id).toBe(project.base_language_id) }) }) + + describe('getBaseLanguageForProject', () => { + it('should return the base language for a project', async () => { + const project = await createProject(projectCreationObject) + const baseLanguage = await getBaseLanguageForProject(project.slug) + + expect(baseLanguage).toMatchObject({ + id: expect.any(Number), + code: 'en', + label: 'English', + fallback_language: null + }) + }) + + it('should return the correct base language when multiple languages exist', async () => { + const project = await createProject(projectCreationObject) + + // Add another language + await db + .insertInto('languages') + .values({ + code: 'fr', + label: 'French', + project_id: project.id, + fallback_language: project.base_language_id + }) + .execute() + + const baseLanguage = await getBaseLanguageForProject(project.slug) + + expect(baseLanguage).toMatchObject({ + id: project.base_language_id, + code: 'en', + label: 'English', + fallback_language: null + }) + }) + + it('should throw an error when the project does not exist', async () => { + const nonExistentSlug = 'non-existent-project' + + await expect(getBaseLanguageForProject(nonExistentSlug)).rejects.toThrow() + }) + }) }) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index 1b538ee4..b8386969 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -26,6 +26,17 @@ export function getLanguagesForProject(slug: string): Promise { + const baseLanguage = await db + .selectFrom('languages') + .leftJoin('projects', 'languages.id', 'projects.base_language_id') + .where('projects.slug', '=', slug) + .select(['languages.id', 'languages.code', 'languages.label']) + .executeTakeFirstOrThrow() + + return { ...baseLanguage, fallback_language: null } +} + export async function updateLanguage(language: LanguageSchema): Promise { const updatedLanguages = await db .updateTable('languages') diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts index cd402fd4..609c9533 100644 --- a/services/src/language/language-service.ts +++ b/services/src/language/language-service.ts @@ -18,6 +18,13 @@ export async function getLanguagesForProject(slug: string): Promise { + const baseLanguage = await repository.getBaseLanguageForProject(slug) + if (!baseLanguage) throw new Error('No base language found for project') + + return mapToLanguage(baseLanguage) +} + export async function updateLanguage(language: LanguageSchema): Promise { if (!language.id) { throw new Error('Language ID is required for updating') diff --git a/services/src/language/language-service.unit.test.ts b/services/src/language/language-service.unit.test.ts index c98cc35e..9f03531d 100644 --- a/services/src/language/language-service.unit.test.ts +++ b/services/src/language/language-service.unit.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { deleteLanguage, + getBaseLanguageForProject, getLanguagesForProject, updateLanguage, upsertLanguagesForProject @@ -12,6 +13,7 @@ import type { LanguageCode } from '$components/container/language/languages' vi.mock('./language-repository', () => ({ getLanguagesForProject: vi.fn(), + getBaseLanguageForProject: vi.fn(), updateLanguage: vi.fn(), upsertLanguages: vi.fn(), deleteLanguage: vi.fn() @@ -242,4 +244,62 @@ describe('Language Service', () => { ) }) }) + + describe('getBaseLanguageForProject', () => { + const mockedProjectSlug = 'test-slug' + const mockBaseLanguage: SelectableLanguage = { + id: 1, + code: 'en', + label: 'English', + fallback_language: null + } + + const expectedBaseLanguageSchema: LanguageSchema = { + id: 1, + code: 'en' as LanguageCode, + label: 'English', + fallback: undefined + } + + it('should call the repository to get the base language for a project', async () => { + vi.mocked(repository.getBaseLanguageForProject).mockResolvedValue(mockBaseLanguage) + + const baseLanguage = await getBaseLanguageForProject(mockedProjectSlug) + + expect(repository.getBaseLanguageForProject).toHaveBeenCalledWith(mockedProjectSlug) + expect(baseLanguage).toEqual(expectedBaseLanguageSchema) + }) + + it('should handle a base language with a fallback correctly', async () => { + const mockBaseLanguageWithFallback: SelectableLanguage = { + ...mockBaseLanguage, + fallback_language: 'fr' + } + vi.mocked(repository.getBaseLanguageForProject).mockResolvedValue( + mockBaseLanguageWithFallback + ) + + const baseLanguage = await getBaseLanguageForProject(mockedProjectSlug) + + expect(baseLanguage.fallback).toBe('fr') + }) + + it('should throw an error if the repository throws an error', async () => { + vi.mocked(repository.getBaseLanguageForProject).mockRejectedValue( + new Error('Repository error') + ) + + await expect(getBaseLanguageForProject(mockedProjectSlug)).rejects.toThrow('Repository error') + }) + + it('should throw an error if no base language is found', async () => { + vi.mocked(repository.getBaseLanguageForProject).mockResolvedValue( + null as unknown as SelectableLanguage + ) + + await expect(getBaseLanguageForProject(mockedProjectSlug)).rejects.toThrow( + 'No base language found for project' + ) + }) + }) }) diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte index af430575..2bedcf39 100644 --- a/src/components/container/language/LanguageTable.svelte +++ b/src/components/container/language/LanguageTable.svelte @@ -3,11 +3,12 @@ import * as Form from '$components/ui/form' import { Input } from '$components/ui/input' import { Trash2 } from 'lucide-svelte' - import type { LanguagesSchema } from './schema' + import type { LanguageSchema, LanguagesSchema } from './schema' import type { SuperForm } from 'sveltekit-superforms' // https://superforms.rocks/concepts/nested-data export let form: SuperForm + export let baseLanguage: LanguageSchema const { form: formData } = form @@ -52,29 +53,33 @@ - - - - - - + {#if $formData.languages[i].code !== baseLanguage.code} + + + + + + + {/if} - + {#if $formData.languages[i].code !== baseLanguage.code} + + {/if} {/if} diff --git a/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts index b28f8f49..6a88879c 100644 --- a/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts +++ b/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts @@ -1,5 +1,6 @@ import { deleteLanguage, + getBaseLanguageForProject, getLanguagesForProject, upsertLanguagesForProject } from 'services/language/language-service' @@ -11,9 +12,11 @@ import { fail } from '@sveltejs/kit' export const load: PageServerLoad = async ({ params }) => { const languages = await getLanguagesForProject(params.slug) + const baseLanguage = await getBaseLanguageForProject(params.slug) return { - form: await superValidate({ languages }, zod(languagesSchema)) + form: await superValidate({ languages }, zod(languagesSchema)), + baseLanguage } } diff --git a/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte b/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte index 91787c73..11f04d58 100644 --- a/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte @@ -17,7 +17,6 @@ let selectedLanguage: LanguageCode | undefined = undefined - // let form: SuperForm const form = superForm(data.form, { validators: zodClient(languagesSchema), dataType: 'json', @@ -71,6 +70,6 @@
- + From 10656aa9d5cf510290d79d9980f7765d7ef9b23c Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 31 Aug 2024 11:41:39 +0200 Subject: [PATCH 29/39] makes create project e2e test use language select --- e2e/specs/create-project-flow.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/specs/create-project-flow.spec.ts b/e2e/specs/create-project-flow.spec.ts index 063da438..35f047c3 100644 --- a/e2e/specs/create-project-flow.spec.ts +++ b/e2e/specs/create-project-flow.spec.ts @@ -12,8 +12,8 @@ test.describe('create project', () => { await page.getByTestId('create-project-modal-trigger').click() await page.getByTestId('create-project-name-input').fill(projectName) - await page.getByTestId('create-project-base-language-select').fill('en') - + await page.getByTestId('create-project-base-language-select').click() + await page.getByRole('option', { name: 'en - English' }).click() await page.getByTestId('create-project-submit-button').click() await expect(page.getByTestId('project-card-name')).toHaveText(projectName) From d0d13ddfc198ff9f4d6653bdcdfb62afbe05dbc0 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 31 Aug 2024 11:57:03 +0200 Subject: [PATCH 30/39] only allows for saving languages once form got tainted --- .../projects/[slug]/languages/+page.svelte | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte b/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte index 11f04d58..44f768bd 100644 --- a/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte @@ -11,7 +11,7 @@ import { languagesSchema } from '$components/container/language/schema' import LanguageTable from '$components/container/language/LanguageTable.svelte' import { Button } from '$components/ui/button' - import { Plus } from 'lucide-svelte' + import { Check, Plus } from 'lucide-svelte' export let data: PageData @@ -32,7 +32,7 @@ } }) - const { enhance, form: formData } = form + const { enhance, form: formData, tainted, isTainted } = form function addLanguage() { if (selectedLanguage && availableLanguages[selectedLanguage]) { @@ -56,7 +56,10 @@
- Save + + + Save +
From 45735b5cc8a9dfd34c9d0320d3d41a90d07c4dba Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 14 Sep 2024 15:54:20 +0200 Subject: [PATCH 31/39] relaxes schema to have code as string and add deletion of language --- services/src/language/language-repository.ts | 11 ++- services/src/language/language-service.ts | 12 ++- services/src/project/project-service.ts | 1 - .../container/language/LanguageSelect.svelte | 18 ++-- .../container/language/LanguageTable.svelte | 83 ++++++++++++------- src/components/container/language/schema.ts | 7 +- .../projects/[slug]/languages/+page.server.ts | 8 +- .../projects/[slug]/languages/+page.svelte | 10 ++- 8 files changed, 99 insertions(+), 51 deletions(-) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index b8386969..e865767b 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -67,6 +67,7 @@ export async function upsertLanguages( const languagesToUpsert = await Promise.all( languages.map(async (language) => ({ + id: language.id, project_id: project.id, code: language.code, label: language.label, @@ -78,7 +79,8 @@ export async function upsertLanguages( .insertInto('languages') .values(languagesToUpsert) .onConflict((oc) => - oc.columns(['project_id', 'code']).doUpdateSet({ + oc.column('id').doUpdateSet({ + code: (eb) => eb.ref('excluded.code'), label: (eb) => eb.ref('excluded.label'), fallback_language: (eb) => eb.ref('excluded.fallback_language') }) @@ -93,8 +95,11 @@ export async function upsertLanguages( })) } +/** + * @throws {Error} if language id does not exist or language could not be deleted + */ export async function deleteLanguage(id: number): Promise { - const result = await db.deleteFrom('languages').where('id', '=', id).execute() + const result = await db.deleteFrom('languages').where('id', '=', id).executeTakeFirstOrThrow() - if (result[0]?.numDeletedRows === 0n) throw new Error(`Failed to delete language with id ${id}`) + if (result.numDeletedRows === 0n) throw new Error(`Failed to delete language with id ${id}`) } diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts index 609c9533..adbbb47f 100644 --- a/services/src/language/language-service.ts +++ b/services/src/language/language-service.ts @@ -1,5 +1,6 @@ import type { LanguageCode } from '$components/container/language/languages' import type { LanguageId, LanguageSchema } from '$components/container/language/schema' +import type { Logger } from 'pino' import * as repository from './language-repository' import type { SelectableLanguage } from './language.model' @@ -44,8 +45,15 @@ export async function upsertLanguagesForProject( return upsertedLanguages.map(mapToLanguage) } -export async function deleteLanguage(projectSlug: string, languageId: number) { - await repository.deleteLanguage(languageId) +export async function deleteLanguage(projectSlug: string, languageId: number, logger: Logger) { + try { + await repository.deleteLanguage(languageId) + } catch (err: unknown) { + let message = 'Failed to delete language.' + if (err instanceof Error) message = err.message + + logger.info(message) + } return getLanguagesForProject(projectSlug) } diff --git a/services/src/project/project-service.ts b/services/src/project/project-service.ts index f86a1064..3f7b6449 100644 --- a/services/src/project/project-service.ts +++ b/services/src/project/project-service.ts @@ -10,7 +10,6 @@ export async function createProject(project: CreateProjectFormSchema) { return await repository.createProject({ ...project, slug }) } catch (e: unknown) { - console.warn(e) if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT_UNIQUE') { throw new CreateProjectNameNotUniqueError() } diff --git a/src/components/container/language/LanguageSelect.svelte b/src/components/container/language/LanguageSelect.svelte index 53bef938..da8ead63 100644 --- a/src/components/container/language/LanguageSelect.svelte +++ b/src/components/container/language/LanguageSelect.svelte @@ -2,16 +2,24 @@ import * as Select from '$components/ui/select' import { type LanguageCode, availableLanguages } from './languages' + type LanguageOption = { + value: string + label: string + } + export let name: string - export let value: LanguageCode | undefined + export let value: string | undefined + export let languages: LanguageOption[] | undefined = undefined export let placeholder = 'Select Language' export let disabled = false export let typeahead = true - const items = Object.entries(availableLanguages).map(([value, label]) => ({ - value: value as LanguageCode, - label - })) + const items = + languages ?? + Object.entries(availableLanguages).map(([value, label]) => ({ + value: value as LanguageCode, + label + })) (value = s?.value)}> diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte index 2bedcf39..9b6b09f1 100644 --- a/src/components/container/language/LanguageTable.svelte +++ b/src/components/container/language/LanguageTable.svelte @@ -5,12 +5,17 @@ import { Trash2 } from 'lucide-svelte' import type { LanguageSchema, LanguagesSchema } from './schema' import type { SuperForm } from 'sveltekit-superforms' + import LanguageSelect from './LanguageSelect.svelte' + import { createEventDispatcher } from 'svelte' // https://superforms.rocks/concepts/nested-data export let form: SuperForm export let baseLanguage: LanguageSchema - const { form: formData } = form + const dispatch = createEventDispatcher<{ deleteLanguage: string | undefined }>() + $: ({ form: formData } = form) + + $: fallbackLanguages = $formData.languages.map(({ code, label }) => ({ value: code, label })) @@ -23,18 +28,20 @@ - {#each $formData.languages as _, i} + {#each $formData.languages as language, i (language.id)} {#if $formData.languages[i]} - + {#if $formData.languages[i]} + + {/if} @@ -42,12 +49,14 @@ - + {#if $formData.languages[i]} + + {/if} @@ -56,12 +65,15 @@ {#if $formData.languages[i].code !== baseLanguage.code} - + {#if $formData.languages[i]} + + {/if} @@ -69,16 +81,27 @@ {#if $formData.languages[i].code !== baseLanguage.code} - + {#if $formData.languages[i].id} + + {:else} + + {/if} {/if} diff --git a/src/components/container/language/schema.ts b/src/components/container/language/schema.ts index 347ec7e1..cef5b67a 100644 --- a/src/components/container/language/schema.ts +++ b/src/components/container/language/schema.ts @@ -1,17 +1,14 @@ import { z } from 'zod' -import { type LanguageCode, availableLanguages } from './languages' export const languageSchema = z.object({ id: z.number().optional(), - code: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]], { - required_error: 'Language code is required' - }), + code: z.string().min(1, { message: 'Language code is required' }), label: z .string({ required_error: 'Language label is required' }) .min(1, 'Language label must at least consist of a single character'), - fallback: z.enum(Object.keys(availableLanguages) as [LanguageCode, ...LanguageCode[]]).optional() + fallback: z.string().optional() }) export const languagesSchema = z.object({ languages: z.array(languageSchema) }) diff --git a/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts b/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts index 6a88879c..23aed559 100644 --- a/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts +++ b/src/routes/(authenticated)/projects/[slug]/languages/+page.server.ts @@ -36,13 +36,13 @@ export const actions: Actions = { return fail(500, { form, error: 'Failed to update languages' }) } }, - delete: async ({ request, params }) => { + delete: async ({ request, params, locals: { logger } }) => { const data = await request.formData() - const languageId = data.get('deleteLanguage') - if (typeof languageId !== 'string') return fail(400, { message: 'Invalid language to delete.' }) + const languageId = Number(data.get('deleteLanguage')) + if (isNaN(languageId)) return fail(400, { message: 'Invalid language to delete.' }) - const languages = await deleteLanguage(params.slug, Number(languageId)) + const languages = await deleteLanguage(params.slug, languageId, logger) return { form: await superValidate({ languages }, zod(languagesSchema)) } } diff --git a/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte b/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte index 44f768bd..23d749df 100644 --- a/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte +++ b/src/routes/(authenticated)/projects/[slug]/languages/+page.svelte @@ -50,6 +50,10 @@ toast.error('Please select a valid language') } } + function deleteUnpersistedLanguge(e: CustomEvent) { + const code = e.detail + if (code) $formData.languages = $formData.languages.filter((language) => language.code !== code) + } @@ -73,6 +77,10 @@ - + From d0e256edb3fd1843f5cf074ee21a6e1fafc9e971 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 21 Sep 2024 12:37:20 +0200 Subject: [PATCH 32/39] checks for locale already used --- src/components/container/language/schema.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/container/language/schema.ts b/src/components/container/language/schema.ts index cef5b67a..8132d1a2 100644 --- a/src/components/container/language/schema.ts +++ b/src/components/container/language/schema.ts @@ -11,7 +11,23 @@ export const languageSchema = z.object({ fallback: z.string().optional() }) -export const languagesSchema = z.object({ languages: z.array(languageSchema) }) +export const languagesSchema = z.object({ + languages: z.array(languageSchema).superRefine((languages, ctx) => { + const seenCodes = new Set() + + languages.forEach((language, index) => { + if (seenCodes.has(language.code)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Language codes must be unique', + path: [`${index}`, 'code'] + }) + } else { + seenCodes.add(language.code) + } + }) + }) +}) export type LanguageSchema = z.infer export type LanguagesSchema = z.infer From 04d5778c9c4c6362abb2f6b48e6798e0162292ca Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 21 Sep 2024 13:43:05 +0200 Subject: [PATCH 33/39] improves deletion and upserting of languages --- services/src/language/language-repository.ts | 22 ++++++++++++++------ services/src/language/language-service.ts | 18 ++++++++++++++-- services/src/project/project-repository.ts | 6 ++++-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index e865767b..b83081ec 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -3,12 +3,14 @@ import { db } from '../db/database' import type { LanguageSchema } from '$components/container/language/schema' import type { LanguageCode } from '$components/container/language/languages' import { getProjectBySlug } from 'services/project/project-repository' +import type { Transaction } from 'kysely' +import type { DB } from 'kysely-codegen' -async function getFallbackLanguageId(fallback: LanguageCode | undefined) { +async function getFallbackLanguageId(fallback: LanguageCode | undefined, tx?: Transaction) { if (!fallback) return null return ( - await db + await (tx ?? db) .selectFrom('languages') .where('code', '=', fallback) .select('id') @@ -61,9 +63,10 @@ export async function updateLanguage(language: LanguageSchema): Promise ): Promise { - const project = await getProjectBySlug(projectSlug) + const project = await getProjectBySlug(projectSlug, tx) const languagesToUpsert = await Promise.all( languages.map(async (language) => ({ @@ -71,11 +74,11 @@ export async function upsertLanguages( project_id: project.id, code: language.code, label: language.label, - fallback_language: await getFallbackLanguageId(language.fallback) + fallback_language: await getFallbackLanguageId(language.fallback, tx) })) ) - const upsertedLanguages = await db + const upsertedLanguages = await (tx ?? db) .insertInto('languages') .values(languagesToUpsert) .onConflict((oc) => @@ -99,6 +102,13 @@ export async function upsertLanguages( * @throws {Error} if language id does not exist or language could not be deleted */ export async function deleteLanguage(id: number): Promise { + // remove from fallback languages first + await db + .updateTable('languages') + .set({ fallback_language: null }) + .where('languages.fallback_language', '=', id) + .execute() + const result = await db.deleteFrom('languages').where('id', '=', id).executeTakeFirstOrThrow() if (result.numDeletedRows === 0n) throw new Error(`Failed to delete language with id ${id}`) diff --git a/services/src/language/language-service.ts b/services/src/language/language-service.ts index adbbb47f..7479af07 100644 --- a/services/src/language/language-service.ts +++ b/services/src/language/language-service.ts @@ -3,6 +3,7 @@ import type { LanguageId, LanguageSchema } from '$components/container/language/ import type { Logger } from 'pino' import * as repository from './language-repository' import type { SelectableLanguage } from './language.model' +import { db } from 'services/db/database' function mapToLanguage(language: SelectableLanguage): LanguageSchema { return { @@ -40,9 +41,22 @@ export async function upsertLanguagesForProject( projectSlug: string, languages: LanguageSchema[] ): Promise { - const upsertedLanguages = await repository.upsertLanguages(projectSlug, languages) + return db.transaction().execute(async (tx) => { + // add new languages first to make them referencable as fallback + const addedLanguages = await repository.upsertLanguages( + projectSlug, + languages.filter((l) => l.id === undefined), + tx + ) - return upsertedLanguages.map(mapToLanguage) + const updatedLanguages = await repository.upsertLanguages( + projectSlug, + languages.filter((l) => l.id !== undefined), + tx + ) + + return [...updatedLanguages, ...addedLanguages].map(mapToLanguage) + }) } export async function deleteLanguage(projectSlug: string, languageId: number, logger: Logger) { diff --git a/services/src/project/project-repository.ts b/services/src/project/project-repository.ts index 9b93af1c..beaec7aa 100644 --- a/services/src/project/project-repository.ts +++ b/services/src/project/project-repository.ts @@ -1,5 +1,7 @@ import type { CreateProjectFormSchema, SelectableProject } from './project' import { db } from '../db/database' +import type { Transaction } from 'kysely' +import type { DB } from 'kysely-codegen' export function createProject(project: CreateProjectFormSchema): Promise { return db.transaction().execute(async (tx) => { @@ -34,8 +36,8 @@ export function getAllProjects(): Promise { return db.selectFrom('projects').selectAll().execute() } -export function getProjectBySlug(slug: string): Promise { - return db +export function getProjectBySlug(slug: string, tx?: Transaction): Promise { + return (tx ?? db) .selectFrom('projects') .selectAll() .where('slug', '=', slug) From adcc415b1f18ffa7882265a6a1f7f69e4dcbb867 Mon Sep 17 00:00:00 2001 From: mledl Date: Sat, 21 Sep 2024 13:50:51 +0200 Subject: [PATCH 34/39] fixes calls after refacoring service methods --- services/src/language/language-repository.ts | 2 +- services/src/language/language-service.unit.test.ts | 11 +++++++---- {tests => services/src}/unit-test.utils.ts | 0 3 files changed, 8 insertions(+), 5 deletions(-) rename {tests => services/src}/unit-test.utils.ts (100%) diff --git a/services/src/language/language-repository.ts b/services/src/language/language-repository.ts index b83081ec..44a29134 100644 --- a/services/src/language/language-repository.ts +++ b/services/src/language/language-repository.ts @@ -6,7 +6,7 @@ import { getProjectBySlug } from 'services/project/project-repository' import type { Transaction } from 'kysely' import type { DB } from 'kysely-codegen' -async function getFallbackLanguageId(fallback: LanguageCode | undefined, tx?: Transaction) { +async function getFallbackLanguageId(fallback: string | undefined, tx?: Transaction) { if (!fallback) return null return ( diff --git a/services/src/language/language-service.unit.test.ts b/services/src/language/language-service.unit.test.ts index 9f03531d..cb2d6a13 100644 --- a/services/src/language/language-service.unit.test.ts +++ b/services/src/language/language-service.unit.test.ts @@ -10,6 +10,7 @@ import * as repository from './language-repository' import type { SelectableLanguage } from './language.model' import type { LanguageSchema } from '$components/container/language/schema' import type { LanguageCode } from '$components/container/language/languages' +import { mockedLogger } from 'services/unit-test.utils' vi.mock('./language-repository', () => ({ getLanguagesForProject: vi.fn(), @@ -209,7 +210,7 @@ describe('Language Service', () => { mockSelectableLanguages[0] as SelectableLanguage ]) - const updatedLanguages = await deleteLanguage(mockProjectSlug, mockLanguageId) + const updatedLanguages = await deleteLanguage(mockProjectSlug, mockLanguageId, mockedLogger) expect(repository.deleteLanguage).toHaveBeenCalledWith(mockLanguageId) expect(repository.getLanguagesForProject).toHaveBeenCalledWith(mockProjectSlug) @@ -220,7 +221,7 @@ describe('Language Service', () => { vi.mocked(repository.deleteLanguage).mockResolvedValue() vi.mocked(repository.getLanguagesForProject).mockResolvedValue([]) - const updatedLanguages = await deleteLanguage(mockProjectSlug, mockLanguageId) + const updatedLanguages = await deleteLanguage(mockProjectSlug, mockLanguageId, mockedLogger) expect(repository.deleteLanguage).toHaveBeenCalledWith(mockLanguageId) expect(repository.getLanguagesForProject).toHaveBeenCalledWith(mockProjectSlug) @@ -230,7 +231,9 @@ describe('Language Service', () => { it('should throw an error if the repository throws an error during deletion', async () => { vi.mocked(repository.deleteLanguage).mockRejectedValue(new Error('Delete failed')) - await expect(deleteLanguage(mockProjectSlug, mockLanguageId)).rejects.toThrow('Delete failed') + await expect(deleteLanguage(mockProjectSlug, mockLanguageId, mockedLogger)).rejects.toThrow( + 'Delete failed' + ) }) it('should throw an error if getting updated languages fails', async () => { @@ -239,7 +242,7 @@ describe('Language Service', () => { new Error('Get languages failed') ) - await expect(deleteLanguage(mockProjectSlug, mockLanguageId)).rejects.toThrow( + await expect(deleteLanguage(mockProjectSlug, mockLanguageId, mockedLogger)).rejects.toThrow( 'Get languages failed' ) }) diff --git a/tests/unit-test.utils.ts b/services/src/unit-test.utils.ts similarity index 100% rename from tests/unit-test.utils.ts rename to services/src/unit-test.utils.ts From 78119172b93e90eb4301934eabf8b68112ef221b Mon Sep 17 00:00:00 2001 From: mledl Date: Tue, 24 Sep 2024 22:39:33 +0200 Subject: [PATCH 35/39] adds confirmation dialog to confirm deletion of language --- .../container/language/LanguageSelect.svelte | 11 +-- .../container/language/LanguageTable.svelte | 77 ++++++++++++++++--- .../layout/dialog/ConfirmationDialog.svelte | 53 +++++++++++++ src/components/ui/button/index.ts | 1 + .../projects/[slug]/languages/+page.svelte | 8 +- 5 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 src/components/layout/dialog/ConfirmationDialog.svelte diff --git a/src/components/container/language/LanguageSelect.svelte b/src/components/container/language/LanguageSelect.svelte index da8ead63..99ec1a50 100644 --- a/src/components/container/language/LanguageSelect.svelte +++ b/src/components/container/language/LanguageSelect.svelte @@ -1,11 +1,9 @@ - (value = s?.value)}> + diff --git a/src/components/container/language/LanguageTable.svelte b/src/components/container/language/LanguageTable.svelte index 9b6b09f1..38b6cc81 100644 --- a/src/components/container/language/LanguageTable.svelte +++ b/src/components/container/language/LanguageTable.svelte @@ -7,15 +7,55 @@ import type { SuperForm } from 'sveltekit-superforms' import LanguageSelect from './LanguageSelect.svelte' import { createEventDispatcher } from 'svelte' + import ConfirmationDialog, { + type DialogCtaProps + } from '$components/layout/dialog/ConfirmationDialog.svelte' // https://superforms.rocks/concepts/nested-data export let form: SuperForm export let baseLanguage: LanguageSchema + let isDeleteModalOpen = false + let deleteCtaProps: DialogCtaProps + let deleteLanguage = '' + const dispatch = createEventDispatcher<{ deleteLanguage: string | undefined }>() $: ({ form: formData } = form) - $: fallbackLanguages = $formData.languages.map(({ code, label }) => ({ value: code, label })) + const getFallbackLanguages = (code: string) => + $formData.languages + .filter((l) => l.code !== code) + .map(({ code, label }) => ({ value: code, label })) + + const openDeleteModal = (language: LanguageSchema | undefined) => { + if (!language) return + + const baseProps: Omit = { + label: 'Delete', + name: 'deleteLanguage', + value: language.id, + variant: 'destructive' + } + + if (language.id) { + deleteCtaProps = { + ...baseProps, + formaction: '?/delete', + formId: 'languagesForm', + onClick: undefined + } + } else if (language.code) { + deleteCtaProps = { + ...baseProps, + onClick: () => dispatch('deleteLanguage', language.code), + formaction: undefined, + formId: undefined + } + } + + deleteLanguage = language.label + isDeleteModalOpen = true + } @@ -66,13 +106,15 @@ {#if $formData.languages[i]} - + {#key $formData.languages.length} + + {/key} {/if} @@ -81,7 +123,7 @@ {#if $formData.languages[i].code !== baseLanguage.code} - {#if $formData.languages[i].id} + + {/if} @@ -110,6 +160,13 @@ + +