@tensorix/metahub 0.4.0 → 0.4.1

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.
@@ -639,16 +639,25 @@ var updateProperty = grouped(function updateProperty2(db, id, fields) {
639
639
  if (fields.type !== undefined && !PROP_TYPES.has(fields.type))
640
640
  throw new MhError("invalid_input", `unknown property type: ${fields.type}`);
641
641
  const effectiveType = fields.type ?? cur.type;
642
- if (fields.config !== undefined)
643
- validateConfig(effectiveType, fields.config);
644
- else if (typeChanged)
645
- validateConfig(fields.type, undefined);
642
+ let mergedConfig;
643
+ if (fields.config !== undefined) {
644
+ const merged = { ...cur.config ?? {} };
645
+ for (const [k, v] of Object.entries(fields.config)) {
646
+ if (v === null)
647
+ delete merged[k];
648
+ else
649
+ merged[k] = v;
650
+ }
651
+ mergedConfig = merged;
652
+ validateConfig(effectiveType, mergedConfig);
653
+ } else if (typeChanged)
654
+ validateConfig(fields.type, cur.config ?? undefined);
646
655
  if (fields.name !== undefined)
647
656
  emit(db, "properties", id, "name", fields.name);
648
657
  if (fields.type !== undefined)
649
658
  emit(db, "properties", id, "type", fields.type);
650
- if (fields.config !== undefined)
651
- emit(db, "properties", id, "config", fields.config);
659
+ if (mergedConfig !== undefined)
660
+ emit(db, "properties", id, "config", mergedConfig);
652
661
  if (fields.position !== undefined)
653
662
  emit(db, "properties", id, "position", fields.position);
654
663
  if (typeChanged) {
@@ -658,6 +667,75 @@ var updateProperty = grouped(function updateProperty2(db, id, fields) {
658
667
  }
659
668
  return getProperty(db, id);
660
669
  });
670
+ function getSelectProperty(db, id) {
671
+ const prop = getProperty(db, id);
672
+ if (!prop)
673
+ throw new MhError("not_found", `no such property: ${id}`);
674
+ if (prop.type !== "select" && prop.type !== "multi_select")
675
+ throw new MhError("invalid_input", `${prop.type} property has no options`);
676
+ return { prop, options: prop.config?.options ?? [] };
677
+ }
678
+ var renameSelectOption = grouped(function renameSelectOption2(db, id, from, to) {
679
+ const { prop, options } = getSelectProperty(db, id);
680
+ to = to.trim();
681
+ if (!to)
682
+ throw new MhError("invalid_input", "option name must not be empty");
683
+ if (!options.includes(from))
684
+ throw new MhError("not_found", `no such option: ${from}`);
685
+ if (to === from)
686
+ return { property: prop, renamed: 0 };
687
+ if (options.includes(to))
688
+ throw new MhError("conflict", `option already exists: ${to}`);
689
+ emit(db, "properties", id, "config", {
690
+ ...prop.config ?? {},
691
+ options: options.map((o) => o === from ? to : o)
692
+ });
693
+ let renamed = 0;
694
+ if (prop.type === "select") {
695
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? = ?").all(prop.database_id, id, from);
696
+ for (const r of rows)
697
+ emit(db, "records", r.id, id, to);
698
+ renamed = rows.length;
699
+ } else {
700
+ const rows = db.query("SELECT id, data ->> ? AS v FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(id, prop.database_id, id);
701
+ for (const r of rows) {
702
+ const vals = JSON.parse(r.v);
703
+ if (!Array.isArray(vals) || !vals.includes(from))
704
+ continue;
705
+ emit(db, "records", r.id, id, [...new Set(vals.map((v) => v === from ? to : v))]);
706
+ renamed++;
707
+ }
708
+ }
709
+ return { property: getProperty(db, id), renamed };
710
+ });
711
+ var removeSelectOption = grouped(function removeSelectOption2(db, id, name) {
712
+ const { prop, options } = getSelectProperty(db, id);
713
+ if (!options.includes(name))
714
+ throw new MhError("not_found", `no such option: ${name}`);
715
+ if (options.length === 1)
716
+ throw new MhError("invalid_input", "cannot remove the last option; delete the property instead");
717
+ emit(db, "properties", id, "config", {
718
+ ...prop.config ?? {},
719
+ options: options.filter((o) => o !== name)
720
+ });
721
+ let cleared = 0;
722
+ if (prop.type === "select") {
723
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? = ?").all(prop.database_id, id, name);
724
+ for (const r of rows)
725
+ emit(db, "records", r.id, id, null);
726
+ cleared = rows.length;
727
+ } else {
728
+ const rows = db.query("SELECT id, data ->> ? AS v FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(id, prop.database_id, id);
729
+ for (const r of rows) {
730
+ const vals = JSON.parse(r.v);
731
+ if (!Array.isArray(vals) || !vals.includes(name))
732
+ continue;
733
+ emit(db, "records", r.id, id, vals.filter((v) => v !== name));
734
+ cleared++;
735
+ }
736
+ }
737
+ return { property: getProperty(db, id), cleared };
738
+ });
661
739
  var removeProperty = grouped(function removeProperty2(db, id) {
662
740
  const prop = getProperty(db, id);
663
741
  if (!prop)
package/dist/index.js CHANGED
@@ -953,7 +953,15 @@ function setPropertyWidth(db, id, width) {
953
953
  const w = Math.max(80, Math.min(2000, Math.round(width)));
954
954
  return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w } });
955
955
  }
956
- var PROP_TYPES, PROPERTY_COLS, PROPERTY_SELECT, addProperty, updateProperty, removeProperty;
956
+ function getSelectProperty(db, id) {
957
+ const prop = getProperty(db, id);
958
+ if (!prop)
959
+ throw new MhError("not_found", `no such property: ${id}`);
960
+ if (prop.type !== "select" && prop.type !== "multi_select")
961
+ throw new MhError("invalid_input", `${prop.type} property has no options`);
962
+ return { prop, options: prop.config?.options ?? [] };
963
+ }
964
+ var PROP_TYPES, PROPERTY_COLS, PROPERTY_SELECT, addProperty, updateProperty, renameSelectOption, removeSelectOption, removeProperty;
957
965
  var init_properties = __esm(() => {
958
966
  init_ids();
959
967
  init_crdt();
@@ -996,16 +1004,25 @@ var init_properties = __esm(() => {
996
1004
  if (fields.type !== undefined && !PROP_TYPES.has(fields.type))
997
1005
  throw new MhError("invalid_input", `unknown property type: ${fields.type}`);
998
1006
  const effectiveType = fields.type ?? cur.type;
999
- if (fields.config !== undefined)
1000
- validateConfig(effectiveType, fields.config);
1001
- else if (typeChanged)
1002
- validateConfig(fields.type, undefined);
1007
+ let mergedConfig;
1008
+ if (fields.config !== undefined) {
1009
+ const merged = { ...cur.config ?? {} };
1010
+ for (const [k, v] of Object.entries(fields.config)) {
1011
+ if (v === null)
1012
+ delete merged[k];
1013
+ else
1014
+ merged[k] = v;
1015
+ }
1016
+ mergedConfig = merged;
1017
+ validateConfig(effectiveType, mergedConfig);
1018
+ } else if (typeChanged)
1019
+ validateConfig(fields.type, cur.config ?? undefined);
1003
1020
  if (fields.name !== undefined)
1004
1021
  emit(db, "properties", id, "name", fields.name);
1005
1022
  if (fields.type !== undefined)
1006
1023
  emit(db, "properties", id, "type", fields.type);
1007
- if (fields.config !== undefined)
1008
- emit(db, "properties", id, "config", fields.config);
1024
+ if (mergedConfig !== undefined)
1025
+ emit(db, "properties", id, "config", mergedConfig);
1009
1026
  if (fields.position !== undefined)
1010
1027
  emit(db, "properties", id, "position", fields.position);
1011
1028
  if (typeChanged) {
@@ -1015,6 +1032,67 @@ var init_properties = __esm(() => {
1015
1032
  }
1016
1033
  return getProperty(db, id);
1017
1034
  });
1035
+ renameSelectOption = grouped(function renameSelectOption2(db, id, from, to) {
1036
+ const { prop, options } = getSelectProperty(db, id);
1037
+ to = to.trim();
1038
+ if (!to)
1039
+ throw new MhError("invalid_input", "option name must not be empty");
1040
+ if (!options.includes(from))
1041
+ throw new MhError("not_found", `no such option: ${from}`);
1042
+ if (to === from)
1043
+ return { property: prop, renamed: 0 };
1044
+ if (options.includes(to))
1045
+ throw new MhError("conflict", `option already exists: ${to}`);
1046
+ emit(db, "properties", id, "config", {
1047
+ ...prop.config ?? {},
1048
+ options: options.map((o) => o === from ? to : o)
1049
+ });
1050
+ let renamed = 0;
1051
+ if (prop.type === "select") {
1052
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? = ?").all(prop.database_id, id, from);
1053
+ for (const r of rows)
1054
+ emit(db, "records", r.id, id, to);
1055
+ renamed = rows.length;
1056
+ } else {
1057
+ const rows = db.query("SELECT id, data ->> ? AS v FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(id, prop.database_id, id);
1058
+ for (const r of rows) {
1059
+ const vals = JSON.parse(r.v);
1060
+ if (!Array.isArray(vals) || !vals.includes(from))
1061
+ continue;
1062
+ emit(db, "records", r.id, id, [...new Set(vals.map((v) => v === from ? to : v))]);
1063
+ renamed++;
1064
+ }
1065
+ }
1066
+ return { property: getProperty(db, id), renamed };
1067
+ });
1068
+ removeSelectOption = grouped(function removeSelectOption2(db, id, name) {
1069
+ const { prop, options } = getSelectProperty(db, id);
1070
+ if (!options.includes(name))
1071
+ throw new MhError("not_found", `no such option: ${name}`);
1072
+ if (options.length === 1)
1073
+ throw new MhError("invalid_input", "cannot remove the last option; delete the property instead");
1074
+ emit(db, "properties", id, "config", {
1075
+ ...prop.config ?? {},
1076
+ options: options.filter((o) => o !== name)
1077
+ });
1078
+ let cleared = 0;
1079
+ if (prop.type === "select") {
1080
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? = ?").all(prop.database_id, id, name);
1081
+ for (const r of rows)
1082
+ emit(db, "records", r.id, id, null);
1083
+ cleared = rows.length;
1084
+ } else {
1085
+ const rows = db.query("SELECT id, data ->> ? AS v FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(id, prop.database_id, id);
1086
+ for (const r of rows) {
1087
+ const vals = JSON.parse(r.v);
1088
+ if (!Array.isArray(vals) || !vals.includes(name))
1089
+ continue;
1090
+ emit(db, "records", r.id, id, vals.filter((v) => v !== name));
1091
+ cleared++;
1092
+ }
1093
+ }
1094
+ return { property: getProperty(db, id), cleared };
1095
+ });
1018
1096
  removeProperty = grouped(function removeProperty2(db, id) {
1019
1097
  const prop = getProperty(db, id);
1020
1098
  if (!prop)
@@ -18488,7 +18566,7 @@ var package_default;
18488
18566
  var init_package = __esm(() => {
18489
18567
  package_default = {
18490
18568
  name: "@tensorix/metahub",
18491
- version: "0.4.0",
18569
+ version: "0.4.1",
18492
18570
  description: "Metahub core library and CLI",
18493
18571
  license: "AGPL-3.0-only",
18494
18572
  type: "module",
@@ -18528,6 +18606,7 @@ var init_package = __esm(() => {
18528
18606
  "@codemirror/commands": "6.10.4",
18529
18607
  "@codemirror/state": "6.7.0",
18530
18608
  "@codemirror/view": "6.43.4",
18609
+ "@joplin/turndown-plugin-gfm": "^1.0.67",
18531
18610
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
18532
18611
  aws4fetch: "^1.0.20",
18533
18612
  citty: "^0.1.6",
@@ -18536,7 +18615,6 @@ var init_package = __esm(() => {
18536
18615
  preact: "^10.29.2",
18537
18616
  "qrcode-generator": "^2.0.4",
18538
18617
  turndown: "^7.2.4",
18539
- "turndown-plugin-gfm": "^1.0.2",
18540
18618
  zod: "^4.4.3"
18541
18619
  },
18542
18620
  devDependencies: {
@@ -26896,15 +26974,57 @@ var styles_default = ` :root {
26896
26974
  .pop .typegrid .item .lico { margin:0 auto; }
26897
26975
  .pop input.field { width:100%; border:1px solid var(--line-strong); border-radius:var(--radius-sm); padding:7px 9px; outline:none; background:var(--surface); margin:2px; }
26898
26976
  .pop input.field:focus { border-color:var(--accent); }
26977
+ /* select-cell picker: search/create input at the top of the popover */
26978
+ .pop .selsearch { display:flex; align-items:center; gap:7px; margin:2px 2px 6px; padding:6px 9px;
26979
+ border:1px solid var(--line-strong); border-radius:var(--radius-sm); background:var(--surface); color:var(--muted); }
26980
+ .pop .selsearch:focus-within { border-color:var(--accent); }
26981
+ .pop .selsearch input { flex:1; width:0; min-width:0; border:0; background:transparent; outline:none;
26982
+ font:inherit; font-size:var(--fs-ui); color:var(--fg); }
26983
+ .pop .selsearch input::placeholder { color:var(--muted); }
26899
26984
  .optrow { display:flex; align-items:center; gap:6px; padding:3px 4px; }
26900
26985
  .optrow .chip { flex:1; }
26901
- .optrow .x { width:22px; height:22px; display:grid; place-items:center; color:var(--muted); border-radius:5px; }
26902
- .optrow .x:hover { background:var(--hover-2); color:var(--danger); }
26986
+ .optrow .grip { width:18px; height:22px; display:grid; place-items:center; color:var(--muted); cursor:grab; touch-action:none; flex:none; }
26987
+ .optrow .grip.dim { color:color-mix(in srgb,var(--muted) 40%,transparent); cursor:default; }
26988
+ .optrow .optlabel { flex:1; display:flex; min-width:0; text-align:left; cursor:pointer; }
26989
+ .optrow .x { width:22px; height:22px; display:grid; place-items:center; color:var(--muted); border-radius:5px; flex:none; }
26990
+ .optrow .x:hover { background:var(--hover-2); color:var(--fg); }
26991
+ .optrow .x.del:hover { color:var(--danger); }
26992
+ .optrow .x.ok:hover { color:var(--success); }
26993
+ /* edit-state buttons fade in alongside the pill's ring */
26994
+ .optrow .x.ok, .optrow .x.cancel { animation:optbtn-in .15s ease; }
26995
+ @keyframes optbtn-in { from { opacity:0; } }
26996
+ .optrow.drop-before { box-shadow:inset 0 2px 0 0 var(--accent); }
26997
+ .optrow.drop-after { box-shadow:inset 0 -2px 0 0 var(--accent); }
26998
+ /* In-place pill editor: mirrors .chip metrics exactly (padding/radius/type)
26999
+ so entering edit never changes the row's size \u2014 only a color-matched ring
27000
+ appears. Caret and selection reuse the option's own hue. */
27001
+ /* width:0 kills the input's intrinsic width (browser default ~20ch) so the
27002
+ popover doesn't widen when the row enters edit \u2014 flex:1 does the sizing.
27003
+ The entry animation only fades the ring in: the pill background must stay
27004
+ put or the swap from the chip reads as a flash. */
27005
+ .optrow input.optedit { flex:1; width:0; min-width:0; margin:0; border:0; font:inherit; font-size:var(--fs-sm); font-weight:500;
27006
+ padding:2px 8px; border-radius:5px; background:color-mix(in srgb,var(--c) 15%,transparent); color:var(--c);
27007
+ caret-color:var(--c); outline:1.5px solid color-mix(in srgb,var(--c) 50%,transparent); outline-offset:0;
27008
+ animation:optedit-in .18s ease; }
27009
+ @keyframes optedit-in { from { outline-color:transparent; } }
27010
+ .optrow input.optedit::selection { background:color-mix(in srgb,var(--c) 28%,transparent); }
27011
+ :root[data-resolved="dark"] .optrow input.optedit { background:color-mix(in srgb,var(--c) 22%,transparent);
27012
+ color:color-mix(in srgb,var(--c) 75%,#fff); caret-color:color-mix(in srgb,var(--c) 75%,#fff); }
27013
+ /* Ghost add-row: quiet until it's needed \u2014 plus icon in the grip slot, a
27014
+ bare borderless input; hover/focus tints the row like a menu item. */
27015
+ .optrow.optadd { border-radius:var(--radius-sm); }
27016
+ .optrow.optadd:hover, .optrow.optadd:focus-within { background:var(--hover-2); }
27017
+ .optrow.optadd input { flex:1; width:0; min-width:0; margin:0; border:0; background:transparent; font:inherit;
27018
+ font-size:var(--fs-sm); padding:2px 8px; color:var(--fg); outline:none; }
27019
+ .optrow.optadd input::placeholder { color:var(--muted); }
26903
27020
 
26904
27021
  /* modal */
26905
27022
  .modal-scrim { position:fixed; inset:0; background:rgba(15,15,25,.4); z-index:100; display:flex; align-items:center; justify-content:center;
26906
27023
  opacity:0; pointer-events:none; transition:opacity .15s; padding:20px; }
26907
27024
  .modal-scrim.open { opacity:1; pointer-events:auto; }
27025
+ /* opt-in (openModal aboveMenus): stack one dialog over the menu layer (115)
27026
+ so a confirm can appear without dismissing an open popover */
27027
+ .modal-scrim.above-menus { z-index:130; }
26908
27028
  .modal { background:var(--bg); border-radius:var(--radius-xl); box-shadow:var(--shadow-lg); width:440px; max-width:100%; max-height:100%; display:flex; flex-direction:column; overflow:hidden; }
26909
27029
  .modal-head { padding:20px 22px 0; }
26910
27030
  .modal-head h3 { margin:0; font-size:18px; font-weight:700; letter-spacing:-.01em; }
@@ -31024,6 +31144,8 @@ var UpdatePropertyReq = exports_external.object({
31024
31144
  position: exports_external.number().optional()
31025
31145
  });
31026
31146
  var SetWidthReq = exports_external.object({ width: exports_external.number() });
31147
+ var RenameOptionReq = exports_external.object({ from: exports_external.string(), to: exports_external.string() });
31148
+ var RemoveOptionReq = exports_external.object({ name: exports_external.string() });
31027
31149
  var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any()).describe("Flat { column: value } cell patch (keyed by property name).");
31028
31150
  var CreateDocumentReq = exports_external.object({
31029
31151
  title: exports_external.string(),
@@ -31335,7 +31457,7 @@ var webuiRoutes = [
31335
31457
  {
31336
31458
  method: "PATCH",
31337
31459
  path: "/api/property",
31338
- summary: "Update a property: rename, change type, edit config/options, reorder. Query: ?id=<id>",
31460
+ summary: "Update a property: rename, change type, edit config, reorder. `config` is merged key-wise into the current config (set a key to null to remove it). Query: ?id=<id>",
31339
31461
  request: UpdatePropertyReq,
31340
31462
  response: PropertySchema,
31341
31463
  handler: handle3(async (req, { db: db2 }) => {
@@ -31343,6 +31465,28 @@ var webuiRoutes = [
31343
31465
  return updateProperty(db2, need3(req, "id"), body);
31344
31466
  })
31345
31467
  },
31468
+ {
31469
+ method: "POST",
31470
+ path: "/api/property/option/rename",
31471
+ summary: "Rename a select/multi_select option and rewrite every cell using the old name. Query: ?id=<id>",
31472
+ request: RenameOptionReq,
31473
+ response: exports_external.object({ property: PropertySchema, renamed: exports_external.number() }),
31474
+ handler: handle3(async (req, { db: db2 }) => {
31475
+ const body = await req.json();
31476
+ return renameSelectOption(db2, need3(req, "id"), body.from, body.to);
31477
+ })
31478
+ },
31479
+ {
31480
+ method: "POST",
31481
+ path: "/api/property/option/remove",
31482
+ summary: "Remove a select/multi_select option and clear it from every cell using it. Query: ?id=<id>",
31483
+ request: RemoveOptionReq,
31484
+ response: exports_external.object({ property: PropertySchema, cleared: exports_external.number() }),
31485
+ handler: handle3(async (req, { db: db2 }) => {
31486
+ const body = await req.json();
31487
+ return removeSelectOption(db2, need3(req, "id"), body.name);
31488
+ })
31489
+ },
31346
31490
  {
31347
31491
  method: "PATCH",
31348
31492
  path: "/api/property/width",
@@ -31730,6 +31874,8 @@ export {
31730
31874
  resolveEntity,
31731
31875
  resolveData,
31732
31876
  resolveCandidates,
31877
+ renameSelectOption,
31878
+ removeSelectOption,
31733
31879
  removeProperty,
31734
31880
  reconcile,
31735
31881
  rebuildSearchIndex,
@@ -31868,4 +32014,4 @@ export {
31868
32014
  BLOB_HASH_HEX
31869
32015
  };
31870
32016
 
31871
- //# debugId=DA3A2F257B2F30C464756E2164756E21
32017
+ //# debugId=9052D340AA681E6D64756E2164756E21