@ai-matrx/records-ui 0.113.36 → 0.113.38

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.js CHANGED
@@ -82,13 +82,20 @@ var NOT_ANSWERED_YET = whatYouMayDo(null, false);
82
82
  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.";
83
83
  function whatYouMayDoWithTable(table, level, known) {
84
84
  const base2 = whatYouMayDo(level, known);
85
- if (!table?.is_kernel) return base2;
85
+ if (!table?.is_kernel) return table && tableIsLocked(table) ? lockedStructure(base2) : base2;
86
86
  return {
87
87
  ...base2,
88
88
  structure: false,
89
89
  why: (what) => what === "structure" ? KERNEL_REASON : base2.why(what)
90
90
  };
91
91
  }
92
+ function tableIsLocked(table) {
93
+ return table?.metadata?.["locked"] === true;
94
+ }
95
+ var DATABASE_LOCKED_REASON = "This database is locked, so its columns and settings are read-only. Unlock it to change them.";
96
+ function lockedStructure(base2) {
97
+ return { ...base2, structure: false, why: (what) => what === "structure" ? DATABASE_LOCKED_REASON : base2.why(what) };
98
+ }
92
99
 
93
100
  // src/host.tsx
94
101
  import { jsx } from "react/jsx-runtime";
@@ -637,6 +644,7 @@ import { dateFilterBounds, dateFilterDayBounds, dayInstantWindow } from "@ai-mat
637
644
 
638
645
  // src/personTime.ts
639
646
  import { useSyncExternalStore } from "react";
647
+ import { setColorRuleTimeZone } from "@ai-matrx/design-system/data-table/table-style";
640
648
  var zone = null;
641
649
  var listeners = /* @__PURE__ */ new Set();
642
650
  function valid(z) {
@@ -652,6 +660,7 @@ function setPersonTimeZone(next) {
652
660
  const clean = valid(next) ? next : null;
653
661
  if (clean === zone) return;
654
662
  zone = clean;
663
+ setColorRuleTimeZone(clean);
655
664
  listeners.forEach((l) => l());
656
665
  }
657
666
  function getPersonTimeZone() {
@@ -2813,6 +2822,28 @@ function descriptionPatch(table, text) {
2813
2822
  else delete metadata["description"];
2814
2823
  return { metadata };
2815
2824
  }
2825
+ function lockPatch(table, locked) {
2826
+ const metadata = { ...table.metadata ?? {} };
2827
+ if (locked) metadata["locked"] = true;
2828
+ else delete metadata["locked"];
2829
+ return { metadata };
2830
+ }
2831
+ async function saveTableLock(client, tableId, locked) {
2832
+ const read = await client.tableRead({ table_id: tableId });
2833
+ if (!read.ok) return { ok: false, says: refusalSentence(read.error) };
2834
+ if (!read.data) return { ok: false, says: "This table could not be read, so it was not locked." };
2835
+ const table = read.data;
2836
+ const written = await (0, writeOnTheWinningVersion_exports.updateOnTheWinningVersion)(
2837
+ client,
2838
+ tableId,
2839
+ lockPatch(table, locked),
2840
+ await (0, recordVersions_exports.readTableVersionNow)(client, tableId),
2841
+ { metadata: table.metadata ?? {} }
2842
+ );
2843
+ if (!written.ok) return { ok: false, says: refusalSentence(written.error) };
2844
+ tableShapeChanged2(tableId);
2845
+ return { ok: true };
2846
+ }
2816
2847
  function TableDetails({
2817
2848
  tableId,
2818
2849
  table,
@@ -2894,6 +2925,20 @@ function TableDetails({
2894
2925
  }
2895
2926
  )
2896
2927
  ] }),
2928
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between gap-2", "data-table-lock": "", children: [
2929
+ /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground", children: "Lock database" }),
2930
+ /* @__PURE__ */ jsx10(
2931
+ Button4,
2932
+ {
2933
+ size: "sm",
2934
+ variant: "outline",
2935
+ disabled: busy,
2936
+ onClick: () => void write(lockPatch(table, true), { metadata: table.metadata ?? {} }),
2937
+ "data-table-lock-on": "",
2938
+ children: "Lock"
2939
+ }
2940
+ )
2941
+ ] }),
2897
2942
  refused ? /* @__PURE__ */ jsx10("p", { className: "text-destructive", children: refused }) : null
2898
2943
  ] });
2899
2944
  }
@@ -3742,6 +3787,7 @@ function parseGridPresentation(raw) {
3742
3787
  if (typeof value["boardHideEmpty"] === "boolean") out.boardHideEmpty = value["boardHideEmpty"];
3743
3788
  if (typeof value["boardColorColumns"] === "boolean") out.boardColorColumns = value["boardColorColumns"];
3744
3789
  if (typeof value["boardCounts"] === "boolean") out.boardCounts = value["boardCounts"];
3790
+ if (value["locked"] === true) out.locked = true;
3745
3791
  if (Array.isArray(value["boardLanesShut"])) {
3746
3792
  const shut = value["boardLanesShut"].filter((k) => typeof k === "string");
3747
3793
  if (shut.length > 0) out.boardLanesShut = shut;
@@ -3828,7 +3874,7 @@ function parseGrouping(raw) {
3828
3874
  return out;
3829
3875
  }
3830
3876
  function presentationIsEmpty(p) {
3831
- return (p.style === void 0 || 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;
3877
+ return (p.style === void 0 || 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;
3832
3878
  }
3833
3879
  function presentationDocument(p) {
3834
3880
  return presentationIsEmpty(p) ? "" : JSON.stringify(p);
@@ -6752,7 +6798,7 @@ function SourceArchivedNotice({ fieldId, tableId }) {
6752
6798
  }
6753
6799
 
6754
6800
  // src/Condition.tsx
6755
- import { useEffect as useEffect15, useState as useState22 } from "react";
6801
+ import { createContext as createContext5, useContext as useContext5, useEffect as useEffect15, useState as useState22 } from "react";
6756
6802
  import { BasicInput as BasicInput6, Button as Button13 } from "@ai-matrx/design-system";
6757
6803
  import { optionKey as optionKey2, optionLabel, rankTypedMatches as rankTypedMatches3 } from "@ai-matrx/records";
6758
6804
  import { useOptionalRecordsClient as useOptionalRecordsClient6 } from "@ai-matrx/records/react";
@@ -6766,6 +6812,48 @@ var CONDITION_OPS = [
6766
6812
  { op: "gte", label: "is at least" },
6767
6813
  { op: "lte", label: "is at most" }
6768
6814
  ];
6815
+ var FilterTermsContext = createContext5(false);
6816
+ var RELATIVE_DAY_TERMS = [
6817
+ { kind: "today", label: "is today" },
6818
+ { kind: "tomorrow", label: "is tomorrow" },
6819
+ { kind: "yesterday", label: "is yesterday" },
6820
+ { kind: "this_week", label: "is this week" },
6821
+ { kind: "next_week", label: "is next week" },
6822
+ { kind: "last_week", label: "is last week" },
6823
+ { kind: "this_month", label: "is this month" },
6824
+ { kind: "next_month", label: "is next month" },
6825
+ { kind: "last_month", label: "is last month" },
6826
+ { kind: "before_today", label: "is before today" },
6827
+ { kind: "next_days", label: "is in the next \u2026 days", takesDays: true },
6828
+ { kind: "past_days", label: "is in the past \u2026 days", takesDays: true }
6829
+ ];
6830
+ function relativeDayNode(fieldId, kind, days) {
6831
+ const zone2 = getPersonTimeZone();
6832
+ const window2 = { relative: kind };
6833
+ if (RELATIVE_DAY_TERMS.find((t) => t.kind === kind)?.takesDays) window2["n"] = days ?? 7;
6834
+ if (zone2) window2["tz"] = zone2;
6835
+ return { op: "within", args: [{ field: fieldId }, { const: window2 }] };
6836
+ }
6837
+ function relativeDayOf(expr) {
6838
+ const node = asNode(expr);
6839
+ if (!node || node["op"] !== "within") return null;
6840
+ const [a, b] = argsOf(node);
6841
+ const window2 = asNode(b?.["const"]);
6842
+ if (!a || typeof a["field"] !== "string" || !window2 || typeof window2["relative"] !== "string") return null;
6843
+ const days = typeof window2["n"] === "number" ? window2["n"] : null;
6844
+ return { kind: window2["relative"], days, field: a["field"] };
6845
+ }
6846
+ function meNode(fieldId, negate) {
6847
+ return { op: negate ? "ne" : "eq", args: [{ field: fieldId }, { const: { me: true } }] };
6848
+ }
6849
+ function meOf(expr) {
6850
+ const node = asNode(expr);
6851
+ if (!node || node["op"] !== "eq" && node["op"] !== "ne") return null;
6852
+ const [a, b] = argsOf(node);
6853
+ const value = asNode(b?.["const"]);
6854
+ if (!a || typeof a["field"] !== "string" || !value || value["me"] !== true) return null;
6855
+ return { which: node["op"] === "eq" ? "me" : "not_me", field: a["field"] };
6856
+ }
6769
6857
  function pointsAtARecord(f) {
6770
6858
  return !!f?.field && f.field.type === "relation" && !!f.field.relation_target;
6771
6859
  }
@@ -6836,6 +6924,7 @@ function ChoiceValue({ field: field2, value, onChange }) {
6836
6924
  }
6837
6925
  function conditionIsSimple(expr) {
6838
6926
  if (expr === null || expr === void 0) return true;
6927
+ if (relativeDayOf(expr) !== null || meOf(expr) !== null) return true;
6839
6928
  const node = expr;
6840
6929
  if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
6841
6930
  if (!Array.isArray(node.args)) return false;
@@ -6880,6 +6969,14 @@ function conditionInWords(expr, fields) {
6880
6969
  const args = Array.isArray(n["args"]) ? n["args"] : [];
6881
6970
  if (op2 === "not") return `not ${walk(args[0])}`;
6882
6971
  if (op2 === "present") return `${walk(args[0])} has been answered`;
6972
+ const dayRange = relativeDayOf(n);
6973
+ if (dayRange) {
6974
+ const term = RELATIVE_DAY_TERMS.find((t) => t.kind === dayRange.kind);
6975
+ const said3 = term ? term.label.replace("\u2026", String(dayRange.days ?? 7)) : "is in a day range";
6976
+ return `${name(dayRange.field)} ${said3}`;
6977
+ }
6978
+ const reader = meOf(n);
6979
+ if (reader) return `${name(reader.field)} ${reader.which === "me" ? "is me" : "is not me"}`;
6883
6980
  const word = said2[op2] ?? op2;
6884
6981
  if (op2 === "and" || op2 === "or") return args.map(walk).join(` ${word} `);
6885
6982
  if (args.length === 2) return `${walk(args[0])} ${word} ${walk(args[1])}`;
@@ -6901,11 +6998,31 @@ function conditionValue(typed2) {
6901
6998
  }
6902
6999
  function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", className }) {
6903
7000
  const node = expr ?? null;
6904
- const op2 = node?.op ?? "";
7001
+ const filterTerms = useContext5(FilterTermsContext);
7002
+ const relative = relativeDayOf(expr);
7003
+ const reader = meOf(expr);
7004
+ const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : node?.op ?? "";
6905
7005
  const onField = node?.args?.[0]?.field ?? "";
6906
- const value = node?.args?.[1]?.const ?? "";
7006
+ const value = relative || reader ? "" : node?.args?.[1]?.const ?? "";
6907
7007
  const chosen = fields.find((f) => f.id === onField);
7008
+ const chosenKind = chosen?.field ? String(editorKindFor(chosen.field)) : "";
7009
+ 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" }] : [];
7010
+ const relativeTerm = relative ? RELATIVE_DAY_TERMS.find((t) => t.kind === relative.kind) : void 0;
6908
7011
  function write(next) {
7012
+ const picked = next.op;
7013
+ const fieldNow = next.field ?? onField;
7014
+ if (fieldNow && picked !== void 0 && (picked.startsWith("rel:") || picked === "me" || picked === "not_me")) {
7015
+ onChange(picked === "me" || picked === "not_me" ? meNode(fieldNow, picked === "not_me") : relativeDayNode(fieldNow, picked.slice(4), relative?.days ?? void 0));
7016
+ return;
7017
+ }
7018
+ if (fieldNow && next.days !== void 0 && relative) {
7019
+ onChange(relativeDayNode(fieldNow, relative.kind, next.days));
7020
+ return;
7021
+ }
7022
+ if (next.field !== void 0 && (relative || reader)) {
7023
+ onChange(next.field ? { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] } : null);
7024
+ return;
7025
+ }
6909
7026
  const chosenOp = next.op ?? (op2 || (next.field ? CONDITION_OPS[0].op : ""));
6910
7027
  const chosenField = next.field ?? onField;
6911
7028
  const chosenValue = next.value ?? value;
@@ -6947,11 +7064,21 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
6947
7064
  className: "h-8 w-auto min-w-[6rem] text-xs",
6948
7065
  value: op2,
6949
7066
  onChange: (next) => write({ op: next }),
6950
- options: CONDITION_OPS.map((c) => ({ value: c.op, label: c.label })),
7067
+ options: [...CONDITION_OPS, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
6951
7068
  attrs: { "data-condition": "comparison" }
6952
7069
  }
6953
7070
  ),
6954
- op2 === "present" ? null : pointsAtARecord(chosen) ? (
7071
+ relative ? relativeTerm?.takesDays ? /* @__PURE__ */ jsx30(
7072
+ BasicInput6,
7073
+ {
7074
+ className: "h-8 w-16 text-xs",
7075
+ type: "number",
7076
+ min: 0,
7077
+ "aria-label": "Number of days",
7078
+ value: String(relative.days ?? 7),
7079
+ onChange: (e) => write({ days: Math.max(0, Math.floor(Number(e.target.value) || 0)) })
7080
+ }
7081
+ ) : null : reader || op2 === "present" ? null : pointsAtARecord(chosen) ? (
6955
7082
  // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
6956
7083
  // cell uses, on the same `relation_words_many` the cell reads, so a filter and
6957
7084
  // the column it filters can never disagree about what a record is called. It
@@ -10905,7 +11032,7 @@ function ChoiceAnswer({
10905
11032
  }
10906
11033
 
10907
11034
  // src/whoChangedSource.tsx
10908
- import { createContext as createContext5, useCallback as useCallback9, useContext as useContext5, useEffect as useEffect23, useMemo as useMemo13, useRef as useRef19, useState as useState33 } from "react";
11035
+ import { createContext as createContext6, useCallback as useCallback9, useContext as useContext6, useEffect as useEffect23, useMemo as useMemo13, useRef as useRef19, useState as useState33 } from "react";
10909
11036
  import { useRecordsClient as useRecordsClient12 } from "@ai-matrx/records/react";
10910
11037
 
10911
11038
  // src/whoChanged.ts
@@ -10970,7 +11097,7 @@ function actorBadge(actor, resolve) {
10970
11097
 
10971
11098
  // src/whoChangedSource.tsx
10972
11099
  import { jsx as jsx43 } from "react/jsx-runtime";
10973
- var WhoChangedContext = createContext5(null);
11100
+ var WhoChangedContext = createContext6(null);
10974
11101
  function WhoChangedSource({
10975
11102
  tableId,
10976
11103
  labelOf,
@@ -11025,7 +11152,7 @@ function WhoChangedSource({
11025
11152
  return /* @__PURE__ */ jsx43(WhoChangedContext.Provider, { value, children });
11026
11153
  }
11027
11154
  function useWhoChanged(fieldKey, recordId) {
11028
- const source = useContext5(WhoChangedContext);
11155
+ const source = useContext6(WhoChangedContext);
11029
11156
  if (!source) return { badge: null, sentence: null, ask: () => void 0 };
11030
11157
  const answer = source.answerFor(fieldKey, recordId);
11031
11158
  const label = source.labelOf(fieldKey);
@@ -12685,6 +12812,7 @@ import {
12685
12812
  DialogTitle as DialogTitle3
12686
12813
  } from "@ai-matrx/design-system";
12687
12814
  import {
12815
+ COLOR_RULE_DATE_OPS,
12688
12816
  COLOR_RULE_OPS,
12689
12817
  COLOR_RULE_OP_LABELS,
12690
12818
  OPS_WITH_VALUE,
@@ -12806,7 +12934,7 @@ function rulesLeaf(drafts, fields) {
12806
12934
  const out = [];
12807
12935
  for (const d of drafts) {
12808
12936
  const field2 = fields.find((f) => f.key === d.field);
12809
- if (!field2) continue;
12937
+ if (!field2 || COLOR_RULE_DATE_OPS.includes(d.op)) continue;
12810
12938
  out.push({
12811
12939
  ...d.id ? { id: d.id } : {},
12812
12940
  field: field2.id,
@@ -12818,6 +12946,22 @@ function rulesLeaf(drafts, fields) {
12818
12946
  }
12819
12947
  return out;
12820
12948
  }
12949
+ function draftsFromView(rules, fields) {
12950
+ 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 }));
12951
+ }
12952
+ function viewRulesFrom(drafts) {
12953
+ return drafts.map((d, i) => ({
12954
+ id: d.id ?? `rule-${Date.now().toString(36)}-${i}`,
12955
+ field: d.field,
12956
+ op: d.op,
12957
+ ...OPS_WITH_VALUE.includes(d.op) ? { value: d.value } : {},
12958
+ color: d.color,
12959
+ target: d.target
12960
+ }));
12961
+ }
12962
+ function isDateField2(field2) {
12963
+ return field2 ? String(editorKindFor(field2)) === "datetime" : false;
12964
+ }
12821
12965
  function swatches(prefix, current, write) {
12822
12966
  return [
12823
12967
  ...STYLE_COLORS4.map((color) => ({
@@ -12829,7 +12973,7 @@ function swatches(prefix, current, write) {
12829
12973
  ];
12830
12974
  }
12831
12975
  function useGridColours(args) {
12832
- const { enabled, tableId, fields, decorations, mayColour, whyNot, notify } = args;
12976
+ const { enabled, tableId, fields, decorations, mayColour, whyNot, notify, view } = args;
12833
12977
  const client = useRecordsClient17();
12834
12978
  const [rulesOpen, setRulesOpen] = useState38(null);
12835
12979
  const dialogOptions = useChoiceOptions(fields, rulesOpen !== null ? fields.filter(isChoiceField).map((f) => f.key) : []);
@@ -12977,6 +13121,16 @@ function useGridColours(args) {
12977
13121
  decorations: decorations.data,
12978
13122
  options: dialogOptions,
12979
13123
  onClose: () => setRulesOpen(null),
13124
+ ...view ? {
13125
+ viewRules: {
13126
+ drafts: draftsFromView(view.rules, fields),
13127
+ onSave: async (drafts) => {
13128
+ view.save(viewRulesFrom(drafts));
13129
+ notify?.success(drafts.length === 0 ? "This view's colour rules are cleared." : "This view's colour rules are kept.");
13130
+ setRulesOpen(null);
13131
+ }
13132
+ }
13133
+ } : {},
12980
13134
  onSave: async (drafts) => {
12981
13135
  const kept = await write(["rules"], rulesLeaf(drafts, fields), drafts.length === 0 ? "The colour rules are cleared." : "The colour rules are kept.");
12982
13136
  if (kept) setRulesOpen(null);
@@ -13020,17 +13174,27 @@ function ColourRulesDialog({
13020
13174
  decorations,
13021
13175
  options,
13022
13176
  onClose,
13023
- onSave
13177
+ onSave,
13178
+ viewRules
13024
13179
  }) {
13025
13180
  const [drafts, setDrafts] = useState38(null);
13026
13181
  const [busy, setBusy] = useState38(false);
13027
- const held = useMemo16(() => draftsFrom(decorations, fields), [decorations, fields]);
13182
+ const [scope, setScope] = useState38("table");
13183
+ const keptTable = useMemo16(() => draftsFrom(decorations, fields), [decorations, fields]);
13184
+ const held = scope === "view" && viewRules ? viewRules.drafts : keptTable;
13185
+ const opsFor = (key) => {
13186
+ const field2 = fields.find((f) => f.key === key);
13187
+ if (choicesOf(key)) return CHOICE_OPS;
13188
+ const dateTests = scope === "view" && isDateField2(field2);
13189
+ return COLOR_RULE_OPS.filter((op2) => dateTests || !COLOR_RULE_DATE_OPS.includes(op2));
13190
+ };
13028
13191
  const rows = drafts ?? held;
13029
13192
  const set = (i, patch) => setDrafts(
13030
13193
  rows.map((r, j) => {
13031
13194
  if (j !== i) return r;
13032
13195
  const next = { ...r, ...patch };
13033
13196
  if (patch.field !== void 0 && choicesOf(next.field) && !CHOICE_OPS.includes(next.op)) return { ...next, op: "is", value: "" };
13197
+ if (patch.field !== void 0 && !opsFor(next.field).includes(next.op)) return { ...next, op: "is", value: "" };
13034
13198
  return next;
13035
13199
  })
13036
13200
  );
@@ -13046,9 +13210,28 @@ function ColourRulesDialog({
13046
13210
  return /* @__PURE__ */ jsx48(Dialog3, { open, onOpenChange: (next) => !next ? close() : void 0, children: /* @__PURE__ */ jsxs43(DialogContent3, { className: "sm:max-w-2xl", "data-records-colour-rules": "", children: [
13047
13211
  /* @__PURE__ */ jsxs43(DialogHeader3, { children: [
13048
13212
  /* @__PURE__ */ jsx48(DialogTitle3, { children: "Colour rules" }),
13049
- /* @__PURE__ */ jsx48(DialogDescription3, { 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." })
13213
+ /* @__PURE__ */ jsx48(DialogDescription3, { 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." })
13050
13214
  ] }),
13051
13215
  /* @__PURE__ */ jsxs43("div", { className: "flex flex-col gap-2", children: [
13216
+ viewRules ? /* @__PURE__ */ jsxs43("div", { className: "flex items-center gap-1.5 text-xs", "data-records-colour-rules-scope": "", children: [
13217
+ /* @__PURE__ */ jsx48("span", { className: "text-muted-foreground", children: "Keep them on" }),
13218
+ /* @__PURE__ */ jsx48(
13219
+ PickOne,
13220
+ {
13221
+ label: "Where the rules are kept",
13222
+ className: SELECT2,
13223
+ value: scope,
13224
+ onChange: (picked) => {
13225
+ setScope(picked === "view" ? "view" : "table");
13226
+ setDrafts(null);
13227
+ },
13228
+ options: [
13229
+ { value: "table", label: "the table, for everyone" },
13230
+ { value: "view", label: "this view" }
13231
+ ]
13232
+ }
13233
+ )
13234
+ ] }) : null,
13052
13235
  rows.length === 0 ? /* @__PURE__ */ jsx48("p", { className: "text-xs text-muted-foreground", children: "No rules yet." }) : null,
13053
13236
  rows.map((rule, i) => /* @__PURE__ */ jsxs43("div", { className: "flex flex-wrap items-center gap-1.5 text-xs", "data-records-colour-rule": "", children: [
13054
13237
  /* @__PURE__ */ jsx48("span", { className: "text-muted-foreground", children: i === 0 ? "When" : "Else when" }),
@@ -13070,7 +13253,7 @@ function ColourRulesDialog({
13070
13253
  className: SELECT2,
13071
13254
  value: rule.op,
13072
13255
  onChange: (picked) => set(i, { op: picked }),
13073
- options: (choicesOf(rule.field) ? COLOR_RULE_OPS.filter((op2) => CHOICE_OPS.includes(op2)) : COLOR_RULE_OPS).map((op2) => ({
13256
+ options: opsFor(rule.field).map((op2) => ({
13074
13257
  value: op2,
13075
13258
  label: COLOR_RULE_OP_LABELS[op2]
13076
13259
  }))
@@ -13147,7 +13330,7 @@ function ColourRulesDialog({
13147
13330
  disabled: busy || drafts === null,
13148
13331
  onClick: async () => {
13149
13332
  setBusy(true);
13150
- await onSave(rows);
13333
+ await (scope === "view" && viewRules ? viewRules.onSave(rows) : onSave(rows));
13151
13334
  setBusy(false);
13152
13335
  setDrafts(null);
13153
13336
  },
@@ -15220,7 +15403,12 @@ function useMergedGridControls(args) {
15220
15403
  decorations: args.decorations ?? noDecorations,
15221
15404
  mayColour: editable && rights.write,
15222
15405
  whyNot: editable ? rights.why("write") : "Editing is off here.",
15223
- notify
15406
+ notify,
15407
+ // THIS VIEW'S OWN RULES: kept on the look like every other view setting (and saved to the view the same way).
15408
+ view: {
15409
+ rules: presentation.style?.rules,
15410
+ save: (rules) => writeLook({ ...presentation, style: { ...presentation.style ?? { version: 1 }, rules } })
15411
+ }
15224
15412
  });
15225
15413
  const [held, setHeld] = useState47(null);
15226
15414
  const keptOrder = presentation.columnOrder;
@@ -21311,12 +21499,12 @@ function shapeTextOfNode(node) {
21311
21499
  }
21312
21500
 
21313
21501
  // src/toolbarSlot.tsx
21314
- import { createContext as createContext6, useContext as useContext6 } from "react";
21502
+ import { createContext as createContext7, useContext as useContext7 } from "react";
21315
21503
  import { createPortal as createPortal2 } from "react-dom";
21316
21504
  import { Fragment as Fragment22, jsx as jsx70 } from "react/jsx-runtime";
21317
- var TableToolbarSlotContext = createContext6(void 0);
21505
+ var TableToolbarSlotContext = createContext7(void 0);
21318
21506
  function useTableToolbarSlot() {
21319
- return useContext6(TableToolbarSlotContext);
21507
+ return useContext7(TableToolbarSlotContext);
21320
21508
  }
21321
21509
  function InTableToolbar({ children }) {
21322
21510
  const slot = useTableToolbarSlot();
@@ -21324,13 +21512,13 @@ function InTableToolbar({ children }) {
21324
21512
  if (slot === null) return null;
21325
21513
  return createPortal2(children, slot);
21326
21514
  }
21327
- var TableToolbarFoldContext = createContext6(0);
21515
+ var TableToolbarFoldContext = createContext7(0);
21328
21516
  function useTableToolbarFold() {
21329
- return useContext6(TableToolbarFoldContext);
21517
+ return useContext7(TableToolbarFoldContext);
21330
21518
  }
21331
- var TableToolbarSheetExtrasContext = createContext6(void 0);
21519
+ var TableToolbarSheetExtrasContext = createContext7(void 0);
21332
21520
  function useTableToolbarSheetExtras() {
21333
- return useContext6(TableToolbarSheetExtrasContext);
21521
+ return useContext7(TableToolbarSheetExtrasContext);
21334
21522
  }
21335
21523
 
21336
21524
  // src/GridLayoutMenu.tsx
@@ -22932,7 +23120,7 @@ function HandOrderDialog({
22932
23120
  }
22933
23121
 
22934
23122
  // src/viewAddress.ts
22935
- import { createContext as createContext7, useContext as useContext7 } from "react";
23123
+ import { createContext as createContext8, useContext as useContext8 } from "react";
22936
23124
  var VIEW_ADDRESS_KEYS = ["sort", "cf", "q", "hide", "page"];
22937
23125
  function isViewAddressEmpty(state) {
22938
23126
  if (!state) return true;
@@ -22986,8 +23174,8 @@ function viewAddressFromParams(get) {
22986
23174
  function sameViewAddress(a, b) {
22987
23175
  return JSON.stringify(viewAddressToParams(a)) === JSON.stringify(viewAddressToParams(b));
22988
23176
  }
22989
- var ViewAddressContext = createContext7(null);
22990
- var useViewAddress = () => useContext7(ViewAddressContext);
23177
+ var ViewAddressContext = createContext8(null);
23178
+ var useViewAddress = () => useContext8(ViewAddressContext);
22991
23179
 
22992
23180
  // src/Grid.tsx
22993
23181
  import { Fragment as Fragment26, jsx as jsx77, jsxs as jsxs70 } from "react/jsx-runtime";
@@ -26831,7 +27019,9 @@ var GLYPHS = {
26831
27019
  "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" }]],
26832
27020
  "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" }]],
26833
27021
  "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" }]],
26834
- "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" }]]
27022
+ "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" }]],
27023
+ "lock": [["rect", { "width": "18", "height": "11", "x": "3", "y": "11", "rx": "2", "ry": "2" }], ["path", { "d": "M7 11V7a5 5 0 0 1 10 0v4" }]],
27024
+ "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" }]]
26835
27025
  };
26836
27026
  function ObjectActionGlyph({ name, ...props }) {
26837
27027
  return /* @__PURE__ */ jsx85(
@@ -29421,7 +29611,29 @@ function TableSettingsPanel({
29421
29611
  const [enriching, setEnriching] = useState80(null);
29422
29612
  if (table.error) return /* @__PURE__ */ jsx99(RefusalNotice, { error: table.error, className });
29423
29613
  if (fields.error) return /* @__PURE__ */ jsx99(RefusalNotice, { error: fields.error, className });
29424
- if (!rights.structure) return /* @__PURE__ */ jsx99("p", { className: cn50("text-xs text-muted-foreground", className), children: rights.why("structure") });
29614
+ if (!rights.structure) {
29615
+ if (tableIsLocked(table.data)) {
29616
+ return /* @__PURE__ */ jsxs90("div", { className: cn50("flex flex-col items-start gap-2 text-xs text-muted-foreground", className), "data-table-locked": "", children: [
29617
+ /* @__PURE__ */ jsx99("p", { children: DATABASE_LOCKED_REASON }),
29618
+ /* @__PURE__ */ jsx99(
29619
+ Button61,
29620
+ {
29621
+ size: "sm",
29622
+ variant: "outline",
29623
+ onClick: () => {
29624
+ void saveTableLock(client, tableId, false).then((done) => {
29625
+ if (done.ok) table.reload();
29626
+ else ui.notify?.error(done.says);
29627
+ });
29628
+ },
29629
+ "data-table-unlock": "",
29630
+ children: "Unlock database"
29631
+ }
29632
+ )
29633
+ ] });
29634
+ }
29635
+ return /* @__PURE__ */ jsx99("p", { className: cn50("text-xs text-muted-foreground", className), children: rights.why("structure") });
29636
+ }
29425
29637
  const rows = [...fields.data ?? []].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
29426
29638
  const remove = async (field2) => {
29427
29639
  const gone = await shape.retire({ field_id: field2.id });
@@ -36553,20 +36765,23 @@ function useSeedGuard() {
36553
36765
  import { useTable as useTable20 } from "@ai-matrx/records/react";
36554
36766
 
36555
36767
  // src/viewActions.ts
36768
+ var VIEW_LOCKED_REASON = "This view is locked. Unlock it to change it.";
36556
36769
  var NOOP3 = () => {
36557
36770
  };
36558
- function viewActions(whyNot, host) {
36771
+ function viewActions(whyNot, host, locked = false) {
36559
36772
  const one = (base2, run) => {
36560
36773
  if (whyNot !== null) return { ...base2, disabledReason: whyNot, run: NOOP3 };
36561
36774
  if (!run) return { ...base2, disabledReason: ACTION_NOT_HERE_REASON, run: NOOP3 };
36562
36775
  return { ...base2, run };
36563
36776
  };
36777
+ const whenUnlocked = (action3) => locked && action3.disabledReason === void 0 ? { ...action3, disabledReason: VIEW_LOCKED_REASON, run: NOOP3 } : action3;
36778
+ const lockVerb = host.toggleLock ? [one({ id: "lock", label: locked ? "Unlock view" : "Lock view", icon: locked ? "lock-open" : "lock", group: "organize" }, host.toggleLock)] : [];
36564
36779
  return [
36565
- one({ id: "rename", label: "Rename view\u2026", icon: "pencil", group: "organize", verb: "rename" }, host.rename),
36780
+ whenUnlocked(one({ id: "rename", label: "Rename view\u2026", icon: "pencil", group: "organize", verb: "rename" }, host.rename)),
36566
36781
  one({ id: "duplicate", label: "Duplicate view", icon: "copy", group: "organize", verb: "duplicate" }, host.duplicate),
36567
- one(
36568
- { id: "archive", label: "Archive view", icon: "archive", group: "danger", destructive: true, verb: "archive" },
36569
- host.archive
36782
+ ...lockVerb,
36783
+ whenUnlocked(
36784
+ one({ id: "archive", label: "Archive view", icon: "archive", group: "danger", destructive: true, verb: "archive" }, host.archive)
36570
36785
  )
36571
36786
  ];
36572
36787
  }
@@ -36685,6 +36900,17 @@ function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews,
36685
36900
  setRenaming(null);
36686
36901
  await load();
36687
36902
  }
36903
+ async function toggleLock(view) {
36904
+ const presentation = { ...view.presentation ?? {} };
36905
+ if (presentation.locked === true) delete presentation.locked;
36906
+ else presentation.locked = true;
36907
+ const written = await saveViewPatch(client, tableId, view.id, { presentation });
36908
+ if (!written.ok) {
36909
+ setError(written.error);
36910
+ return;
36911
+ }
36912
+ await load();
36913
+ }
36688
36914
  async function duplicate(view) {
36689
36915
  const { id: _id, movedFrom: _moved, ...spec } = view;
36690
36916
  const written = await declareView(client, {
@@ -36840,8 +37066,9 @@ function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews,
36840
37066
  ) : viewActions(rights.admin ? null : rightsReason(rights, "structure") ?? ACTION_NO_ACCESS_REASON, {
36841
37067
  rename: () => setRenaming(current.name),
36842
37068
  duplicate: () => duplicate(current),
36843
- archive: () => archive(current)
36844
- }).map((action3) => /* @__PURE__ */ jsxs112(
37069
+ archive: () => archive(current),
37070
+ toggleLock: () => toggleLock(current)
37071
+ }, current.presentation?.locked === true).map((action3) => /* @__PURE__ */ jsxs112(
36845
37072
  "button",
36846
37073
  {
36847
37074
  type: "button",
@@ -49109,7 +49336,7 @@ function ViewFilter({ tableId, view, onApply, className }) {
49109
49336
  "This view asks: ",
49110
49337
  conditionInWords(saved, conditionFields),
49111
49338
  ". It cannot be drawn here; clear it to start a new filter."
49112
- ] }) : /* @__PURE__ */ jsx164(
49339
+ ] }) : /* @__PURE__ */ jsx164(FilterTermsContext.Provider, { value: true, children: /* @__PURE__ */ jsx164(
49113
49340
  ConditionGroup,
49114
49341
  {
49115
49342
  lead: "Show records where",
@@ -49118,7 +49345,7 @@ function ViewFilter({ tableId, view, onApply, className }) {
49118
49345
  fields: conditionFields,
49119
49346
  onChange: setDraft
49120
49347
  }
49121
- ),
49348
+ ) }),
49122
49349
  view.ruleId ? null : /* @__PURE__ */ jsxs153("div", { className: "flex items-center gap-1", children: [
49123
49350
  changed ? /* @__PURE__ */ jsx164(
49124
49351
  Button110,
@@ -49891,10 +50118,31 @@ function TablePage({
49891
50118
  setSurface((now) => ({ ...now, main: isLayout ? "records" : named }));
49892
50119
  }, [activeView]);
49893
50120
  const [viewSaveRefused, setViewSaveRefused] = useState140(null);
50121
+ const viewLocked = view?.presentation?.locked === true;
50122
+ const unlockView = useCallback69(async () => {
50123
+ const current = view;
50124
+ if (!current) return;
50125
+ const presentation = { ...current.presentation ?? {} };
50126
+ delete presentation.locked;
50127
+ const written = await saveViewPatch(client, tableId, current.id, { presentation });
50128
+ if (!written.ok) {
50129
+ recordsUi.notify?.error(refusalSentence(written.error));
50130
+ return;
50131
+ }
50132
+ setView({ ...current, presentation });
50133
+ setViewsEpoch((n) => n + 1);
50134
+ }, [client, tableId, view, recordsUi.notify]);
50135
+ const sayLocked = useCallback69(() => {
50136
+ recordsUi.notify?.success(VIEW_LOCKED_REASON, { label: "Unlock", run: () => void unlockView() });
50137
+ }, [recordsUi.notify, unlockView]);
49894
50138
  const patchView = useCallback69(
49895
50139
  async (asked2) => {
49896
50140
  const current = view;
49897
50141
  if (!current) return;
50142
+ if (viewLocked) {
50143
+ sayLocked();
50144
+ return;
50145
+ }
49898
50146
  const part = lookPart(asked2);
49899
50147
  if (Object.keys(part).length === 0) return;
49900
50148
  const before = looks?.[current.id];
@@ -49905,7 +50153,7 @@ function TablePage({
49905
50153
  if (kept.ok) return;
49906
50154
  setViewSaveRefused({ error: kept.error, patch: asked2, lookBefore: before });
49907
50155
  },
49908
- [client, tableId, view, looks]
50156
+ [client, tableId, view, looks, viewLocked, sayLocked]
49909
50157
  );
49910
50158
  const arrival = useRef62(viewAddress ?? null);
49911
50159
  const [gridQuery, setGridQuery] = useState140(null);
@@ -49978,6 +50226,10 @@ function TablePage({
49978
50226
  const current = view;
49979
50227
  const mine = current && looks ? looks[current.id] : void 0;
49980
50228
  if (!current || !mine) return;
50229
+ if (viewLocked) {
50230
+ sayLocked();
50231
+ return;
50232
+ }
49981
50233
  setLookRefused(null);
49982
50234
  const { patch, left } = saveToViewPlan(current, mine);
49983
50235
  if (Object.keys(viewPatchDocument(patch)).length > 0) {
@@ -50024,6 +50276,10 @@ function TablePage({
50024
50276
  const designate = async () => {
50025
50277
  const current = view;
50026
50278
  if (!current || !layoutShowing) return;
50279
+ if (viewLocked) {
50280
+ sayLocked();
50281
+ return;
50282
+ }
50027
50283
  setDesignateRefused(null);
50028
50284
  const written = await designateView(client, tableId, current.id, layoutShowing);
50029
50285
  if (!written.ok) {