Skip to content
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

Feature/delete board edit title #8

Merged
merged 3 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
12 changes: 10 additions & 2 deletions packages/webapp/app/modules/BoardPage/BoardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { RemoveButton } from '../DepartmentPage/components/buttons/RemoveButton'
import { DefaultAwaitErrorElement } from '../../components/errors/DefaultAwaitErrorElement';
import { BoardPageLoading } from './BoardPage.loading';
import { StatusColumn } from './components/StatusColumn/StatusColumn';
import NiceModal from '@ebay/nice-modal-react';
import { modalIds } from '../../utils/modalIds';
import { DeleteBoard } from './components/DeleteBoard';
import { BoardTitleInput } from './components/inputs/BoardTitleInput';

interface BoardPageProps {
params: BoardIdParams;
Expand All @@ -25,10 +29,11 @@ export function BoardPage(props: BoardPageProps) {

const data = useLoaderData<BoardIdLoader>();
const { statusQueries, statuses, board } = data;

return (
<ModuleLayout.Main>
<ModuleLayout.Toolbar
title={<div />}
title={<BoardTitleInput defaultValue={board.name} id={board.id} />}
actions={
<>
<ModuleAddButton
Expand All @@ -44,7 +49,9 @@ export function BoardPage(props: BoardPageProps) {
Add a Task
</ModuleAddButton>
<Search placeholder="Search Tasks" />
<RemoveButton />
<RemoveButton
onClick={() => NiceModal.show(modalIds.deleteBoard)}
/>
</>
}
/>
Expand All @@ -60,6 +67,7 @@ export function BoardPage(props: BoardPageProps) {
</Await>
</Suspense>
</ModuleLayout.Content>
<DeleteBoard id={modalIds.deleteBoard} boardId={params.boardId} />
</ModuleLayout.Main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { useNavigation, useSubmit } from '@remix-run/react';
import { CancelButton } from '../../../../components/modals/ConfirmModal/CancelButton';
import { ConfirmButton } from '../../../../components/modals/ConfirmModal/ConfirmButton';
import { ConfirmModal } from '../../../../components/modals/ConfirmModal/ConfirmModal';
import * as boardIdForm from '../../../../routes/app.$orgId.boards.$boardId/form';
import { departmentFetcherKeys } from '../../../../services/queries/department/departmentFetcherKeys';
import { serialiseFormData } from '../../../../utils/Form/serialiseFormData';
import { specialFields } from '../../../../utils/Form/specialFields';

export interface DeleteBoardProps {
boardId: string;
}

function DeleteBoardModal(props: DeleteBoardProps) {
const { boardId } = props;

const modal = useModal();

const onClose = () => modal.remove();
const submit = useSubmit();
const navigation = useNavigation();
const handleSubmit = () => {
const formData: boardIdForm.DeleteBoardFormData = {
intent: boardIdForm.BoardIdFormIntent.DELETE_BOARD,
boardId,
};
submit(serialiseFormData(formData), {
method: 'post',
fetcherKey: departmentFetcherKeys.deleteFilter(boardId),
});
};
const isSubmitting =
navigation.state !== 'idle' &&
navigation.formData?.get(specialFields.intent) ===
boardIdForm.BoardIdFormIntent.DELETE_BOARD;
return (
<ConfirmModal onClose={onClose}>
<ConfirmButton color="red" loading={isSubmitting} onClick={handleSubmit}>
Delete
</ConfirmButton>
<CancelButton onClick={onClose}>Cancel</CancelButton>
</ConfirmModal>
);
}
export const DeleteBoard = NiceModal.create(DeleteBoardModal);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DeleteBoard } from './DeleteBoard';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { SubmissionResult } from '@conform-to/dom';

import { getFormProps, getInputProps, useForm } from '@conform-to/react';
import { useFetcher } from '@remix-run/react';
import { ModuleLayoutTitleInput } from '../../../../layouts/ModuleLayout/ModuleLayoutTitleInput';
import { boardFetcherKeys } from '../../../../services/queries/board/boardFetcherKeys';
import { getLastResultToReset } from '../../logic/getLastResultToReset';
import * as boardIdForm from '../../../../routes/app.$orgId.boards.$boardId/form';
import { hiddenInputs } from '../../../../utils/Form/hiddenInputs';

export interface BoardInputProps {
defaultValue: string;
id: string;
}

// Todo: Add optimistic ui
export function BoardTitleInput(props: BoardInputProps) {
const { defaultValue, id } = props;
const fetcher = useFetcher<SubmissionResult>({
key: boardFetcherKeys.nameFilter(id),
});

const [form, fields] = useForm<boardIdForm.NameFormData>({
lastResult: getLastResultToReset(fetcher),
defaultValue: boardIdForm.nameDefaultData({ name: defaultValue, id }),
onSubmit(event) {
// Prevent a lot of saves
if (!form.dirty) event.preventDefault();
},
});
return (
<fetcher.Form {...getFormProps(form)} method="post">
<ModuleLayoutTitleInput
{...getInputProps(fields.name, {
type: 'text',
})}
error={fields.name.errors?.join()}
key={fields.name.key}
onBlur={() => {
if (form.dirty) form.reset();
}}
/>
{hiddenInputs([
{ field: fields.intent },
{ field: fields.id, value: id },
])}
</fetcher.Form>
);
}
47 changes: 47 additions & 0 deletions packages/webapp/app/routes/app.$orgId.boards.$boardId/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ZodOf } from '../../models/types';
import { z } from 'zod';

export enum BoardIdFormIntent {
NAME = 'boardIdForm/name',
DELETE_BOARD = 'boardIdForm/deleteDepartment',
}

export interface DeleteBoardFormData {
intent: BoardIdFormIntent.DELETE_BOARD;
boardId: string;
}
export interface NameFormData {
intent: BoardIdFormIntent.NAME;
name: string;
id: string;
}
type BoardIdFormData = DeleteBoardFormData | NameFormData;

interface NameDefaultDataArgs {
name: string;
id: string;
}

export function nameDefaultData(args: NameDefaultDataArgs): NameFormData {
const { name, id } = args;
return {
id,
name,
intent: BoardIdFormIntent.NAME,
};
}

const deleteBaordSchema = z.object({
intent: z.literal(BoardIdFormIntent.DELETE_BOARD),
boardId: z.string().min(1),
}) satisfies ZodOf<DeleteBoardFormData>;
const nameSchema = z.object({
intent: z.literal(BoardIdFormIntent.NAME),
name: z.string().min(1),
id: z.string().min(1),
}) satisfies ZodOf<NameFormData>;

export const schema = z.union([
deleteBaordSchema,
nameSchema,
]) satisfies ZodOf<BoardIdFormData>;
35 changes: 35 additions & 0 deletions packages/webapp/app/routes/app.$orgId.boards.$boardId/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {
defer,
useParams,
type ClientLoaderFunctionArgs,
type ClientActionFunctionArgs,
json,
redirect,
} from '@remix-run/react';
import { BoardPage } from '../../modules/BoardPage/BoardPage';
import {
Expand All @@ -16,6 +19,38 @@ import { queryClient } from '../../utils/queryClient';
import { type BoardIdLoaderData } from './types';
import { useMemo } from 'react';
import { boardIdSchema } from './utils';
import { parseWithZod } from '@conform-to/zod';
import * as boardIdForm from './form';
import { deleteBoard } from '../../services/queries/board/deleteBoard';
import { catchPostSubmissionError } from '../../utils/Form/catchPostSubmissionError';
import { patchBoardById } from '../../services/queries/board/patchBoardById';

export async function clientAction(args: ClientActionFunctionArgs) {
const { request } = args;
const formData = await request.formData();
const submission = parseWithZod(formData, {
schema: boardIdForm.schema,
});
if (submission.status !== 'success') {
return json(submission.reply());
}
const { value } = submission;
try {
switch (value.intent) {
case boardIdForm.BoardIdFormIntent.DELETE_BOARD:
await deleteBoard({ boardId: value.boardId });
return redirect('../');
case boardIdForm.BoardIdFormIntent.NAME:
await patchBoardById({ body: { name: value.name }, id: value.id });
break;
default:
break;
}
return json(submission.reply({ resetForm: true }));
} catch (error) {
return catchPostSubmissionError(error, submission);
}
}

export async function clientLoader(args: ClientLoaderFunctionArgs) {
const user = await requireUser();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const boardFetcherKeys = {
all: 'board',
deleteBoard: () => `${boardFetcherKeys.all}/deleteBoard`,
name: () => `${boardFetcherKeys.all}/name`,
deleteBoardFilter: (id: string) => `${boardFetcherKeys.deleteBoard()}/${id}`,
nameFilter: (boardId: string) => `${boardFetcherKeys.name()}/${boardId}`,
};
23 changes: 23 additions & 0 deletions packages/webapp/app/services/queries/board/patchBoardById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import BoardModel, { BoardApi } from '../../../models/Board.model';
import { collections } from '../../pocketbase/collections';
import { pb } from '../../pocketbase/setup';
import { forwardError } from '../../../utils/forwardError';
import { parseClientResponseError } from '../../../utils/parseClientResponseError';

interface PatchBoardByIdBody {
name?: string;
}
interface PatchBoardByIdArgs {
body: PatchBoardByIdBody;
id: string;
}
export async function patchBoardById(args: PatchBoardByIdArgs) {
const { body, id } = args;

const record = await pb
.collection<BoardApi>(collections.board)
.update(id, body)
.catch(forwardError(parseClientResponseError));

return BoardModel.fromApi(record);
}
1 change: 1 addition & 0 deletions packages/webapp/app/utils/modalIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export const modalIds = {
createDepartment: 'create-department-modal',
deleteDepartment: 'delete-department-modal',
createBoard: 'create-board-modal',
deleteBoard: 'delete-board-modal',
};
48 changes: 48 additions & 0 deletions packages/webapp/pb_migrations/1714079842_updated_task_assignee.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("7gxj5bvheyycz1c")

// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "exwjaqtm",
"name": "assigneeId",
"type": "relation",
"required": true,
"presentable": false,
"unique": false,
"options": {
"collectionId": "_pb_users_auth_",
"cascadeDelete": true,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}))

return dao.saveCollection(collection)
}, (db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId("7gxj5bvheyycz1c")

// update
collection.schema.addField(new SchemaField({
"system": false,
"id": "exwjaqtm",
"name": "assigneeId",
"type": "relation",
"required": true,
"presentable": false,
"unique": false,
"options": {
"collectionId": "_pb_users_auth_",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
}))

return dao.saveCollection(collection)
})
Loading