From ff608b2f3563006d2a3090f105fcc179bc05701a Mon Sep 17 00:00:00 2001 From: nourshoreibah Date: Sun, 30 Aug 2026 15:30:11 -0400 Subject: [PATCH] feat(projects): make assigned staff optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admins reach every project through `users.is_admin`, not through a membership row, so a project with an empty roster is still fully reachable by the people who administer it. The "select at least 1 staff member" rule was the only thing forcing a roster at creation time — the API already accepted an empty `members` array and the schema never required one — so it is dropped along with the `*` on the field. Co-Authored-By: Claude Opus 5 (1M context) --- .../lambdas/projects/controllers/projects.ts | 6 +++--- .../src/app/components/ProjectFormModal.tsx | 7 ------- .../test/components/ProjectFormModal.test.tsx | 14 +++++++------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/apps/backend/lambdas/projects/controllers/projects.ts b/apps/backend/lambdas/projects/controllers/projects.ts index 30376e41..ceee9836 100644 --- a/apps/backend/lambdas/projects/controllers/projects.ts +++ b/apps/backend/lambdas/projects/controllers/projects.ts @@ -237,9 +237,9 @@ export const createProject: RouteHandler = async ({ event, auth }) => { } try { - // Creating the project and its roster together: a project that saved - // without its staff would look complete but fail the form's own - // "at least one staff member" rule on the next read. + // Creating the project and its roster together so a partial save cannot + // leave a project without the staff the caller picked. The roster may be + // empty — admins reach every project through `users.is_admin`. const inserted = await db.transaction().execute(async (trx) => { const row = await trx .insertInto('branch.projects') diff --git a/apps/frontend/src/app/components/ProjectFormModal.tsx b/apps/frontend/src/app/components/ProjectFormModal.tsx index b12ffe84..e9d740ce 100644 --- a/apps/frontend/src/app/components/ProjectFormModal.tsx +++ b/apps/frontend/src/app/components/ProjectFormModal.tsx @@ -85,10 +85,6 @@ function validate(values: ProjectFormValues): FieldErrors { } } - if (values.members.length === 0) { - errors.members = 'Select AT LEAST 1 staff member for the project'; - } - return errors; } @@ -343,13 +339,10 @@ export default function ProjectFormModal({ update('members', v)} - isError={showError('members')} - errorMessage={errors.members} disabled={saving} /> diff --git a/apps/frontend/test/components/ProjectFormModal.test.tsx b/apps/frontend/test/components/ProjectFormModal.test.tsx index a5599b33..b07d9221 100644 --- a/apps/frontend/test/components/ProjectFormModal.test.tsx +++ b/apps/frontend/test/components/ProjectFormModal.test.tsx @@ -161,7 +161,7 @@ describe('ProjectFormModal staff roles', () => { ]); }); - it('drops a removed member from the roster it saves', async () => { + it('saves an empty roster, since admins reach every project anyway', async () => { renderEdit(); const list = await screen.findByRole('list', { @@ -173,11 +173,11 @@ describe('ProjectFormModal staff roles', () => { fireEvent.click(screen.getByRole('button', { name: 'Save' })); - expect( - await screen.findByText('Select AT LEAST 1 staff member for the project'), - ).toBeInTheDocument(); - expect( - mockFetch.mock.calls.some(([, init]) => init?.method === 'PUT'), - ).toBe(false); + await waitFor(() => { + expect( + mockFetch.mock.calls.some(([, init]) => init?.method === 'PUT'), + ).toBe(true); + }); + expect(savedBody().members).toEqual([]); }); });