2026-10-04 13:46:39 +00:00
|
|
|
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";
|
|
|
|
|
|
2026-10-08 17:25:02 +00:00
|
|
|
import type { AccessFieldsDTO, AccessItemDTO } from "../../../../common";
|
|
|
|
|
import { AccessFieldsSchema } from "../../../../common";
|
|
|
|
|
import { EntityHelp } from "../../help";
|
|
|
|
|
import { AccessApiError, accessApi, accessKeys } from "../../shared/api";
|
2026-10-04 13:46:39 +00:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|