-
Notifications
You must be signed in to change notification settings - Fork 0
(feat) projects overview page #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
8f4366e
Added header to projects
benjaminstrasser 8fb54c3
added basic card layout and design
benjaminstrasser 29251b3
added basic navigation
benjaminstrasser 547cb8a
added popover to project card
benjaminstrasser 383e43d
added shadcn dialog
benjaminstrasser 6bd620b
fixed type errors in autogenerated dialog component
benjaminstrasser 6c552fa
removed unnecesarry comment
benjaminstrasser e2a9a57
added description to create project dialog
benjaminstrasser dc7729e
added project backend functionality
benjaminstrasser 37f7c7b
added getAllProjects to repository
benjaminstrasser 7c041d3
added getAllProject to porject service
benjaminstrasser c12ceee
added getAllProjects to frontend
benjaminstrasser 0472b7a
fixed timezone issue
benjaminstrasser e009a35
rearranged route components into components/container folder
benjaminstrasser c4da4da
added empty projects view
benjaminstrasser f0e547e
added e2e test to create project
benjaminstrasser b0a86a8
moved project card into seperate component
benjaminstrasser 959733c
fixed a bug with language codes and added user form error if name is …
benjaminstrasser 55f86cd
imporved grid layout
benjaminstrasser 6e0899c
feedback
benjaminstrasser 0f7315f
added domain model for Project
benjaminstrasser File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,21 @@ | ||
| 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') | ||
| // TODO add test | ||
| await waitForHydration(page) | ||
|
|
||
| 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-submit-button').click() | ||
|
|
||
| await expect(page.getByTestId('project-card-name')).toHaveText(projectName) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export class CreateProjectNameNotUniqueError extends Error { | ||
| constructor() { | ||
| super('Project name must be unique') | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
services/src/project/project-repository.integration.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import { beforeEach, describe, expect, it } from 'vitest' | ||
| import { createProject, getAllProjects } 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<Languages> | ||
|
|
||
| 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) | ||
| }) | ||
|
|
||
| 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<Languages>) => language.code) | ||
| expect(languageCodes.filter((code) => code === 'en')).toHaveLength(2) | ||
| }) | ||
| }) | ||
|
|
||
| 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') | ||
| }) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { CreateProjectFormSchema, SelectableProject } from './project' | ||
| import { db } from '../db/database' | ||
|
|
||
| export function createProject(project: CreateProjectFormSchema): Promise<SelectableProject> { | ||
| 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 | ||
| }) | ||
| } | ||
|
|
||
| export function getAllProjects(): Promise<SelectableProject[]> { | ||
| return db.selectFrom('projects').selectAll().execute() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| 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 { | ||
| return await repository.createProject(project) | ||
| } catch (e: unknown) { | ||
| if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT_UNIQUE') { | ||
| throw new CreateProjectNameNotUniqueError() | ||
| } | ||
|
|
||
| throw new Error('Error Creating Project') | ||
benjaminstrasser marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| export async function getAllProjects() { | ||
| try { | ||
| return await repository.getAllProjects() | ||
| } catch (e) { | ||
| throw new Error('Error Getting Projects') | ||
benjaminstrasser marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| 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(), | ||
| getAllProjects: 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') | ||
| }) | ||
|
|
||
| it('should throw a CreateProjectNameNotUniqueError if the repository throws a SQLITE_CONSTRAINT_UNIQUE error', async () => { | ||
benjaminstrasser marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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', () => { | ||
| 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') | ||
| }) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import type { Insertable, Selectable } from 'kysely' | ||
| import type { Projects } from 'kysely-codegen' | ||
| import { z } from 'zod' | ||
|
|
||
| export type ProjectCreationParams = Insertable<Omit<Projects, 'id' | 'created_at' | 'updated_at'>> | ||
| export type Project = SelectableProject | ||
|
|
||
| export type SelectableProject = Selectable<Projects> | ||
| export type InsertableProject = Insertable<Projects> | ||
|
|
||
| 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<typeof createProjectSchema> |
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.