@ai-matrx/records-ui 0.113.36 → 0.113.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -700,13 +700,20 @@ var NOT_ANSWERED_YET = whatYouMayDo(null, false);
700
700
  var KERNEL_REASON = "This is one of the tables the platform ships, so its columns are not changed from a screen. Make your own table to change columns.";
701
701
  function whatYouMayDoWithTable(table, level, known) {
702
702
  const base2 = whatYouMayDo(level, known);
703
- if (!table?.is_kernel) return base2;
703
+ if (!table?.is_kernel) return table && tableIsLocked(table) ? lockedStructure(base2) : base2;
704
704
  return {
705
705
  ...base2,
706
706
  structure: false,
707
707
  why: (what) => what === "structure" ? KERNEL_REASON : base2.why(what)
708
708
  };
709
709
  }
710
+ function tableIsLocked(table) {
711
+ return table?.metadata?.["locked"] === true;
712
+ }
713
+ var DATABASE_LOCKED_REASON = "This database is locked, so its columns and settings are read-only. Unlock it to change them.";
714
+ function lockedStructure(base2) {
715
+ return { ...base2, structure: false, why: (what) => what === "structure" ? DATABASE_LOCKED_REASON : base2.why(what) };
716
+ }
710
717
 
711
718
  // src/host.tsx
712
719
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -1255,6 +1262,7 @@ var import_data_table2 = require("@ai-matrx/design-system/data-table");
1255
1262
 
1256
1263
  // src/personTime.ts
1257
1264
  var import_react4 = require("react");
1265
+ var import_table_style = require("@ai-matrx/design-system/data-table/table-style");
1258
1266
  var zone = null;
1259
1267
  var listeners = /* @__PURE__ */ new Set();
1260
1268
  function valid(z) {
@@ -1270,6 +1278,7 @@ function setPersonTimeZone(next) {
1270
1278
  const clean = valid(next) ? next : null;
1271
1279
  if (clean === zone) return;
1272
1280
  zone = clean;
1281
+ (0, import_table_style.setColorRuleTimeZone)(clean);
1273
1282
  listeners.forEach((l) => l());
1274
1283
  }
1275
1284
  function getPersonTimeZone() {
@@ -3419,6 +3428,28 @@ function descriptionPatch(table, text) {
3419
3428
  else delete metadata["description"];
3420
3429
  return { metadata };
3421
3430
  }
3431
+ function lockPatch(table, locked) {
3432
+ const metadata = { ...table.metadata ?? {} };
3433
+ if (locked) metadata["locked"] = true;
3434
+ else delete metadata["locked"];
3435
+ return { metadata };
3436
+ }
3437
+ async function saveTableLock(client, tableId, locked) {
3438
+ const read = await client.tableRead({ table_id: tableId });
3439
+ if (!read.ok) return { ok: false, says: refusalSentence(read.error) };
3440
+ if (!read.data) return { ok: false, says: "This table could not be read, so it was not locked." };
3441
+ const table = read.data;
3442
+ const written = await (0, writeOnTheWinningVersion_exports.updateOnTheWinningVersion)(
3443
+ client,
3444
+ tableId,
3445
+ lockPatch(table, locked),
3446
+ await (0, recordVersions_exports.readTableVersionNow)(client, tableId),
3447
+ { metadata: table.metadata ?? {} }
3448
+ );
3449
+ if (!written.ok) return { ok: false, says: refusalSentence(written.error) };
3450
+ (0, import_react15.tableShapeChanged)(tableId);
3451
+ return { ok: true };
3452
+ }
3422
3453
  function TableDetails({
3423
3454
  tableId,
3424
3455
  table,
@@ -3500,6 +3531,20 @@ function TableDetails({
3500
3531
  }
3501
3532
  )
3502
3533
  ] }),
3534
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center justify-between gap-2", "data-table-lock": "", children: [
3535
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-muted-foreground", children: "Lock database" }),
3536
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3537
+ import_design_system8.Button,
3538
+ {
3539
+ size: "sm",
3540
+ variant: "outline",
3541
+ disabled: busy,
3542
+ onClick: () => void write(lockPatch(table, true), { metadata: table.metadata ?? {} }),
3543
+ "data-table-lock-on": "",
3544
+ children: "Lock"
3545
+ }
3546
+ )
3547
+ ] }),
3503
3548
  refused ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-destructive", children: refused }) : null
3504
3549
  ] });
3505
3550
  }
@@ -4140,7 +4185,7 @@ var import_react65 = require("@ai-matrx/records/react");
4140
4185
  var import_design_system37 = require("@ai-matrx/design-system");
4141
4186
 
4142
4187
  // src/presentation.ts
4143
- var import_table_style = require("@ai-matrx/design-system/data-table/table-style");
4188
+ var import_table_style2 = require("@ai-matrx/design-system/data-table/table-style");
4144
4189
  var import_data_table3 = require("@ai-matrx/design-system/data-table");
4145
4190
  var import_field_formats2 = require("@ai-matrx/design-system/field-formats");
4146
4191
  var import_records12 = require("@ai-matrx/records");
@@ -4273,8 +4318,8 @@ function parseGridPresentation(raw) {
4273
4318
  }
4274
4319
  if (!isRecord(value)) return {};
4275
4320
  const out = {};
4276
- const style = (0, import_table_style.parseTableStyle)(value["style"]);
4277
- if (!(0, import_table_style.tableStyleIsEmpty)(style)) out.style = style;
4321
+ const style = (0, import_table_style2.parseTableStyle)(value["style"]);
4322
+ if (!(0, import_table_style2.tableStyleIsEmpty)(style)) out.style = style;
4278
4323
  if (isRecord(value["formats"])) {
4279
4324
  const formats = {};
4280
4325
  for (const [key, cfg] of Object.entries(value["formats"])) {
@@ -4328,6 +4373,7 @@ function parseGridPresentation(raw) {
4328
4373
  if (typeof value["boardHideEmpty"] === "boolean") out.boardHideEmpty = value["boardHideEmpty"];
4329
4374
  if (typeof value["boardColorColumns"] === "boolean") out.boardColorColumns = value["boardColorColumns"];
4330
4375
  if (typeof value["boardCounts"] === "boolean") out.boardCounts = value["boardCounts"];
4376
+ if (value["locked"] === true) out.locked = true;
4331
4377
  if (Array.isArray(value["boardLanesShut"])) {
4332
4378
  const shut = value["boardLanesShut"].filter((k) => typeof k === "string");
4333
4379
  if (shut.length > 0) out.boardLanesShut = shut;
@@ -4414,7 +4460,7 @@ function parseGrouping(raw) {
4414
4460
  return out;
4415
4461
  }
4416
4462
  function presentationIsEmpty(p) {
4417
- return (p.style === void 0 || (0, import_table_style.tableStyleIsEmpty)(p.style)) && Object.keys(p.formats ?? {}).length === 0 && (p.frozen?.length ?? 0) === 0 && p.grouping === void 0 && Object.keys(p.widths ?? {}).length === 0 && p.rowHeight === void 0 && (p.hiddenFields?.length ?? 0) === 0 && p.cardHidden === void 0 && (p.columnOrder?.length ?? 0) === 0 && p.gallerySize === void 0 && p.boardSize === void 0 && p.boardPreview === void 0 && p.boardHideEmpty === void 0 && p.boardColorColumns === void 0 && p.boardCounts === void 0 && (p.boardLanesShut?.length ?? 0) === 0 && p.wrap === void 0 && p.fit === void 0 && p.freezeFirst === void 0 && p.footer === void 0 && Object.keys(p.summaries ?? {}).length === 0;
4463
+ return (p.style === void 0 || (0, import_table_style2.tableStyleIsEmpty)(p.style)) && Object.keys(p.formats ?? {}).length === 0 && (p.frozen?.length ?? 0) === 0 && p.grouping === void 0 && Object.keys(p.widths ?? {}).length === 0 && p.rowHeight === void 0 && (p.hiddenFields?.length ?? 0) === 0 && p.cardHidden === void 0 && (p.columnOrder?.length ?? 0) === 0 && p.gallerySize === void 0 && p.boardSize === void 0 && p.boardPreview === void 0 && p.boardHideEmpty === void 0 && p.boardColorColumns === void 0 && p.boardCounts === void 0 && (p.boardLanesShut?.length ?? 0) === 0 && p.locked === void 0 && p.wrap === void 0 && p.fit === void 0 && p.freezeFirst === void 0 && p.footer === void 0 && Object.keys(p.summaries ?? {}).length === 0;
4418
4464
  }
4419
4465
  function presentationDocument(p) {
4420
4466
  return presentationIsEmpty(p) ? "" : JSON.stringify(p);
@@ -7283,6 +7329,48 @@ var CONDITION_OPS = [
7283
7329
  { op: "gte", label: "is at least" },
7284
7330
  { op: "lte", label: "is at most" }
7285
7331
  ];
7332
+ var FilterTermsContext = (0, import_react42.createContext)(false);
7333
+ var RELATIVE_DAY_TERMS = [
7334
+ { kind: "today", label: "is today" },
7335
+ { kind: "tomorrow", label: "is tomorrow" },
7336
+ { kind: "yesterday", label: "is yesterday" },
7337
+ { kind: "this_week", label: "is this week" },
7338
+ { kind: "next_week", label: "is next week" },
7339
+ { kind: "last_week", label: "is last week" },
7340
+ { kind: "this_month", label: "is this month" },
7341
+ { kind: "next_month", label: "is next month" },
7342
+ { kind: "last_month", label: "is last month" },
7343
+ { kind: "before_today", label: "is before today" },
7344
+ { kind: "next_days", label: "is in the next \u2026 days", takesDays: true },
7345
+ { kind: "past_days", label: "is in the past \u2026 days", takesDays: true }
7346
+ ];
7347
+ function relativeDayNode(fieldId, kind, days) {
7348
+ const zone2 = getPersonTimeZone();
7349
+ const window2 = { relative: kind };
7350
+ if (RELATIVE_DAY_TERMS.find((t) => t.kind === kind)?.takesDays) window2["n"] = days ?? 7;
7351
+ if (zone2) window2["tz"] = zone2;
7352
+ return { op: "within", args: [{ field: fieldId }, { const: window2 }] };
7353
+ }
7354
+ function relativeDayOf(expr) {
7355
+ const node = asNode(expr);
7356
+ if (!node || node["op"] !== "within") return null;
7357
+ const [a, b] = argsOf(node);
7358
+ const window2 = asNode(b?.["const"]);
7359
+ if (!a || typeof a["field"] !== "string" || !window2 || typeof window2["relative"] !== "string") return null;
7360
+ const days = typeof window2["n"] === "number" ? window2["n"] : null;
7361
+ return { kind: window2["relative"], days, field: a["field"] };
7362
+ }
7363
+ function meNode(fieldId, negate) {
7364
+ return { op: negate ? "ne" : "eq", args: [{ field: fieldId }, { const: { me: true } }] };
7365
+ }
7366
+ function meOf(expr) {
7367
+ const node = asNode(expr);
7368
+ if (!node || node["op"] !== "eq" && node["op"] !== "ne") return null;
7369
+ const [a, b] = argsOf(node);
7370
+ const value = asNode(b?.["const"]);
7371
+ if (!a || typeof a["field"] !== "string" || !value || value["me"] !== true) return null;
7372
+ return { which: node["op"] === "eq" ? "me" : "not_me", field: a["field"] };
7373
+ }
7286
7374
  function pointsAtARecord(f) {
7287
7375
  return !!f?.field && f.field.type === "relation" && !!f.field.relation_target;
7288
7376
  }
@@ -7353,6 +7441,7 @@ function ChoiceValue({ field: field2, value, onChange }) {
7353
7441
  }
7354
7442
  function conditionIsSimple(expr) {
7355
7443
  if (expr === null || expr === void 0) return true;
7444
+ if (relativeDayOf(expr) !== null || meOf(expr) !== null) return true;
7356
7445
  const node = expr;
7357
7446
  if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
7358
7447
  if (!Array.isArray(node.args)) return false;
@@ -7397,6 +7486,14 @@ function conditionInWords(expr, fields) {
7397
7486
  const args = Array.isArray(n["args"]) ? n["args"] : [];
7398
7487
  if (op2 === "not") return `not ${walk(args[0])}`;
7399
7488
  if (op2 === "present") return `${walk(args[0])} has been answered`;
7489
+ const dayRange = relativeDayOf(n);
7490
+ if (dayRange) {
7491
+ const term = RELATIVE_DAY_TERMS.find((t) => t.kind === dayRange.kind);
7492
+ const said3 = term ? term.label.replace("\u2026", String(dayRange.days ?? 7)) : "is in a day range";
7493
+ return `${name(dayRange.field)} ${said3}`;
7494
+ }
7495
+ const reader = meOf(n);
7496
+ if (reader) return `${name(reader.field)} ${reader.which === "me" ? "is me" : "is not me"}`;
7400
7497
  const word = said2[op2] ?? op2;
7401
7498
  if (op2 === "and" || op2 === "or") return args.map(walk).join(` ${word} `);
7402
7499
  if (args.length === 2) return `${walk(args[0])} ${word} ${walk(args[1])}`;
@@ -7418,11 +7515,31 @@ function conditionValue(typed2) {
7418
7515
  }
7419
7516
  function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", className }) {
7420
7517
  const node = expr ?? null;
7421
- const op2 = node?.op ?? "";
7518
+ const filterTerms = (0, import_react42.useContext)(FilterTermsContext);
7519
+ const relative = relativeDayOf(expr);
7520
+ const reader = meOf(expr);
7521
+ const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : node?.op ?? "";
7422
7522
  const onField = node?.args?.[0]?.field ?? "";
7423
- const value = node?.args?.[1]?.const ?? "";
7523
+ const value = relative || reader ? "" : node?.args?.[1]?.const ?? "";
7424
7524
  const chosen = fields.find((f) => f.id === onField);
7525
+ const chosenKind = chosen?.field ? String(editorKindFor(chosen.field)) : "";
7526
+ const extraTerms = !filterTerms ? [] : chosenKind === "datetime" ? RELATIVE_DAY_TERMS.map((t) => ({ op: `rel:${t.kind}`, label: t.label })) : chosenKind === "member" ? [{ op: "me", label: "is me" }, { op: "not_me", label: "is not me" }] : [];
7527
+ const relativeTerm = relative ? RELATIVE_DAY_TERMS.find((t) => t.kind === relative.kind) : void 0;
7425
7528
  function write(next) {
7529
+ const picked = next.op;
7530
+ const fieldNow = next.field ?? onField;
7531
+ if (fieldNow && picked !== void 0 && (picked.startsWith("rel:") || picked === "me" || picked === "not_me")) {
7532
+ onChange(picked === "me" || picked === "not_me" ? meNode(fieldNow, picked === "not_me") : relativeDayNode(fieldNow, picked.slice(4), relative?.days ?? void 0));
7533
+ return;
7534
+ }
7535
+ if (fieldNow && next.days !== void 0 && relative) {
7536
+ onChange(relativeDayNode(fieldNow, relative.kind, next.days));
7537
+ return;
7538
+ }
7539
+ if (next.field !== void 0 && (relative || reader)) {
7540
+ onChange(next.field ? { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] } : null);
7541
+ return;
7542
+ }
7426
7543
  const chosenOp = next.op ?? (op2 || (next.field ? CONDITION_OPS[0].op : ""));
7427
7544
  const chosenField = next.field ?? onField;
7428
7545
  const chosenValue = next.value ?? value;
@@ -7464,11 +7581,21 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7464
7581
  className: "h-8 w-auto min-w-[6rem] text-xs",
7465
7582
  value: op2,
7466
7583
  onChange: (next) => write({ op: next }),
7467
- options: CONDITION_OPS.map((c) => ({ value: c.op, label: c.label })),
7584
+ options: [...CONDITION_OPS, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
7468
7585
  attrs: { "data-condition": "comparison" }
7469
7586
  }
7470
7587
  ),
7471
- op2 === "present" ? null : pointsAtARecord(chosen) ? (
7588
+ relative ? relativeTerm?.takesDays ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
7589
+ import_design_system26.BasicInput,
7590
+ {
7591
+ className: "h-8 w-16 text-xs",
7592
+ type: "number",
7593
+ min: 0,
7594
+ "aria-label": "Number of days",
7595
+ value: String(relative.days ?? 7),
7596
+ onChange: (e) => write({ days: Math.max(0, Math.floor(Number(e.target.value) || 0)) })
7597
+ }
7598
+ ) : null : reader || op2 === "present" ? null : pointsAtARecord(chosen) ? (
7472
7599
  // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
7473
7600
  // cell uses, on the same `relation_words_many` the cell reads, so a filter and
7474
7601
  // the column it filters can never disagree about what a record is called. It
@@ -13261,7 +13388,7 @@ function rulesLeaf(drafts, fields) {
13261
13388
  const out = [];
13262
13389
  for (const d of drafts) {
13263
13390
  const field2 = fields.find((f) => f.key === d.field);
13264
- if (!field2) continue;
13391
+ if (!field2 || import_data_table11.COLOR_RULE_DATE_OPS.includes(d.op)) continue;
13265
13392
  out.push({
13266
13393
  ...d.id ? { id: d.id } : {},
13267
13394
  field: field2.id,
@@ -13273,6 +13400,22 @@ function rulesLeaf(drafts, fields) {
13273
13400
  }
13274
13401
  return out;
13275
13402
  }
13403
+ function draftsFromView(rules, fields) {
13404
+ return (rules ?? []).filter((rule) => fields.some((f) => f.key === rule.field)).map((rule) => ({ id: rule.id, field: rule.field, op: rule.op, value: rule.value ?? "", color: rule.color, target: rule.target }));
13405
+ }
13406
+ function viewRulesFrom(drafts) {
13407
+ return drafts.map((d, i) => ({
13408
+ id: d.id ?? `rule-${Date.now().toString(36)}-${i}`,
13409
+ field: d.field,
13410
+ op: d.op,
13411
+ ...import_data_table11.OPS_WITH_VALUE.includes(d.op) ? { value: d.value } : {},
13412
+ color: d.color,
13413
+ target: d.target
13414
+ }));
13415
+ }
13416
+ function isDateField2(field2) {
13417
+ return field2 ? String(editorKindFor(field2)) === "datetime" : false;
13418
+ }
13276
13419
  function swatches(prefix, current, write) {
13277
13420
  return [
13278
13421
  ...import_data_table11.STYLE_COLORS.map((color) => ({
@@ -13284,7 +13427,7 @@ function swatches(prefix, current, write) {
13284
13427
  ];
13285
13428
  }
13286
13429
  function useGridColours(args) {
13287
- const { enabled, tableId, fields, decorations, mayColour, whyNot, notify } = args;
13430
+ const { enabled, tableId, fields, decorations, mayColour, whyNot, notify, view } = args;
13288
13431
  const client = (0, import_react74.useRecordsClient)();
13289
13432
  const [rulesOpen, setRulesOpen] = (0, import_react73.useState)(null);
13290
13433
  const dialogOptions = useChoiceOptions(fields, rulesOpen !== null ? fields.filter(isChoiceField).map((f) => f.key) : []);
@@ -13432,6 +13575,16 @@ function useGridColours(args) {
13432
13575
  decorations: decorations.data,
13433
13576
  options: dialogOptions,
13434
13577
  onClose: () => setRulesOpen(null),
13578
+ ...view ? {
13579
+ viewRules: {
13580
+ drafts: draftsFromView(view.rules, fields),
13581
+ onSave: async (drafts) => {
13582
+ view.save(viewRulesFrom(drafts));
13583
+ notify?.success(drafts.length === 0 ? "This view's colour rules are cleared." : "This view's colour rules are kept.");
13584
+ setRulesOpen(null);
13585
+ }
13586
+ }
13587
+ } : {},
13435
13588
  onSave: async (drafts) => {
13436
13589
  const kept = await write(["rules"], rulesLeaf(drafts, fields), drafts.length === 0 ? "The colour rules are cleared." : "The colour rules are kept.");
13437
13590
  if (kept) setRulesOpen(null);
@@ -13475,17 +13628,27 @@ function ColourRulesDialog({
13475
13628
  decorations,
13476
13629
  options,
13477
13630
  onClose,
13478
- onSave
13631
+ onSave,
13632
+ viewRules
13479
13633
  }) {
13480
13634
  const [drafts, setDrafts] = (0, import_react73.useState)(null);
13481
13635
  const [busy, setBusy] = (0, import_react73.useState)(false);
13482
- const held = (0, import_react73.useMemo)(() => draftsFrom(decorations, fields), [decorations, fields]);
13636
+ const [scope, setScope] = (0, import_react73.useState)("table");
13637
+ const keptTable = (0, import_react73.useMemo)(() => draftsFrom(decorations, fields), [decorations, fields]);
13638
+ const held = scope === "view" && viewRules ? viewRules.drafts : keptTable;
13639
+ const opsFor = (key) => {
13640
+ const field2 = fields.find((f) => f.key === key);
13641
+ if (choicesOf(key)) return CHOICE_OPS;
13642
+ const dateTests = scope === "view" && isDateField2(field2);
13643
+ return import_data_table11.COLOR_RULE_OPS.filter((op2) => dateTests || !import_data_table11.COLOR_RULE_DATE_OPS.includes(op2));
13644
+ };
13483
13645
  const rows = drafts ?? held;
13484
13646
  const set = (i, patch) => setDrafts(
13485
13647
  rows.map((r, j) => {
13486
13648
  if (j !== i) return r;
13487
13649
  const next = { ...r, ...patch };
13488
13650
  if (patch.field !== void 0 && choicesOf(next.field) && !CHOICE_OPS.includes(next.op)) return { ...next, op: "is", value: "" };
13651
+ if (patch.field !== void 0 && !opsFor(next.field).includes(next.op)) return { ...next, op: "is", value: "" };
13489
13652
  return next;
13490
13653
  })
13491
13654
  );
@@ -13501,9 +13664,28 @@ function ColourRulesDialog({
13501
13664
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_design_system41.Dialog, { open, onOpenChange: (next) => !next ? close() : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_design_system41.DialogContent, { className: "sm:max-w-2xl", "data-records-colour-rules": "", children: [
13502
13665
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_design_system41.DialogHeader, { children: [
13503
13666
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_design_system41.DialogTitle, { children: "Colour rules" }),
13504
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_design_system41.DialogDescription, { children: "The first rule a row matches wins. A rule tints the whole row, or only the cell it reads. Everyone who opens this table sees them." })
13667
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_design_system41.DialogDescription, { children: scope === "view" ? "The first rule a row matches wins. These rules belong to this view alone, and a date can be tested against today." : "The first rule a row matches wins. A rule tints the whole row, or only the cell it reads. Everyone who opens this table sees them." })
13505
13668
  ] }),
13506
13669
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col gap-2", children: [
13670
+ viewRules ? /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-1.5 text-xs", "data-records-colour-rules-scope": "", children: [
13671
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-muted-foreground", children: "Keep them on" }),
13672
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
13673
+ PickOne,
13674
+ {
13675
+ label: "Where the rules are kept",
13676
+ className: SELECT2,
13677
+ value: scope,
13678
+ onChange: (picked) => {
13679
+ setScope(picked === "view" ? "view" : "table");
13680
+ setDrafts(null);
13681
+ },
13682
+ options: [
13683
+ { value: "table", label: "the table, for everyone" },
13684
+ { value: "view", label: "this view" }
13685
+ ]
13686
+ }
13687
+ )
13688
+ ] }) : null,
13507
13689
  rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-xs text-muted-foreground", children: "No rules yet." }) : null,
13508
13690
  rows.map((rule, i) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-wrap items-center gap-1.5 text-xs", "data-records-colour-rule": "", children: [
13509
13691
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-muted-foreground", children: i === 0 ? "When" : "Else when" }),
@@ -13525,7 +13707,7 @@ function ColourRulesDialog({
13525
13707
  className: SELECT2,
13526
13708
  value: rule.op,
13527
13709
  onChange: (picked) => set(i, { op: picked }),
13528
- options: (choicesOf(rule.field) ? import_data_table11.COLOR_RULE_OPS.filter((op2) => CHOICE_OPS.includes(op2)) : import_data_table11.COLOR_RULE_OPS).map((op2) => ({
13710
+ options: opsFor(rule.field).map((op2) => ({
13529
13711
  value: op2,
13530
13712
  label: import_data_table11.COLOR_RULE_OP_LABELS[op2]
13531
13713
  }))
@@ -13602,7 +13784,7 @@ function ColourRulesDialog({
13602
13784
  disabled: busy || drafts === null,
13603
13785
  onClick: async () => {
13604
13786
  setBusy(true);
13605
- await onSave(rows);
13787
+ await (scope === "view" && viewRules ? viewRules.onSave(rows) : onSave(rows));
13606
13788
  setBusy(false);
13607
13789
  setDrafts(null);
13608
13790
  },
@@ -15614,7 +15796,12 @@ function useMergedGridControls(args) {
15614
15796
  decorations: args.decorations ?? noDecorations,
15615
15797
  mayColour: editable && rights.write,
15616
15798
  whyNot: editable ? rights.why("write") : "Editing is off here.",
15617
- notify
15799
+ notify,
15800
+ // THIS VIEW'S OWN RULES: kept on the look like every other view setting (and saved to the view the same way).
15801
+ view: {
15802
+ rules: presentation.style?.rules,
15803
+ save: (rules) => writeLook({ ...presentation, style: { ...presentation.style ?? { version: 1 }, rules } })
15804
+ }
15618
15805
  });
15619
15806
  const [held, setHeld] = (0, import_react90.useState)(null);
15620
15807
  const keptOrder = presentation.columnOrder;
@@ -27083,7 +27270,9 @@ var GLYPHS = {
27083
27270
  "archive": [["rect", { "width": "20", "height": "5", "x": "2", "y": "3", "rx": "1" }], ["path", { "d": "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" }], ["path", { "d": "M10 12h4" }]],
27084
27271
  "link-2": [["path", { "d": "M9 17H7A5 5 0 0 1 7 7h2" }], ["path", { "d": "M15 7h2a5 5 0 1 1 0 10h-2" }], ["line", { "x1": "8", "x2": "16", "y1": "12", "y2": "12" }]],
27085
27272
  "layout-grid": [["rect", { "width": "7", "height": "7", "x": "3", "y": "3", "rx": "1" }], ["rect", { "width": "7", "height": "7", "x": "14", "y": "3", "rx": "1" }], ["rect", { "width": "7", "height": "7", "x": "14", "y": "14", "rx": "1" }], ["rect", { "width": "7", "height": "7", "x": "3", "y": "14", "rx": "1" }]],
27086
- "git-merge": [["circle", { "cx": "18", "cy": "18", "r": "3" }], ["circle", { "cx": "6", "cy": "6", "r": "3" }], ["path", { "d": "M6 21V9a9 9 0 0 0 9 9" }]]
27273
+ "git-merge": [["circle", { "cx": "18", "cy": "18", "r": "3" }], ["circle", { "cx": "6", "cy": "6", "r": "3" }], ["path", { "d": "M6 21V9a9 9 0 0 0 9 9" }]],
27274
+ "lock": [["rect", { "width": "18", "height": "11", "x": "3", "y": "11", "rx": "2", "ry": "2" }], ["path", { "d": "M7 11V7a5 5 0 0 1 10 0v4" }]],
27275
+ "lock-open": [["rect", { "width": "18", "height": "11", "x": "3", "y": "11", "rx": "2", "ry": "2" }], ["path", { "d": "M7 11V7a5 5 0 0 1 9.9-1" }]]
27087
27276
  };
27088
27277
  function ObjectActionGlyph({ name, ...props }) {
27089
27278
  return /* @__PURE__ */ (0, import_jsx_runtime85.jsx)(
@@ -29645,7 +29834,29 @@ function TableSettingsPanel({
29645
29834
  const [enriching, setEnriching] = (0, import_react165.useState)(null);
29646
29835
  if (table.error) return /* @__PURE__ */ (0, import_jsx_runtime99.jsx)(RefusalNotice, { error: table.error, className });
29647
29836
  if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime99.jsx)(RefusalNotice, { error: fields.error, className });
29648
- if (!rights.structure) return /* @__PURE__ */ (0, import_jsx_runtime99.jsx)("p", { className: (0, import_design_system86.cn)("text-xs text-muted-foreground", className), children: rights.why("structure") });
29837
+ if (!rights.structure) {
29838
+ if (tableIsLocked(table.data)) {
29839
+ return /* @__PURE__ */ (0, import_jsx_runtime99.jsxs)("div", { className: (0, import_design_system86.cn)("flex flex-col items-start gap-2 text-xs text-muted-foreground", className), "data-table-locked": "", children: [
29840
+ /* @__PURE__ */ (0, import_jsx_runtime99.jsx)("p", { children: DATABASE_LOCKED_REASON }),
29841
+ /* @__PURE__ */ (0, import_jsx_runtime99.jsx)(
29842
+ import_design_system86.Button,
29843
+ {
29844
+ size: "sm",
29845
+ variant: "outline",
29846
+ onClick: () => {
29847
+ void saveTableLock(client, tableId, false).then((done) => {
29848
+ if (done.ok) table.reload();
29849
+ else ui.notify?.error(done.says);
29850
+ });
29851
+ },
29852
+ "data-table-unlock": "",
29853
+ children: "Unlock database"
29854
+ }
29855
+ )
29856
+ ] });
29857
+ }
29858
+ return /* @__PURE__ */ (0, import_jsx_runtime99.jsx)("p", { className: (0, import_design_system86.cn)("text-xs text-muted-foreground", className), children: rights.why("structure") });
29859
+ }
29649
29860
  const rows = [...fields.data ?? []].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
29650
29861
  const remove = async (field2) => {
29651
29862
  const gone = await shape.retire({ field_id: field2.id });
@@ -36709,20 +36920,23 @@ function useSeedGuard() {
36709
36920
  var import_react213 = require("@ai-matrx/records/react");
36710
36921
 
36711
36922
  // src/viewActions.ts
36923
+ var VIEW_LOCKED_REASON = "This view is locked. Unlock it to change it.";
36712
36924
  var NOOP3 = () => {
36713
36925
  };
36714
- function viewActions(whyNot, host) {
36926
+ function viewActions(whyNot, host, locked = false) {
36715
36927
  const one = (base2, run) => {
36716
36928
  if (whyNot !== null) return { ...base2, disabledReason: whyNot, run: NOOP3 };
36717
36929
  if (!run) return { ...base2, disabledReason: ACTION_NOT_HERE_REASON, run: NOOP3 };
36718
36930
  return { ...base2, run };
36719
36931
  };
36932
+ const whenUnlocked = (action3) => locked && action3.disabledReason === void 0 ? { ...action3, disabledReason: VIEW_LOCKED_REASON, run: NOOP3 } : action3;
36933
+ const lockVerb = host.toggleLock ? [one({ id: "lock", label: locked ? "Unlock view" : "Lock view", icon: locked ? "lock-open" : "lock", group: "organize" }, host.toggleLock)] : [];
36720
36934
  return [
36721
- one({ id: "rename", label: "Rename view\u2026", icon: "pencil", group: "organize", verb: "rename" }, host.rename),
36935
+ whenUnlocked(one({ id: "rename", label: "Rename view\u2026", icon: "pencil", group: "organize", verb: "rename" }, host.rename)),
36722
36936
  one({ id: "duplicate", label: "Duplicate view", icon: "copy", group: "organize", verb: "duplicate" }, host.duplicate),
36723
- one(
36724
- { id: "archive", label: "Archive view", icon: "archive", group: "danger", destructive: true, verb: "archive" },
36725
- host.archive
36937
+ ...lockVerb,
36938
+ whenUnlocked(
36939
+ one({ id: "archive", label: "Archive view", icon: "archive", group: "danger", destructive: true, verb: "archive" }, host.archive)
36726
36940
  )
36727
36941
  ];
36728
36942
  }
@@ -36841,6 +37055,17 @@ function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews,
36841
37055
  setRenaming(null);
36842
37056
  await load();
36843
37057
  }
37058
+ async function toggleLock(view) {
37059
+ const presentation = { ...view.presentation ?? {} };
37060
+ if (presentation.locked === true) delete presentation.locked;
37061
+ else presentation.locked = true;
37062
+ const written = await saveViewPatch(client, tableId, view.id, { presentation });
37063
+ if (!written.ok) {
37064
+ setError(written.error);
37065
+ return;
37066
+ }
37067
+ await load();
37068
+ }
36844
37069
  async function duplicate(view) {
36845
37070
  const { id: _id, movedFrom: _moved, ...spec } = view;
36846
37071
  const written = await declareView(client, {
@@ -36996,8 +37221,9 @@ function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews,
36996
37221
  ) : viewActions(rights.admin ? null : rightsReason(rights, "structure") ?? ACTION_NO_ACCESS_REASON, {
36997
37222
  rename: () => setRenaming(current.name),
36998
37223
  duplicate: () => duplicate(current),
36999
- archive: () => archive(current)
37000
- }).map((action3) => /* @__PURE__ */ (0, import_jsx_runtime122.jsxs)(
37224
+ archive: () => archive(current),
37225
+ toggleLock: () => toggleLock(current)
37226
+ }, current.presentation?.locked === true).map((action3) => /* @__PURE__ */ (0, import_jsx_runtime122.jsxs)(
37001
37227
  "button",
37002
37228
  {
37003
37229
  type: "button",
@@ -49151,7 +49377,7 @@ function ViewFilter({ tableId, view, onApply, className }) {
49151
49377
  "This view asks: ",
49152
49378
  conditionInWords(saved, conditionFields),
49153
49379
  ". It cannot be drawn here; clear it to start a new filter."
49154
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime164.jsx)(
49380
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime164.jsx)(FilterTermsContext.Provider, { value: true, children: /* @__PURE__ */ (0, import_jsx_runtime164.jsx)(
49155
49381
  ConditionGroup,
49156
49382
  {
49157
49383
  lead: "Show records where",
@@ -49160,7 +49386,7 @@ function ViewFilter({ tableId, view, onApply, className }) {
49160
49386
  fields: conditionFields,
49161
49387
  onChange: setDraft
49162
49388
  }
49163
- ),
49389
+ ) }),
49164
49390
  view.ruleId ? null : /* @__PURE__ */ (0, import_jsx_runtime164.jsxs)("div", { className: "flex items-center gap-1", children: [
49165
49391
  changed ? /* @__PURE__ */ (0, import_jsx_runtime164.jsx)(
49166
49392
  import_design_system147.Button,
@@ -49933,10 +50159,31 @@ function TablePage({
49933
50159
  setSurface((now) => ({ ...now, main: isLayout ? "records" : named }));
49934
50160
  }, [activeView]);
49935
50161
  const [viewSaveRefused, setViewSaveRefused] = (0, import_react291.useState)(null);
50162
+ const viewLocked = view?.presentation?.locked === true;
50163
+ const unlockView = (0, import_react291.useCallback)(async () => {
50164
+ const current = view;
50165
+ if (!current) return;
50166
+ const presentation = { ...current.presentation ?? {} };
50167
+ delete presentation.locked;
50168
+ const written = await saveViewPatch(client, tableId, current.id, { presentation });
50169
+ if (!written.ok) {
50170
+ recordsUi.notify?.error(refusalSentence(written.error));
50171
+ return;
50172
+ }
50173
+ setView({ ...current, presentation });
50174
+ setViewsEpoch((n) => n + 1);
50175
+ }, [client, tableId, view, recordsUi.notify]);
50176
+ const sayLocked = (0, import_react291.useCallback)(() => {
50177
+ recordsUi.notify?.success(VIEW_LOCKED_REASON, { label: "Unlock", run: () => void unlockView() });
50178
+ }, [recordsUi.notify, unlockView]);
49936
50179
  const patchView = (0, import_react291.useCallback)(
49937
50180
  async (asked2) => {
49938
50181
  const current = view;
49939
50182
  if (!current) return;
50183
+ if (viewLocked) {
50184
+ sayLocked();
50185
+ return;
50186
+ }
49940
50187
  const part = (0, import_views3.lookPart)(asked2);
49941
50188
  if (Object.keys(part).length === 0) return;
49942
50189
  const before = looks?.[current.id];
@@ -49947,7 +50194,7 @@ function TablePage({
49947
50194
  if (kept.ok) return;
49948
50195
  setViewSaveRefused({ error: kept.error, patch: asked2, lookBefore: before });
49949
50196
  },
49950
- [client, tableId, view, looks]
50197
+ [client, tableId, view, looks, viewLocked, sayLocked]
49951
50198
  );
49952
50199
  const arrival = (0, import_react291.useRef)(viewAddress ?? null);
49953
50200
  const [gridQuery, setGridQuery] = (0, import_react291.useState)(null);
@@ -50020,6 +50267,10 @@ function TablePage({
50020
50267
  const current = view;
50021
50268
  const mine = current && looks ? looks[current.id] : void 0;
50022
50269
  if (!current || !mine) return;
50270
+ if (viewLocked) {
50271
+ sayLocked();
50272
+ return;
50273
+ }
50023
50274
  setLookRefused(null);
50024
50275
  const { patch, left } = (0, import_views3.saveToViewPlan)(current, mine);
50025
50276
  if (Object.keys(viewPatchDocument(patch)).length > 0) {
@@ -50066,6 +50317,10 @@ function TablePage({
50066
50317
  const designate = async () => {
50067
50318
  const current = view;
50068
50319
  if (!current || !layoutShowing) return;
50320
+ if (viewLocked) {
50321
+ sayLocked();
50322
+ return;
50323
+ }
50069
50324
  setDesignateRefused(null);
50070
50325
  const written = await (0, import_views2.designateView)(client, tableId, current.id, layoutShowing);
50071
50326
  if (!written.ok) {