Changes
4 changed files (+230/-27)
-
-
@@ -4,6 +4,7 @@import { type MessageInitShape } from "@bufbuild/protobuf"; import { CreateResponseSchema } from "@yamori/proto/yamori/workspace/v1/create_response_pb.js"; import { type CreateLeaveDefinitionResponseSchema } from "@yamori/proto/yamori/workspace/v1/create_leave_definition_response_pb.js"; import { type DeleteLeaveDefinitionResponseSchema } from "@yamori/proto/yamori/workspace/v1/delete_leave_definition_response_pb.js"; import { WorkspaceService } from "@yamori/proto/yamori/workspace/v1/workspace_service_pb.js"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js";
-
@@ -47,6 +48,7 @@ },{ id: { value: "lv-bar" }, displayName: "忌引", deletionKey: { key: new Uint8Array([]) }, currentRevision: { revisionId: { value: "lr-bar-foo" }, startAt: { year: 2010, month: 10, day: 1 },
-
@@ -68,6 +70,7 @@ },{ id: { value: "lv-baz" }, displayName: "リフレッシュ休暇", deletionKey: { key: new Uint8Array([]) }, revisions: [ { revisionId: { value: "lr-baz-foo" },
-
@@ -256,3 +259,53 @@ },options, ); } export interface DeleteLeaveDefinitionOptions extends MockOptions { failureRate?: number; error?: Exclude< MessageInitShape<typeof DeleteLeaveDefinitionResponseSchema>["result"], { case: "ok" | undefined } >; result?: Extract< MessageInitShape<typeof DeleteLeaveDefinitionResponseSchema>["result"], { case: "ok" } >; } export function DeleteLeaveDefinition({ failureRate = 0, error = { case: "systemError", value: { code: "MOCK_ERR", message: "This is sample error message", }, }, result = { case: "ok", value: { deleted: { displayName: "Foo", }, }, }, ...options }: DeleteLeaveDefinitionOptions = {}) { return mock( WorkspaceService.method.deleteLeaveDefinition, (_req) => { if (Math.random() < failureRate) { return { result: error, }; } return { result, }; }, options, ); }
-
-
-
@@ -3,14 +3,17 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { CardStackPlusIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; import { AlertDialog, Badge, Box, Button, Code, DataList, DropdownMenu, Flex, IconButton, Select, Text, } from "@radix-ui/themes"; import { type Leave } from "@yamori/proto/yamori/work_record/v1/leave_pb.js"; import { type LeaveRevision } from "@yamori/proto/yamori/work_record/v1/leave_revision_pb.js";
-
@@ -37,9 +40,18 @@ export interface ListItemProps {leaveDefinition: Leave; pageTitle: ReactNode; pending?: boolean; onDelete?(def: Leave): void; } export const ListItem: FC<ListItemProps> = ({ leaveDefinition, pageTitle }) => { export const ListItem: FC<ListItemProps> = ({ leaveDefinition, pending = false, pageTitle, onDelete, }) => { const [revision, setRevision] = useState<LeaveRevision | null>( () => leaveDefinition.currentRevision ?? leaveDefinition.revisions[0] ?? null, );
-
@@ -149,28 +161,58 @@ </DataList.Item>)} </DataList.Root> </Box> <DropdownMenu.Root> <DropdownMenu.Trigger> <IconButton variant="ghost" color="gray" m="1"> <DotsHorizontalIcon /> </IconButton> </DropdownMenu.Trigger> <DropdownMenu.Content> <DropdownMenu.Item disabled>表示名の編集</DropdownMenu.Item> <DropdownMenu.Sub> <DropdownMenu.SubTrigger>バージョン管理</DropdownMenu.SubTrigger> <DropdownMenu.SubContent> <DropdownMenu.Item disabled>このバージョンを編集</DropdownMenu.Item> <DropdownMenu.Item disabled>このバージョンを削除</DropdownMenu.Item> <DropdownMenu.Item disabled>新規バージョンを作成</DropdownMenu.Item> </DropdownMenu.SubContent> </DropdownMenu.Sub> <DropdownMenu.Separator /> <DropdownMenu.Item color="red" disabled> 削除 </DropdownMenu.Item> </DropdownMenu.Content> </DropdownMenu.Root> <AlertDialog.Root open={ // AlertDialog が disabled な DropdownMenu.Item / AlertDialog.Trigger の // onSelect を拾う Radix UI のバグがあるため上流で無理やり止める。 leaveDefinition.deletionKey ? undefined : false } > <DropdownMenu.Root> <DropdownMenu.Trigger> <IconButton disabled={pending} variant="ghost" color="gray" m="1"> <DotsHorizontalIcon /> </IconButton> </DropdownMenu.Trigger> <DropdownMenu.Content> <DropdownMenu.Item disabled>表示名の編集</DropdownMenu.Item> <DropdownMenu.Sub> <DropdownMenu.SubTrigger>バージョン管理</DropdownMenu.SubTrigger> <DropdownMenu.SubContent> <DropdownMenu.Item disabled>このバージョンを編集</DropdownMenu.Item> <DropdownMenu.Item disabled>このバージョンを削除</DropdownMenu.Item> <DropdownMenu.Item disabled>新規バージョンを作成</DropdownMenu.Item> </DropdownMenu.SubContent> </DropdownMenu.Sub> <DropdownMenu.Separator /> <AlertDialog.Trigger disabled={!leaveDefinition.deletionKey}> <DropdownMenu.Item color="red">削除</DropdownMenu.Item> </AlertDialog.Trigger> </DropdownMenu.Content> </DropdownMenu.Root> <AlertDialog.Content maxWidth="30rem"> <AlertDialog.Title>休暇・休業種別の削除</AlertDialog.Title> <AlertDialog.Description size="2"> 「{leaveDefinition.displayName}」を削除しようとしています。 一度削除した種別は戻せません。 </AlertDialog.Description> <Text as="p" mt="4" size="2"> 本当に削除してよろしいですか? </Text> <Flex mt="5" justify="end" gap="3"> <AlertDialog.Cancel> <Button variant="soft" color="gray"> キャンセル </Button> </AlertDialog.Cancel> <AlertDialog.Action> <Button color="red" onClick={() => void onDelete?.(leaveDefinition)}> 削除 </Button> </AlertDialog.Action> </Flex> </AlertDialog.Content> </AlertDialog.Root> </Flex> ); };
-
-
-
@@ -7,7 +7,10 @@ import type { Meta, StoryObj } from "@storybook/react";import { withMockedBackend } from "../../../../.storybook/decorators/withMockedBackend.tsx"; import { withInmemoryRouter } from "../../../../.storybook/decorators/withInmemoryRouter.tsx"; import { Get } from "../../../mocks/yamori/workspace/v1/workspace_service.ts"; import { Get, DeleteLeaveDefinition, } from "../../../mocks/yamori/workspace/v1/workspace_service.ts"; import { Page } from "./page.tsx";
-
@@ -22,6 +25,7 @@ id: { value: "lv-foo" },displayName: "なにかの休業", isWorkerDeemedToBeWorked: true, updateKey: { key: new Uint8Array([]) }, deletionKey: { key: new Uint8Array([]) }, currentRevision: { revisionId: { value: "lr-bar" }, startAt: { year: 2000, month: 1, day: 2 },
-
@@ -79,6 +83,7 @@ withMockedBackend([Get({ workspace: success, }), DeleteLeaveDefinition(), ]), ], } satisfies Meta<typeof Page>;
-
@@ -94,6 +99,9 @@ Get({workspace: success, delayMs: 3_000, }), DeleteLeaveDefinition({ delayMs: 1_000, }), ]), ], };
-
@@ -104,6 +112,7 @@ withMockedBackend([Get({ workspace: {}, }), DeleteLeaveDefinition(), ]), ], };
-
@@ -125,6 +134,21 @@ withMockedBackend([Get({ workspace: success, failureRate: 0.5, }), DeleteLeaveDefinition({ failureRate: 0.5, }), ]), ], }; export const DeleteError: Story = { decorators: [ withMockedBackend([ Get(), DeleteLeaveDefinition({ delayMs: 1_000, failureRate: 1, }), ]), ],
-
-
-
@@ -3,20 +3,24 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport "../../../polyfill.ts"; import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons"; import { Button, Flex, Separator, Spinner, Text } from "@radix-ui/themes"; import { LeaveSchema } from "@yamori/proto/yamori/work_record/v1/leave_pb.js"; import { DeleteLeaveDefinitionRequestSchema } from "@yamori/proto/yamori/workspace/v1/delete_leave_definition_request_pb.js"; import { DeleteLeaveDefinitionResponseSchema } from "@yamori/proto/yamori/workspace/v1/delete_leave_definition_response_pb.js"; import { GetRequestSchema } from "@yamori/proto/yamori/workspace/v1/get_request_pb.js"; import { GetResponseSchema } from "@yamori/proto/yamori/workspace/v1/get_response_pb.js"; import { WorkspaceSchema, type Workspace, } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { type FC, Fragment } from "react"; import { type FC, Fragment, useRef } from "react"; import * as Empty from "../../../components/Empty.ts"; import { ManagedErrorCallout } from "../../../components/ErrorCallout.ts"; import * as HelpDialog from "../../../components/HelpDialog.ts"; import { useMethodQuery } from "../../../contexts/Service.tsx"; import { useMethodQuery, useMethodMutation } from "../../../contexts/Service.tsx"; import { useToast } from "../../../contexts/Toast.tsx"; import { IllegalMessageError } from "../../../errors/IllegalMessageError.ts"; import { Layout } from "../Layout.tsx";
-
@@ -27,6 +31,8 @@ workspace: Workspace;} const Body: FC<BodyProps> = ({ workspace: { id } }) => { const toast = useToast(); const definitions = useMethodQuery({ service: "yamori.workspace.v1.WorkspaceService", method: "Get",
-
@@ -42,6 +48,7 @@ LeaveSchema.field.id.number,LeaveSchema.field.displayName.number, LeaveSchema.field.currentRevision.number, LeaveSchema.field.revisions.number, LeaveSchema.field.deletionKey.number, ], }, },
-
@@ -62,6 +69,71 @@ }}, }); const deletingToastCleanup = useRef<(() => void) | null>(null); const deleteLeaveDefinition = useMethodMutation({ service: "yamori.workspace.v1.WorkspaceService", method: "DeleteLeaveDefinition", request: { schema: DeleteLeaveDefinitionRequestSchema, }, response: { schema: DeleteLeaveDefinitionResponseSchema, }, mapResponse(resp) { switch (resp.result.case) { case "ok": if (!resp.result.value.deleted) { throw new IllegalMessageError(resp); } return resp.result.value; case undefined: throw new IllegalMessageError(resp); default: throw resp.result.value; } }, options: { onMutate() { deletingToastCleanup.current = toast.open({ severity: "info", title: "休暇・休業種別を削除しています...", onOpenChange(isOpen) { if (!isOpen) { deletingToastCleanup.current = null; } }, }); }, onSettled() { deletingToastCleanup.current?.(); deletingToastCleanup.current = null; }, onSuccess({ deleted }) { toast.open({ severity: "success", icon: <CheckCircledIcon />, title: "休暇・休業種別を削除しました", description: `「${deleted?.displayName}」をワークスペースから削除しました。`, }); definitions.refetch(); }, onError(error) { // TODO: Handle error console.error(error); toast.open({ severity: "danger", icon: <CrossCircledIcon />, title: "休暇・休業種別の削除に失敗しました", duration: 10_000, dismissible: true, }); }, }, }); if (definitions.data?.length === 0 && !definitions.isError) { return ( <Empty.Root>
-
@@ -113,7 +185,19 @@ <Flex direction="column" gap="4" role="list">{definitions.data.map((def, i) => ( <Fragment key={def.id?.value}> {i > 0 && <Separator size="4" />} <ListItem leaveDefinition={def} pageTitle={<Title />} /> <ListItem leaveDefinition={def} pending={deleteLeaveDefinition.isPaused} pageTitle={<Title />} onDelete={() => void deleteLeaveDefinition.mutate({ workspaceId: id, leaveDefinitionId: def.id, deletionKey: def.deletionKey, readMask: { fields: [LeaveSchema.field.displayName.number] }, }) } /> </Fragment> ))} </Flex>
-