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
|
|
@ -74,6 +74,51 @@ const affiliationRole = [
|
|||
bool("is_public"),
|
||||
];
|
||||
|
||||
/* The editable half of a timeline entry, shared by the standalone
|
||||
editor and by the in_timeline extension on events and organizations.
|
||||
|
||||
occurred_on is text, not date. "2012" and "2025-07" are legitimate
|
||||
values — a backfilled entry often knows the year and nothing more —
|
||||
and the date coercion would reject both. `precision` is what says how
|
||||
much of it to believe. */
|
||||
const timelineFields = [
|
||||
text("occurred_on"),
|
||||
enumeration("precision", ["year", "month", "day"]),
|
||||
text("title"),
|
||||
text("blurb"),
|
||||
text("meta"),
|
||||
text("link_url"),
|
||||
bool("is_featured"),
|
||||
bool("is_published"),
|
||||
int("sort_order"),
|
||||
];
|
||||
|
||||
/* The extension that the in_timeline checkbox drives. Ticked, the row
|
||||
is upserted; unticked, writeExtensions deletes it. Both happen in the
|
||||
parent's transaction, so the flag and the row cannot disagree.
|
||||
|
||||
The conflict target is the UNIQUE (ref_kind, ref_id) index from
|
||||
migration 007, which is also what stops a second save creating a
|
||||
duplicate instead of updating the first. */
|
||||
const timelineExtension = (refKind) => ({
|
||||
key: "timeline",
|
||||
table: "timeline_entries",
|
||||
owner: { column: "ref_id", kindColumn: "ref_kind", kindValue: refKind },
|
||||
conflict: ["ref_kind", "ref_id"],
|
||||
when: { column: "in_timeline", value: 1 },
|
||||
columns: [
|
||||
// Fixed for this end: an event's entry is always an event entry.
|
||||
// Declared as a default rather than a form field so the column is
|
||||
// written without asking.
|
||||
enumeration(
|
||||
"kind",
|
||||
["milestone", "event", "organization", "award", "people"],
|
||||
{ default: refKind === "organization" ? "organization" : refKind },
|
||||
),
|
||||
...timelineFields,
|
||||
],
|
||||
});
|
||||
|
||||
/* The two polymorphic collections, parameterised by owner_kind. */
|
||||
const linksChild = (ownerKind) => ({
|
||||
key: "links",
|
||||
|
|
@ -131,6 +176,26 @@ const blocksChild = (ownerKind) => ({
|
|||
],
|
||||
});
|
||||
|
||||
/* Hosts. One row is one host, ordered, each either an organization
|
||||
or a person — the CHECK on event_hosts rejects both and the blank
|
||||
filter drops neither, so the only bad row that reaches SQLite is
|
||||
one with both selects filled, and that comes back keyed to the
|
||||
row like any other field error.
|
||||
|
||||
Not parameterised the way links and blocks are: this table is
|
||||
events-only, and the owner column says so.
|
||||
|
||||
sort_order isn't declared. The engine writes it from the row's
|
||||
position because `order` is "sort_order", which is what makes
|
||||
the first row the one v_events takes the logo and colour from. */
|
||||
const hostsChild = {
|
||||
key: "event_hosts",
|
||||
table: "event_hosts",
|
||||
owner: { column: "event_id" },
|
||||
order: "sort_order",
|
||||
columns: [text("org_id"), text("person_id")],
|
||||
};
|
||||
|
||||
/* ── Organizations ───────────────────────────────────────────── */
|
||||
|
||||
const organizations = {
|
||||
|
|
@ -169,9 +234,11 @@ const organizations = {
|
|||
...placeColumns,
|
||||
bool("is_published"),
|
||||
int("sort_order"),
|
||||
bool("in_timeline"),
|
||||
],
|
||||
|
||||
extensions: [
|
||||
timelineExtension("organization"),
|
||||
{
|
||||
key: "region",
|
||||
table: "regions",
|
||||
|
|
@ -226,7 +293,7 @@ const events = {
|
|||
"id",
|
||||
"title",
|
||||
"section_id",
|
||||
"host_org_id",
|
||||
"event_type",
|
||||
"date_label",
|
||||
"starts_on",
|
||||
"status",
|
||||
|
|
@ -234,14 +301,29 @@ const events = {
|
|||
"sort_order",
|
||||
"updated_at",
|
||||
],
|
||||
filters: ["section_id", "status", "is_published", "host_org_id"],
|
||||
// No host filter: hosts are rows in another table now, and the
|
||||
// engine's filters are columns on this one. The events a host
|
||||
// owns are on that host's own page.
|
||||
filters: ["section_id", "event_type", "status", "is_published"],
|
||||
search: ["title", "id", "theme"],
|
||||
order: "sort_order, starts_on DESC, title",
|
||||
},
|
||||
|
||||
columns: [
|
||||
text("section_id", { required: true }),
|
||||
text("host_org_id"),
|
||||
|
||||
// What kind of gathering, as against section_id's which band of
|
||||
// the page. Declared required even though the column has a
|
||||
// DEFAULT: every select renders a blank first option, so without
|
||||
// it a new event files itself as a retreat while nobody is
|
||||
// looking. An existing row always loads with its value set, so
|
||||
// this only ever asks on create.
|
||||
enumeration(
|
||||
"event_type",
|
||||
["retreat", "class", "workshop", "meeting", "other"],
|
||||
{ required: true },
|
||||
),
|
||||
|
||||
text("title", { required: true }),
|
||||
text("theme"),
|
||||
text("tagline"),
|
||||
|
|
@ -256,9 +338,13 @@ const events = {
|
|||
text("gradient"),
|
||||
bool("is_published"),
|
||||
int("sort_order"),
|
||||
bool("in_timeline"),
|
||||
],
|
||||
|
||||
extensions: [timelineExtension("event")],
|
||||
|
||||
children: [
|
||||
hostsChild,
|
||||
linksChild("event"),
|
||||
blocksChild("event"),
|
||||
{
|
||||
|
|
@ -485,7 +571,69 @@ const awards = {
|
|||
],
|
||||
};
|
||||
|
||||
export const ENTITIES = { organizations, events, people, teams, awards };
|
||||
/* ── Timeline ────────────────────────────────────────────────── */
|
||||
|
||||
// The history page's spine, and the only entity whose id the table
|
||||
// assigns. There is nothing to slug: an entry referencing an event has
|
||||
// no name of its own, and one gets created every time somebody ticks a
|
||||
// checkbox. idKind "auto" is what lets createRow skip the id entirely.
|
||||
//
|
||||
// ref_kind and ref_id are writable here and only here. The extension on
|
||||
// events and organizations owns those two columns for rows it created,
|
||||
// which is why they aren't in timelineFields.
|
||||
//
|
||||
// Deleting an entry takes its people with it (ON DELETE CASCADE) and
|
||||
// nothing points at an entry, so the delete-and-reinsert child engine
|
||||
// is safe on this one.
|
||||
const timeline = {
|
||||
key: "timeline",
|
||||
table: "timeline_entries",
|
||||
idColumn: "id",
|
||||
idKind: "auto",
|
||||
concurrency: "updated_at",
|
||||
|
||||
list: {
|
||||
columns: [
|
||||
"id",
|
||||
"kind",
|
||||
"ref_kind",
|
||||
"ref_id",
|
||||
"occurred_on",
|
||||
"title",
|
||||
"is_featured",
|
||||
"is_published",
|
||||
"updated_at",
|
||||
],
|
||||
filters: ["kind", "ref_kind", "is_featured", "is_published"],
|
||||
search: ["title", "blurb", "meta", "ref_id"],
|
||||
// Undated entries sort last rather than first, so a missing date
|
||||
// reads as something to fix instead of something to scroll past.
|
||||
order: "occurred_on IS NULL, occurred_on DESC, sort_order",
|
||||
},
|
||||
|
||||
columns: [
|
||||
enumeration(
|
||||
"kind",
|
||||
["milestone", "event", "organization", "award", "people"],
|
||||
{ required: true },
|
||||
),
|
||||
enumeration("ref_kind", ["event", "organization", "award", "person", "team"]),
|
||||
text("ref_id"),
|
||||
...timelineFields,
|
||||
],
|
||||
|
||||
children: [
|
||||
{
|
||||
key: "people",
|
||||
table: "timeline_entry_people",
|
||||
owner: { column: "entry_id" },
|
||||
order: "sort_order",
|
||||
columns: [text("person_id", { required: true }), text("note")],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ENTITIES = { organizations, events, people, teams, awards, timeline };
|
||||
|
||||
/* ── Options for the form's select inputs ────────────────────── */
|
||||
|
||||
|
|
@ -503,6 +651,22 @@ export const OPTION_QUERIES = {
|
|||
teams:
|
||||
"SELECT id, name AS label, org_id FROM teams ORDER BY org_id, sort_order, name",
|
||||
|
||||
// One flat list the ref picker filters by ref_kind, rather than five
|
||||
// dropdowns of which four are always wrong. `kind` is the discriminator
|
||||
// the client's filterBy matches on; org_kind disambiguates the label,
|
||||
// since a region and a chapter can share a name.
|
||||
timeline_refs: `
|
||||
SELECT 'event' AS kind, id, title AS label FROM events
|
||||
UNION ALL
|
||||
SELECT 'organization', id, name || ' (' || kind || ')' FROM organizations
|
||||
UNION ALL
|
||||
SELECT 'award', id, name FROM awards
|
||||
UNION ALL
|
||||
SELECT 'person', id, display_name FROM people
|
||||
UNION ALL
|
||||
SELECT 'team', id, name FROM teams
|
||||
ORDER BY kind, label`,
|
||||
|
||||
// The awarding organization is folded into the label instead,
|
||||
// because a person can receive an award from any organization —
|
||||
// there is nothing to filter on, only something to disambiguate
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue