291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
import { RowPermission, Tenant } from "@toolctrl/db/prisma-browser";
|
|
import { Form, useLocation, useSubmit, useNavigation, useActionData } from "react-router";
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Colors } from "~/application/enums/shared/Colors";
|
|
import ErrorBanner from "~/components/ui/banners/ErrorBanner";
|
|
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
|
|
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
|
|
import PlusIcon from "~/components/ui/icons/PlusIcon";
|
|
import InputSelect from "~/components/ui/input/InputSelect";
|
|
import InputSelector from "~/components/ui/input/InputSelector";
|
|
import { useAppData } from "~/utils/data/useAppData";
|
|
import { useAppOrAdminData } from "~/utils/data/useAppOrAdminData";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
import { RowWithDetails } from "~/utils/db/entities/rows.db.server";
|
|
import { UserWithDetails } from "~/utils/db/users.db.server";
|
|
|
|
type PermissionDto = {
|
|
tenantId: string | null;
|
|
userId: string | null;
|
|
groupId: string | null;
|
|
roleId: string | null;
|
|
};
|
|
export default function RowSettingsPermissions({
|
|
item,
|
|
items,
|
|
tenants,
|
|
users,
|
|
withTitle,
|
|
}: {
|
|
item: RowWithDetails;
|
|
items: RowPermission[];
|
|
tenants: Tenant[];
|
|
users: UserWithDetails[];
|
|
withTitle?: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const rootData = useRootData();
|
|
const location = useLocation();
|
|
const submit = useSubmit();
|
|
const appOrAdminData = useAppOrAdminData();
|
|
const actionData = useActionData<{ error: string; success: string }>();
|
|
|
|
const [adding, setAdding] = useState(false);
|
|
|
|
function getTypeOrder(permission: PermissionDto) {
|
|
if (permission.tenantId) {
|
|
return 1;
|
|
} else if (permission.userId) {
|
|
return 2;
|
|
} else if (permission.roleId) {
|
|
return 3;
|
|
} else if (permission.groupId) {
|
|
return 4;
|
|
}
|
|
return 0;
|
|
}
|
|
function getName(permission: PermissionDto) {
|
|
if (permission.tenantId) {
|
|
return tenants.find((x) => x.id === permission.tenantId)?.name;
|
|
} else if (permission.userId) {
|
|
return users.find((x) => x.id === permission.userId)?.email;
|
|
} else if (permission.roleId) {
|
|
return appOrAdminData.allRoles.find((x) => x.id === permission.roleId)?.name;
|
|
} else if (permission.groupId) {
|
|
return appOrAdminData.myGroups.find((x) => x.id === permission.groupId)?.name;
|
|
}
|
|
return "";
|
|
}
|
|
function getTypes() {
|
|
const options: { name: string; value: string; color: Colors }[] = [];
|
|
if (users.length > 0) {
|
|
options.push({ name: t("models.user.object") + "...", value: "user", color: Colors.BLUE });
|
|
}
|
|
if (tenants.length > 0) {
|
|
options.push({ name: t("models.tenant.users") + "...", value: "tenant", color: Colors.GREEN });
|
|
}
|
|
if (appOrAdminData.myGroups.length > 0 && rootData.featureFlags?.includes("row-groups")) {
|
|
options.push({ name: t("models.group.object") + "...", value: "group", color: Colors.INDIGO });
|
|
}
|
|
if (appOrAdminData.allRoles.length > 0) {
|
|
options.push({ name: t("models.role.object") + "...", value: "role", color: Colors.RED });
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function sortedItems() {
|
|
return items.filter((permission) => !permission.public).sort((a, b) => {
|
|
const aOrder = getTypeOrder(a);
|
|
const bOrder = getTypeOrder(b);
|
|
if (aOrder === bOrder) {
|
|
return getName(a)?.localeCompare(getName(b) ?? "") ?? 0;
|
|
}
|
|
return aOrder - bOrder;
|
|
});
|
|
}
|
|
return (
|
|
<div className="text-foreground space-y-2">
|
|
{withTitle && <div className="text-foreground text-sm font-medium">{t("shared.shareWith")}...</div>}
|
|
{(sortedItems().length > 0 || item.createdByUser) && (
|
|
<ul className="border-border bg-secondary/70 divide-border divide-y rounded-md border border-dashed">
|
|
{item.createdByUser && (
|
|
<div className="flex justify-between space-x-2 truncate px-2 py-2">
|
|
<div className="flex w-full space-x-2 truncate">
|
|
<div className="flex flex-col truncate">
|
|
<div className="text-muted-foreground text-xs font-bold uppercase">
|
|
<span>{t("models.row.permissionLabels.creator")}</span>
|
|
</div>
|
|
<div className="text-foreground truncate text-sm">{item.createdByUser.email}</div>
|
|
</div>
|
|
</div>
|
|
<InputSelect className="w-32 shrink-0" disabled options={[{ name: t("models.row.permissionLabels.owner"), value: "owner" }]} />
|
|
</div>
|
|
)}
|
|
|
|
{sortedItems().map((item, idx) => {
|
|
return (
|
|
<div key={idx} className="flex justify-between space-x-2 px-2 py-2">
|
|
<div className="flex w-1/2 flex-col">
|
|
<div className="text-muted-foreground text-xs font-bold uppercase">
|
|
{item.tenantId ? (
|
|
<span>{t("models.tenant.object")}</span>
|
|
) : item.roleId ? (
|
|
<span>{t("models.role.object")}</span>
|
|
) : item.groupId ? (
|
|
<span>{t("models.group.object")}</span>
|
|
) : item.userId ? (
|
|
<span>{t("models.user.object")}</span>
|
|
) : null}
|
|
</div>
|
|
<div className="text-foreground truncate text-sm">{getName(item)}</div>
|
|
</div>
|
|
<InputSelect
|
|
className="w-1/2"
|
|
value={item.access}
|
|
options={[
|
|
{ name: t("shared.permissions.canView"), value: "view" },
|
|
{ name: t("shared.permissions.canComment"), value: "comment" },
|
|
{ name: t("shared.permissions.canEdit"), value: "edit" },
|
|
{ name: t("shared.permissions.canDelete"), value: "delete" },
|
|
{ name: t("shared.remove"), value: "remove" },
|
|
]}
|
|
setValue={(value) => {
|
|
if (value === "remove") {
|
|
const form = new FormData();
|
|
form.set("action", "remove");
|
|
form.set("id", item?.id ?? "");
|
|
submit(form, {
|
|
method: "post",
|
|
action: location.pathname + location.search,
|
|
});
|
|
} else {
|
|
const form = new FormData();
|
|
form.set("action", "set-access");
|
|
form.set("id", item?.id ?? "");
|
|
form.set("access", value?.toString() ?? "");
|
|
submit(form, {
|
|
method: "post",
|
|
action: location.pathname + location.search,
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
{!adding && getTypes().length > 0 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setAdding(true)}
|
|
className="border-border hover:border-border relative block w-full rounded-lg border-2 border-dashed p-2 text-center focus:outline-hidden focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
|
|
>
|
|
<PlusIcon className="text-muted-foreground mx-auto h-4" />
|
|
<span className="text-foreground mt-2 block text-sm font-medium">{t("shared.shareWith")}...</span>
|
|
</button>
|
|
)}
|
|
{adding && <NewPermissionForm tenants={tenants} users={users} onClose={() => setAdding(false)} types={getTypes()} />}
|
|
{actionData?.error && <ErrorBanner title={t("shared.error")} text={actionData?.error} />}
|
|
{/* {actionData?.success && <InfoBanner title={t("shared.success")} text={actionData?.success} />} */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NewPermissionForm({
|
|
tenants,
|
|
users,
|
|
types,
|
|
onClose,
|
|
}: {
|
|
tenants: Tenant[];
|
|
users: UserWithDetails[];
|
|
types: { name: string; value: string; color: Colors }[];
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const appData = useAppData();
|
|
const location = useLocation();
|
|
const appOrAdminData = useAppOrAdminData();
|
|
const navigation = useNavigation();
|
|
const isAdding = navigation.state === "submitting" && navigation.formData?.get("action") === "share";
|
|
|
|
const [type, setType] = useState<string>("");
|
|
const [id, setId] = useState<string>("");
|
|
const [access, setAccess] = useState<string>("view");
|
|
|
|
useEffect(() => {
|
|
if (isAdding) {
|
|
onClose();
|
|
}
|
|
}, [isAdding, onClose]);
|
|
|
|
useEffect(() => {
|
|
if (users.length > 0) {
|
|
setType("user");
|
|
} else if (tenants.length > 0) {
|
|
setType("tenant");
|
|
} else if (appOrAdminData.allRoles.length > 0) {
|
|
setType("role");
|
|
} else if (appOrAdminData.myGroups.length > 0) {
|
|
setType("group");
|
|
}
|
|
}, [appOrAdminData.allRoles.length, appOrAdminData.myGroups.length, tenants.length, users.length]);
|
|
|
|
function getOptionValues() {
|
|
if (type === "user") {
|
|
return users.map((i) => {
|
|
return { name: i.email + ` (${i.firstName} ${i.lastName})` + (appOrAdminData.user?.id === i.id ? ` (${t("shared.you")})` : ""), value: i.id };
|
|
});
|
|
} else if (type === "tenant") {
|
|
return tenants.map((i) => {
|
|
return { name: i.name + (appData.currentTenant?.id === i.id ? ` (${t("shared.current")})` : ""), value: i.id };
|
|
});
|
|
} else if (type === "role") {
|
|
return appOrAdminData.allRoles.map((i) => {
|
|
return { name: i.name, value: i.id };
|
|
});
|
|
} else if (type === "group") {
|
|
return appOrAdminData.myGroups.map((i) => {
|
|
return { name: i.name, value: i.id };
|
|
});
|
|
}
|
|
return [];
|
|
}
|
|
function missingFields() {
|
|
return !type || !id || !access;
|
|
}
|
|
return (
|
|
<Form method="post" action={location.pathname + location.search}>
|
|
<input type="hidden" name="action" value="share" hidden readOnly />
|
|
|
|
<div className="space-y-2">
|
|
<InputSelector
|
|
name="type"
|
|
withSearch={false}
|
|
withColors={false}
|
|
className="w-full"
|
|
title={t("shared.shareWith") + "..."}
|
|
value={type}
|
|
setValue={(e) => setType(e?.toString() ?? "")}
|
|
options={types}
|
|
/>
|
|
<InputSelector name="id" withSearch={true} className="col-span-2" value={id} setValue={(e) => setId(e?.toString() ?? "")} options={getOptionValues()} />
|
|
|
|
<InputSelector
|
|
name="access"
|
|
withSearch={false}
|
|
value={access}
|
|
setValue={(e) => setAccess(e?.toString() ?? "")}
|
|
options={[
|
|
{ name: t("shared.permissions.canView"), value: "view" },
|
|
{ name: t("shared.permissions.canComment"), value: "comment" },
|
|
{ name: t("shared.permissions.canEdit"), value: "edit" },
|
|
{ name: t("shared.permissions.canDelete"), value: "delete" },
|
|
]}
|
|
/>
|
|
|
|
<div className="flex justify-end">
|
|
<div className="flex space-x-2">
|
|
<ButtonSecondary onClick={onClose}>{t("shared.cancel")}</ButtonSecondary>
|
|
<ButtonPrimary type="submit" disabled={missingFields()}>
|
|
{t("shared.share")}
|
|
</ButtonPrimary>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
);
|
|
}
|