metis-desk/modules/companies/src/web/access/create/ui/quick-create-dialog.tsx

151 lines
5.0 KiB
TypeScript
Raw Normal View History

import { Button } from "@repo/shadcn-ui/components/button";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useId, useRef } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { AccessFieldsDTO, AccessItemDTO } from "../../../../common";
import { AccessFieldsSchema } from "../../../../common";
import { EntityHelp } from "../../help";
import { AccessApiError, accessApi, accessKeys } from "../../shared/api";
export type QuickCreateTarget =
| { kind: "system" }
| { kind: "environment"; systemId: string; systemName: string };
export function QuickCreateDialog({
target,
scope,
csrfToken,
onClose,
onCreated,
items,
}: {
target: QuickCreateTarget;
scope: string;
csrfToken: string;
onClose: () => void;
onCreated: (id: string) => void;
items: readonly AccessItemDTO[];
}) {
const { t } = useTranslation("access");
const id = useId();
const client = useQueryClient();
const dismissed = useRef(false);
const form = useForm<{ name: string }>({ defaultValues: { name: "" } });
const enteredName = form.watch("name").trim().toLocaleLowerCase();
const existing = items.find((item) => {
const values = item.values;
return (
"name" in values &&
"active" in values &&
values.active &&
values.name.toLocaleLowerCase() === enteredName &&
(target.kind === "system"
? values.kind === "system"
: values.kind === "environment" && values.systemId === target.systemId)
);
});
const mutation = useMutation({
mutationFn: async ({ name }: { name: string }) => {
const values = AccessFieldsSchema.parse(
target.kind === "system"
? { kind: "system", name, active: true }
: { kind: "environment", name, active: true, systemId: target.systemId }
);
const result = await accessApi.request(scope, values, undefined, csrfToken);
const created = result.items.find(
(item) =>
item.companyId === (scope === "global" ? null : scope) && matches(item.values, values)
);
if (!created) {
throw new AccessApiError("INTERNAL_ERROR");
}
return { result, id: created.id };
},
onSuccess: async ({ result, id }) => {
await client.invalidateQueries({ queryKey: ["access"] });
client.setQueryData(accessKeys.scope(scope), result);
if (!dismissed.current) {
onCreated(id);
}
},
});
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
dismissed.current = true;
onClose();
}
}}
open
title={t(target.kind === "system" ? "quickAddSystem" : "quickAddEnvironment")}
>
<form className="space-y-4" onSubmit={form.handleSubmit((values) => mutation.mutate(values))}>
{target.kind === "environment" && (
<p className="text-sm text-muted-foreground">
{t("quickEnvironmentContext", { name: target.systemName })}
</p>
)}
<div className="space-y-2">
<Label htmlFor={id}>{t("name")}</Label>
<Input
id={id}
maxLength={100}
required
{...form.register("name", { validate: (value) => value.trim().length > 0 })}
/>
{form.formState.errors.name && (
<p className="text-destructive" role="alert">
{t("invalid")}
</p>
)}
</div>
{mutation.isError && (
<p className="text-destructive" role="alert">
{t(
`errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
)}
</p>
)}
{existing && (
<div className="space-y-2">
<p>{t("quickExisting")}</p>
<Button
disabled={mutation.isPending}
onClick={() => onCreated(existing.id)}
type="button"
variant="outline"
>
{t("quickUseExisting")}
</Button>
</div>
)}
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p>
<EntityHelp kind={target.kind} scope={scope} />
<FormActions>
<Button disabled={mutation.isPending} type="submit">
{t(mutation.isPending ? "saving" : "quickSaveSelect")}
</Button>
</FormActions>
</form>
</FormDialog>
);
}
function matches(item: AccessFieldsDTO, values: AccessFieldsDTO) {
if (values.kind === "system" && item.kind === "system") {
return item.name === values.name;
}
if (values.kind === "environment" && item.kind === "environment") {
return item.name === values.name && item.systemId === values.systemId;
}
return false;
}