Changes
10 changed files (+220/-33)
-
-
@@ -0,0 +1,28 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { action } from "@storybook/addon-actions"; import { type Decorator } from "@storybook/react"; import { InmemoryRouterProvider, type InmemoryRouterProviderProps, } from "../../src/contexts/Router.tsx"; export type WithInmemoryRouterOptions = Pick<InmemoryRouterProviderProps, "initialURL">; export function withInmemoryRouter<Args>({ initialURL, }: WithInmemoryRouterOptions = {}): Decorator<Args> { return (Story) => { return ( <InmemoryRouterProvider baseURL="/" initialURL={initialURL} onRouteChange={action("onRouteChange")} > <Story /> </InmemoryRouterProvider> ); }; }
-
-
-
@@ -7,6 +7,7 @@ import { ThemeProvider } from "../src/lib.ts";import css from "./preview.module.css"; import { withIDBBackend } from "./decorators/withIDBBackend.tsx"; import { withInmemoryRouter } from "./decorators/withInmemoryRouter.tsx"; export default { decorators: [
-
@@ -15,6 +16,7 @@ <ThemeProvider className={css.theme}><Story /> </ThemeProvider> ), withInmemoryRouter(), withIDBBackend(), ], } satisfies Preview;
-
-
-
@@ -12,7 +12,7 @@ children: (<> <NavigationMenu.Group title="Foo"> <NavigationMenu.Item> <a href="#">Link</a> <a href="/link">Link</a> </NavigationMenu.Item> </NavigationMenu.Group> <NavigationMenu.Group title="Bar">
-
-
-
@@ -3,7 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { create } from "@bufbuild/protobuf"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; import { withMockedBackend } from "../../.storybook/decorators/withMockedBackend.tsx";
-
@@ -23,7 +22,6 @@ layout: "fullscreen",}, args: { workspace, createButton: <button onClick={action("onCreate")}>登録画面へ</button>, }, } satisfies Meta<(typeof WorkerListPage)["Page"]>;
-
-
-
@@ -43,10 +43,13 @@export interface PageProps { workspace: Workspace; createButton: ReactElement; /** * @deprecated */ createButton?: ReactElement; } export const Page: FC<PageProps> = ({ workspace, createButton }) => { export const Page: FC<PageProps> = ({ workspace }) => { const list = useWorkerList(workspace); if (list.fetchStatus === "idle" && list.data?.workers.length === 0) {
-
@@ -59,7 +62,7 @@ <Text as="p" size="2" align="center">ワークスペース内に労働者がまだ一人も登録されていません。 </Text> <Button asChild mt="4" size="3"> {createButton} <a href={`/${workspace.id?.value}/workers?new`}>登録する</a> </Button> </Flex> );
-
-
-
@@ -11,12 +11,13 @@ import { Selector } from "./WorkspaceSelectionPage/Selector.tsx";import { CreationForm } from "./WorkspaceSelectionPage/CreationForm.tsx"; export interface WorkspaceSelectionPageProps { /** * @deprecated */ onOpenWorkspace?(workspace: Workspace): void; } export const WorkspaceSelectionPage: FC<WorkspaceSelectionPageProps> = ({ onOpenWorkspace, }) => { export const WorkspaceSelectionPage: FC<WorkspaceSelectionPageProps> = () => { const viewTransition = useViewTransition(); const [isOpeningCreationForm, setIsOpeningCreationForm] = useState(false);
-
@@ -43,10 +44,7 @@ }return ( <Layout title="ワークスペース選択"> <Selector onOpenCreationForm={openCreationForm} onOpenWorkspace={onOpenWorkspace ?? (() => void 0)} /> <Selector onOpenCreationForm={openCreationForm} /> </Layout> ); };
-
-
-
@@ -9,7 +9,6 @@ import {ListResponseSchema, type ListResponse_Result, } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { type Workspace } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { type FC, useEffect } from "react"; import { useMethodQuery } from "../../contexts/Service.tsx";
-
@@ -21,11 +20,9 @@ import { WorkspaceList } from "./WorkspaceList.tsx";export interface SelectorProps { onOpenCreationForm?(): void; onOpenWorkspace(workspace: Workspace): void; } export const Selector: FC<SelectorProps> = ({ onOpenCreationForm, onOpenWorkspace }) => { export const Selector: FC<SelectorProps> = ({ onOpenCreationForm }) => { const list = useMethodQuery({ service: "yamori.workspace.v1.WorkspaceService", method: "List",
-
@@ -73,18 +70,16 @@ <PlusIcon />作成 </Button> </Flex> <Contents list={list} onOpenWorkspace={onOpenWorkspace} /> <Contents list={list} /> </Flex> ); }; interface ContentsProps { list: UseQueryResult<ListResponse_Result, unknown>; onOpenWorkspace(workspace: Workspace): void; } const Contents: FC<ContentsProps> = ({ list, onOpenWorkspace }) => { const Contents: FC<ContentsProps> = ({ list }) => { if (list.status === "pending") { return ( <Flex align="center" justify="center" pt="7" gap="2">
-
@@ -128,10 +123,7 @@ </Button>} /> )} <WorkspaceList workspaces={list.data.workspaces} onOpenWorkspace={onOpenWorkspace} /> <WorkspaceList workspaces={list.data.workspaces} /> </Flex> ); };
-
-
-
@@ -7,14 +7,9 @@ import { type FC } from "react";export interface WorkspaceListProps { workspaces: readonly Workspace[]; onOpenWorkspace(workspace: Workspace): void; } export const WorkspaceList: FC<WorkspaceListProps> = ({ workspaces, onOpenWorkspace, }) => { export const WorkspaceList: FC<WorkspaceListProps> = ({ workspaces }) => { return ( <Flex role="list" direction="column" gap="3"> {workspaces.map((workspace) => (
-
@@ -24,8 +19,8 @@ <Box><Text weight="bold">{workspace.displayName}</Text> </Box> <Flex justify="end"> <Button variant="soft" onClick={() => void onOpenWorkspace?.(workspace)}> 開く <Button asChild variant="soft"> <a href={`/${workspace.id?.value}/workers`}>開く</a> </Button> </Flex> </Flex>
-
-
-
@@ -0,0 +1,171 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createContext, type FC, type ReactNode, useEffect, useMemo, useRef, useState, } from "react"; export interface Route { url: Readonly<URL>; } export const RouteContext = createContext<Route>({ url: new URL(location.href), }); export interface Navigation { push(url: string | URL): void; forward(): void; back(): void; } const nativeNavigation: Navigation = { push(url) { location.href = url instanceof URL ? url.href : url; }, forward() { history.forward(); }, back() { history.back(); }, }; export const NavigationContext = createContext<Navigation>(nativeNavigation); export interface InmemoryRouterProviderProps { initialURL?: string | URL | undefined; baseURL?: string | URL | undefined; children: ReactNode; onRouteChange?(url: URL): void; } export const InmemoryRouterProvider: FC<InmemoryRouterProviderProps> = ({ initialURL, baseURL, children, onRouteChange, }) => { const [history, setHistory] = useState<{ current: URL; backwards: readonly URL[]; forwards: readonly URL[]; }>(() => { return { current: new URL(initialURL ?? location.href, location.href), forwards: [], backwards: [], }; }); const routeChangeCallback = useRef(onRouteChange); routeChangeCallback.current = onRouteChange; const navigation = useMemo<Navigation>(() => { return { push(url) { setHistory(({ current, backwards }) => { const next = new URL(url, current); routeChangeCallback.current?.(next); return { current: next, backwards: [current, ...backwards], forwards: [], }; }); }, forward() { setHistory((history) => { if (!history.forwards[0]) { return history; } routeChangeCallback.current?.(history.forwards[0]); return { current: history.forwards[0], backwards: [history.current, ...history.backwards], forwards: history.forwards.slice(1), }; }); }, back() { setHistory((history) => { if (!history.backwards[0]) { return history; } routeChangeCallback.current?.(history.backwards[0]); return { current: history.backwards[0], backwards: history.backwards.slice(1), forwards: [history.current, ...history.forwards], }; }); }, }; }, []); const root = useMemo(() => new URL(baseURL ?? location.href, location.href), [baseURL]); useEffect(() => { const listener = (event: MouseEvent) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { return; } if (!(event.target instanceof Element)) { return; } const anchor = event.target instanceof HTMLAnchorElement ? event.target : event.target.closest("a"); if ( !anchor || !anchor.href || (anchor.hasAttribute("target") && anchor.getAttribute("target") !== "_self") ) { return; } const href = new URL(anchor.href, history.current); if (!href.toString().startsWith(root.toString())) { return; } event.preventDefault(); event.stopPropagation(); navigation.push(href); }; document.addEventListener("click", listener); return () => void document.removeEventListener("click", listener); }, [root, history.current, navigation]); const route = useMemo<Route>(() => { return { url: history.current }; }, [history.current]); return ( <NavigationContext.Provider value={navigation}> <RouteContext.Provider value={route}>{children}</RouteContext.Provider> </NavigationContext.Provider> ); };
-
-