v1.5 - history and timeline as well as many datastructure updates added, polished, fixes
This commit is contained in:
parent
1f0aa3078f
commit
1d84400aef
63 changed files with 7927 additions and 208 deletions
|
|
@ -23,6 +23,16 @@
|
|||
updated_at column simply never get one, and the 409 path stays
|
||||
dormant for them.
|
||||
|
||||
Two capabilities, not one role. An editor may create and update
|
||||
but not delete, so the action bar asks canWrite/canDelete rather
|
||||
than comparing user.role to a string. The comparison this
|
||||
replaced — role === "admin" — locked superadmins out of saving
|
||||
the moment a rank above admin existed, which is what an equality
|
||||
test against a ladder always eventually does.
|
||||
|
||||
None of this is protection. The server refuses the request; this
|
||||
only decides whether to draw a button that would be refused.
|
||||
|
||||
slugFrom may name one field or several. Most ids are unique
|
||||
because the name is: two organizations aren't both called
|
||||
Northwest. Team ids are the exception — teams.id is a global
|
||||
|
|
@ -37,6 +47,7 @@ import { get, post, patch, del, ApiError } from "../../lib/api.js";
|
|||
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
|
||||
import { useAdminDetail } from "../../lib/adminTitle.tsx";
|
||||
import { ADMIN_ENTITIES, slugify } from "../../lib/adminSchema.js";
|
||||
import { atLeast } from "../../lib/roles.ts";
|
||||
import { Field, FieldGrid, Repeater, getPath, setPath } from "../../components/admin/fields.tsx";
|
||||
|
||||
/* A foreign key refusing to budge is the most common way a save or
|
||||
|
|
@ -56,7 +67,14 @@ export default function EntityEdit() {
|
|||
const { user } = useAuth();
|
||||
|
||||
const isNew = id === "new";
|
||||
const canWrite = user?.role === "admin";
|
||||
const canWrite = atLeast(user, "editor");
|
||||
const canDelete = atLeast(user, "admin");
|
||||
|
||||
// Some entities have no slug: the table assigns an integer id, so
|
||||
// there is nothing to type on create and nothing to compose from
|
||||
// other fields. Timeline entries are the first — an entry that
|
||||
// references an event has no name of its own.
|
||||
const autoId = manifest?.idKind === "auto";
|
||||
|
||||
// Hoisted above the loading guards: the title hook below is a
|
||||
// hook, so it can't sit after an early return, and it needs the
|
||||
|
|
@ -92,8 +110,9 @@ export default function EntityEdit() {
|
|||
};
|
||||
|
||||
// The heading wants the specific half, not the qualifier: a team
|
||||
// page reads "Board", not "northwest Board".
|
||||
const headingPath = slugPaths[slugPaths.length - 1];
|
||||
// page reads "Board", not "northwest Board". An entity with no slug
|
||||
// names the field to read instead.
|
||||
const headingPath = slugPaths[slugPaths.length - 1] ?? manifest?.titleFrom;
|
||||
|
||||
const [form, setForm] = useState(null);
|
||||
const [options, setOptions] = useState({});
|
||||
|
|
@ -120,7 +139,9 @@ export default function EntityEdit() {
|
|||
// touch", which is wrong for a row that doesn't exist yet.
|
||||
// The parent's own fields stay absent on purpose so the
|
||||
// server's column defaults apply to whatever isn't filled in.
|
||||
const blank = { id: "" };
|
||||
// No id key for an auto entity: the table assigns it, and
|
||||
// sending "" would be an explicit value rather than an absence.
|
||||
const blank = autoId ? {} : { id: "" };
|
||||
for (const child of manifest.children ?? []) blank[child.key] = [];
|
||||
setForm(blank);
|
||||
baseline.current = JSON.stringify(blank);
|
||||
|
|
@ -139,7 +160,7 @@ export default function EntityEdit() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [manifest, id, isNew, navigate]);
|
||||
}, [manifest, id, isNew, autoId, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
|
|
@ -288,7 +309,19 @@ export default function EntityEdit() {
|
|||
{isNew ? `New ${manifest.singular}` : heading}
|
||||
</h1>
|
||||
|
||||
{/* Slug */}
|
||||
{/* Slug. An auto-id entity has nothing to ask for on create, and
|
||||
nothing editable afterwards — so it gets a plain line rather
|
||||
than a disabled box pretending to be a field. */}
|
||||
{autoId ? (
|
||||
!isNew && (
|
||||
<div className="mt-6 rounded-2xl border border-[#138ba0]/20 bg-white p-5">
|
||||
<p className="text-sm text-[#4a6b72]">
|
||||
{manifest.idLabel} #{form.id}
|
||||
{form.updated_at && <> · last saved {form.updated_at}</>}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="mt-6 rounded-2xl border border-[#138ba0]/20 bg-white p-5">
|
||||
<Field
|
||||
field={{
|
||||
|
|
@ -317,6 +350,7 @@ export default function EntityEdit() {
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Field groups */}
|
||||
{manifest.groups.filter((group) => visible(group.when)).map((group) => (
|
||||
|
|
@ -332,6 +366,7 @@ export default function EntityEdit() {
|
|||
<Field
|
||||
key={field.path}
|
||||
field={field}
|
||||
row={form}
|
||||
value={getPath(form, field.path)}
|
||||
options={options}
|
||||
error={errors[field.path]}
|
||||
|
|
@ -376,7 +411,7 @@ export default function EntityEdit() {
|
|||
>
|
||||
{saving ? "Saving…" : isNew ? "Create" : "Save changes"}
|
||||
</button>
|
||||
{!isNew && (
|
||||
{!isNew && canDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={remove}
|
||||
|
|
@ -388,7 +423,7 @@ export default function EntityEdit() {
|
|||
</>
|
||||
) : (
|
||||
<span className="text-sm text-[#4a6b72]">
|
||||
Read-only: your account can't save changes.
|
||||
Read-only: your account can view this but not change it.
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue