Changes
2 changed files (+441/-319)
-
-
@@ -0,0 +1,408 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Button, Flex, Heading, Switch, TextField } from "@radix-ui/themes"; import { type User } from "@yamori/proto/yamori/workspace/v2/user_pb.js"; import { type FC, type FormEventHandler, type ReactElement, type ReactNode } from "react"; import { Controller, useForm, type UseFormReturn } from "react-hook-form"; import * as FormField from "./FormField.ts"; export interface UpdateFields { name: string; displayName: string; canAddUser: boolean; canDeleteRegularUser: boolean; canReadOtherUserProfile: boolean; canUpdateOtherRegularUserProfile: boolean; canUpdateSelfProfile: boolean; canUpdateOtherRegularUserLoginMethod: boolean; canUpdateWorkspace: boolean; } export interface CreateFields extends UpdateFields { loginPassword: string; } interface RootProps { children: ReactNode; onSubmit?: FormEventHandler<HTMLFormElement>; } const Root: FC<RootProps> = ({ children, onSubmit }) => { return ( <Flex direction="column" gap="5"> <Flex asChild direction="column" gap="5" mt="2"> <form onSubmit={onSubmit}>{children}</form> </Flex> </Flex> ); }; interface ProfileFieldsProps { children?: ReactElement; disabled?: boolean; form: UseFormReturn<UpdateFields, any, undefined>; } const ProfileFields: FC<ProfileFieldsProps> = ({ children, disabled, form }) => { return ( <> <Heading as="h2">基本情報</Heading> <FormField.Root> <FormField.Label htmlFor="c_name">ユーザ名</FormField.Label> <TextField.Root id="c_name" disabled={disabled} placeholder="hanako_nihon" color={form.formState.errors.name ? "red" : undefined} aria-invalid={!!form.formState.errors.name} autoComplete="username" {...form.register("name", { required: "必須です", setValueAs(value: string) { return value.trim(); }, })} /> <FormField.Description error={form.formState.errors.name?.message}> ログイン時に利用するユーザ名です。 前後の空白は無視されます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_displayName">表示名</FormField.Label> <TextField.Root id="c_displayName" disabled={disabled} placeholder="日本 花子" color={form.formState.errors.displayName ? "red" : undefined} aria-invalid={!!form.formState.errors.displayName} autoComplete="off" {...form.register("displayName", { setValueAs(value: string) { return value.trim(); }, })} /> <FormField.Description error={form.formState.errors.displayName?.message}> 他のユーザも閲覧可能な画面上の表示名です。 未指定の場合はユーザ名が設定されます。 前後の空白は無視されます。 </FormField.Description> </FormField.Root> {children} </> ); }; interface PasswordFieldProps { disabled?: boolean; form: UseFormReturn<CreateFields>; } const PasswordField: FC<PasswordFieldProps> = ({ disabled, form }) => { return ( <FormField.Root> <FormField.Label htmlFor="c_loginPassword">ログインパスワード</FormField.Label> <TextField.Root id="c_loginPassword" disabled={disabled} color={form.formState.errors.loginPassword ? "red" : undefined} aria-invalid={!!form.formState.errors.loginPassword} type="password" autoComplete="do_not_complete_idiot" {...form.register("loginPassword", { required: "必須です", minLength: { value: 8, message: "8文字以上を入力してください", }, })} /> <FormField.Description error={form.formState.errors.loginPassword?.message}> 新たに作成されるユーザが利用するログインパスワードです。 </FormField.Description> </FormField.Root> ); }; interface PermissionFieldsProps { loginUser: User; disabled?: boolean; form: UseFormReturn<UpdateFields>; } const PermissionFields: FC<PermissionFieldsProps> = ({ loginUser, disabled, form }) => { return ( <> <Heading as="h2">権限</Heading> <FormField.Root> <FormField.Label htmlFor="c_canUpdateSelfProfile"> アカウント情報編集 </FormField.Label> <Controller control={form.control} name="canUpdateSelfProfile" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateSelfProfile" disabled={disabled || !loginUser.permissions?.canUpdateSelfProfile} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 作成されたユーザが自身の表示名を変更するための権限です。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_canAddUser">ユーザ作成</FormField.Label> <Controller control={form.control} name="canAddUser" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canAddUser" disabled={disabled || !loginUser.permissions?.canAddUser} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> システムに新しいユーザを作成・登録する権限です。 このユーザに与えられた権限と同じかそれ未満のユーザのみ作成・登録ができます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_canDeleteRegularUser">ユーザ削除</FormField.Label> <Controller control={form.control} name="canDeleteRegularUser" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canDeleteRegularUser" disabled={disabled || !loginUser.permissions?.canDeleteRegularUser} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> ユーザを削除する権限です。 管理者の削除は管理者のみ行えます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_canReadOtherUserProfile"> 他ユーザ情報閲覧 </FormField.Label> <Controller control={form.control} name="canReadOtherUserProfile" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canReadOtherUserProfile" disabled={disabled || !loginUser.permissions?.canReadOtherUserProfile} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 他のユーザの基本情報を閲覧する権限です。 ユーザ一覧の表示に必要となります。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_canUpdateOtherRegularUserProfile"> 他ユーザ情報編集 </FormField.Label> <Controller control={form.control} name="canUpdateOtherRegularUserProfile" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateOtherRegularUserProfile" disabled={ disabled || !loginUser.permissions?.canUpdateOtherRegularUserProfile } checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 他のユーザの基本情報を編集する権限です。 管理者に対する変更は管理者のみ行えます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_canUpdateOtherRegularUserLoginMethod"> 他ユーザログイン設定変更 </FormField.Label> <Controller control={form.control} name="canUpdateOtherRegularUserLoginMethod" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateOtherRegularUserLoginMethod" disabled={ disabled || !loginUser.permissions?.canUpdateOtherRegularUserLoginMethod } checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 他のユーザのパスワードを変更する権限です。 管理者に対する変更は管理者のみ行えます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_canUpdateWorkspace"> ワークスペース設定変更 </FormField.Label> <Controller control={form.control} name="canUpdateWorkspace" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateWorkspace" disabled={disabled || !loginUser.permissions?.canUpdateWorkspace} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> カスタム休暇の登録やカスタム属性の登録、年次有給休暇の払い出しテーブルの変更といった ワークスペース全体の変更を行う権限です。 </FormField.Description> </FormField.Root> </> ); }; export interface CreateFormProps { loginUser: User; pending?: boolean; onCreate?(fields: CreateFields): void; } export const CreateForm: FC<CreateFormProps> = ({ loginUser, pending, onCreate }) => { const form = useForm<CreateFields>({ defaultValues: { name: "", displayName: "", loginPassword: "", canAddUser: false, canDeleteRegularUser: false, canReadOtherUserProfile: false, canUpdateOtherRegularUserProfile: false, canUpdateSelfProfile: loginUser.permissions?.canUpdateSelfProfile ?? false, canUpdateOtherRegularUserLoginMethod: false, canUpdateWorkspace: false, }, mode: "onBlur", }); return ( <Root onSubmit={form.handleSubmit((values) => { onCreate?.(values); })} > <ProfileFields disabled={pending} // @ts-expect-error react-hook-form における型設計ミス // https://github.com/react-hook-form/react-hook-form/issues/6726 form={form} > <PasswordField disabled={pending} form={form} /> </ProfileFields> <PermissionFields disabled={pending} // @ts-expect-error react-hook-form における型設計ミス // https://github.com/react-hook-form/react-hook-form/issues/6726 form={form} loginUser={loginUser} /> <Button loading={pending}>作成</Button> </Root> ); }; export interface UpdateFormProps { loginUser: User; user: User; pending?: boolean; onUpdate?(fields: UpdateFields): void; } export const UpdateForm: FC<UpdateFormProps> = ({ loginUser, user, pending, onUpdate, }) => { const form = useForm<UpdateFields>({ defaultValues: { name: user.name, displayName: user.displayName, canAddUser: !!user.permissions?.canAddUser, canDeleteRegularUser: !!user.permissions?.canDeleteRegularUser, canReadOtherUserProfile: !!user.permissions?.canReadOtherUserProfile, canUpdateOtherRegularUserProfile: !!user.permissions?.canUpdateOtherRegularUserProfile, canUpdateSelfProfile: !!user.permissions?.canUpdateSelfProfile, canUpdateOtherRegularUserLoginMethod: !!user.permissions?.canUpdateOtherRegularUserLoginMethod, canUpdateWorkspace: !!user.permissions?.canUpdateWorkspace, }, mode: "onBlur", }); return ( <Root onSubmit={form.handleSubmit((values) => { onUpdate?.(values); })} > <ProfileFields disabled={pending} form={form} /> <PermissionFields disabled={pending} form={form} loginUser={loginUser} /> <Button loading={pending}>更新</Button> </Root> ); };
-
-
-
@@ -5,25 +5,16 @@ import "../../../polyfill.ts";import { type MessageInitShape } from "@bufbuild/protobuf"; import { createClient } from "@connectrpc/connect"; import { Button, Box, Container, Flex, Heading, Switch, TextField, } from "@radix-ui/themes"; import { Button, Box, Container } from "@radix-ui/themes"; import { useMutation } from "@tanstack/react-query"; import { type CreateUserRequestSchema } from "@yamori/proto/yamori/workspace/v2/create_user_request_pb.js"; import { type User } from "@yamori/proto/yamori/workspace/v2/user_pb.js"; import { WorkspaceService } from "@yamori/proto/yamori/workspace/v2/workspace_service_pb.js"; import { type FC, use } from "react"; import { Controller, useForm } from "react-hook-form"; import * as Empty from "../../../components/Empty.ts"; import * as FormField from "../../../components/FormField.ts"; import { ManagedErrorCallout } from "../../../components/ErrorCallout.ts"; import { CreateForm } from "../../../components/UserEditForm.tsx"; import { useConnectTransport } from "../../../contexts/ConnectTransport.tsx"; import { NavigationContext } from "../../../contexts/Router.tsx"; import { useToast } from "../../../contexts/Toast.tsx";
-
@@ -96,315 +87,38 @@ });}, }); const form = useForm<{ name: string; displayName: string; loginPassword: string; canAddUser: boolean; canDeleteRegularUser: boolean; canReadOtherUserProfile: boolean; canUpdateOtherRegularUserProfile: boolean; canUpdateSelfProfile: boolean; canUpdateOtherRegularUserLoginMethod: boolean; canUpdateWorkspace: boolean; }>({ defaultValues: { name: "", displayName: "", loginPassword: "", canAddUser: false, canDeleteRegularUser: false, canReadOtherUserProfile: false, canUpdateOtherRegularUserProfile: false, canUpdateSelfProfile: loginUser.permissions?.canUpdateSelfProfile ?? false, canUpdateOtherRegularUserLoginMethod: false, canUpdateWorkspace: false, }, mode: "onBlur", }); return ( <Flex direction="column" mt="5" gap="5"> <Flex asChild direction="column" gap="5" mt="2"> <form onSubmit={form.handleSubmit(({ name, displayName, loginPassword, ...rest }) => { creation.mutate({ name, displayName, password: loginPassword, permissions: rest, }); })} > {creation.error ? ( <Box position="sticky" top="0" pt="2"> <ManagedErrorCallout style={{ backdropFilter: "blur(2em)" }} role="alert" error={creation.error} actions={ <Button size="1" variant="outline" type="button" onClick={() => void creation.reset()} > 閉じる </Button> } title="作成に失敗しました" /> </Box> ) : ( <div /> )} <Heading as="h2">基本情報</Heading> <FormField.Root> <FormField.Label htmlFor="c_name">ユーザ名</FormField.Label> <TextField.Root id="c_name" disabled={creation.isPending} placeholder="hanako_nihon" color={form.formState.errors.name ? "red" : undefined} aria-invalid={!!form.formState.errors.name} autoComplete="username" {...form.register("name", { required: "必須です", setValueAs(value: string) { return value.trim(); }, })} /> <FormField.Description error={form.formState.errors.name?.message}> ログイン時に利用するユーザ名です。 前後の空白は無視されます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_displayName">表示名</FormField.Label> <TextField.Root id="c_displayName" disabled={creation.isPending} placeholder="日本 花子" color={form.formState.errors.displayName ? "red" : undefined} aria-invalid={!!form.formState.errors.displayName} autoComplete="off" {...form.register("displayName", { setValueAs(value: string) { return value.trim(); }, })} /> <FormField.Description error={form.formState.errors.displayName?.message}> 他のユーザも閲覧可能な画面上の表示名です。 未指定の場合はユーザ名が設定されます。 前後の空白は無視されます。 </FormField.Description> </FormField.Root> <FormField.Root> <FormField.Label htmlFor="c_loginPassword"> ログインパスワード </FormField.Label> <TextField.Root id="c_loginPassword" disabled={creation.isPending} color={form.formState.errors.loginPassword ? "red" : undefined} aria-invalid={!!form.formState.errors.loginPassword} type="password" autoComplete="do_not_complete_idiot" {...form.register("loginPassword", { required: "必須です", minLength: { value: 8, message: "8文字以上を入力してください", }, })} /> <FormField.Description error={form.formState.errors.loginPassword?.message}> 新たに作成されるユーザが利用するログインパスワードです。 </FormField.Description> </FormField.Root> <Heading as="h2">権限</Heading> {loginUser.permissions?.canUpdateSelfProfile && ( <FormField.Root> <FormField.Label htmlFor="c_canUpdateSelfProfile"> アカウント情報編集 </FormField.Label> <Controller control={form.control} name="canUpdateSelfProfile" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateSelfProfile" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 作成されたユーザが自身の表示名を変更するための権限です。 </FormField.Description> </FormField.Root> )} {loginUser.permissions?.canAddUser && ( <FormField.Root> <FormField.Label htmlFor="c_canAddUser">ユーザ作成</FormField.Label> <Controller control={form.control} name="canAddUser" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canAddUser" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> システムに新しいユーザを作成・登録する権限です。 このユーザに与えられた権限と同じかそれ未満のユーザのみ作成・登録ができます。 </FormField.Description> </FormField.Root> )} {loginUser.permissions?.canDeleteRegularUser && ( <FormField.Root> <FormField.Label htmlFor="c_canDeleteRegularUser"> ユーザ削除 </FormField.Label> <Controller control={form.control} name="canDeleteRegularUser" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canDeleteRegularUser" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> ユーザを削除する権限です。 管理者の削除は管理者のみ行えます。 </FormField.Description> </FormField.Root> )} {loginUser.permissions?.canReadOtherUserProfile && ( <FormField.Root> <FormField.Label htmlFor="c_canReadOtherUserProfile"> 他ユーザ情報閲覧 </FormField.Label> <Controller control={form.control} name="canReadOtherUserProfile" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canReadOtherUserProfile" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 他のユーザの基本情報を閲覧する権限です。 ユーザ一覧の表示に必要となります。 </FormField.Description> </FormField.Root> )} {loginUser.permissions?.canUpdateOtherRegularUserProfile && ( <FormField.Root> <FormField.Label htmlFor="c_canUpdateOtherRegularUserProfile"> 他ユーザ情報編集 </FormField.Label> <Controller control={form.control} name="canUpdateOtherRegularUserProfile" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateOtherRegularUserProfile" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 他のユーザの基本情報を編集する権限です。 管理者に対する変更は管理者のみ行えます。 </FormField.Description> </FormField.Root> )} {loginUser.permissions?.canUpdateOtherRegularUserLoginMethod && ( <FormField.Root> <FormField.Label htmlFor="c_canUpdateOtherRegularUserLoginMethod"> 他ユーザログイン設定変更 </FormField.Label> <Controller control={form.control} name="canUpdateOtherRegularUserLoginMethod" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateOtherRegularUserLoginMethod" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> 他のユーザのパスワードを変更する権限です。 管理者に対する変更は管理者のみ行えます。 </FormField.Description> </FormField.Root> )} {loginUser.permissions?.canUpdateWorkspace && ( <FormField.Root> <FormField.Label htmlFor="c_canUpdateWorkspace"> ワークスペース設定変更 </FormField.Label> <Controller control={form.control} name="canUpdateWorkspace" render={({ field: { onChange: _onChange, value, ...field } }) => ( <Switch id="c_canUpdateWorkspace" disabled={creation.isPending} checked={value} onCheckedChange={(newValue) => { form.setValue(field.name, newValue); }} {...field} /> )} /> <FormField.Description> カスタム休暇の登録やカスタム属性の登録、年次有給休暇の払い出しテーブルの変更といった ワークスペース全体の変更を行う権限です。 </FormField.Description> </FormField.Root> )} <Button loading={creation.isPending}>作成</Button> </form> </Flex> </Flex> <Box mt="3"> {creation.error ? ( <Box position="sticky" top="0" pt="2" mb="2" style={{ zIndex: 10 }}> <ManagedErrorCallout style={{ backdropFilter: "blur(2em)" }} role="alert" error={creation.error} actions={ <Button size="1" variant="outline" type="button" onClick={() => void creation.reset()} > 閉じる </Button> } title="作成に失敗しました" /> </Box> ) : ( <Box mb="2" /> )} <CreateForm pending={creation.isPending} loginUser={loginUser} onCreate={({ name, displayName, loginPassword, ...permissions }) => { creation.mutate({ name, displayName, password: loginPassword, permissions }); }} /> </Box> ); };
-