fix(permissions): correct row copy, column alignment and dirty state in the matrix

- a row with only the create capability described itself as "no access", contradicting
  the checkbox next to it; it now shows the create permission's description
- the level controls and create checkboxes now line up in columns across rows
- saving left the unsaved-changes bar on screen: setInitialValues only moves the values
  snapshot, resetDirty is what clears the dirty flags
- dropped the manual onValuesChange dirty bookkeeping, which compared against the
  permissions the page was loaded with rather than the last saved state
This commit is contained in:
Thomas Camlong committed 2026-08-18 15:40:28 +02:00
1 parent 6728d0a63d
commit 3bbe7f2a98
1 file changed
+31 -32
@@ -4,6 +4,7 @@ import type { PropsWithChildren } from "react";
import { useCallback, useId } from "react";
import {
Badge,
Box,
Button,
Card,
Checkbox,
@@ -44,23 +45,13 @@ interface PermissionFormProps {
}
export const PermissionForm = ({ children, initialPermissions }: PropsWithChildren<PermissionFormProps>) => {
// Mantine compares each field against the values snapshot, so toggling a control back to its
// saved value clears the dirty state on its own - no manual bookkeeping needed here.
const form = useForm({
initialValues: groupPermissionKeys.reduce((acc, key) => {
acc[key] = initialPermissions.includes(key);
return acc;
}, {} as FormType),
onValuesChange(values) {
const currentKeys = objectEntries(values)
.filter(([_key, value]) => Boolean(value))
.map(([key]) => key);
if (
currentKeys.every((key) => initialPermissions.includes(key)) &&
initialPermissions.every((key) => currentKeys.includes(key))
) {
form.resetDirty(); // Reset dirty state if all keys are the same as initial
}
},
});
return (
@@ -220,13 +211,16 @@ export const MatrixRow = ({ category, permissionLabels, permissionDescriptions }
}),
];
const selectedKey = level === 0 ? undefined : levels[level - 1];
const description =
selectedKey === undefined ? tPermissions("matrix.noneDescription") : (permissionDescriptions[selectedKey] ?? "");
const createImplied = isCreateImpliedByLevel(category, level);
const createChecked = hasMatrixCreate(values, category);
const selectedKey = level === 0 ? undefined : levels[level - 1];
// With no access level but the create capability granted, describing the row as "no access" would
// contradict the checkbox next to it, so the create permission describes the row instead.
const describedKey = selectedKey ?? (createChecked ? (createKey ?? undefined) : undefined);
const description =
describedKey === undefined ? tPermissions("matrix.noneDescription") : (permissionDescriptions[describedKey] ?? "");
return (
<Stack gap={6}>
<Group gap="sm" wrap="wrap" align="center" justify="space-between">
@@ -236,7 +230,7 @@ export const MatrixRow = ({ category, permissionLabels, permissionDescriptions }
{tPermissions(`${category}.title`)}
</Text>
</Group>
<Group gap="sm" wrap="wrap" align="center">
<Group gap="md" wrap="wrap" align="center">
<SegmentedControl
size="sm"
data={data}
@@ -245,20 +239,23 @@ export const MatrixRow = ({ category, permissionLabels, permissionDescriptions }
aria-label={tPermissions(`${category}.title`)}
onChange={(value) => applyMatrixLevel(form, category, Number(value))}
/>
{createKey !== null && (
<Tooltip
label={tPermissions("matrix.createIncluded", { level: data[level]?.label ?? "" })}
disabled={!createImplied}
>
<Checkbox
size="sm"
label={permissionLabels[createKey] ?? createKey}
checked={createChecked}
disabled={disabledByAdmin || createImplied}
onChange={(event) => applyMatrixCreate(form, category, event.currentTarget.checked)}
/>
</Tooltip>
)}
{/* Fixed-width column so every level control lines up, with or without a create option. */}
<Box w={{ base: "auto", sm: 190 }}>
{createKey !== null && (
<Tooltip
label={tPermissions("matrix.createIncluded", { level: data[level]?.label ?? "" })}
disabled={!createImplied}
>
<Checkbox
size="sm"
label={permissionLabels[createKey] ?? createKey}
checked={createChecked}
disabled={disabledByAdmin || createImplied}
onChange={(event) => applyMatrixCreate(form, category, event.currentTarget.checked)}
/>
</Tooltip>
)}
</Box>
</Group>
</Group>
<Text size="xs" c="dimmed">
@@ -405,8 +402,10 @@ export const SaveAffix = ({ groupId }: SaveAffixProps) => {
},
{
onSuccess: () => {
// Set new initial values for discard and reset dirty state
// setInitialValues moves the snapshot so Discard returns here, but only resetDirty clears
// the dirty flags that keep the unsaved-changes bar on screen.
form.setInitialValues(values);
form.resetDirty(values);
showSuccessNotification({
title: tNotification("success.title"),
message: tNotification("success.message"),