@abraca/dabra 2.7.0 → 2.8.0
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/abracadabra-provider.cjs +41 -4
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +41 -5
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +25 -2
- package/package.json +2 -2
- package/src/DocUtils.ts +62 -3
- package/src/index.ts +1 -0
|
@@ -15715,6 +15715,19 @@ function makeEntryMap(fields) {
|
|
|
15715
15715
|
return m;
|
|
15716
15716
|
}
|
|
15717
15717
|
/**
|
|
15718
|
+
* A label is a "placeholder" — i.e. carries no real user title — when it is
|
|
15719
|
+
* empty/whitespace, null/undefined, or the literal `"Untitled"` sentinel
|
|
15720
|
+
* (case-insensitive, so `"untitled"` from a `labelToFilename` round-trip is
|
|
15721
|
+
* caught too). The whole tree-label corruption class boils down to a
|
|
15722
|
+
* placeholder being allowed to overwrite a real title; `patchEntry` refuses
|
|
15723
|
+
* exactly that. Mirrors cou-sh's `isEmptyTreeLabel`.
|
|
15724
|
+
*/
|
|
15725
|
+
function isPlaceholderLabel(label) {
|
|
15726
|
+
if (typeof label !== "string") return label == null;
|
|
15727
|
+
const t = label.trim();
|
|
15728
|
+
return t === "" || t.toLowerCase() === "untitled";
|
|
15729
|
+
}
|
|
15730
|
+
/**
|
|
15718
15731
|
* Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
|
|
15719
15732
|
* concurrent edit to a *different* field by a peer is preserved instead
|
|
15720
15733
|
* of being clobbered by a whole-entry write — the whole-entry-LWW fix
|
|
@@ -15729,21 +15742,44 @@ function makeEntryMap(fields) {
|
|
|
15729
15742
|
* Self-transacting: it batches its writes in one `Y.Doc` transaction
|
|
15730
15743
|
* (a safe reentrant no-op join when already inside one), so callers
|
|
15731
15744
|
* don't need to pass or own a transaction.
|
|
15745
|
+
*
|
|
15746
|
+
* ── NO-DESTROY LABEL INVARIANT ──────────────────────────────────────────
|
|
15747
|
+
* A `label` patch that is a placeholder (empty/whitespace/"Untitled") is
|
|
15748
|
+
* DROPPED when the entry already holds a real (non-placeholder) label —
|
|
15749
|
+
* regardless of which consumer (cou-sh title-sync, fs-sync rename
|
|
15750
|
+
* detection, MCP, table renderers, a stale snapshot) tried it. This is the
|
|
15751
|
+
* source-of-truth guard against the "card title silently becomes Untitled
|
|
15752
|
+
* / files renamed to untitled.md" corruption: a placeholder must never win
|
|
15753
|
+
* over a real title. Creating a brand-new entry with an empty label (e.g.
|
|
15754
|
+
* a fresh kanban card) is still allowed — the guard only fires when a real
|
|
15755
|
+
* label already exists. Pass `{ allowLabelClear: true }` to override (the
|
|
15756
|
+
* single legitimate "user explicitly cleared it" path).
|
|
15732
15757
|
*/
|
|
15733
|
-
function patchEntry(treeMap, id, patch, removeKeys = []) {
|
|
15758
|
+
function patchEntry(treeMap, id, patch, removeKeys = [], opts = {}) {
|
|
15734
15759
|
const apply = () => {
|
|
15735
15760
|
const raw = treeMap.get(id);
|
|
15761
|
+
let effectivePatch = patch;
|
|
15762
|
+
if (!opts.allowLabelClear && Object.hasOwn(patch, "label") && isPlaceholderLabel(patch.label)) {
|
|
15763
|
+
let existingLabel;
|
|
15764
|
+
if (raw != null && typeof raw.get === "function") existingLabel = raw.get("label");
|
|
15765
|
+
else if (raw != null && typeof raw.toJSON === "function") existingLabel = raw.toJSON()?.label;
|
|
15766
|
+
else if (raw != null && typeof raw === "object") existingLabel = raw.label;
|
|
15767
|
+
if (!isPlaceholderLabel(existingLabel)) {
|
|
15768
|
+
const { label: _dropped, ...rest } = patch;
|
|
15769
|
+
effectivePatch = rest;
|
|
15770
|
+
}
|
|
15771
|
+
}
|
|
15736
15772
|
if (raw instanceof yjs.Map) {
|
|
15737
|
-
for (const [k, v] of Object.entries(
|
|
15773
|
+
for (const [k, v] of Object.entries(effectivePatch)) if (v === void 0) raw.delete(k);
|
|
15738
15774
|
else raw.set(k, v);
|
|
15739
15775
|
for (const k of removeKeys) raw.delete(k);
|
|
15740
15776
|
return;
|
|
15741
15777
|
}
|
|
15742
15778
|
const merged = {
|
|
15743
15779
|
...raw == null ? {} : toPlain(raw),
|
|
15744
|
-
...
|
|
15780
|
+
...effectivePatch
|
|
15745
15781
|
};
|
|
15746
|
-
for (const [k, v] of Object.entries(
|
|
15782
|
+
for (const [k, v] of Object.entries(effectivePatch)) if (v === void 0) delete merged[k];
|
|
15747
15783
|
for (const k of removeKeys) delete merged[k];
|
|
15748
15784
|
treeMap.set(id, makeEntryMap(merged));
|
|
15749
15785
|
};
|
|
@@ -20900,6 +20936,7 @@ exports.filenameToLabel = filenameToLabel;
|
|
|
20900
20936
|
exports.foldRecords = foldRecords;
|
|
20901
20937
|
exports.generateMnemonic = generateMnemonic;
|
|
20902
20938
|
exports.isEncryptedContent = isEncryptedContent;
|
|
20939
|
+
exports.isPlaceholderLabel = isPlaceholderLabel;
|
|
20903
20940
|
exports.makeEncryptedYMap = makeEncryptedYMap;
|
|
20904
20941
|
exports.makeEncryptedYText = makeEncryptedYText;
|
|
20905
20942
|
exports.makeEntryMap = makeEntryMap;
|