From 8f4366e38c6bf6836dd4cf83516eb2ffa78aa4b6 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Tue, 4 Jun 2024 22:33:16 +0200 Subject: [PATCH 01/21] Added header to projects Signed-off-by: Benjamin Strasser --- src/components/layout/main-content/header.svelte | 8 ++++++++ src/components/layout/main-content/index.ts | 4 ++++ .../layout/main-content/main-content.svelte | 3 +++ src/routes/(authenticated)/projects/+page.svelte | 11 ++++++++++- 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/components/layout/main-content/header.svelte create mode 100644 src/components/layout/main-content/index.ts create mode 100644 src/components/layout/main-content/main-content.svelte diff --git a/src/components/layout/main-content/header.svelte b/src/components/layout/main-content/header.svelte new file mode 100644 index 00000000..5f2bdf16 --- /dev/null +++ b/src/components/layout/main-content/header.svelte @@ -0,0 +1,8 @@ + + +
+

{title}

+ +
diff --git a/src/components/layout/main-content/index.ts b/src/components/layout/main-content/index.ts new file mode 100644 index 00000000..4ea24cff --- /dev/null +++ b/src/components/layout/main-content/index.ts @@ -0,0 +1,4 @@ +import MainContent from './main-content.svelte' +import MainContentHeader from './header.svelte' + +export { MainContent, MainContentHeader } diff --git a/src/components/layout/main-content/main-content.svelte b/src/components/layout/main-content/main-content.svelte new file mode 100644 index 00000000..f1250348 --- /dev/null +++ b/src/components/layout/main-content/main-content.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/routes/(authenticated)/projects/+page.svelte b/src/routes/(authenticated)/projects/+page.svelte index c07db15a..0dea9d5a 100644 --- a/src/routes/(authenticated)/projects/+page.svelte +++ b/src/routes/(authenticated)/projects/+page.svelte @@ -1 +1,10 @@ -Projects + + + + + + + From 8fb54c3fa10136f11e4da63117e78f0cc9830f01 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 5 Jun 2024 10:07:09 +0200 Subject: [PATCH 02/21] added basic card layout and design Signed-off-by: Benjamin Strasser --- .../layout/main-content/main-content.svelte | 2 +- .../(authenticated)/projects/+page.svelte | 2 + .../projects/project-cards.svelte | 53 +++++++++++++++++++ src/routes/+layout.svelte | 2 +- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/routes/(authenticated)/projects/project-cards.svelte diff --git a/src/components/layout/main-content/main-content.svelte b/src/components/layout/main-content/main-content.svelte index f1250348..5bd2dbbc 100644 --- a/src/components/layout/main-content/main-content.svelte +++ b/src/components/layout/main-content/main-content.svelte @@ -1,3 +1,3 @@ -
+
diff --git a/src/routes/(authenticated)/projects/+page.svelte b/src/routes/(authenticated)/projects/+page.svelte index 0dea9d5a..75e6b00b 100644 --- a/src/routes/(authenticated)/projects/+page.svelte +++ b/src/routes/(authenticated)/projects/+page.svelte @@ -1,10 +1,12 @@ + diff --git a/src/routes/(authenticated)/projects/project-cards.svelte b/src/routes/(authenticated)/projects/project-cards.svelte new file mode 100644 index 00000000..c898e327 --- /dev/null +++ b/src/routes/(authenticated)/projects/project-cards.svelte @@ -0,0 +1,53 @@ + + +
+ {#each projects as project} + + + {project.name} + + +
+
+
+
+
+ + Last updated: {project.lastUpdated} + +
+ {/each} +
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b3b38ee2..dee0a318 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,7 +4,7 @@ import { browser } from '$app/environment' -
+
From 29251b3fcd46ce3851296df22737187fb6736093 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 5 Jun 2024 12:14:02 +0200 Subject: [PATCH 03/21] added basic navigation Signed-off-by: Benjamin Strasser --- .../projects/project-cards.svelte | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/routes/(authenticated)/projects/project-cards.svelte b/src/routes/(authenticated)/projects/project-cards.svelte index c898e327..74810bdc 100644 --- a/src/routes/(authenticated)/projects/project-cards.svelte +++ b/src/routes/(authenticated)/projects/project-cards.svelte @@ -34,20 +34,22 @@
- {#each projects as project} - - - {project.name} - - -
-
-
-
-
- - Last updated: {project.lastUpdated} - -
+ {#each projects as project, index} + + + + {project.name} + + +
+
+
+
+
+ + Last updated: {project.lastUpdated} + +
+
{/each}
From 547cb8ae5e7bfae16532ef7c5f5ca99608b7e0c2 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 5 Jun 2024 13:33:59 +0200 Subject: [PATCH 04/21] added popover to project card Signed-off-by: Benjamin Strasser --- src/components/ui/popover/index.ts | 18 +++++++++++++++ .../ui/popover/popover-content.svelte | 22 +++++++++++++++++++ .../projects/project-cards.svelte | 15 ++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/popover/index.ts create mode 100644 src/components/ui/popover/popover-content.svelte diff --git a/src/components/ui/popover/index.ts b/src/components/ui/popover/index.ts new file mode 100644 index 00000000..4ffbcda7 --- /dev/null +++ b/src/components/ui/popover/index.ts @@ -0,0 +1,18 @@ +import { Popover as PopoverPrimitive } from 'bits-ui' +import Content from './popover-content.svelte' + +const Root = PopoverPrimitive.Root +const Trigger = PopoverPrimitive.Trigger +const Close = PopoverPrimitive.Close + +export { + Root, + Content, + Trigger, + Close, + // + Root as Popover, + Content as PopoverContent, + Trigger as PopoverTrigger, + Close as PopoverClose +} diff --git a/src/components/ui/popover/popover-content.svelte b/src/components/ui/popover/popover-content.svelte new file mode 100644 index 00000000..c103592a --- /dev/null +++ b/src/components/ui/popover/popover-content.svelte @@ -0,0 +1,22 @@ + + + + + diff --git a/src/routes/(authenticated)/projects/project-cards.svelte b/src/routes/(authenticated)/projects/project-cards.svelte index 74810bdc..d00f51a5 100644 --- a/src/routes/(authenticated)/projects/project-cards.svelte +++ b/src/routes/(authenticated)/projects/project-cards.svelte @@ -1,6 +1,9 @@ + + + + + + + + Close + + + diff --git a/src/components/ui/dialog/dialog-description.svelte b/src/components/ui/dialog/dialog-description.svelte new file mode 100644 index 00000000..49dca809 --- /dev/null +++ b/src/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/components/ui/dialog/dialog-footer.svelte b/src/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 00000000..6481771b --- /dev/null +++ b/src/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/src/components/ui/dialog/dialog-header.svelte b/src/components/ui/dialog/dialog-header.svelte new file mode 100644 index 00000000..11826bef --- /dev/null +++ b/src/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/components/ui/dialog/dialog-overlay.svelte b/src/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 00000000..e5682861 --- /dev/null +++ b/src/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/components/ui/dialog/dialog-portal.svelte b/src/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 00000000..1ad7eb9d --- /dev/null +++ b/src/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/components/ui/dialog/dialog-title.svelte b/src/components/ui/dialog/dialog-title.svelte new file mode 100644 index 00000000..c2a2db60 --- /dev/null +++ b/src/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/components/ui/dialog/index.ts b/src/components/ui/dialog/index.ts new file mode 100644 index 00000000..8989ed80 --- /dev/null +++ b/src/components/ui/dialog/index.ts @@ -0,0 +1,36 @@ +import { Dialog as DialogPrimitive } from 'bits-ui' +import Title from './dialog-title.svelte' +import Portal from './dialog-portal.svelte' +import Footer from './dialog-footer.svelte' +import Header from './dialog-header.svelte' +import Overlay from './dialog-overlay.svelte' +import Content from './dialog-content.svelte' +import Description from './dialog-description.svelte' + +const Root = DialogPrimitive.Root +const Trigger = DialogPrimitive.Trigger +const Close = DialogPrimitive.Close + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose +} diff --git a/src/routes/(authenticated)/projects/+page.server.ts b/src/routes/(authenticated)/projects/+page.server.ts new file mode 100644 index 00000000..85d0ba2c --- /dev/null +++ b/src/routes/(authenticated)/projects/+page.server.ts @@ -0,0 +1,34 @@ +/** @type {import('./$types').Actions} */ + +import type { Actions, PageServerLoad } from './$types' +import { message, superValidate } from 'sveltekit-superforms' +import { zod } from 'sveltekit-superforms/adapters' +import { createProjectSchema } from './create-project-schema' + +export const load: PageServerLoad = async () => { + return { + form: await superValidate(zod(createProjectSchema)) + } +} + +export const actions: Actions = { + default: async ({ request }) => { + const form = await superValidate(request, zod(createProjectSchema)) + + if (!form.valid) { + return message(form, 'Invalid form', { + status: 500 + }) + } + + try { + // TODO create project + } catch (error) { + return message(form, 'Project Creation Failed', { + status: 500 + }) + } + + return message(form, 'Created Project succesfully') + } +} diff --git a/src/routes/(authenticated)/projects/+page.svelte b/src/routes/(authenticated)/projects/+page.svelte index 75e6b00b..d972bfed 100644 --- a/src/routes/(authenticated)/projects/+page.svelte +++ b/src/routes/(authenticated)/projects/+page.svelte @@ -1,12 +1,15 @@ - + diff --git a/src/routes/(authenticated)/projects/create-project-schema.ts b/src/routes/(authenticated)/projects/create-project-schema.ts new file mode 100644 index 00000000..998e9d1f --- /dev/null +++ b/src/routes/(authenticated)/projects/create-project-schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +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') +}) + +export type CreateProjectFormSchema = typeof createProjectSchema diff --git a/src/routes/(authenticated)/projects/create-project.svelte b/src/routes/(authenticated)/projects/create-project.svelte new file mode 100644 index 00000000..6abadf08 --- /dev/null +++ b/src/routes/(authenticated)/projects/create-project.svelte @@ -0,0 +1,67 @@ + + + + + Create Project + +
+ + New Project + + +
+ + + Name + + + + + + + Base Language + + + + +
+ + Create Project + +
+
+
From 6bd620bc5cf6dc64ee0785e1908efa9c7b1f5a53 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 5 Jun 2024 18:14:48 +0200 Subject: [PATCH 06/21] fixed type errors in autogenerated dialog component Signed-off-by: Benjamin Strasser --- src/components/ui/dialog/dialog-content.svelte | 2 +- src/components/ui/dialog/dialog-overlay.svelte | 2 +- src/components/ui/popover/popover-content.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/dialog/dialog-content.svelte b/src/components/ui/dialog/dialog-content.svelte index fc12ba3a..53640e09 100644 --- a/src/components/ui/dialog/dialog-content.svelte +++ b/src/components/ui/dialog/dialog-content.svelte @@ -7,7 +7,7 @@ type $$Props = DialogPrimitive.ContentProps let className: $$Props['class'] = undefined - export let transition: $$Props['transition'] = flyAndScale + export let transition: NonNullable<$$Props['transition']> = flyAndScale export let transitionConfig: $$Props['transitionConfig'] = { duration: 200 } diff --git a/src/components/ui/dialog/dialog-overlay.svelte b/src/components/ui/dialog/dialog-overlay.svelte index e5682861..78bdbdb3 100644 --- a/src/components/ui/dialog/dialog-overlay.svelte +++ b/src/components/ui/dialog/dialog-overlay.svelte @@ -6,7 +6,7 @@ type $$Props = DialogPrimitive.OverlayProps let className: $$Props['class'] = undefined - export let transition: $$Props['transition'] = fade + export let transition: NonNullable<$$Props['transition']> = fade export let transitionConfig: $$Props['transitionConfig'] = { duration: 150 } diff --git a/src/components/ui/popover/popover-content.svelte b/src/components/ui/popover/popover-content.svelte index c103592a..7579150a 100644 --- a/src/components/ui/popover/popover-content.svelte +++ b/src/components/ui/popover/popover-content.svelte @@ -4,7 +4,7 @@ type $$Props = PopoverPrimitive.ContentProps let className: $$Props['class'] = undefined - export let transition: $$Props['transition'] = flyAndScale + export let transition: NonNullable<$$Props['transition']> = flyAndScale export let transitionConfig: $$Props['transitionConfig'] = undefined export { className as class } From 6c552fae302060461db4fcc4339d2993b2bbaa7a Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 5 Jun 2024 18:21:16 +0200 Subject: [PATCH 07/21] removed unnecesarry comment Signed-off-by: Benjamin Strasser --- src/routes/(authenticated)/projects/+page.server.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/(authenticated)/projects/+page.server.ts b/src/routes/(authenticated)/projects/+page.server.ts index 85d0ba2c..ad0d376e 100644 --- a/src/routes/(authenticated)/projects/+page.server.ts +++ b/src/routes/(authenticated)/projects/+page.server.ts @@ -1,5 +1,3 @@ -/** @type {import('./$types').Actions} */ - import type { Actions, PageServerLoad } from './$types' import { message, superValidate } from 'sveltekit-superforms' import { zod } from 'sveltekit-superforms/adapters' From e2a9a57ba08fdb9d6df780ab102a115b97369192 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Fri, 7 Jun 2024 23:09:28 +0200 Subject: [PATCH 08/21] added description to create project dialog Signed-off-by: Benjamin Strasser --- src/routes/(authenticated)/projects/create-project.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/(authenticated)/projects/create-project.svelte b/src/routes/(authenticated)/projects/create-project.svelte index 6abadf08..2522a465 100644 --- a/src/routes/(authenticated)/projects/create-project.svelte +++ b/src/routes/(authenticated)/projects/create-project.svelte @@ -35,9 +35,9 @@
New Project - + + Create a new project and start translating your application. +
From dc7729e8f542eb6cc3fdda6853c8421f4e4b9e79 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 12 Jun 2024 12:57:15 +0200 Subject: [PATCH 09/21] added project backend functionality Signed-off-by: Benjamin Strasser --- .../kysely/migrations/2024-04-28T09_init.ts | 10 ++- .../project-repository.integration.test.ts | 73 +++++++++++++++++++ services/src/project/project-repository.ts | 27 +++++++ services/src/project/project-service.ts | 12 +++ .../src/project/project-service.unit.test.ts | 44 +++++++++++ services/src/project/project.ts | 18 +++++ src/routes/(auth)/signup/+page.server.ts | 5 +- .../(authenticated)/projects/+page.server.ts | 8 +- .../projects/create-project.svelte | 7 +- 9 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 services/src/project/project-repository.integration.test.ts create mode 100644 services/src/project/project-repository.ts create mode 100644 services/src/project/project-service.ts create mode 100644 services/src/project/project-service.unit.test.ts create mode 100644 services/src/project/project.ts diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index e8160db9..bad922b0 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -14,15 +14,19 @@ export async function up(db: Kysely): Promise { await createTableMigration(tx, 'projects') .addColumn('name', 'text', (col) => col.unique().notNull()) .addColumn('base_language', 'integer', (col) => - col.references('languages.id').onDelete('restrict').notNull() + col + .references('languages.id') + .onDelete('restrict') + .notNull() + .modifyEnd(sql`DEFERRABLE INITIALLY DEFERRED`) ) .execute() - await createTableMigration(tx, 'languages') + await createTableMigration(db, 'languages') .addColumn('code', 'text', (col) => col.unique().notNull()) .addColumn('fallback_language', 'integer', (col) => col.references('languages.id')) .addColumn('project_id', 'integer', (col) => - col.references('project.id').onDelete('cascade').notNull() + col.references('projects.id').onDelete('cascade').notNull() ) .execute() diff --git a/services/src/project/project-repository.integration.test.ts b/services/src/project/project-repository.integration.test.ts new file mode 100644 index 00000000..055ef2ea --- /dev/null +++ b/services/src/project/project-repository.integration.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { createProject } from './project-repository' +import { runMigration } from '../db/database-migration-util' +import { db } from '../db/database' +import type { CreateProjectFormSchema, SelectableProject } from './project' +import type { Languages } from 'kysely-codegen' +import type { Selectable } from 'kysely' + +const projectCreationObject: CreateProjectFormSchema = { + name: 'Test Project', + base_language: 'en' +} + +beforeEach(async () => { + db.reset() + await runMigration() +}) + +describe('Project Repository', () => { + describe('createProject', () => { + it('should create a project with the correct attributes', async () => { + const createdProject = await createProject(projectCreationObject) + + const projects = await db.selectFrom('projects').selectAll().execute() + expect(projects).toHaveLength(1) + + const project = projects[0] as SelectableProject + + expect(project).toMatchObject({ + id: createdProject.id, + name: projectCreationObject.name, + base_language: createdProject.base_language + }) + + expect(project.id).toBeTypeOf('number') + }) + + it('should not allow creation of projects with duplicate names', async () => { + await createProject(projectCreationObject) + + await expect(createProject(projectCreationObject)).rejects.toThrow() + + const projects = await db.selectFrom('projects').selectAll().execute() + expect(projects).toHaveLength(1) + }) + + it('should create a base language for the project', async () => { + const createdProject = await createProject(projectCreationObject) + + const languages = await db.selectFrom('languages').selectAll().execute() + expect(languages).toHaveLength(1) + + const language = languages[0] as Selectable + + expect(language.project_id).toBe(createdProject.id) + expect(language.code).toBe(projectCreationObject.base_language) + }) + + it('should link the base language to the project', async () => { + const createdProject = await createProject(projectCreationObject) + + expect(createdProject.base_language).not.toBe(0) + + const language = await db + .selectFrom('languages') + .where('id', '==', createdProject.base_language) + .selectAll() + .executeTakeFirstOrThrow() + + expect(language.project_id).toBe(createdProject.id) + }) + }) +}) diff --git a/services/src/project/project-repository.ts b/services/src/project/project-repository.ts new file mode 100644 index 00000000..12b1d0ac --- /dev/null +++ b/services/src/project/project-repository.ts @@ -0,0 +1,27 @@ +import type { CreateProjectFormSchema, SelectableProject } from './project' +import { db } from '../db/database' + +export function createProject(project: CreateProjectFormSchema): Promise { + return db.transaction().execute(async (tx) => { + const tempProject = await tx + .insertInto('projects') + .values({ name: project.name, base_language: 0 }) + .returning('id') + .executeTakeFirstOrThrow(() => new Error('Error Creating Project')) + + const baseLanguage = await tx + .insertInto('languages') + .values({ code: project.base_language, project_id: tempProject.id }) + .returning('id') + .executeTakeFirstOrThrow(() => new Error('Error Creating Base Language')) + + const createdProject = await tx + .updateTable('projects') + .set({ base_language: baseLanguage.id }) + .where('id', '==', tempProject.id) + .returningAll() + .executeTakeFirstOrThrow(() => new Error('Error Updating Project')) + + return createdProject + }) +} diff --git a/services/src/project/project-service.ts b/services/src/project/project-service.ts new file mode 100644 index 00000000..e137e723 --- /dev/null +++ b/services/src/project/project-service.ts @@ -0,0 +1,12 @@ +import { type CreateProjectFormSchema, createProjectSchema } from './project' +import * as repository from './project-repository' + +export async function createProject(project: CreateProjectFormSchema) { + try { + const validatedProject = createProjectSchema.parse(project) + + return await repository.createProject(validatedProject) + } catch (e) { + throw new Error('Error Creating Project') + } +} diff --git a/services/src/project/project-service.unit.test.ts b/services/src/project/project-service.unit.test.ts new file mode 100644 index 00000000..9d97508c --- /dev/null +++ b/services/src/project/project-service.unit.test.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createProject } from './project-service' +import * as repository from './project-repository' +import type { CreateProjectFormSchema } from './project' + +vi.mock('./project-repository', () => ({ + createProject: vi.fn() +})) + +const projectCreationObject: CreateProjectFormSchema = { + name: 'Test Project', + base_language: 'en' +} + +const mockSelectableProject = { + id: 1, + name: 'Test Project', + base_language: 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() +} + +beforeEach(() => { + vi.resetAllMocks() +}) + +describe('Project Service', () => { + describe('createProject', () => { + it('should call the repository to create a project', async () => { + vi.mocked(repository.createProject).mockResolvedValue(mockSelectableProject) + + const project = await createProject(projectCreationObject) + + expect(repository.createProject).toHaveBeenCalledWith(projectCreationObject) + expect(project).toEqual(mockSelectableProject) + }) + + it('should throw an error if the repository throws an error', async () => { + vi.mocked(repository.createProject).mockRejectedValue(new Error('Repository error')) + + await expect(createProject(projectCreationObject)).rejects.toThrow('Error Creating Project') + }) + }) +}) diff --git a/services/src/project/project.ts b/services/src/project/project.ts new file mode 100644 index 00000000..5c5ee684 --- /dev/null +++ b/services/src/project/project.ts @@ -0,0 +1,18 @@ +import type { Insertable, Selectable } from 'kysely' +import type { Projects } from 'kysely-codegen' +import { z } from 'zod' + +export type ProjectCreationParams = Insertable> +export type SelectableProject = Selectable +export type InsertableProject = Insertable + +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') +}) + +export type CreateProjectFormSchema = z.infer diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index 3b178c08..6f174b76 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -1,4 +1,4 @@ -import type { PageServerLoad } from './$types' +import type { Actions, PageServerLoad } from './$types' import { message, superValidate } from 'sveltekit-superforms' import { signupSchema } from './schema' import { zod } from 'sveltekit-superforms/adapters' @@ -15,8 +15,7 @@ export const load: PageServerLoad = async (event) => { } } -/** @type {import('./$types').Actions} */ -export const actions = { +export const actions: Actions = { default: async ({ request }) => { const form = await superValidate(request, zod(signupSchema)) diff --git a/src/routes/(authenticated)/projects/+page.server.ts b/src/routes/(authenticated)/projects/+page.server.ts index ad0d376e..37660f8a 100644 --- a/src/routes/(authenticated)/projects/+page.server.ts +++ b/src/routes/(authenticated)/projects/+page.server.ts @@ -1,7 +1,8 @@ import type { Actions, PageServerLoad } from './$types' import { message, superValidate } from 'sveltekit-superforms' import { zod } from 'sveltekit-superforms/adapters' -import { createProjectSchema } from './create-project-schema' +import { createProjectSchema } from 'services/project/project' +import { createProject } from 'services/project/project-service' export const load: PageServerLoad = async () => { return { @@ -19,14 +20,15 @@ export const actions: Actions = { }) } + let project try { - // TODO create project + project = await createProject(form.data) } catch (error) { return message(form, 'Project Creation Failed', { status: 500 }) } - return message(form, 'Created Project succesfully') + return message(form, { message: 'Project Created', project }) } } diff --git a/src/routes/(authenticated)/projects/create-project.svelte b/src/routes/(authenticated)/projects/create-project.svelte index 2522a465..ebe9353b 100644 --- a/src/routes/(authenticated)/projects/create-project.svelte +++ b/src/routes/(authenticated)/projects/create-project.svelte @@ -13,14 +13,13 @@ const form = superForm(data, { validators: zodClient(createProjectSchema), - async onUpdated({ form }) { + async onUpdate({ form }) { if (form.message) { if ($page.status >= 400) { toast.error(form.message) } else { - toast.success(form.message) - // TODO navigate based on id - // await goto('/projects/1/languages') + toast.success(form.message.message) + // console.log(form.message.project) } } } From 37f7c7b782d91df7ac9516bba071583024eb5472 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Sun, 16 Jun 2024 13:51:30 +0200 Subject: [PATCH 10/21] added getAllProjects to repository Signed-off-by: Benjamin Strasser --- .../project-repository.integration.test.ts | 41 ++++++++++++++++++- services/src/project/project-repository.ts | 4 ++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/services/src/project/project-repository.integration.test.ts b/services/src/project/project-repository.integration.test.ts index 055ef2ea..c07808d8 100644 --- a/services/src/project/project-repository.integration.test.ts +++ b/services/src/project/project-repository.integration.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import { createProject } from './project-repository' +import { createProject, getAllProjects } from './project-repository' import { runMigration } from '../db/database-migration-util' import { db } from '../db/database' import type { CreateProjectFormSchema, SelectableProject } from './project' @@ -70,4 +70,43 @@ describe('Project Repository', () => { expect(language.project_id).toBe(createdProject.id) }) }) + + describe('getAllProjects', () => { + it('should return an empty array when there are no projects', async () => { + const projects = await getAllProjects() + expect(projects).toHaveLength(0) + }) + + it('should return all created projects', async () => { + const project1 = { name: 'Project 1', base_language: 'en' } + const project2 = { name: 'Project 2', base_language: 'fr' } + + await createProject(project1) + await createProject(project2) + + const projects = await getAllProjects() + expect(projects).toHaveLength(2) + + const projectNames = projects.map((project: SelectableProject) => project.name) + expect(projectNames).toContain('Project 1') + expect(projectNames).toContain('Project 2') + }) + + it('should return projects with correct attributes', async () => { + const createdProject = await createProject(projectCreationObject) + + const projects = await getAllProjects() + expect(projects).toHaveLength(1) + + const project = projects[0] as SelectableProject + + expect(project).toMatchObject({ + id: createdProject.id, + name: projectCreationObject.name, + base_language: createdProject.base_language + }) + + expect(project.id).toBeTypeOf('number') + }) + }) }) diff --git a/services/src/project/project-repository.ts b/services/src/project/project-repository.ts index 12b1d0ac..f7065bb5 100644 --- a/services/src/project/project-repository.ts +++ b/services/src/project/project-repository.ts @@ -25,3 +25,7 @@ export function createProject(project: CreateProjectFormSchema): Promise { + return db.selectFrom('projects').selectAll().execute() +} From 7c041d34154891939de0f9fdd40bd7c50e7b0511 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Sun, 16 Jun 2024 13:54:23 +0200 Subject: [PATCH 11/21] added getAllProject to porject service Signed-off-by: Benjamin Strasser --- services/src/project/project-service.ts | 8 +++++ .../src/project/project-service.unit.test.ts | 32 +++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/services/src/project/project-service.ts b/services/src/project/project-service.ts index e137e723..9c037104 100644 --- a/services/src/project/project-service.ts +++ b/services/src/project/project-service.ts @@ -10,3 +10,11 @@ export async function createProject(project: CreateProjectFormSchema) { throw new Error('Error Creating Project') } } + +export async function getAllProjects() { + try { + return await repository.getAllProjects() + } catch (e) { + throw new Error('Error Getting Projects') + } +} diff --git a/services/src/project/project-service.unit.test.ts b/services/src/project/project-service.unit.test.ts index 9d97508c..06c35101 100644 --- a/services/src/project/project-service.unit.test.ts +++ b/services/src/project/project-service.unit.test.ts @@ -1,10 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { createProject } from './project-service' +import { createProject, getAllProjects } from './project-service' import * as repository from './project-repository' import type { CreateProjectFormSchema } from './project' vi.mock('./project-repository', () => ({ - createProject: vi.fn() + createProject: vi.fn(), + getAllProjects: vi.fn() })) const projectCreationObject: CreateProjectFormSchema = { @@ -41,4 +42,31 @@ describe('Project Service', () => { await expect(createProject(projectCreationObject)).rejects.toThrow('Error Creating Project') }) }) + + describe('getAllProjects', () => { + it('should call the repository to get all projects', async () => { + const mockProjects = [mockSelectableProject] + vi.mocked(repository.getAllProjects).mockResolvedValue(mockProjects) + + const projects = await getAllProjects() + + expect(repository.getAllProjects).toHaveBeenCalled() + expect(projects).toEqual(mockProjects) + }) + + it('should return an empty array when there are no projects', async () => { + vi.mocked(repository.getAllProjects).mockResolvedValue([]) + + const projects = await getAllProjects() + + expect(repository.getAllProjects).toHaveBeenCalled() + expect(projects).toEqual([]) + }) + + it('should throw an error if the repository throws an error', async () => { + vi.mocked(repository.getAllProjects).mockRejectedValue(new Error('Repository error')) + + await expect(getAllProjects()).rejects.toThrow('Error Getting Projects') + }) + }) }) From c12ceeef438a01047266d423f0ece6fed78e5595 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Sun, 16 Jun 2024 14:23:52 +0200 Subject: [PATCH 12/21] added getAllProjects to frontend Signed-off-by: Benjamin Strasser --- package.json | 1 + pnpm-lock.yaml | 8 +++++ .../(authenticated)/projects/+page.server.ts | 2 ++ .../(authenticated)/projects/+page.svelte | 2 +- .../projects/create-project.svelte | 6 ++-- .../projects/project-cards.svelte | 35 +++---------------- 6 files changed, 20 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 9b96a171..183ecc4a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "better-sqlite3": "^10.0.0", "bits-ui": "^0.21.7", "clsx": "^2.1.1", + "date-fns": "^3.6.0", "formsnap": "^1.0.0", "jsonwebtoken": "^9.0.2", "kysely": "^0.27.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7691795..64cc9bfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 formsnap: specifier: ^1.0.0 version: 1.0.1(svelte@4.2.18)(sveltekit-superforms@2.15.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.1(@types/node@20.14.8)))(svelte@4.2.18)(vite@5.3.1(@types/node@20.14.8)))(svelte@4.2.18)) @@ -1381,6 +1384,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -4973,6 +4979,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + date-fns@3.6.0: {} + dateformat@4.6.3: {} dayjs@1.11.11: diff --git a/src/routes/(authenticated)/projects/+page.server.ts b/src/routes/(authenticated)/projects/+page.server.ts index 37660f8a..c0a5e070 100644 --- a/src/routes/(authenticated)/projects/+page.server.ts +++ b/src/routes/(authenticated)/projects/+page.server.ts @@ -3,9 +3,11 @@ import { message, 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' export const load: PageServerLoad = async () => { return { + projects: await getAllProjects(), form: await superValidate(zod(createProjectSchema)) } } diff --git a/src/routes/(authenticated)/projects/+page.svelte b/src/routes/(authenticated)/projects/+page.svelte index d972bfed..4dc1766d 100644 --- a/src/routes/(authenticated)/projects/+page.svelte +++ b/src/routes/(authenticated)/projects/+page.svelte @@ -11,5 +11,5 @@ - + diff --git a/src/routes/(authenticated)/projects/create-project.svelte b/src/routes/(authenticated)/projects/create-project.svelte index ebe9353b..fac053d8 100644 --- a/src/routes/(authenticated)/projects/create-project.svelte +++ b/src/routes/(authenticated)/projects/create-project.svelte @@ -11,6 +11,8 @@ export let data: SuperValidated> + let open: boolean + const form = superForm(data, { validators: zodClient(createProjectSchema), async onUpdate({ form }) { @@ -19,7 +21,7 @@ toast.error(form.message) } else { toast.success(form.message.message) - // console.log(form.message.project) + open = false } } } @@ -28,7 +30,7 @@ const { form: formData, enhance } = form - + + Create Project diff --git a/src/routes/(authenticated)/projects/project-cards.svelte b/src/routes/(authenticated)/projects/project-cards.svelte index d00f51a5..6257ea24 100644 --- a/src/routes/(authenticated)/projects/project-cards.svelte +++ b/src/routes/(authenticated)/projects/project-cards.svelte @@ -3,37 +3,10 @@ import Ellipsis from 'lucide-svelte/icons/ellipsis' import * as Popover from '$components/ui/popover' + import type { SelectableProject } from 'services/project/project' + import { formatDistanceToNow } from 'date-fns' - const projects = [ - { - name: 'Project 1', - lastUpdated: '2 Hours ago' - }, - { - name: 'Project 2', - lastUpdated: '3 Hours ago' - }, - { - name: 'Project 3', - lastUpdated: '4 Hours ago' - }, - { - name: 'Project 4', - lastUpdated: '5 Hours ago' - }, - { - name: 'Project 1', - lastUpdated: '2 Hours ago' - }, - { - name: 'Project 2', - lastUpdated: '3 Hours ago' - }, - { - name: 'Project 3', - lastUpdated: '4 Hours ago' - } - ] + export let projects: SelectableProject[] = []
@@ -60,7 +33,7 @@
- Last updated: {project.lastUpdated} + Last updated: {formatDistanceToNow(project.updated_at)} ago From 0472b7a686fbc1d50e23ce326c87da5aae2d714c Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Sun, 16 Jun 2024 15:17:52 +0200 Subject: [PATCH 13/21] fixed timezone issue Signed-off-by: Benjamin Strasser --- src/routes/(authenticated)/projects/project-cards.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/(authenticated)/projects/project-cards.svelte b/src/routes/(authenticated)/projects/project-cards.svelte index 6257ea24..0663799b 100644 --- a/src/routes/(authenticated)/projects/project-cards.svelte +++ b/src/routes/(authenticated)/projects/project-cards.svelte @@ -33,7 +33,9 @@
- Last updated: {formatDistanceToNow(project.updated_at)} ago + Last updated: {formatDistanceToNow(project.updated_at + 'Z', { + addSuffix: true + })} From e009a35fca0306948101e6fa9480c3a3c57c4899 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Sun, 16 Jun 2024 15:49:07 +0200 Subject: [PATCH 14/21] rearranged route components into components/container folder Signed-off-by: Benjamin Strasser --- .../login => components/container/auth}/login-form.svelte | 0 .../(auth)/signup => components/container/auth}/schema.ts | 7 +++++++ .../container/auth}/signup-form.svelte | 2 +- .../container}/projects/create-project-schema.ts | 0 .../container}/projects/create-project.svelte | 6 +++--- .../container}/projects/project-cards.svelte | 0 src/components/{ => layout}/sidebar/index.ts | 0 src/components/{ => layout}/sidebar/sidebar.svelte | 0 src/routes/(auth)/login/+page.server.ts | 2 +- src/routes/(auth)/login/+page.svelte | 2 +- src/routes/(auth)/login/schema.ts | 8 -------- src/routes/(auth)/signup/+page.server.ts | 2 +- src/routes/(auth)/signup/+page.svelte | 2 +- src/routes/(authenticated)/+layout.svelte | 2 +- src/routes/(authenticated)/projects/+page.svelte | 4 ++-- 15 files changed, 18 insertions(+), 19 deletions(-) rename src/{routes/(auth)/login => components/container/auth}/login-form.svelte (100%) rename src/{routes/(auth)/signup => components/container/auth}/schema.ts (84%) rename src/{routes/(auth)/signup => components/container/auth}/signup-form.svelte (97%) rename src/{routes/(authenticated) => components/container}/projects/create-project-schema.ts (100%) rename src/{routes/(authenticated) => components/container}/projects/create-project.svelte (91%) rename src/{routes/(authenticated) => components/container}/projects/project-cards.svelte (100%) rename src/components/{ => layout}/sidebar/index.ts (100%) rename src/components/{ => layout}/sidebar/sidebar.svelte (100%) delete mode 100644 src/routes/(auth)/login/schema.ts diff --git a/src/routes/(auth)/login/login-form.svelte b/src/components/container/auth/login-form.svelte similarity index 100% rename from src/routes/(auth)/login/login-form.svelte rename to src/components/container/auth/login-form.svelte diff --git a/src/routes/(auth)/signup/schema.ts b/src/components/container/auth/schema.ts similarity index 84% rename from src/routes/(auth)/signup/schema.ts rename to src/components/container/auth/schema.ts index 25a41441..dbbfd9fa 100644 --- a/src/routes/(auth)/signup/schema.ts +++ b/src/components/container/auth/schema.ts @@ -1,5 +1,12 @@ import { z } from 'zod' +export const loginSchema = z.object({ + email: z.string().email(), + password: z.string() +}) + +export type LoginFormSchema = typeof loginSchema + export const signupSchema = z .object({ email: z diff --git a/src/routes/(auth)/signup/signup-form.svelte b/src/components/container/auth/signup-form.svelte similarity index 97% rename from src/routes/(auth)/signup/signup-form.svelte rename to src/components/container/auth/signup-form.svelte index 1ec65841..3e971787 100644 --- a/src/routes/(auth)/signup/signup-form.svelte +++ b/src/components/container/auth/signup-form.svelte @@ -2,7 +2,7 @@ import * as Form from '$components/ui/form' import { Input } from '$components/ui/input' import { Checkbox } from '$components/ui/checkbox' - import { type SignupFormSchema, signupSchema } from './schema' + import { type SignupFormSchema, signupSchema } from '$components/container/auth/schema' import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms' import { zodClient } from 'sveltekit-superforms/adapters' import { page } from '$app/stores' diff --git a/src/routes/(authenticated)/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts similarity index 100% rename from src/routes/(authenticated)/projects/create-project-schema.ts rename to src/components/container/projects/create-project-schema.ts diff --git a/src/routes/(authenticated)/projects/create-project.svelte b/src/components/container/projects/create-project.svelte similarity index 91% rename from src/routes/(authenticated)/projects/create-project.svelte rename to src/components/container/projects/create-project.svelte index fac053d8..efad4abe 100644 --- a/src/routes/(authenticated)/projects/create-project.svelte +++ b/src/components/container/projects/create-project.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/(auth)/login/schema.ts b/src/routes/(auth)/login/schema.ts deleted file mode 100644 index d2583ee5..00000000 --- a/src/routes/(auth)/login/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod' - -export const loginSchema = z.object({ - email: z.string().email(), - password: z.string() -}) - -export type LoginFormSchema = typeof loginSchema diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index 6f174b76..6557ed24 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -1,6 +1,6 @@ import type { Actions, PageServerLoad } from './$types' import { message, superValidate } from 'sveltekit-superforms' -import { signupSchema } from './schema' +import { signupSchema } from '$components/container/auth/schema' import { zod } from 'sveltekit-superforms/adapters' import { register } from 'services/user/user-auth-service' import { redirect } from '@sveltejs/kit' diff --git a/src/routes/(auth)/signup/+page.svelte b/src/routes/(auth)/signup/+page.svelte index 0d97e41b..3fb7a03b 100644 --- a/src/routes/(auth)/signup/+page.svelte +++ b/src/routes/(auth)/signup/+page.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/(authenticated)/+layout.svelte b/src/routes/(authenticated)/+layout.svelte index 9130cc4f..90828c49 100644 --- a/src/routes/(authenticated)/+layout.svelte +++ b/src/routes/(authenticated)/+layout.svelte @@ -1,5 +1,5 @@ - + Create Project + + + Create Project + @@ -44,7 +49,12 @@ Name - + @@ -53,6 +63,7 @@ Base Language @@ -61,7 +72,7 @@
- Create Project + Create Project
diff --git a/src/components/container/projects/project-cards.svelte b/src/components/container/projects/project-cards.svelte index 3793dfc6..fc253b68 100644 --- a/src/components/container/projects/project-cards.svelte +++ b/src/components/container/projects/project-cards.svelte @@ -15,7 +15,9 @@
- {project.name} + + {project.name} + +
+
+ +
+
+
+
+
+ + Last updated: {formatDistanceToNow(project.updated_at + 'Z', { + addSuffix: true + })} + +
+ diff --git a/src/components/container/projects/project-cards-layout.svelte b/src/components/container/projects/project-cards-layout.svelte new file mode 100644 index 00000000..b4947721 --- /dev/null +++ b/src/components/container/projects/project-cards-layout.svelte @@ -0,0 +1,23 @@ + + +
+ {#each projects as project} + + {/each} +
+ +{#if projects.length === 0} +
+
+

You have no projects

+

+ You can start translating as soon as you add a project +

+
+
+{/if} diff --git a/src/components/container/projects/project-cards.svelte b/src/components/container/projects/project-cards.svelte deleted file mode 100644 index fc253b68..00000000 --- a/src/components/container/projects/project-cards.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - - - -{#if projects.length === 0} -
-
-

You have no projects

-

- You can start translating as soon as you add a project -

-
-
-{/if} diff --git a/src/routes/(authenticated)/projects/+page.svelte b/src/routes/(authenticated)/projects/+page.svelte index 30f8bc32..249cd3ac 100644 --- a/src/routes/(authenticated)/projects/+page.svelte +++ b/src/routes/(authenticated)/projects/+page.svelte @@ -1,8 +1,8 @@ @@ -11,5 +11,5 @@ - + From 959733cce4b4c9e4b8a11ba2210901c1ba5b50ce Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 1 Jul 2024 20:02:33 +0200 Subject: [PATCH 18/21] fixed a bug with language codes and added user form error if name is already in use Signed-off-by: Benjamin Strasser --- services/src/error/index.ts | 5 +++++ .../src/kysely/migrations/2024-04-28T09_init.ts | 3 ++- .../project-repository.integration.test.ts | 17 +++++++++++++++++ services/src/project/project-service.ts | 12 ++++++++---- .../src/project/project-service.unit.test.ts | 14 ++++++++++++++ .../container/projects/create-project.svelte | 6 +++--- .../(authenticated)/projects/+page.server.ts | 7 ++++++- 7 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 services/src/error/index.ts diff --git a/services/src/error/index.ts b/services/src/error/index.ts new file mode 100644 index 00000000..02dd394f --- /dev/null +++ b/services/src/error/index.ts @@ -0,0 +1,5 @@ +export class CreateProjectNameNotUniqueError extends Error { + constructor() { + super('Project name must be unique') + } +} diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index 72effcc5..a9c0baa8 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -23,11 +23,12 @@ export async function up(db: Kysely): Promise { .execute() await createTableMigration(tx, 'languages') - .addColumn('code', 'text', (col) => col.unique().notNull()) + .addColumn('code', '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() ) + .addUniqueConstraint('languages_code_project_id_unique', ['code', 'project_id']) .execute() await createTableMigration(tx, 'keys') diff --git a/services/src/project/project-repository.integration.test.ts b/services/src/project/project-repository.integration.test.ts index c07808d8..4145f8a4 100644 --- a/services/src/project/project-repository.integration.test.ts +++ b/services/src/project/project-repository.integration.test.ts @@ -69,6 +69,23 @@ describe('Project Repository', () => { expect(language.project_id).toBe(createdProject.id) }) + + 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' } + + await createProject(project1) + await createProject(project2) + + const projects = await db.selectFrom('projects').selectAll().execute() + expect(projects).toHaveLength(2) + + const languages = await db.selectFrom('languages').selectAll().execute() + expect(languages).toHaveLength(2) + + const languageCodes = languages.map((language: Selectable) => language.code) + expect(languageCodes.filter((code) => code === 'en')).toHaveLength(2) + }) }) describe('getAllProjects', () => { diff --git a/services/src/project/project-service.ts b/services/src/project/project-service.ts index 9c037104..f2652f1c 100644 --- a/services/src/project/project-service.ts +++ b/services/src/project/project-service.ts @@ -1,12 +1,16 @@ -import { type CreateProjectFormSchema, createProjectSchema } from './project' +import { CreateProjectNameNotUniqueError } from '../error' +import { type CreateProjectFormSchema } from './project' import * as repository from './project-repository' +import { SqliteError } from 'better-sqlite3' export async function createProject(project: CreateProjectFormSchema) { try { - const validatedProject = createProjectSchema.parse(project) + return await repository.createProject(project) + } catch (e: unknown) { + if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT_UNIQUE') { + throw new CreateProjectNameNotUniqueError() + } - return await repository.createProject(validatedProject) - } catch (e) { throw new Error('Error Creating Project') } } diff --git a/services/src/project/project-service.unit.test.ts b/services/src/project/project-service.unit.test.ts index 06c35101..ca629c93 100644 --- a/services/src/project/project-service.unit.test.ts +++ b/services/src/project/project-service.unit.test.ts @@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createProject, getAllProjects } from './project-service' import * as repository from './project-repository' import type { CreateProjectFormSchema } from './project' +import { CreateProjectNameNotUniqueError } from '../error' +import { SqliteError } from 'better-sqlite3' vi.mock('./project-repository', () => ({ createProject: vi.fn(), @@ -41,6 +43,18 @@ describe('Project Service', () => { await expect(createProject(projectCreationObject)).rejects.toThrow('Error Creating Project') }) + + it('should throw a CreateProjectNameNotUniqueError if the repository throws a SQLITE_CONSTRAINT_UNIQUE error', async () => { + const sqliteError = new SqliteError( + 'SQLITE_CONSTRAINT_UNIQUE: UNIQUE constraint failed: projects.name', + 'SQLITE_CONSTRAINT_UNIQUE' + ) + vi.mocked(repository.createProject).mockRejectedValue(sqliteError) + + await expect(createProject(projectCreationObject)).rejects.toThrow( + new CreateProjectNameNotUniqueError() + ) + }) }) describe('getAllProjects', () => { diff --git a/src/components/container/projects/create-project.svelte b/src/components/container/projects/create-project.svelte index 659d1c8c..d0de6d83 100644 --- a/src/components/container/projects/create-project.svelte +++ b/src/components/container/projects/create-project.svelte @@ -11,11 +11,11 @@ export let data: SuperValidated> - let open: boolean + let open = false const form = superForm(data, { validators: zodClient(createProjectSchema), - async onUpdate({ form }) { + async onUpdated({ form }) { if (form.message) { if ($page.status >= 400) { toast.error(form.message) @@ -30,7 +30,7 @@ const { form: formData, enhance } = form - + { return { @@ -26,6 +27,10 @@ export const actions: Actions = { try { project = await createProject(form.data) } catch (error) { + if (error instanceof CreateProjectNameNotUniqueError) { + return setError(form, 'name', 'Name already in use') + } + return message(form, 'Project Creation Failed', { status: 500 }) From 55f86cdc7cd6ce567c69fd55dacb3469bdde04d6 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Mon, 1 Jul 2024 20:19:56 +0200 Subject: [PATCH 19/21] imporved grid layout Signed-off-by: Benjamin Strasser --- src/components/container/projects/project-card.svelte | 2 +- .../container/projects/project-cards-layout.svelte | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/container/projects/project-card.svelte b/src/components/container/projects/project-card.svelte index 327f4bc6..efc27ca1 100644 --- a/src/components/container/projects/project-card.svelte +++ b/src/components/container/projects/project-card.svelte @@ -10,7 +10,7 @@ - +
diff --git a/src/components/container/projects/project-cards-layout.svelte b/src/components/container/projects/project-cards-layout.svelte index b4947721..7900d46d 100644 --- a/src/components/container/projects/project-cards-layout.svelte +++ b/src/components/container/projects/project-cards-layout.svelte @@ -5,7 +5,7 @@ export let projects: SelectableProject[] = [] -
+
{#each projects as project} {/each} @@ -21,3 +21,9 @@
{/if} + + From 6e0899c8873d916b6893c94bb23e101cb9b0eeb4 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Tue, 2 Jul 2024 14:01:14 +0200 Subject: [PATCH 20/21] feedback Signed-off-by: Benjamin Strasser --- e2e/specs/create-project-flow.spec.ts | 8 +++++--- services/src/kysely/migrations/2024-04-28T09_init.ts | 2 +- src/components/container/auth/schema.ts | 2 -- src/components/container/auth/signup-form.svelte | 4 ++-- .../container/projects/create-project-schema.ts | 2 -- src/components/container/projects/create-project.svelte | 4 ++-- src/routes/(authenticated)/projects/+page.server.ts | 1 + 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/e2e/specs/create-project-flow.spec.ts b/e2e/specs/create-project-flow.spec.ts index c8238ece..7025ab3b 100644 --- a/e2e/specs/create-project-flow.spec.ts +++ b/e2e/specs/create-project-flow.spec.ts @@ -2,18 +2,20 @@ import { expect } from '@playwright/test' import { testWithUser as test } from './fixtures' import { waitForHydration } from './util' -test.describe('create project', { tag: ['@foo-bar'] }, () => { +test.describe('create project', () => { test('projects', async ({ page }) => { + const projectName = 'My new project' + await page.goto('/projects') await waitForHydration(page) await page.getByTestId('create-project-modal-trigger').click() - await page.getByTestId('create-project-name-input').fill('My new project') + await page.getByTestId('create-project-name-input').fill(projectName) await page.getByTestId('create-project-base-language-input').fill('en') await page.getByTestId('create-project-submit-button').click() - await expect(page.getByTestId('project-card-name')).toHaveText('My new project') + await expect(page.getByTestId('project-card-name')).toHaveText(projectName) }) }) diff --git a/services/src/kysely/migrations/2024-04-28T09_init.ts b/services/src/kysely/migrations/2024-04-28T09_init.ts index a9c0baa8..625f4df4 100644 --- a/services/src/kysely/migrations/2024-04-28T09_init.ts +++ b/services/src/kysely/migrations/2024-04-28T09_init.ts @@ -65,7 +65,7 @@ export async function down(db: Kysely): Promise { await tx.schema.dropTable('projects_users').execute() await tx.schema.dropTable('translations').execute() await tx.schema.dropTable('keys').execute() - await tx.schema.dropTable('languages').execute() await tx.schema.dropTable('projects').execute() + await tx.schema.dropTable('languages').execute() }) } diff --git a/src/components/container/auth/schema.ts b/src/components/container/auth/schema.ts index dbbfd9fa..2124f0d1 100644 --- a/src/components/container/auth/schema.ts +++ b/src/components/container/auth/schema.ts @@ -24,5 +24,3 @@ export const signupSchema = z message: "Passwords don't match", path: ['confirmPassword'] }) - -export type SignupFormSchema = typeof signupSchema diff --git a/src/components/container/auth/signup-form.svelte b/src/components/container/auth/signup-form.svelte index 3e971787..900b6417 100644 --- a/src/components/container/auth/signup-form.svelte +++ b/src/components/container/auth/signup-form.svelte @@ -2,14 +2,14 @@ import * as Form from '$components/ui/form' import { Input } from '$components/ui/input' import { Checkbox } from '$components/ui/checkbox' - import { type SignupFormSchema, signupSchema } from '$components/container/auth/schema' + import { signupSchema } from '$components/container/auth/schema' import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms' import { zodClient } from 'sveltekit-superforms/adapters' import { page } from '$app/stores' import { toast } from 'svelte-sonner' import { goto } from '$app/navigation' - export let data: SuperValidated> + export let data: SuperValidated> const form = superForm(data, { validators: zodClient(signupSchema), diff --git a/src/components/container/projects/create-project-schema.ts b/src/components/container/projects/create-project-schema.ts index 998e9d1f..b47cf002 100644 --- a/src/components/container/projects/create-project-schema.ts +++ b/src/components/container/projects/create-project-schema.ts @@ -8,5 +8,3 @@ export const createProjectSchema = z.object({ .string({ required_error: 'Base language is required' }) .min(1, 'Base language must have at least one character') }) - -export type CreateProjectFormSchema = typeof createProjectSchema diff --git a/src/components/container/projects/create-project.svelte b/src/components/container/projects/create-project.svelte index d0de6d83..6919e43f 100644 --- a/src/components/container/projects/create-project.svelte +++ b/src/components/container/projects/create-project.svelte @@ -4,12 +4,12 @@ import { Input } from '$components/ui/input' import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms' import { zodClient } from 'sveltekit-superforms/adapters' - import { type CreateProjectFormSchema, createProjectSchema } from './create-project-schema' + import { createProjectSchema } from './create-project-schema' import * as Form from '$components/ui/form' import { page } from '$app/stores' import { toast } from 'svelte-sonner' - export let data: SuperValidated> + export let data: SuperValidated> let open = false diff --git a/src/routes/(authenticated)/projects/+page.server.ts b/src/routes/(authenticated)/projects/+page.server.ts index c3098afa..f0f1f90e 100644 --- a/src/routes/(authenticated)/projects/+page.server.ts +++ b/src/routes/(authenticated)/projects/+page.server.ts @@ -25,6 +25,7 @@ export const actions: Actions = { let project try { + // TODO: User authentication project = await createProject(form.data) } catch (error) { if (error instanceof CreateProjectNameNotUniqueError) { From 0f7315f4f317ebe3f64203abdbe05e8361f04881 Mon Sep 17 00:00:00 2001 From: Benjamin Strasser Date: Wed, 3 Jul 2024 10:47:29 +0200 Subject: [PATCH 21/21] added domain model for Project Signed-off-by: Benjamin Strasser --- services/src/project/project.ts | 2 ++ src/components/container/projects/project-card.svelte | 4 ++-- src/components/container/projects/project-cards-layout.svelte | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/src/project/project.ts b/services/src/project/project.ts index 5c5ee684..57b99836 100644 --- a/services/src/project/project.ts +++ b/services/src/project/project.ts @@ -3,6 +3,8 @@ import type { Projects } from 'kysely-codegen' import { z } from 'zod' export type ProjectCreationParams = Insertable> +export type Project = SelectableProject + export type SelectableProject = Selectable export type InsertableProject = Insertable diff --git a/src/components/container/projects/project-card.svelte b/src/components/container/projects/project-card.svelte index efc27ca1..60b50839 100644 --- a/src/components/container/projects/project-card.svelte +++ b/src/components/container/projects/project-card.svelte @@ -3,10 +3,10 @@ import Ellipsis from 'lucide-svelte/icons/ellipsis' import * as Popover from '$components/ui/popover' - import type { SelectableProject } from 'services/project/project' + import type { Project } from 'services/project/project' import { formatDistanceToNow } from 'date-fns' - export let project: SelectableProject + export let project: Project
diff --git a/src/components/container/projects/project-cards-layout.svelte b/src/components/container/projects/project-cards-layout.svelte index 7900d46d..8f241e18 100644 --- a/src/components/container/projects/project-cards-layout.svelte +++ b/src/components/container/projects/project-cards-layout.svelte @@ -1,8 +1,8 @@