Changes
6 changed files (+329/-0)
-
-
@@ -0,0 +1,30 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import * as proto from "@bufbuild/protobuf"; import type { Meta, StoryObj } from "@storybook/react"; import { PaidLeaveProvisionTableSchema } from "@yamori/proto/yamori/paid_leave_provision/v1/paid_leave_provision_table_pb.js"; import { PaidLeaveProvisionTableView } from "./PaidLeaveProvisionTableView.tsx"; export default { component: PaidLeaveProvisionTableView, args: { paidLeaveProvisionTable: proto.create(PaidLeaveProvisionTableSchema), }, } satisfies Meta<typeof PaidLeaveProvisionTableView>; type Story = StoryObj<typeof PaidLeaveProvisionTableView>; export const Empty: Story = {}; export const Filled: Story = { args: { paidLeaveProvisionTable: proto.create(PaidLeaveProvisionTableSchema, { currentRevision: { firstProvisionAmountDays: 10, subsequentProvisionAmountDays: [11, 12, 14, 16, 18], }, }), }, };
-
-
-
@@ -0,0 +1,51 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import * as proto from "@bufbuild/protobuf"; import { Table } from "@radix-ui/themes"; import { type PaidLeaveProvisionTableSchema } from "@yamori/proto/yamori/paid_leave_provision/v1/paid_leave_provision_table_pb.js"; import { type FC } from "react"; export interface PaidLeaveProvisionTableViewProps extends Omit<Table.RootProps, "children"> { paidLeaveProvisionTable: proto.MessageShape<typeof PaidLeaveProvisionTableSchema>; } export const PaidLeaveProvisionTableView: FC<PaidLeaveProvisionTableViewProps> = ({ paidLeaveProvisionTable, ...rest }) => { const { currentRevision } = paidLeaveProvisionTable; return ( <Table.Root {...rest}> <Table.Header> <Table.Row> <Table.ColumnHeaderCell>付与回</Table.ColumnHeaderCell> <Table.ColumnHeaderCell>付与日数</Table.ColumnHeaderCell> </Table.Row> </Table.Header> <Table.Body> {currentRevision && ( <> <Table.Row> <Table.RowHeaderCell>初回</Table.RowHeaderCell> <Table.Cell>{currentRevision.firstProvisionAmountDays}日</Table.Cell> </Table.Row> {currentRevision.subsequentProvisionAmountDays.map((days, offset, list) => { return ( <Table.Row key={offset}> <Table.RowHeaderCell> {offset + 2}回目{offset === list.length - 1 && "~"} </Table.RowHeaderCell> <Table.Cell>{days}日</Table.Cell> </Table.Row> ); })} </> )} </Table.Body> </Table.Root> ); };
-
-
-
@@ -24,6 +24,7 @@ import { URLContext } from "../../contexts/Router.tsx";import { useViewTransition } from "../../hooks/useViewTransition.ts"; import * as calendar from "./calendar/page.tsx"; import * as paidLeaveProvisionTable from "./paid-leave-provision-table/page.tsx"; import * as workers from "./workers/page.tsx"; import * as workersNew from "./workers/new/page.tsx"; import * as workerDashboard from "./workers/:id/Dashboard.tsx";
-
@@ -155,6 +156,11 @@ <leaveDefinitionsNew.Title /></a> </NavigationMenu.Item> )} <NavigationMenu.Item current={paidLeaveProvisionTable.pattern.test(url)}> <a href={paidLeaveProvisionTable.href({ workspace })}> <paidLeaveProvisionTable.Title /> </a> </NavigationMenu.Item> </NavigationMenu.Group> </NavigationMenu.Root> </ScrollArea>
-
-
-
@@ -18,6 +18,7 @@ import { useMethodQuery } from "../../contexts/Service.tsx";import { IllegalMessageError } from "../../errors/IllegalMessageError.ts"; import * as calendar from "./calendar/page.tsx"; import * as paidLeaveProvisionTable from "./paid-leave-provision-table/page.tsx"; import * as workers from "./workers/page.tsx"; import * as workersNew from "./workers/new/page.tsx"; import * as workerSubRoute from "./workers/:id/page.tsx";
-
@@ -138,6 +139,10 @@ },{ pattern: calendar.pattern, children: <calendar.Page workspace={workspace} />, }, { pattern: paidLeaveProvisionTable.pattern, children: <paidLeaveProvisionTable.Page workspace={workspace} />, }, { pattern: workerSubRoute.pattern,
-
-
-
@@ -0,0 +1,91 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import * as proto from "@bufbuild/protobuf"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; 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 { Page } from "./page.tsx"; const success = proto.create(WorkspaceSchema, { paidLeaveProvisionTables: [ { id: { value: "pt-a" }, displayName: "テーブルA", currentRevision: { firstProvisionAmountDays: 1, subsequentProvisionAmountDays: [2, 3, 4, 5], }, }, { id: { value: "pt-b" }, displayName: "テーブルB", currentRevision: { firstProvisionAmountDays: 10, subsequentProvisionAmountDays: [20, 30, 40, 50], }, }, ], }); export default { component: Page, parameters: { layout: "fullscreen", }, args: { workspace: proto.create(WorkspaceSchema, { id: { value: "ws-foo" }, }), }, decorators: [ withInmemoryRouter({ initialURL: "/ws-foo/paid-leave-provision-table" }), withMockedBackend([ Get({ workspace: success, }), ]), ], } satisfies Meta<typeof Page>; type Story = StoryObj<typeof Page>; export const Success: Story = {}; export const SlowLoad: Story = { decorators: [ withMockedBackend([ Get({ workspace: success, delayMs: 3_000, }), ]), ], }; export const LoadError: Story = { decorators: [ withMockedBackend([ Get({ workspace: success, failureRate: 1, }), ]), ], }; export const Flaky: Story = { decorators: [ withMockedBackend([ Get({ workspace: success, failureRate: 0.5, delayMs: 1_000, }), ]), ], };
-
-
-
@@ -0,0 +1,146 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import "../../../polyfill.ts"; import * as proto from "@bufbuild/protobuf"; import { Box, Button, Container, Flex, Heading, Section, Spinner, Text, } from "@radix-ui/themes"; 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 } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { PaidLeaveProvisionTableSchema } from "@yamori/proto/yamori/paid_leave_provision/v1/paid_leave_provision_table_pb.js"; import { type FC } from "react"; import { ManagedErrorCallout } from "../../../components/ErrorCallout.ts"; import { PaidLeaveProvisionTableView } from "../../../components/PaidLeaveProvisionTableView.tsx"; import { useMethodQuery } from "../../../contexts/Service.tsx"; import { IllegalMessageError } from "../../../errors/IllegalMessageError.ts"; import { Layout } from "../Layout.tsx"; export const Title: FC = () => "有給休暇付与テーブル"; export interface HrefInput { workspace: proto.MessageShape<typeof WorkspaceSchema>; } export function href({ workspace }: HrefInput): string { if (!workspace.id) { return "/"; } return `/${workspace.id.value}/paid-leave-provision-table`; } export const pattern = new URLPattern({ pathname: "/:workspace/paid-leave-provision-table", }); export interface PageProps { workspace: proto.MessageShape<typeof WorkspaceSchema>; } export const Page: FC<PageProps> = ({ workspace }) => { return ( <Layout workspace={workspace} title={<Title />}> <Body workspaceID={workspace.id} /> </Layout> ); }; interface BodyProps { workspaceID: proto.MessageShape<typeof WorkspaceSchema>["id"]; } const Body: FC<BodyProps> = ({ workspaceID }) => { const query = useMethodQuery({ service: "yamori.workspace.v1.WorkspaceService", method: "Get", request: { schema: GetRequestSchema, data: { workspaceId: workspaceID, readMask: { fields: [WorkspaceSchema.field.paidLeaveProvisionTables.number], paidLeaveProvisionTableMask: { fields: [ PaidLeaveProvisionTableSchema.field.id.number, PaidLeaveProvisionTableSchema.field.displayName.number, PaidLeaveProvisionTableSchema.field.currentRevision.number, ], }, }, }, }, response: { schema: GetResponseSchema, }, mapResponse(resp) { switch (resp.result.case) { case "ok": return resp.result.value.paidLeaveProvisionTables.sort( (a, b) => (b.currentRevision?.firstProvisionAmountDays ?? 0) - (a.currentRevision?.firstProvisionAmountDays ?? 0), ); case undefined: throw new IllegalMessageError(resp); default: throw resp.result.value; } }, }); return ( <Container asChild p="2" size="2"> <Flex direction="column" gap="2"> {query.isError && ( <ManagedErrorCallout error={query.error} title="一覧の取得に失敗しました" actions={ <Button size="1" loading={query.isFetching} onClick={() => void query.refetch()} > 再取得 </Button> } /> )} {query.isLoading && ( <Flex mt="5" justify="center" align="center" gap="2"> <Spinner /> <Text size="2" color="gray"> テーブルの一覧を取得中... </Text> </Flex> )} {query.data && ( <Box> {query.data.map((table) => { return ( <Section key={table.id?.value}> <Heading as="h2" mb="2"> {table.displayName} </Heading> <PaidLeaveProvisionTableView paidLeaveProvisionTable={table} /> </Section> ); })} </Box> )} </Flex> </Container> ); };
-