@proveanything/smartlinks-utils-ui 0.7.9 → 0.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/{chunk-B34MMOND.js → chunk-F2YH3NVP.js} +3 -3
- package/dist/{chunk-B34MMOND.js.map → chunk-F2YH3NVP.js.map} +1 -1
- package/dist/components/AssetPicker/index.js +1 -1
- package/dist/components/ConditionsEditor/index.js +1 -1
- package/dist/components/FontPicker/index.js +1 -1
- package/dist/components/IconPicker/index.js +1 -1
- package/dist/components/RecordsAdmin/index.d.ts +13 -1
- package/dist/components/RecordsAdmin/index.js +2286 -1340
- package/dist/components/RecordsAdmin/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { styleInject } from '../../chunk-
|
|
1
|
+
import { styleInject } from '../../chunk-F2YH3NVP.js';
|
|
2
2
|
import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
|
|
3
3
|
import { useFacets } from '../../chunk-4LHF5JB7.js';
|
|
4
4
|
import { cn } from '../../chunk-L7FQ52F5.js';
|
|
5
|
-
import {
|
|
5
|
+
import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KFKVGUUP.js';
|
|
6
6
|
export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KFKVGUUP.js';
|
|
7
|
-
import { createContext,
|
|
7
|
+
import { createContext, useState, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, createElement, useId } from 'react';
|
|
8
8
|
import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Rows3, List, ChevronRight, Eraser, ClipboardPaste, Box, X, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, Search, CornerDownLeft, Circle, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Target, Settings2 } from 'lucide-react';
|
|
9
|
-
import { useQueryClient, useInfiniteQuery
|
|
9
|
+
import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
|
|
10
10
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
11
11
|
import { createPortal } from 'react-dom';
|
|
12
12
|
|
|
@@ -223,155 +223,6 @@ var resolutionChain = (target, supportedScopes) => {
|
|
|
223
223
|
}
|
|
224
224
|
return Array.from(new Set(chain));
|
|
225
225
|
};
|
|
226
|
-
var defaultClassify = (r) => {
|
|
227
|
-
if (!r.data) return "empty";
|
|
228
|
-
const keys = Object.keys(r.data);
|
|
229
|
-
if (keys.length === 0) return "empty";
|
|
230
|
-
return "configured";
|
|
231
|
-
};
|
|
232
|
-
var matchesScope = (kind, rec, _p) => {
|
|
233
|
-
const hasRule = !!rec.facetRule;
|
|
234
|
-
if (kind === "rule") return hasRule;
|
|
235
|
-
if (hasRule) return false;
|
|
236
|
-
const productId = rec.productId ?? void 0;
|
|
237
|
-
const variantId = rec.variantId ?? void 0;
|
|
238
|
-
const batchId = rec.batchId ?? void 0;
|
|
239
|
-
const proofId = rec.proofId ?? void 0;
|
|
240
|
-
if (kind === "collection") {
|
|
241
|
-
return !productId && !variantId && !batchId && !proofId;
|
|
242
|
-
}
|
|
243
|
-
if (kind === "product") return !!productId && !variantId && !batchId;
|
|
244
|
-
if (kind === "variant") return !!variantId;
|
|
245
|
-
if (kind === "batch") return !!batchId;
|
|
246
|
-
return false;
|
|
247
|
-
};
|
|
248
|
-
var matchesContext = (p, ctx) => {
|
|
249
|
-
if (!ctx) return true;
|
|
250
|
-
if (ctx.productId && p.productId !== ctx.productId) return false;
|
|
251
|
-
if (ctx.variantId && p.variantId !== ctx.variantId) return false;
|
|
252
|
-
if (ctx.batchId && p.batchId !== ctx.batchId) return false;
|
|
253
|
-
return true;
|
|
254
|
-
};
|
|
255
|
-
var toSummary = (rec) => {
|
|
256
|
-
const ref = rec.ref ?? "";
|
|
257
|
-
const productId = rec.productId ?? void 0;
|
|
258
|
-
const variantId = rec.variantId ?? void 0;
|
|
259
|
-
const batchId = rec.batchId ?? void 0;
|
|
260
|
-
const proofId = rec.proofId ?? void 0;
|
|
261
|
-
const scope = {
|
|
262
|
-
kind: batchId ? "batch" : variantId ? "variant" : productId ? "product" : "collection",
|
|
263
|
-
raw: ref,
|
|
264
|
-
productId,
|
|
265
|
-
variantId,
|
|
266
|
-
batchId,
|
|
267
|
-
proofId
|
|
268
|
-
};
|
|
269
|
-
const facetRule = rec.facetRule ?? null;
|
|
270
|
-
if (facetRule) scope.kind = "rule";
|
|
271
|
-
const ruleLabel = facetRule && facetRule.all && facetRule.all.length > 0 ? facetRule.all.length === 1 ? "Rule \xB7 1 facet" : `Rule \xB7 ${facetRule.all.length} facets` : null;
|
|
272
|
-
const fallbackLabel = scope.batchId ?? scope.variantId ?? scope.productId ?? ruleLabel ?? (ref || "Global record");
|
|
273
|
-
return {
|
|
274
|
-
id: rec.id,
|
|
275
|
-
ref,
|
|
276
|
-
scope,
|
|
277
|
-
data: rec.data ?? null,
|
|
278
|
-
status: "configured",
|
|
279
|
-
label: fallbackLabel,
|
|
280
|
-
updatedAt: rec.updatedAt,
|
|
281
|
-
facetRule
|
|
282
|
-
};
|
|
283
|
-
};
|
|
284
|
-
var QK_BASE = ["records-admin", "list"];
|
|
285
|
-
var useRecordList = (args) => {
|
|
286
|
-
const {
|
|
287
|
-
ctx,
|
|
288
|
-
scopeKind,
|
|
289
|
-
search = "",
|
|
290
|
-
filter = "all",
|
|
291
|
-
classify: classify2,
|
|
292
|
-
enabled = true,
|
|
293
|
-
scaffolder,
|
|
294
|
-
contextScope,
|
|
295
|
-
pageSize = 100
|
|
296
|
-
} = args;
|
|
297
|
-
const queryClient = useQueryClient();
|
|
298
|
-
const queryKey = useMemo(
|
|
299
|
-
() => [
|
|
300
|
-
...QK_BASE,
|
|
301
|
-
ctx.collectionId,
|
|
302
|
-
ctx.appId,
|
|
303
|
-
ctx.recordType,
|
|
304
|
-
scopeKind,
|
|
305
|
-
contextScope?.productId ?? null,
|
|
306
|
-
contextScope?.variantId ?? null,
|
|
307
|
-
contextScope?.batchId ?? null
|
|
308
|
-
],
|
|
309
|
-
[ctx.collectionId, ctx.appId, ctx.recordType, scopeKind, contextScope?.productId, contextScope?.variantId, contextScope?.batchId]
|
|
310
|
-
);
|
|
311
|
-
const query = useInfiniteQuery({
|
|
312
|
-
queryKey,
|
|
313
|
-
enabled,
|
|
314
|
-
initialPageParam: 0,
|
|
315
|
-
queryFn: async ({ pageParam }) => {
|
|
316
|
-
const offset = pageParam;
|
|
317
|
-
const { data, total: total2, hasMore } = await listRecords(ctx, { limit: pageSize, offset });
|
|
318
|
-
return { data, total: total2, hasMore, nextOffset: offset + data.length };
|
|
319
|
-
},
|
|
320
|
-
getNextPageParam: (last) => last.hasMore ? last.nextOffset : void 0,
|
|
321
|
-
staleTime: 3e4
|
|
322
|
-
});
|
|
323
|
-
const [scaffolded, setScaffolded] = useState(null);
|
|
324
|
-
const rawItems = useMemo(() => {
|
|
325
|
-
const all = query.data?.pages.flatMap((p) => p.data) ?? [];
|
|
326
|
-
const cls = classify2 ?? defaultClassify;
|
|
327
|
-
return all.map((rec) => ({ rec, summary: toSummary(rec) })).filter(({ rec, summary }) => matchesScope(scopeKind, rec, summary.scope)).filter(({ summary }) => matchesContext(summary.scope, contextScope)).map(({ summary }) => ({ ...summary, status: cls(summary) }));
|
|
328
|
-
}, [query.data, scopeKind, classify2, contextScope]);
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
if (!scaffolder) {
|
|
331
|
-
setScaffolded(null);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
let cancelled = false;
|
|
335
|
-
Promise.resolve(scaffolder(rawItems)).then((next) => {
|
|
336
|
-
if (!cancelled) setScaffolded(next);
|
|
337
|
-
});
|
|
338
|
-
return () => {
|
|
339
|
-
cancelled = true;
|
|
340
|
-
};
|
|
341
|
-
}, [rawItems, scaffolder]);
|
|
342
|
-
const items = scaffolded ?? rawItems;
|
|
343
|
-
const filtered = useMemo(() => {
|
|
344
|
-
let out = items;
|
|
345
|
-
if (filter !== "all") out = out.filter((r) => r.status === filter);
|
|
346
|
-
if (search.trim()) {
|
|
347
|
-
const q = search.trim().toLowerCase();
|
|
348
|
-
out = out.filter((r) => `${r.label} ${r.subtitle ?? ""} ${r.ref}`.toLowerCase().includes(q));
|
|
349
|
-
}
|
|
350
|
-
return out;
|
|
351
|
-
}, [items, filter, search]);
|
|
352
|
-
const counts = useMemo(() => ({
|
|
353
|
-
all: items.length,
|
|
354
|
-
configured: items.filter((r) => r.status === "configured").length,
|
|
355
|
-
partial: items.filter((r) => r.status === "partial").length,
|
|
356
|
-
empty: items.filter((r) => r.status === "empty").length
|
|
357
|
-
}), [items]);
|
|
358
|
-
const refetch = useCallback(() => {
|
|
359
|
-
queryClient.invalidateQueries({ queryKey: [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType] });
|
|
360
|
-
}, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
|
|
361
|
-
const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
|
|
362
|
-
return {
|
|
363
|
-
allItems: items,
|
|
364
|
-
items: filtered,
|
|
365
|
-
total,
|
|
366
|
-
counts,
|
|
367
|
-
isLoading: query.isLoading,
|
|
368
|
-
error: query.error ?? null,
|
|
369
|
-
refetch,
|
|
370
|
-
hasNextPage: query.hasNextPage,
|
|
371
|
-
isFetchingNextPage: query.isFetchingNextPage,
|
|
372
|
-
fetchNextPage: query.fetchNextPage
|
|
373
|
-
};
|
|
374
|
-
};
|
|
375
226
|
|
|
376
227
|
// src/components/RecordsAdmin/data/resolveRecord.ts
|
|
377
228
|
var resolveRecord = async (args) => {
|
|
@@ -525,591 +376,303 @@ function useResolvedRecord(args) {
|
|
|
525
376
|
error: query.error ?? null
|
|
526
377
|
};
|
|
527
378
|
}
|
|
528
|
-
var
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
appId,
|
|
539
|
-
rule,
|
|
540
|
-
limit = 20,
|
|
541
|
-
debounceMs = 350,
|
|
542
|
-
enabled = true
|
|
543
|
-
} = args;
|
|
544
|
-
const [debouncedRule, setDebouncedRule] = useState(rule);
|
|
545
|
-
const timer = useRef(null);
|
|
379
|
+
var RT_KEY = (recordType) => recordType ?? "_default";
|
|
380
|
+
var lsKey = (appId, recordType) => `ra:intro:${appId}:${RT_KEY(recordType)}`;
|
|
381
|
+
var useIntroDismissed = (SL, collectionId, appId, recordType) => {
|
|
382
|
+
const [dismissed, setDismissed] = useState(() => {
|
|
383
|
+
try {
|
|
384
|
+
return localStorage.getItem(lsKey(appId, recordType)) === "1";
|
|
385
|
+
} catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
546
389
|
useEffect(() => {
|
|
547
|
-
|
|
548
|
-
|
|
390
|
+
let cancelled = false;
|
|
391
|
+
(async () => {
|
|
392
|
+
try {
|
|
393
|
+
const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true });
|
|
394
|
+
if (cancelled) return;
|
|
395
|
+
const flag = cfg?._meta?.introDismissed?.[RT_KEY(recordType)];
|
|
396
|
+
if (flag) setDismissed(true);
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
549
400
|
return () => {
|
|
550
|
-
|
|
401
|
+
cancelled = true;
|
|
551
402
|
};
|
|
552
|
-
}, [
|
|
553
|
-
const
|
|
403
|
+
}, [SL, collectionId, appId, recordType]);
|
|
404
|
+
const dismiss = useCallback(async () => {
|
|
405
|
+
setDismissed(true);
|
|
406
|
+
try {
|
|
407
|
+
localStorage.setItem(lsKey(appId, recordType), "1");
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true }).catch(() => ({}));
|
|
412
|
+
const next = {
|
|
413
|
+
...cfg ?? {},
|
|
414
|
+
_meta: {
|
|
415
|
+
...cfg?._meta ?? {},
|
|
416
|
+
introDismissed: { ...cfg?._meta?.introDismissed ?? {}, [RT_KEY(recordType)]: true }
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
await SL?.appConfiguration?.setConfig?.({ collectionId, appId, admin: true, config: next });
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
}, [SL, collectionId, appId, recordType]);
|
|
423
|
+
const undismiss = useCallback(() => {
|
|
424
|
+
setDismissed(false);
|
|
425
|
+
try {
|
|
426
|
+
localStorage.removeItem(lsKey(appId, recordType));
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}, [appId, recordType]);
|
|
430
|
+
return { dismissed, dismiss, undismiss };
|
|
431
|
+
};
|
|
432
|
+
var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
|
|
554
433
|
const query = useQuery({
|
|
555
|
-
queryKey: ["records-admin", "
|
|
556
|
-
enabled: enabled && !!collectionId && !!
|
|
557
|
-
staleTime:
|
|
558
|
-
queryFn: () =>
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
434
|
+
queryKey: ["records-admin", "scope-probe", collectionId, admin],
|
|
435
|
+
enabled: enabled && !!collectionId && !!SL?.collection?.get,
|
|
436
|
+
staleTime: 5 * 6e4,
|
|
437
|
+
queryFn: async () => {
|
|
438
|
+
const c = await SL.collection.get(collectionId, admin);
|
|
439
|
+
return {
|
|
440
|
+
hasVariants: !!c?.variants,
|
|
441
|
+
hasBatches: !!c?.batches
|
|
442
|
+
};
|
|
443
|
+
}
|
|
562
444
|
});
|
|
563
445
|
return {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
isLoading: query.
|
|
567
|
-
isStale: rule !== debouncedRule,
|
|
446
|
+
hasVariants: query.data?.hasVariants ?? false,
|
|
447
|
+
hasBatches: query.data?.hasBatches ?? false,
|
|
448
|
+
isLoading: query.isLoading,
|
|
568
449
|
error: query.error ?? null
|
|
569
450
|
};
|
|
570
|
-
}
|
|
571
|
-
var createStore = () => {
|
|
572
|
-
const map = /* @__PURE__ */ new Map();
|
|
573
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
574
|
-
let nextOrder = 0;
|
|
575
|
-
let cachedList = [];
|
|
576
|
-
const recompute = () => {
|
|
577
|
-
cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
|
|
578
|
-
};
|
|
579
|
-
const emit = () => {
|
|
580
|
-
recompute();
|
|
581
|
-
listeners.forEach((l) => l());
|
|
582
|
-
};
|
|
583
|
-
return {
|
|
584
|
-
list: () => cachedList,
|
|
585
|
-
get: (key) => map.get(key),
|
|
586
|
-
has: (key) => map.has(key),
|
|
587
|
-
upsertDraft(input) {
|
|
588
|
-
const existing = map.get(input.key);
|
|
589
|
-
const order = existing?.order ?? input.order ?? nextOrder++;
|
|
590
|
-
const status = input.status ?? existing?.status ?? "dirty";
|
|
591
|
-
map.set(input.key, {
|
|
592
|
-
...input,
|
|
593
|
-
order,
|
|
594
|
-
status
|
|
595
|
-
});
|
|
596
|
-
emit();
|
|
597
|
-
},
|
|
598
|
-
setStatus(key, status, error) {
|
|
599
|
-
const existing = map.get(key);
|
|
600
|
-
if (!existing) return;
|
|
601
|
-
map.set(key, { ...existing, status, error });
|
|
602
|
-
emit();
|
|
603
|
-
},
|
|
604
|
-
clearDraft(key) {
|
|
605
|
-
if (map.delete(key)) emit();
|
|
606
|
-
},
|
|
607
|
-
clearAll() {
|
|
608
|
-
if (map.size === 0) return;
|
|
609
|
-
map.clear();
|
|
610
|
-
emit();
|
|
611
|
-
},
|
|
612
|
-
subscribe(listener) {
|
|
613
|
-
listeners.add(listener);
|
|
614
|
-
return () => {
|
|
615
|
-
listeners.delete(listener);
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
};
|
|
620
|
-
var DirtyDraftContext = createContext(null);
|
|
621
|
-
var DirtyDraftProvider = ({ children }) => {
|
|
622
|
-
const storeRef = useRef(null);
|
|
623
|
-
if (!storeRef.current) storeRef.current = createStore();
|
|
624
|
-
return /* @__PURE__ */ jsx(DirtyDraftContext.Provider, { value: storeRef.current, children });
|
|
625
|
-
};
|
|
626
|
-
var useDirtyDraftStore = () => {
|
|
627
|
-
const ctx = useContext(DirtyDraftContext);
|
|
628
|
-
const fallbackRef = useRef(null);
|
|
629
|
-
if (!ctx && !fallbackRef.current) fallbackRef.current = createStore();
|
|
630
|
-
return ctx ?? fallbackRef.current;
|
|
631
|
-
};
|
|
632
|
-
var useDirtyDrafts = () => {
|
|
633
|
-
const store = useDirtyDraftStore();
|
|
634
|
-
return useSyncExternalStore(
|
|
635
|
-
useCallback((cb) => store.subscribe(cb), [store]),
|
|
636
|
-
useCallback(() => store.list(), [store]),
|
|
637
|
-
useCallback(() => store.list(), [store])
|
|
638
|
-
);
|
|
639
|
-
};
|
|
640
|
-
var useDirtyDraft = (key) => {
|
|
641
|
-
const store = useDirtyDraftStore();
|
|
642
|
-
const getSnapshot = useCallback(
|
|
643
|
-
() => key ? store.get(key) : void 0,
|
|
644
|
-
[store, key]
|
|
645
|
-
);
|
|
646
|
-
return useSyncExternalStore(
|
|
647
|
-
useCallback((cb) => store.subscribe(cb), [store]),
|
|
648
|
-
getSnapshot,
|
|
649
|
-
getSnapshot
|
|
650
|
-
);
|
|
651
451
|
};
|
|
652
|
-
var
|
|
653
|
-
if (
|
|
654
|
-
const
|
|
655
|
-
return
|
|
656
|
-
|
|
657
|
-
var useDirtyDraftActions = () => {
|
|
658
|
-
const store = useDirtyDraftStore();
|
|
659
|
-
const drafts = useDirtyDrafts();
|
|
660
|
-
const count = useMemo(() => drafts.filter((d) => d.status !== "saved").length, [drafts]);
|
|
661
|
-
const errorCount = useMemo(() => drafts.filter((d) => d.status === "error").length, [drafts]);
|
|
662
|
-
return { drafts, count, errorCount, store };
|
|
452
|
+
var defaultClassify = (r) => {
|
|
453
|
+
if (!r.data) return "empty";
|
|
454
|
+
const keys = Object.keys(r.data);
|
|
455
|
+
if (keys.length === 0) return "empty";
|
|
456
|
+
return "configured";
|
|
663
457
|
};
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
458
|
+
var matchesScope = (kind, rec, _p) => {
|
|
459
|
+
const hasRule = !!rec.facetRule;
|
|
460
|
+
if (kind === "rule") return hasRule;
|
|
461
|
+
if (hasRule) return false;
|
|
462
|
+
const productId = rec.productId ?? void 0;
|
|
463
|
+
const variantId = rec.variantId ?? void 0;
|
|
464
|
+
const batchId = rec.batchId ?? void 0;
|
|
465
|
+
const proofId = rec.proofId ?? void 0;
|
|
466
|
+
if (kind === "collection") {
|
|
467
|
+
return !productId && !variantId && !batchId && !proofId;
|
|
671
468
|
}
|
|
469
|
+
if (kind === "product") return !!productId && !variantId && !batchId;
|
|
470
|
+
if (kind === "variant") return !!variantId;
|
|
471
|
+
if (kind === "batch") return !!batchId;
|
|
472
|
+
return false;
|
|
672
473
|
};
|
|
673
|
-
var
|
|
674
|
-
if (
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
return JSON.parse(JSON.stringify(resolved.data));
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return defaultData?.() ?? {};
|
|
474
|
+
var matchesContext = (p, ctx) => {
|
|
475
|
+
if (!ctx) return true;
|
|
476
|
+
if (ctx.productId && p.productId !== ctx.productId) return false;
|
|
477
|
+
if (ctx.variantId && p.variantId !== ctx.variantId) return false;
|
|
478
|
+
if (ctx.batchId && p.batchId !== ctx.batchId) return false;
|
|
479
|
+
return true;
|
|
683
480
|
};
|
|
684
|
-
|
|
481
|
+
var toSummary = (rec) => {
|
|
482
|
+
const ref = rec.ref ?? "";
|
|
483
|
+
const productId = rec.productId ?? void 0;
|
|
484
|
+
const variantId = rec.variantId ?? void 0;
|
|
485
|
+
const batchId = rec.batchId ?? void 0;
|
|
486
|
+
const proofId = rec.proofId ?? void 0;
|
|
487
|
+
const scope = {
|
|
488
|
+
kind: batchId ? "batch" : variantId ? "variant" : productId ? "product" : "collection",
|
|
489
|
+
raw: ref,
|
|
490
|
+
productId,
|
|
491
|
+
variantId,
|
|
492
|
+
batchId,
|
|
493
|
+
proofId
|
|
494
|
+
};
|
|
495
|
+
const facetRule = rec.facetRule ?? null;
|
|
496
|
+
if (facetRule) scope.kind = "rule";
|
|
497
|
+
const ruleLabel = facetRule && facetRule.all && facetRule.all.length > 0 ? facetRule.all.length === 1 ? "Rule \xB7 1 facet" : `Rule \xB7 ${facetRule.all.length} facets` : null;
|
|
498
|
+
const fallbackLabel = scope.batchId ?? scope.variantId ?? scope.productId ?? ruleLabel ?? (ref || "Global record");
|
|
499
|
+
return {
|
|
500
|
+
id: rec.id,
|
|
501
|
+
ref,
|
|
502
|
+
scope,
|
|
503
|
+
data: rec.data ?? null,
|
|
504
|
+
status: "configured",
|
|
505
|
+
label: fallbackLabel,
|
|
506
|
+
updatedAt: rec.updatedAt,
|
|
507
|
+
facetRule
|
|
508
|
+
};
|
|
509
|
+
};
|
|
510
|
+
var QK_BASE = ["records-admin", "list"];
|
|
511
|
+
var useRecordList = (args) => {
|
|
685
512
|
const {
|
|
686
513
|
ctx,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
createMode = false,
|
|
696
|
-
deriveDraftLabel
|
|
514
|
+
scopeKind,
|
|
515
|
+
search = "",
|
|
516
|
+
filter = "all",
|
|
517
|
+
classify: classify2,
|
|
518
|
+
enabled = true,
|
|
519
|
+
scaffolder,
|
|
520
|
+
contextScope,
|
|
521
|
+
pageSize = 100
|
|
697
522
|
} = args;
|
|
698
523
|
const queryClient = useQueryClient();
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
initialDraft ? initialDraft.baseline : cleanSeed
|
|
712
|
-
);
|
|
713
|
-
const [facetRule, setFacetRule] = useState(seedFacetRule);
|
|
714
|
-
const [savedFacetRule, setSavedFacetRule] = useState(
|
|
715
|
-
initialDraft ? initialDraft.baselineFacetRule : initialFacetRule
|
|
524
|
+
const queryKey = useMemo(
|
|
525
|
+
() => [
|
|
526
|
+
...QK_BASE,
|
|
527
|
+
ctx.collectionId,
|
|
528
|
+
ctx.appId,
|
|
529
|
+
ctx.recordType,
|
|
530
|
+
scopeKind,
|
|
531
|
+
contextScope?.productId ?? null,
|
|
532
|
+
contextScope?.variantId ?? null,
|
|
533
|
+
contextScope?.batchId ?? null
|
|
534
|
+
],
|
|
535
|
+
[ctx.collectionId, ctx.appId, ctx.recordType, scopeKind, contextScope?.productId, contextScope?.variantId, contextScope?.batchId]
|
|
716
536
|
);
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
537
|
+
const query = useInfiniteQuery({
|
|
538
|
+
queryKey,
|
|
539
|
+
enabled,
|
|
540
|
+
initialPageParam: 0,
|
|
541
|
+
queryFn: async ({ pageParam }) => {
|
|
542
|
+
const offset = pageParam;
|
|
543
|
+
const { data, total: total2, hasMore } = await listRecords(ctx, { limit: pageSize, offset });
|
|
544
|
+
return { data, total: total2, hasMore, nextOffset: offset + data.length };
|
|
545
|
+
},
|
|
546
|
+
getNextPageParam: (last) => last.hasMore ? last.nextOffset : void 0,
|
|
547
|
+
staleTime: 3e4
|
|
728
548
|
});
|
|
549
|
+
const [scaffolded, setScaffolded] = useState(null);
|
|
550
|
+
const rawItems = useMemo(() => {
|
|
551
|
+
const all = query.data?.pages.flatMap((p) => p.data) ?? [];
|
|
552
|
+
const cls = classify2 ?? defaultClassify;
|
|
553
|
+
return all.map((rec) => ({ rec, summary: toSummary(rec) })).filter(({ rec, summary }) => matchesScope(scopeKind, rec, summary.scope)).filter(({ summary }) => matchesContext(summary.scope, contextScope)).map(({ summary }) => ({ ...summary, status: cls(summary) }));
|
|
554
|
+
}, [query.data, scopeKind, classify2, contextScope]);
|
|
729
555
|
useEffect(() => {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
prevTargetRef.current = { scopeRaw: scope.raw, recordId: resolved.recordId };
|
|
733
|
-
const fresh = cloneSeed(resolved, defaultData);
|
|
734
|
-
if (targetChanged) {
|
|
735
|
-
const incomingKey = buildDraftKey({
|
|
736
|
-
recordId: resolved.recordId,
|
|
737
|
-
scopeRaw: scope.raw,
|
|
738
|
-
draftKind: scope.kind
|
|
739
|
-
});
|
|
740
|
-
const incomingDraft = draftStore.get(incomingKey);
|
|
741
|
-
if (incomingDraft && reseed === "preserve-dirty") {
|
|
742
|
-
setValue(incomingDraft.value);
|
|
743
|
-
setSavedSnapshot(incomingDraft.baseline);
|
|
744
|
-
setFacetRule(incomingDraft.facetRule);
|
|
745
|
-
setSavedFacetRule(incomingDraft.baselineFacetRule);
|
|
746
|
-
setUserInteracted(true);
|
|
747
|
-
} else {
|
|
748
|
-
setValue(fresh);
|
|
749
|
-
setSavedSnapshot(fresh);
|
|
750
|
-
setFacetRule(initialFacetRule);
|
|
751
|
-
setSavedFacetRule(initialFacetRule);
|
|
752
|
-
setUserInteracted(false);
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
if (reseed === "preserve-dirty") {
|
|
756
|
-
const hasUnsaved = !isEqual(valueRef.current, savedSnapshot);
|
|
757
|
-
if (!hasUnsaved) setValue(fresh);
|
|
758
|
-
} else {
|
|
759
|
-
setValue(fresh);
|
|
760
|
-
}
|
|
761
|
-
setSavedSnapshot(fresh);
|
|
762
|
-
setFacetRule(initialFacetRule);
|
|
763
|
-
setSavedFacetRule(initialFacetRule);
|
|
764
|
-
}
|
|
765
|
-
setOptimisticSource(null);
|
|
766
|
-
}, [scope.raw, resolved.recordId, resolved.source, resolved.sourceRef]);
|
|
767
|
-
const valueDiff = !isEqual(value, savedSnapshot) || !isEqual(facetRule, savedFacetRule);
|
|
768
|
-
const isDirty = userInteracted && valueDiff;
|
|
769
|
-
const handleChange = useCallback((next) => {
|
|
770
|
-
setUserInteracted(true);
|
|
771
|
-
setValue(next);
|
|
772
|
-
}, []);
|
|
773
|
-
const handleFacetRuleChange = useCallback((next) => {
|
|
774
|
-
setUserInteracted(true);
|
|
775
|
-
setFacetRule(next);
|
|
776
|
-
}, []);
|
|
777
|
-
const save = useCallback(async () => {
|
|
778
|
-
const anchors = parsedRefToScope(scope);
|
|
779
|
-
const hasAnchors = !!(anchors.productId || anchors.variantId || anchors.batchId || anchors.proofId);
|
|
780
|
-
const hasRule = !!(facetRule && facetRule.all && facetRule.all.length > 0);
|
|
781
|
-
const isRuleScope2 = scope.kind === "rule";
|
|
782
|
-
if (isRuleScope2 && !hasAnchors && !hasRule && !resolved.recordId && !createMode) {
|
|
783
|
-
console.warn("[useRecordEditor] save() bailed \u2014 rule scope with no clauses, no recordId, not in createMode");
|
|
556
|
+
if (!scaffolder) {
|
|
557
|
+
setScaffolded(null);
|
|
784
558
|
return;
|
|
785
559
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const cacheKey = resolvedRecordQueryKey({
|
|
790
|
-
collectionId: ctx.collectionId,
|
|
791
|
-
appId: ctx.appId,
|
|
792
|
-
recordType: ctx.recordType,
|
|
793
|
-
productId: scope.productId,
|
|
794
|
-
variantId: scope.variantId,
|
|
795
|
-
batchId: scope.batchId,
|
|
796
|
-
facetId: scope.facetId,
|
|
797
|
-
facetValue: scope.facetValue,
|
|
798
|
-
proofId: scope.proofId,
|
|
799
|
-
recordId: resolved.recordId,
|
|
800
|
-
withParent: true
|
|
801
|
-
});
|
|
802
|
-
const previousCache = queryClient.getQueryData(cacheKey);
|
|
803
|
-
setSaveError(null);
|
|
804
|
-
setIsSaving(true);
|
|
805
|
-
setSavedSnapshot(value);
|
|
806
|
-
setSavedFacetRule(facetRule);
|
|
807
|
-
setOptimisticSource("self");
|
|
808
|
-
queryClient.setQueryData(cacheKey, {
|
|
809
|
-
data: value,
|
|
810
|
-
source: "self",
|
|
811
|
-
sourceRef: scope.raw,
|
|
812
|
-
recordId: resolved.recordId,
|
|
813
|
-
parentValue: previousCache?.parentValue ?? resolved.parentValue
|
|
560
|
+
let cancelled = false;
|
|
561
|
+
Promise.resolve(scaffolder(rawItems)).then((next) => {
|
|
562
|
+
if (!cancelled) setScaffolded(next);
|
|
814
563
|
});
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
564
|
+
return () => {
|
|
565
|
+
cancelled = true;
|
|
566
|
+
};
|
|
567
|
+
}, [rawItems, scaffolder]);
|
|
568
|
+
const items = scaffolded ?? rawItems;
|
|
569
|
+
const filtered = useMemo(() => {
|
|
570
|
+
let out = items;
|
|
571
|
+
if (filter !== "all") out = out.filter((r) => r.status === filter);
|
|
572
|
+
if (search.trim()) {
|
|
573
|
+
const q = search.trim().toLowerCase();
|
|
574
|
+
out = out.filter((r) => `${r.label} ${r.subtitle ?? ""} ${r.ref}`.toLowerCase().includes(q));
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}, [items, filter, search]);
|
|
578
|
+
const counts = useMemo(() => ({
|
|
579
|
+
all: items.length,
|
|
580
|
+
configured: items.filter((r) => r.status === "configured").length,
|
|
581
|
+
partial: items.filter((r) => r.status === "partial").length,
|
|
582
|
+
empty: items.filter((r) => r.status === "empty").length
|
|
583
|
+
}), [items]);
|
|
584
|
+
const refetch = useCallback(() => {
|
|
585
|
+
queryClient.invalidateQueries({ queryKey: [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType] });
|
|
586
|
+
}, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
|
|
587
|
+
const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
|
|
588
|
+
return {
|
|
589
|
+
allItems: items,
|
|
590
|
+
items: filtered,
|
|
591
|
+
total,
|
|
592
|
+
counts,
|
|
593
|
+
isLoading: query.isLoading,
|
|
594
|
+
error: query.error ?? null,
|
|
595
|
+
refetch,
|
|
596
|
+
hasNextPage: query.hasNextPage,
|
|
597
|
+
isFetchingNextPage: query.isFetchingNextPage,
|
|
598
|
+
fetchNextPage: query.fetchNextPage
|
|
599
|
+
};
|
|
600
|
+
};
|
|
601
|
+
var LOG = "[RecordsAdmin/useFacetBrowse]";
|
|
602
|
+
var QK = ["records-admin", "facet-browse"];
|
|
603
|
+
var toScaffoldSummary = (facet, value) => {
|
|
604
|
+
const facetKey = facet.key ?? "";
|
|
605
|
+
const valueKey = value.key ?? "";
|
|
606
|
+
const ref = buildRef({ facetId: facetKey, facetValue: valueKey });
|
|
607
|
+
return {
|
|
608
|
+
id: null,
|
|
609
|
+
ref,
|
|
610
|
+
scope: parseRef(ref),
|
|
611
|
+
data: null,
|
|
612
|
+
status: "empty",
|
|
613
|
+
label: value.name ?? valueKey ?? "Untitled value",
|
|
614
|
+
subtitle: facet.name ?? facetKey ?? void 0
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
var useFacetBrowse = ({
|
|
618
|
+
SL,
|
|
619
|
+
collectionId,
|
|
620
|
+
existing,
|
|
621
|
+
search = "",
|
|
622
|
+
filter = "all",
|
|
623
|
+
enabled = true
|
|
624
|
+
}) => {
|
|
625
|
+
const queryClient = useQueryClient();
|
|
626
|
+
const hasAdminList = !!SL?.facets?.list;
|
|
627
|
+
const hasPublicList = !!SL?.facets?.publicList;
|
|
628
|
+
const hasAnyList = hasAdminList || hasPublicList;
|
|
629
|
+
const queryEnabled = enabled && !!collectionId && hasAnyList;
|
|
630
|
+
const query = useQuery({
|
|
631
|
+
queryKey: [...QK, collectionId],
|
|
632
|
+
enabled: queryEnabled,
|
|
633
|
+
staleTime: 3e4,
|
|
634
|
+
queryFn: async () => {
|
|
635
|
+
const t0 = performance.now();
|
|
636
|
+
try {
|
|
637
|
+
if (SL?.facets?.list) {
|
|
638
|
+
console.info(`${LOG} \u2192 SL.facets.list("${collectionId}", { includeValues: true })`);
|
|
639
|
+
const res = await SL.facets.list(collectionId, { includeValues: true });
|
|
640
|
+
const items = res?.items ?? [];
|
|
641
|
+
console.info(
|
|
642
|
+
`${LOG} \u2190 SL.facets.list ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
|
|
643
|
+
items
|
|
644
|
+
);
|
|
645
|
+
return items;
|
|
646
|
+
}
|
|
647
|
+
if (SL?.facets?.publicList) {
|
|
648
|
+
console.info(`${LOG} \u2192 SL.facets.publicList("${collectionId}", { includeValues: true })`);
|
|
649
|
+
const res = await SL.facets.publicList(collectionId, { includeValues: true });
|
|
650
|
+
const items = res?.items ?? [];
|
|
651
|
+
console.info(
|
|
652
|
+
`${LOG} \u2190 SL.facets.publicList ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
|
|
653
|
+
items
|
|
654
|
+
);
|
|
655
|
+
return items;
|
|
656
|
+
}
|
|
657
|
+
console.warn(`${LOG} queryFn ran but no facets API is available on SL`);
|
|
658
|
+
return [];
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error(`${LOG} \u2716 facets fetch failed`, err);
|
|
661
|
+
throw err;
|
|
849
662
|
}
|
|
850
|
-
setSaveError(err);
|
|
851
|
-
onSaveError?.(err);
|
|
852
|
-
throw err;
|
|
853
|
-
} finally {
|
|
854
|
-
setIsSaving(false);
|
|
855
663
|
}
|
|
856
|
-
}
|
|
857
|
-
const
|
|
858
|
-
setValue(savedSnapshot);
|
|
859
|
-
setFacetRule(savedFacetRule);
|
|
860
|
-
setUserInteracted(false);
|
|
861
|
-
draftStore.clearDraft(draftKey);
|
|
862
|
-
}, [savedSnapshot, savedFacetRule]);
|
|
863
|
-
const remove = useCallback(async () => {
|
|
864
|
-
if (resolved.source !== "self") return;
|
|
865
|
-
if (!resolved.recordId) return;
|
|
866
|
-
await removeRecord(ctx, resolved.recordId);
|
|
867
|
-
draftStore.clearDraft(draftKey);
|
|
868
|
-
onDeleted?.();
|
|
869
|
-
}, [resolved.source, resolved.recordId]);
|
|
870
|
-
const prevDraftKeyRef = useRef(draftKey);
|
|
871
|
-
const prevScopeRawRef = useRef(scope.raw);
|
|
664
|
+
});
|
|
665
|
+
const lastLoggedRef = useRef("");
|
|
872
666
|
useEffect(() => {
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
667
|
+
const signature = `${enabled}|${collectionId}|${hasAdminList}|${hasPublicList}`;
|
|
668
|
+
if (signature === lastLoggedRef.current) return;
|
|
669
|
+
lastLoggedRef.current = signature;
|
|
670
|
+
if (!enabled) {
|
|
671
|
+
console.info(`${LOG} skipped \u2014 enabled=false (shell not on facet tab yet)`);
|
|
672
|
+
return;
|
|
878
673
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}, [draftKey, scope.raw]);
|
|
882
|
-
useEffect(() => {
|
|
883
|
-
if (!isDirty) {
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
const anchors = parsedRefToScope(scope);
|
|
887
|
-
const saveKind = resolved.recordId && resolved.source === "self" ? "update" : createMode ? "create" : "upsert";
|
|
888
|
-
const deriveLabel = () => {
|
|
889
|
-
if (deriveDraftLabel) {
|
|
890
|
-
try {
|
|
891
|
-
const custom = deriveDraftLabel(value, scope);
|
|
892
|
-
if (typeof custom === "string" && custom.trim()) return custom.trim();
|
|
893
|
-
} catch {
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
const KEYS = ["title", "name", "label", "heading", "question", "slug"];
|
|
897
|
-
const pickString = (obj) => {
|
|
898
|
-
if (!obj || typeof obj !== "object") return void 0;
|
|
899
|
-
for (const key of KEYS) {
|
|
900
|
-
const raw = obj[key];
|
|
901
|
-
if (typeof raw === "string" && raw.trim()) return raw.trim();
|
|
902
|
-
}
|
|
903
|
-
return void 0;
|
|
904
|
-
};
|
|
905
|
-
const v = value;
|
|
906
|
-
const top = pickString(v);
|
|
907
|
-
if (top) return top;
|
|
908
|
-
if (v && typeof v === "object") {
|
|
909
|
-
for (const wrapper of ["display", "content", "meta", "data"]) {
|
|
910
|
-
const nested = pickString(v[wrapper]);
|
|
911
|
-
if (nested) return nested;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (scope.raw?.startsWith("item:")) return "Untitled item";
|
|
915
|
-
if (scope.kind === "rule") return "Rule";
|
|
916
|
-
if (scope.kind && scope.kind !== "collection") {
|
|
917
|
-
return scope.kind.charAt(0).toUpperCase() + scope.kind.slice(1);
|
|
918
|
-
}
|
|
919
|
-
return "Default";
|
|
920
|
-
};
|
|
921
|
-
draftStore.upsertDraft({
|
|
922
|
-
key: draftKey,
|
|
923
|
-
label: deriveLabel(),
|
|
924
|
-
context: scope.kind,
|
|
925
|
-
scopeRaw: scope.raw,
|
|
926
|
-
recordId: resolved.recordId,
|
|
927
|
-
value,
|
|
928
|
-
facetRule,
|
|
929
|
-
baseline: savedSnapshot,
|
|
930
|
-
baselineFacetRule: savedFacetRule,
|
|
931
|
-
createMode,
|
|
932
|
-
scopeAnchors: anchors,
|
|
933
|
-
ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
|
|
934
|
-
saveKind,
|
|
935
|
-
save: async () => {
|
|
936
|
-
await save();
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
}, [isDirty, value, facetRule, savedSnapshot, savedFacetRule, scope.raw, resolved.recordId, resolved.source, createMode, save]);
|
|
940
|
-
const effectiveSource = optimisticSource ?? resolved.source;
|
|
941
|
-
const isRuleScope = scope.kind === "rule";
|
|
942
|
-
const ruleValid = !isRuleScope || isFacetRuleValid(facetRule);
|
|
943
|
-
const canSave = ruleValid;
|
|
944
|
-
const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
|
|
945
|
-
return {
|
|
946
|
-
value,
|
|
947
|
-
onChange: handleChange,
|
|
948
|
-
source: effectiveSource,
|
|
949
|
-
recordId: resolved.recordId,
|
|
950
|
-
parentValue: resolved.parentValue,
|
|
951
|
-
scope,
|
|
952
|
-
isDirty,
|
|
953
|
-
save,
|
|
954
|
-
reset,
|
|
955
|
-
remove,
|
|
956
|
-
canRemove: effectiveSource === "self",
|
|
957
|
-
canSave,
|
|
958
|
-
cannotSaveReason,
|
|
959
|
-
isSaving,
|
|
960
|
-
saveError,
|
|
961
|
-
facetRule,
|
|
962
|
-
onFacetRuleChange: handleFacetRuleChange
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
var RT_KEY = (recordType) => recordType ?? "_default";
|
|
966
|
-
var lsKey = (appId, recordType) => `ra:intro:${appId}:${RT_KEY(recordType)}`;
|
|
967
|
-
var useIntroDismissed = (SL, collectionId, appId, recordType) => {
|
|
968
|
-
const [dismissed, setDismissed] = useState(() => {
|
|
969
|
-
try {
|
|
970
|
-
return localStorage.getItem(lsKey(appId, recordType)) === "1";
|
|
971
|
-
} catch {
|
|
972
|
-
return false;
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
useEffect(() => {
|
|
976
|
-
let cancelled = false;
|
|
977
|
-
(async () => {
|
|
978
|
-
try {
|
|
979
|
-
const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true });
|
|
980
|
-
if (cancelled) return;
|
|
981
|
-
const flag = cfg?._meta?.introDismissed?.[RT_KEY(recordType)];
|
|
982
|
-
if (flag) setDismissed(true);
|
|
983
|
-
} catch {
|
|
984
|
-
}
|
|
985
|
-
})();
|
|
986
|
-
return () => {
|
|
987
|
-
cancelled = true;
|
|
988
|
-
};
|
|
989
|
-
}, [SL, collectionId, appId, recordType]);
|
|
990
|
-
const dismiss = useCallback(async () => {
|
|
991
|
-
setDismissed(true);
|
|
992
|
-
try {
|
|
993
|
-
localStorage.setItem(lsKey(appId, recordType), "1");
|
|
994
|
-
} catch {
|
|
995
|
-
}
|
|
996
|
-
try {
|
|
997
|
-
const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true }).catch(() => ({}));
|
|
998
|
-
const next = {
|
|
999
|
-
...cfg ?? {},
|
|
1000
|
-
_meta: {
|
|
1001
|
-
...cfg?._meta ?? {},
|
|
1002
|
-
introDismissed: { ...cfg?._meta?.introDismissed ?? {}, [RT_KEY(recordType)]: true }
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
await SL?.appConfiguration?.setConfig?.({ collectionId, appId, admin: true, config: next });
|
|
1006
|
-
} catch {
|
|
1007
|
-
}
|
|
1008
|
-
}, [SL, collectionId, appId, recordType]);
|
|
1009
|
-
const undismiss = useCallback(() => {
|
|
1010
|
-
setDismissed(false);
|
|
1011
|
-
try {
|
|
1012
|
-
localStorage.removeItem(lsKey(appId, recordType));
|
|
1013
|
-
} catch {
|
|
1014
|
-
}
|
|
1015
|
-
}, [appId, recordType]);
|
|
1016
|
-
return { dismissed, dismiss, undismiss };
|
|
1017
|
-
};
|
|
1018
|
-
var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
|
|
1019
|
-
const query = useQuery({
|
|
1020
|
-
queryKey: ["records-admin", "scope-probe", collectionId, admin],
|
|
1021
|
-
enabled: enabled && !!collectionId && !!SL?.collection?.get,
|
|
1022
|
-
staleTime: 5 * 6e4,
|
|
1023
|
-
queryFn: async () => {
|
|
1024
|
-
const c = await SL.collection.get(collectionId, admin);
|
|
1025
|
-
return {
|
|
1026
|
-
hasVariants: !!c?.variants,
|
|
1027
|
-
hasBatches: !!c?.batches
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
return {
|
|
1032
|
-
hasVariants: query.data?.hasVariants ?? false,
|
|
1033
|
-
hasBatches: query.data?.hasBatches ?? false,
|
|
1034
|
-
isLoading: query.isLoading,
|
|
1035
|
-
error: query.error ?? null
|
|
1036
|
-
};
|
|
1037
|
-
};
|
|
1038
|
-
var LOG = "[RecordsAdmin/useFacetBrowse]";
|
|
1039
|
-
var QK = ["records-admin", "facet-browse"];
|
|
1040
|
-
var toScaffoldSummary = (facet, value) => {
|
|
1041
|
-
const facetKey = facet.key ?? "";
|
|
1042
|
-
const valueKey = value.key ?? "";
|
|
1043
|
-
const ref = buildRef({ facetId: facetKey, facetValue: valueKey });
|
|
1044
|
-
return {
|
|
1045
|
-
id: null,
|
|
1046
|
-
ref,
|
|
1047
|
-
scope: parseRef(ref),
|
|
1048
|
-
data: null,
|
|
1049
|
-
status: "empty",
|
|
1050
|
-
label: value.name ?? valueKey ?? "Untitled value",
|
|
1051
|
-
subtitle: facet.name ?? facetKey ?? void 0
|
|
1052
|
-
};
|
|
1053
|
-
};
|
|
1054
|
-
var useFacetBrowse = ({
|
|
1055
|
-
SL,
|
|
1056
|
-
collectionId,
|
|
1057
|
-
existing,
|
|
1058
|
-
search = "",
|
|
1059
|
-
filter = "all",
|
|
1060
|
-
enabled = true
|
|
1061
|
-
}) => {
|
|
1062
|
-
const queryClient = useQueryClient();
|
|
1063
|
-
const hasAdminList = !!SL?.facets?.list;
|
|
1064
|
-
const hasPublicList = !!SL?.facets?.publicList;
|
|
1065
|
-
const hasAnyList = hasAdminList || hasPublicList;
|
|
1066
|
-
const queryEnabled = enabled && !!collectionId && hasAnyList;
|
|
1067
|
-
const query = useQuery({
|
|
1068
|
-
queryKey: [...QK, collectionId],
|
|
1069
|
-
enabled: queryEnabled,
|
|
1070
|
-
staleTime: 3e4,
|
|
1071
|
-
queryFn: async () => {
|
|
1072
|
-
const t0 = performance.now();
|
|
1073
|
-
try {
|
|
1074
|
-
if (SL?.facets?.list) {
|
|
1075
|
-
console.info(`${LOG} \u2192 SL.facets.list("${collectionId}", { includeValues: true })`);
|
|
1076
|
-
const res = await SL.facets.list(collectionId, { includeValues: true });
|
|
1077
|
-
const items = res?.items ?? [];
|
|
1078
|
-
console.info(
|
|
1079
|
-
`${LOG} \u2190 SL.facets.list ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
|
|
1080
|
-
items
|
|
1081
|
-
);
|
|
1082
|
-
return items;
|
|
1083
|
-
}
|
|
1084
|
-
if (SL?.facets?.publicList) {
|
|
1085
|
-
console.info(`${LOG} \u2192 SL.facets.publicList("${collectionId}", { includeValues: true })`);
|
|
1086
|
-
const res = await SL.facets.publicList(collectionId, { includeValues: true });
|
|
1087
|
-
const items = res?.items ?? [];
|
|
1088
|
-
console.info(
|
|
1089
|
-
`${LOG} \u2190 SL.facets.publicList ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
|
|
1090
|
-
items
|
|
1091
|
-
);
|
|
1092
|
-
return items;
|
|
1093
|
-
}
|
|
1094
|
-
console.warn(`${LOG} queryFn ran but no facets API is available on SL`);
|
|
1095
|
-
return [];
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
console.error(`${LOG} \u2716 facets fetch failed`, err);
|
|
1098
|
-
throw err;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1102
|
-
const lastLoggedRef = useRef("");
|
|
1103
|
-
useEffect(() => {
|
|
1104
|
-
const signature = `${enabled}|${collectionId}|${hasAdminList}|${hasPublicList}`;
|
|
1105
|
-
if (signature === lastLoggedRef.current) return;
|
|
1106
|
-
lastLoggedRef.current = signature;
|
|
1107
|
-
if (!enabled) {
|
|
1108
|
-
console.info(`${LOG} skipped \u2014 enabled=false (shell not on facet tab yet)`);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
if (!collectionId) {
|
|
1112
|
-
console.warn(`${LOG} skipped \u2014 no collectionId provided`);
|
|
674
|
+
if (!collectionId) {
|
|
675
|
+
console.warn(`${LOG} skipped \u2014 no collectionId provided`);
|
|
1113
676
|
return;
|
|
1114
677
|
}
|
|
1115
678
|
if (!hasAnyList) {
|
|
@@ -1277,49 +840,229 @@ var useProductChildren = (args) => {
|
|
|
1277
840
|
refetch
|
|
1278
841
|
};
|
|
1279
842
|
};
|
|
1280
|
-
var
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
e.preventDefault();
|
|
1292
|
-
e.returnValue = "";
|
|
1293
|
-
return "";
|
|
1294
|
-
};
|
|
1295
|
-
window.addEventListener("beforeunload", handler);
|
|
1296
|
-
return () => window.removeEventListener("beforeunload", handler);
|
|
1297
|
-
}, [isDirty, disableBeforeUnload]);
|
|
1298
|
-
useEffect(() => {
|
|
1299
|
-
if (disableParentMessage || typeof window === "undefined") return;
|
|
1300
|
-
if (window.parent === window) return;
|
|
1301
|
-
try {
|
|
1302
|
-
window.parent.postMessage({
|
|
1303
|
-
type: MESSAGE_TYPE,
|
|
1304
|
-
isDirty,
|
|
1305
|
-
label: label ?? null
|
|
1306
|
-
}, "*");
|
|
1307
|
-
} catch {
|
|
1308
|
-
}
|
|
1309
|
-
}, [isDirty, label, disableParentMessage]);
|
|
1310
|
-
useEffect(() => {
|
|
1311
|
-
if (disableParentMessage || typeof window === "undefined") return;
|
|
1312
|
-
if (window.parent === window) return;
|
|
1313
|
-
return () => {
|
|
1314
|
-
try {
|
|
1315
|
-
window.parent.postMessage({ type: MESSAGE_TYPE, isDirty: false, label: label ?? null }, "*");
|
|
1316
|
-
} catch {
|
|
843
|
+
var EMPTY_RULE_FILTERS = { facetKeys: [], minClauses: null };
|
|
844
|
+
var COMPLEXITY_THRESHOLDS = [3, 5, 10];
|
|
845
|
+
var ruleOf = (r) => r.facetRule ?? null;
|
|
846
|
+
function RuleFilterChips({ source, value, onChange }) {
|
|
847
|
+
const facetKeyEntries = useMemo(() => {
|
|
848
|
+
const counts = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const r of source) {
|
|
850
|
+
const rule = ruleOf(r);
|
|
851
|
+
if (!rule) continue;
|
|
852
|
+
for (const c of rule.all ?? []) {
|
|
853
|
+
counts.set(c.facetKey, (counts.get(c.facetKey) ?? 0) + 1);
|
|
1317
854
|
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
855
|
+
}
|
|
856
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
857
|
+
}, [source]);
|
|
858
|
+
const maxClauses = useMemo(() => {
|
|
859
|
+
let max = 0;
|
|
860
|
+
for (const r of source) {
|
|
861
|
+
const rule = ruleOf(r);
|
|
862
|
+
if (!rule) continue;
|
|
863
|
+
max = Math.max(max, rule.all?.length ?? 0);
|
|
864
|
+
}
|
|
865
|
+
return max;
|
|
866
|
+
}, [source]);
|
|
867
|
+
if (facetKeyEntries.length === 0 && maxClauses < 2) return null;
|
|
868
|
+
const toggleKey = (key) => {
|
|
869
|
+
const has = value.facetKeys.includes(key);
|
|
870
|
+
const next = has ? value.facetKeys.filter((k) => k !== key) : [...value.facetKeys, key];
|
|
871
|
+
onChange({ ...value, facetKeys: next });
|
|
872
|
+
};
|
|
873
|
+
const setMin = (n) => onChange({ ...value, minClauses: n });
|
|
874
|
+
const hasAny = value.facetKeys.length > 0 || value.minClauses != null;
|
|
875
|
+
return /* @__PURE__ */ jsxs("div", { className: "ra-rule-filters", children: [
|
|
876
|
+
/* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter rules by facet", children: facetKeyEntries.map(([key, count]) => {
|
|
877
|
+
const active = value.facetKeys.includes(key);
|
|
878
|
+
return /* @__PURE__ */ jsxs(
|
|
879
|
+
"button",
|
|
880
|
+
{
|
|
881
|
+
type: "button",
|
|
882
|
+
onClick: () => toggleKey(key),
|
|
883
|
+
className: "ra-rule-filter-chip",
|
|
884
|
+
"data-active": active ? "true" : "false",
|
|
885
|
+
"aria-pressed": active,
|
|
886
|
+
title: `Show rules using ${key}`,
|
|
887
|
+
children: [
|
|
888
|
+
/* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-label", children: key }),
|
|
889
|
+
/* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-count", children: count })
|
|
890
|
+
]
|
|
891
|
+
},
|
|
892
|
+
key
|
|
893
|
+
);
|
|
894
|
+
}) }),
|
|
895
|
+
maxClauses >= 2 && /* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter by clause count", children: COMPLEXITY_THRESHOLDS.filter((n) => maxClauses >= n).map((n) => {
|
|
896
|
+
const active = value.minClauses === n;
|
|
897
|
+
return /* @__PURE__ */ jsxs(
|
|
898
|
+
"button",
|
|
899
|
+
{
|
|
900
|
+
type: "button",
|
|
901
|
+
onClick: () => setMin(active ? null : n),
|
|
902
|
+
className: "ra-rule-filter-chip",
|
|
903
|
+
"data-tone": "complexity",
|
|
904
|
+
"data-active": active ? "true" : "false",
|
|
905
|
+
"aria-pressed": active,
|
|
906
|
+
title: `Only rules with \u2265 ${n} facets`,
|
|
907
|
+
children: [
|
|
908
|
+
"\u2265 ",
|
|
909
|
+
n,
|
|
910
|
+
" facets"
|
|
911
|
+
]
|
|
912
|
+
},
|
|
913
|
+
n
|
|
914
|
+
);
|
|
915
|
+
}) }),
|
|
916
|
+
hasAny && /* @__PURE__ */ jsx(
|
|
917
|
+
"button",
|
|
918
|
+
{
|
|
919
|
+
type: "button",
|
|
920
|
+
onClick: () => onChange(EMPTY_RULE_FILTERS),
|
|
921
|
+
className: "ra-rule-filter-clear",
|
|
922
|
+
"aria-label": "Clear rule filters",
|
|
923
|
+
children: "Clear filters"
|
|
924
|
+
}
|
|
925
|
+
)
|
|
926
|
+
] });
|
|
927
|
+
}
|
|
928
|
+
function applyRuleFilters(items, filters) {
|
|
929
|
+
if (filters.facetKeys.length === 0 && filters.minClauses == null) return items;
|
|
930
|
+
return items.filter((r) => {
|
|
931
|
+
const rule = ruleOf(r);
|
|
932
|
+
if (!rule) return false;
|
|
933
|
+
if (filters.minClauses != null && (rule.all?.length ?? 0) < filters.minClauses) return false;
|
|
934
|
+
if (filters.facetKeys.length > 0) {
|
|
935
|
+
const keys = new Set((rule.all ?? []).map((c) => c.facetKey));
|
|
936
|
+
if (!filters.facetKeys.some((k) => keys.has(k))) return false;
|
|
937
|
+
}
|
|
938
|
+
return true;
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/components/RecordsAdmin/hooks/useShellBrowser.ts
|
|
943
|
+
function useShellBrowser(opts) {
|
|
944
|
+
const {
|
|
945
|
+
ctx,
|
|
946
|
+
SL,
|
|
947
|
+
collectionId,
|
|
948
|
+
activeScope,
|
|
949
|
+
contextScope,
|
|
950
|
+
probeIsLoading,
|
|
951
|
+
selectedProductId,
|
|
952
|
+
drillTab,
|
|
953
|
+
classify: classify2
|
|
954
|
+
} = opts;
|
|
955
|
+
const [search, setSearch] = useState("");
|
|
956
|
+
const [filter, setFilter] = useState("all");
|
|
957
|
+
const [ruleFilters, setRuleFilters] = useState(EMPTY_RULE_FILTERS);
|
|
958
|
+
const [facetBrowseFilter, setFacetBrowseFilter] = useState(null);
|
|
959
|
+
useEffect(() => {
|
|
960
|
+
setSearch("");
|
|
961
|
+
setFilter("all");
|
|
962
|
+
setRuleFilters(EMPTY_RULE_FILTERS);
|
|
963
|
+
setFacetBrowseFilter(null);
|
|
964
|
+
}, [activeScope]);
|
|
965
|
+
const productBrowse = useProductBrowse({
|
|
966
|
+
SL,
|
|
967
|
+
collectionId,
|
|
968
|
+
search: activeScope === "product" ? search : "",
|
|
969
|
+
enabled: activeScope === "product" && !contextScope?.productId
|
|
970
|
+
});
|
|
971
|
+
const recordListEnabled = (activeScope === "rule" || activeScope === "collection") && !probeIsLoading;
|
|
972
|
+
const recordList = useRecordList({
|
|
973
|
+
ctx,
|
|
974
|
+
scopeKind: activeScope,
|
|
975
|
+
search,
|
|
976
|
+
filter,
|
|
977
|
+
classify: classify2,
|
|
978
|
+
contextScope,
|
|
979
|
+
enabled: recordListEnabled
|
|
980
|
+
});
|
|
981
|
+
const facetBrowse = useFacetBrowse({
|
|
982
|
+
SL,
|
|
983
|
+
collectionId,
|
|
984
|
+
existing: [],
|
|
985
|
+
enabled: (activeScope === "rule" || activeScope === "collection") && !probeIsLoading
|
|
986
|
+
});
|
|
987
|
+
const variantChildren = useProductChildren({
|
|
988
|
+
SL,
|
|
989
|
+
collectionId,
|
|
990
|
+
productId: selectedProductId,
|
|
991
|
+
kind: drillTab === "variant" ? "variant" : null
|
|
992
|
+
});
|
|
993
|
+
const batchChildren = useProductChildren({
|
|
994
|
+
SL,
|
|
995
|
+
collectionId,
|
|
996
|
+
productId: selectedProductId,
|
|
997
|
+
kind: drillTab === "batch" ? "batch" : null
|
|
998
|
+
});
|
|
999
|
+
const refetchAll = useCallback(() => {
|
|
1000
|
+
productBrowse.refetch();
|
|
1001
|
+
recordList.refetch();
|
|
1002
|
+
facetBrowse.refetch();
|
|
1003
|
+
variantChildren.refetch?.();
|
|
1004
|
+
batchChildren.refetch?.();
|
|
1005
|
+
}, [productBrowse, recordList, facetBrowse, variantChildren, batchChildren]);
|
|
1006
|
+
return {
|
|
1007
|
+
search,
|
|
1008
|
+
setSearch,
|
|
1009
|
+
filter,
|
|
1010
|
+
setFilter,
|
|
1011
|
+
ruleFilters,
|
|
1012
|
+
setRuleFilters,
|
|
1013
|
+
facetBrowseFilter,
|
|
1014
|
+
setFacetBrowseFilter,
|
|
1015
|
+
productBrowse,
|
|
1016
|
+
recordList,
|
|
1017
|
+
facetBrowse,
|
|
1018
|
+
variantChildren,
|
|
1019
|
+
batchChildren,
|
|
1020
|
+
refetchAll
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
var MESSAGE_TYPE = "smartlinks:unsaved-changes";
|
|
1024
|
+
function useUnsavedGuard({
|
|
1025
|
+
isDirty,
|
|
1026
|
+
label,
|
|
1027
|
+
confirm,
|
|
1028
|
+
disableBeforeUnload = false,
|
|
1029
|
+
disableParentMessage = false
|
|
1030
|
+
}) {
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (disableBeforeUnload || !isDirty || typeof window === "undefined") return;
|
|
1033
|
+
const handler = (e) => {
|
|
1034
|
+
e.preventDefault();
|
|
1035
|
+
e.returnValue = "";
|
|
1036
|
+
return "";
|
|
1037
|
+
};
|
|
1038
|
+
window.addEventListener("beforeunload", handler);
|
|
1039
|
+
return () => window.removeEventListener("beforeunload", handler);
|
|
1040
|
+
}, [isDirty, disableBeforeUnload]);
|
|
1041
|
+
useEffect(() => {
|
|
1042
|
+
if (disableParentMessage || typeof window === "undefined") return;
|
|
1043
|
+
if (window.parent === window) return;
|
|
1044
|
+
try {
|
|
1045
|
+
window.parent.postMessage({
|
|
1046
|
+
type: MESSAGE_TYPE,
|
|
1047
|
+
isDirty,
|
|
1048
|
+
label: label ?? null
|
|
1049
|
+
}, "*");
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
}, [isDirty, label, disableParentMessage]);
|
|
1053
|
+
useEffect(() => {
|
|
1054
|
+
if (disableParentMessage || typeof window === "undefined") return;
|
|
1055
|
+
if (window.parent === window) return;
|
|
1056
|
+
return () => {
|
|
1057
|
+
try {
|
|
1058
|
+
window.parent.postMessage({ type: MESSAGE_TYPE, isDirty: false, label: label ?? null }, "*");
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
}, [disableParentMessage, label]);
|
|
1063
|
+
const confirmDiscard = useCallback(
|
|
1064
|
+
async (message) => {
|
|
1065
|
+
if (!isDirty) return true;
|
|
1323
1066
|
const msg = message ?? "You have unsaved changes. Discard them?";
|
|
1324
1067
|
if (confirm) {
|
|
1325
1068
|
try {
|
|
@@ -1523,45 +1266,6 @@ var useConfirmDialog = () => {
|
|
|
1523
1266
|
)
|
|
1524
1267
|
};
|
|
1525
1268
|
};
|
|
1526
|
-
var DEFAULTS2 = {
|
|
1527
|
-
title: "Replace existing values?",
|
|
1528
|
-
body: "This destination already has saved values. Replace them?",
|
|
1529
|
-
confirmLabel: "Replace",
|
|
1530
|
-
cancelLabel: "Cancel"
|
|
1531
|
-
};
|
|
1532
|
-
var usePasteConfirm = () => {
|
|
1533
|
-
const [open, setOpen] = useState(false);
|
|
1534
|
-
const [state, setState] = useState(DEFAULTS2);
|
|
1535
|
-
const resolverRef = useRef(null);
|
|
1536
|
-
const confirm = useCallback((i18n) => {
|
|
1537
|
-
setState({ ...DEFAULTS2, ...i18n ?? {} });
|
|
1538
|
-
setOpen(true);
|
|
1539
|
-
return new Promise((resolve) => {
|
|
1540
|
-
resolverRef.current = resolve;
|
|
1541
|
-
});
|
|
1542
|
-
}, []);
|
|
1543
|
-
const handleChoice = useCallback((choice) => {
|
|
1544
|
-
setOpen(false);
|
|
1545
|
-
const r = resolverRef.current;
|
|
1546
|
-
resolverRef.current = null;
|
|
1547
|
-
r?.(choice === "discard");
|
|
1548
|
-
}, []);
|
|
1549
|
-
return {
|
|
1550
|
-
confirm,
|
|
1551
|
-
dialog: /* @__PURE__ */ jsx(
|
|
1552
|
-
ConfirmDialog,
|
|
1553
|
-
{
|
|
1554
|
-
open,
|
|
1555
|
-
title: state.title,
|
|
1556
|
-
body: state.body,
|
|
1557
|
-
saveLabel: "",
|
|
1558
|
-
discardLabel: state.confirmLabel,
|
|
1559
|
-
cancelLabel: state.cancelLabel,
|
|
1560
|
-
onChoice: handleChoice
|
|
1561
|
-
}
|
|
1562
|
-
)
|
|
1563
|
-
};
|
|
1564
|
-
};
|
|
1565
1269
|
var stores = /* @__PURE__ */ new Map();
|
|
1566
1270
|
var storageKey = (key) => `ra:clipboard:${key}`;
|
|
1567
1271
|
var getStore = (key) => {
|
|
@@ -1639,33 +1343,283 @@ function cloneValue(value) {
|
|
|
1639
1343
|
}
|
|
1640
1344
|
}
|
|
1641
1345
|
}
|
|
1642
|
-
var
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
batch: "Batches",
|
|
1648
|
-
rule: "Rules"
|
|
1346
|
+
var DEFAULTS2 = {
|
|
1347
|
+
title: "Replace existing values?",
|
|
1348
|
+
body: "This destination already has saved values. Replace them?",
|
|
1349
|
+
confirmLabel: "Replace",
|
|
1350
|
+
cancelLabel: "Cancel"
|
|
1649
1351
|
};
|
|
1650
|
-
var
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1352
|
+
var usePasteConfirm = () => {
|
|
1353
|
+
const [open, setOpen] = useState(false);
|
|
1354
|
+
const [state, setState] = useState(DEFAULTS2);
|
|
1355
|
+
const resolverRef = useRef(null);
|
|
1356
|
+
const confirm = useCallback((i18n) => {
|
|
1357
|
+
setState({ ...DEFAULTS2, ...i18n ?? {} });
|
|
1358
|
+
setOpen(true);
|
|
1359
|
+
return new Promise((resolve) => {
|
|
1360
|
+
resolverRef.current = resolve;
|
|
1361
|
+
});
|
|
1362
|
+
}, []);
|
|
1363
|
+
const handleChoice = useCallback((choice) => {
|
|
1364
|
+
setOpen(false);
|
|
1365
|
+
const r = resolverRef.current;
|
|
1366
|
+
resolverRef.current = null;
|
|
1367
|
+
r?.(choice === "discard");
|
|
1368
|
+
}, []);
|
|
1369
|
+
return {
|
|
1370
|
+
confirm,
|
|
1371
|
+
dialog: /* @__PURE__ */ jsx(
|
|
1372
|
+
ConfirmDialog,
|
|
1373
|
+
{
|
|
1374
|
+
open,
|
|
1375
|
+
title: state.title,
|
|
1376
|
+
body: state.body,
|
|
1377
|
+
saveLabel: "",
|
|
1378
|
+
discardLabel: state.confirmLabel,
|
|
1379
|
+
cancelLabel: state.cancelLabel,
|
|
1380
|
+
onChoice: handleChoice
|
|
1381
|
+
}
|
|
1382
|
+
)
|
|
1383
|
+
};
|
|
1384
|
+
};
|
|
1385
|
+
var ClipboardToast = ({ message, variant = "copy", onDismiss }) => {
|
|
1386
|
+
useEffect(() => {
|
|
1387
|
+
const t = window.setTimeout(onDismiss, 2500);
|
|
1388
|
+
return () => window.clearTimeout(t);
|
|
1389
|
+
}, [message, onDismiss]);
|
|
1390
|
+
const Icon = variant === "paste" ? ClipboardPaste : Copy;
|
|
1391
|
+
return /* @__PURE__ */ jsxs(
|
|
1392
|
+
"div",
|
|
1393
|
+
{
|
|
1394
|
+
role: "status",
|
|
1395
|
+
"aria-live": "polite",
|
|
1396
|
+
className: "ra-clipboard-toast",
|
|
1397
|
+
children: [
|
|
1398
|
+
/* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5 shrink-0", "aria-hidden": "true" }),
|
|
1399
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: message })
|
|
1400
|
+
]
|
|
1401
|
+
}
|
|
1402
|
+
);
|
|
1403
|
+
};
|
|
1404
|
+
function useShellClipboard(args) {
|
|
1405
|
+
const {
|
|
1406
|
+
enabled,
|
|
1407
|
+
appId,
|
|
1408
|
+
recordType,
|
|
1409
|
+
i18n,
|
|
1410
|
+
editorCtx,
|
|
1411
|
+
editingScope,
|
|
1412
|
+
editorHeaderLabel,
|
|
1413
|
+
isCollection,
|
|
1414
|
+
selectedItemId,
|
|
1415
|
+
selectedRecordId,
|
|
1416
|
+
resolved,
|
|
1417
|
+
onTelemetry,
|
|
1418
|
+
onCopyOverride,
|
|
1419
|
+
onPasteOverride,
|
|
1420
|
+
onLeftSelectRef
|
|
1421
|
+
} = args;
|
|
1422
|
+
const clipboard = useRecordClipboard({
|
|
1423
|
+
appId,
|
|
1424
|
+
recordType: recordType ?? "__default"
|
|
1425
|
+
});
|
|
1426
|
+
const pasteConfirm = usePasteConfirm();
|
|
1427
|
+
const [notice, setNotice] = useState(null);
|
|
1428
|
+
const copyCurrent = useCallback(() => {
|
|
1429
|
+
if (!enabled || !editingScope) return;
|
|
1430
|
+
const sourceValue = onCopyOverride ? onCopyOverride({ value: editorCtx.value, scope: editingScope }) : cloneValue(editorCtx.value);
|
|
1431
|
+
clipboard.set({
|
|
1432
|
+
value: sourceValue,
|
|
1433
|
+
sourceScope: editingScope,
|
|
1434
|
+
sourceRecordId: resolved.recordId,
|
|
1435
|
+
sourceLabel: editorHeaderLabel
|
|
1436
|
+
});
|
|
1437
|
+
onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: editingScope.raw });
|
|
1438
|
+
setNotice({
|
|
1439
|
+
message: i18n.copyToast.replace("{name}", editorHeaderLabel ?? editingScope.raw),
|
|
1440
|
+
variant: "copy"
|
|
1441
|
+
});
|
|
1442
|
+
}, [
|
|
1443
|
+
enabled,
|
|
1444
|
+
editingScope,
|
|
1445
|
+
onCopyOverride,
|
|
1446
|
+
editorCtx.value,
|
|
1447
|
+
clipboard,
|
|
1448
|
+
editorHeaderLabel,
|
|
1449
|
+
onTelemetry,
|
|
1450
|
+
recordType,
|
|
1451
|
+
i18n.copyToast,
|
|
1452
|
+
resolved.recordId
|
|
1453
|
+
]);
|
|
1454
|
+
const pasteCurrent = useCallback(async () => {
|
|
1455
|
+
if (!enabled || !editingScope || !clipboard.entry) return;
|
|
1456
|
+
const compat = checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
|
|
1457
|
+
if (compat.status === "denied") {
|
|
1458
|
+
setNotice({ message: compat.reason ?? "Paste not allowed here", variant: "paste" });
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (compat.status === "warn") {
|
|
1462
|
+
const ok = await pasteConfirm.confirm({
|
|
1463
|
+
title: i18n.pasteWarnTitle,
|
|
1464
|
+
body: compat.reason ?? "",
|
|
1465
|
+
confirmLabel: i18n.pasteWarnContinue,
|
|
1466
|
+
cancelLabel: i18n.pasteConfirmCancel
|
|
1467
|
+
});
|
|
1468
|
+
if (!ok) return;
|
|
1469
|
+
}
|
|
1470
|
+
const willReplace = resolved.source === "self";
|
|
1471
|
+
if (willReplace) {
|
|
1472
|
+
const ok = await pasteConfirm.confirm({
|
|
1473
|
+
title: i18n.pasteConfirmTitle,
|
|
1474
|
+
body: i18n.pasteConfirmBody.replace(
|
|
1475
|
+
"{destination}",
|
|
1476
|
+
editorHeaderLabel ?? editingScope.raw
|
|
1477
|
+
),
|
|
1478
|
+
confirmLabel: i18n.pasteConfirmReplace,
|
|
1479
|
+
cancelLabel: i18n.pasteConfirmCancel
|
|
1480
|
+
});
|
|
1481
|
+
if (!ok) return;
|
|
1482
|
+
}
|
|
1483
|
+
const sourceParsed = clipboard.entry.sourceScope;
|
|
1484
|
+
const transformed = onPasteOverride ? onPasteOverride(
|
|
1485
|
+
{ value: clipboard.entry.value, sourceScope: sourceParsed },
|
|
1486
|
+
{ scope: editingScope, currentValue: editorCtx.value ?? null }
|
|
1487
|
+
) : cloneValue(clipboard.entry.value);
|
|
1488
|
+
if (transformed === null) return;
|
|
1489
|
+
editorCtx.onChange(transformed);
|
|
1490
|
+
onTelemetry?.({
|
|
1491
|
+
type: "clipboard.paste",
|
|
1492
|
+
recordType,
|
|
1493
|
+
sourceRef: clipboard.entry.sourceScope.raw,
|
|
1494
|
+
destinationRef: editingScope.raw,
|
|
1495
|
+
replaced: willReplace
|
|
1496
|
+
});
|
|
1497
|
+
setNotice({
|
|
1498
|
+
message: i18n.pasteToast.replace("{name}", clipboard.entry.sourceLabel ?? clipboard.entry.sourceScope.raw),
|
|
1499
|
+
variant: "paste"
|
|
1500
|
+
});
|
|
1501
|
+
}, [
|
|
1502
|
+
enabled,
|
|
1503
|
+
editingScope,
|
|
1504
|
+
clipboard.entry,
|
|
1505
|
+
pasteConfirm,
|
|
1506
|
+
resolved.source,
|
|
1507
|
+
onPasteOverride,
|
|
1508
|
+
editorCtx,
|
|
1509
|
+
onTelemetry,
|
|
1510
|
+
recordType,
|
|
1511
|
+
editorHeaderLabel,
|
|
1512
|
+
i18n.pasteWarnTitle,
|
|
1513
|
+
i18n.pasteWarnContinue,
|
|
1514
|
+
i18n.pasteConfirmCancel,
|
|
1515
|
+
i18n.pasteConfirmTitle,
|
|
1516
|
+
i18n.pasteConfirmBody,
|
|
1517
|
+
i18n.pasteConfirmReplace,
|
|
1518
|
+
i18n.pasteToast
|
|
1519
|
+
]);
|
|
1520
|
+
const editorPasteCompat = useMemo(() => {
|
|
1521
|
+
if (!enabled || !clipboard.entry || !editingScope) return null;
|
|
1522
|
+
const sameRecord = clipboard.entry.sourceRecordId && resolved.recordId && clipboard.entry.sourceRecordId === resolved.recordId;
|
|
1523
|
+
const sameAnchor = !clipboard.entry.sourceRecordId && clipboard.entry.sourceScope.raw === editingScope.raw;
|
|
1524
|
+
if (sameRecord || sameAnchor) return { status: "denied" };
|
|
1525
|
+
return checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
|
|
1526
|
+
}, [enabled, clipboard.entry, editingScope, resolved.recordId]);
|
|
1527
|
+
const editorClipboard = enabled && editingScope ? {
|
|
1528
|
+
onCopy: copyCurrent,
|
|
1529
|
+
onPaste: () => {
|
|
1530
|
+
void pasteCurrent();
|
|
1531
|
+
},
|
|
1532
|
+
canCopy: true,
|
|
1533
|
+
canPaste: !!clipboard.entry && editorPasteCompat?.status !== "denied",
|
|
1534
|
+
pasteSourceLabel: clipboard.entry?.sourceLabel,
|
|
1535
|
+
pasteWillReplace: resolved.source === "self" && !!clipboard.entry
|
|
1536
|
+
} : void 0;
|
|
1537
|
+
const [pendingPasteTarget, setPendingPasteTarget] = useState(null);
|
|
1538
|
+
useEffect(() => {
|
|
1539
|
+
if (!pendingPasteTarget) return;
|
|
1540
|
+
if (!editingScope) return;
|
|
1541
|
+
const matched = pendingPasteTarget.kind === "record" ? (isCollection ? selectedItemId : selectedRecordId) === pendingPasteTarget.recordId : editingScope.raw === pendingPasteTarget.ref;
|
|
1542
|
+
if (!matched) return;
|
|
1543
|
+
const t = window.setTimeout(() => {
|
|
1544
|
+
setPendingPasteTarget(null);
|
|
1545
|
+
void pasteCurrent();
|
|
1546
|
+
}, 0);
|
|
1547
|
+
return () => window.clearTimeout(t);
|
|
1548
|
+
}, [pendingPasteTarget, editingScope, isCollection, selectedItemId, selectedRecordId, pasteCurrent]);
|
|
1549
|
+
const rowClipboard = enabled ? (record) => {
|
|
1550
|
+
const summaryHasData = record.data != null;
|
|
1551
|
+
const sourceParsed = record.scope;
|
|
1552
|
+
const compat = clipboard.entry ? checkPasteCompatibility(clipboard.entry.sourceScope, sourceParsed) : null;
|
|
1553
|
+
const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === record.ref : false;
|
|
1554
|
+
return {
|
|
1555
|
+
onCopy: summaryHasData ? () => {
|
|
1556
|
+
const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
|
|
1557
|
+
clipboard.set({
|
|
1558
|
+
value,
|
|
1559
|
+
sourceScope: sourceParsed,
|
|
1560
|
+
sourceRecordId: record.id ?? void 0,
|
|
1561
|
+
sourceLabel: record.label
|
|
1562
|
+
});
|
|
1563
|
+
onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: record.ref });
|
|
1564
|
+
setNotice({
|
|
1565
|
+
message: i18n.copyToast.replace("{name}", record.label),
|
|
1566
|
+
variant: "copy"
|
|
1567
|
+
});
|
|
1568
|
+
} : void 0,
|
|
1569
|
+
onPaste: () => {
|
|
1570
|
+
onLeftSelectRef.current?.(record);
|
|
1571
|
+
setPendingPasteTarget(
|
|
1572
|
+
record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: record.ref }
|
|
1573
|
+
);
|
|
1574
|
+
},
|
|
1575
|
+
canPaste: !!clipboard.entry && !sameTarget && compat?.status !== "denied",
|
|
1576
|
+
pasteWillReplace: record.status === "configured",
|
|
1577
|
+
clipboardSourceLabel: clipboard.entry?.sourceLabel
|
|
1578
|
+
};
|
|
1579
|
+
} : void 0;
|
|
1580
|
+
const toast = notice ? /* @__PURE__ */ jsx(
|
|
1581
|
+
ClipboardToast,
|
|
1582
|
+
{
|
|
1583
|
+
message: notice.message,
|
|
1584
|
+
variant: notice.variant,
|
|
1585
|
+
onDismiss: () => setNotice(null)
|
|
1586
|
+
}
|
|
1587
|
+
) : null;
|
|
1588
|
+
return {
|
|
1589
|
+
editorClipboard,
|
|
1590
|
+
rowClipboard,
|
|
1591
|
+
confirmDialog: pasteConfirm.dialog,
|
|
1592
|
+
toast,
|
|
1593
|
+
store: clipboard
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
var LABELS = {
|
|
1597
|
+
collection: "Global",
|
|
1598
|
+
product: "Products",
|
|
1599
|
+
facet: "Shared",
|
|
1600
|
+
variant: "Variants",
|
|
1601
|
+
batch: "Batches",
|
|
1602
|
+
rule: "Rules"
|
|
1603
|
+
};
|
|
1604
|
+
var ScopeTabs = ({
|
|
1605
|
+
scopes,
|
|
1606
|
+
active,
|
|
1607
|
+
onChange,
|
|
1608
|
+
loading = false,
|
|
1609
|
+
counts,
|
|
1610
|
+
icons
|
|
1611
|
+
}) => {
|
|
1612
|
+
const iconMap = icons ?? DEFAULT_ICONS.scope;
|
|
1613
|
+
return /* @__PURE__ */ jsx("div", { role: "tablist", className: "ra-tabs", "aria-label": "Record scope", children: scopes.map((s) => {
|
|
1614
|
+
const Icon = iconMap[s] ?? DEFAULT_ICONS.scope[s];
|
|
1615
|
+
const isActive = active === s;
|
|
1616
|
+
const count = counts?.[s];
|
|
1617
|
+
return /* @__PURE__ */ jsxs(
|
|
1618
|
+
"button",
|
|
1619
|
+
{
|
|
1620
|
+
type: "button",
|
|
1621
|
+
role: "tab",
|
|
1622
|
+
"aria-selected": isActive,
|
|
1669
1623
|
onClick: () => onChange(s),
|
|
1670
1624
|
disabled: loading,
|
|
1671
1625
|
className: "ra-tab",
|
|
@@ -2030,104 +1984,6 @@ var ProductList = RecordList;
|
|
|
2030
1984
|
var FacetList = RecordList;
|
|
2031
1985
|
var VariantList = RecordList;
|
|
2032
1986
|
var BatchList = RecordList;
|
|
2033
|
-
var EMPTY_RULE_FILTERS = { facetKeys: [], minClauses: null };
|
|
2034
|
-
var COMPLEXITY_THRESHOLDS = [3, 5, 10];
|
|
2035
|
-
var ruleOf = (r) => r.facetRule ?? null;
|
|
2036
|
-
function RuleFilterChips({ source, value, onChange }) {
|
|
2037
|
-
const facetKeyEntries = useMemo(() => {
|
|
2038
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2039
|
-
for (const r of source) {
|
|
2040
|
-
const rule = ruleOf(r);
|
|
2041
|
-
if (!rule) continue;
|
|
2042
|
-
for (const c of rule.all ?? []) {
|
|
2043
|
-
counts.set(c.facetKey, (counts.get(c.facetKey) ?? 0) + 1);
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
2047
|
-
}, [source]);
|
|
2048
|
-
const maxClauses = useMemo(() => {
|
|
2049
|
-
let max = 0;
|
|
2050
|
-
for (const r of source) {
|
|
2051
|
-
const rule = ruleOf(r);
|
|
2052
|
-
if (!rule) continue;
|
|
2053
|
-
max = Math.max(max, rule.all?.length ?? 0);
|
|
2054
|
-
}
|
|
2055
|
-
return max;
|
|
2056
|
-
}, [source]);
|
|
2057
|
-
if (facetKeyEntries.length === 0 && maxClauses < 2) return null;
|
|
2058
|
-
const toggleKey = (key) => {
|
|
2059
|
-
const has = value.facetKeys.includes(key);
|
|
2060
|
-
const next = has ? value.facetKeys.filter((k) => k !== key) : [...value.facetKeys, key];
|
|
2061
|
-
onChange({ ...value, facetKeys: next });
|
|
2062
|
-
};
|
|
2063
|
-
const setMin = (n) => onChange({ ...value, minClauses: n });
|
|
2064
|
-
const hasAny = value.facetKeys.length > 0 || value.minClauses != null;
|
|
2065
|
-
return /* @__PURE__ */ jsxs("div", { className: "ra-rule-filters", children: [
|
|
2066
|
-
/* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter rules by facet", children: facetKeyEntries.map(([key, count]) => {
|
|
2067
|
-
const active = value.facetKeys.includes(key);
|
|
2068
|
-
return /* @__PURE__ */ jsxs(
|
|
2069
|
-
"button",
|
|
2070
|
-
{
|
|
2071
|
-
type: "button",
|
|
2072
|
-
onClick: () => toggleKey(key),
|
|
2073
|
-
className: "ra-rule-filter-chip",
|
|
2074
|
-
"data-active": active ? "true" : "false",
|
|
2075
|
-
"aria-pressed": active,
|
|
2076
|
-
title: `Show rules using ${key}`,
|
|
2077
|
-
children: [
|
|
2078
|
-
/* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-label", children: key }),
|
|
2079
|
-
/* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-count", children: count })
|
|
2080
|
-
]
|
|
2081
|
-
},
|
|
2082
|
-
key
|
|
2083
|
-
);
|
|
2084
|
-
}) }),
|
|
2085
|
-
maxClauses >= 2 && /* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter by clause count", children: COMPLEXITY_THRESHOLDS.filter((n) => maxClauses >= n).map((n) => {
|
|
2086
|
-
const active = value.minClauses === n;
|
|
2087
|
-
return /* @__PURE__ */ jsxs(
|
|
2088
|
-
"button",
|
|
2089
|
-
{
|
|
2090
|
-
type: "button",
|
|
2091
|
-
onClick: () => setMin(active ? null : n),
|
|
2092
|
-
className: "ra-rule-filter-chip",
|
|
2093
|
-
"data-tone": "complexity",
|
|
2094
|
-
"data-active": active ? "true" : "false",
|
|
2095
|
-
"aria-pressed": active,
|
|
2096
|
-
title: `Only rules with \u2265 ${n} facets`,
|
|
2097
|
-
children: [
|
|
2098
|
-
"\u2265 ",
|
|
2099
|
-
n,
|
|
2100
|
-
" facets"
|
|
2101
|
-
]
|
|
2102
|
-
},
|
|
2103
|
-
n
|
|
2104
|
-
);
|
|
2105
|
-
}) }),
|
|
2106
|
-
hasAny && /* @__PURE__ */ jsx(
|
|
2107
|
-
"button",
|
|
2108
|
-
{
|
|
2109
|
-
type: "button",
|
|
2110
|
-
onClick: () => onChange(EMPTY_RULE_FILTERS),
|
|
2111
|
-
className: "ra-rule-filter-clear",
|
|
2112
|
-
"aria-label": "Clear rule filters",
|
|
2113
|
-
children: "Clear filters"
|
|
2114
|
-
}
|
|
2115
|
-
)
|
|
2116
|
-
] });
|
|
2117
|
-
}
|
|
2118
|
-
function applyRuleFilters(items, filters) {
|
|
2119
|
-
if (filters.facetKeys.length === 0 && filters.minClauses == null) return items;
|
|
2120
|
-
return items.filter((r) => {
|
|
2121
|
-
const rule = ruleOf(r);
|
|
2122
|
-
if (!rule) return false;
|
|
2123
|
-
if (filters.minClauses != null && (rule.all?.length ?? 0) < filters.minClauses) return false;
|
|
2124
|
-
if (filters.facetKeys.length > 0) {
|
|
2125
|
-
const keys = new Set((rule.all ?? []).map((c) => c.facetKey));
|
|
2126
|
-
if (!filters.facetKeys.some((k) => keys.has(k))) return false;
|
|
2127
|
-
}
|
|
2128
|
-
return true;
|
|
2129
|
-
});
|
|
2130
|
-
}
|
|
2131
1987
|
var COLLAPSED_FACET_CAP = 6;
|
|
2132
1988
|
function FacetBrowseFilter({
|
|
2133
1989
|
facets,
|
|
@@ -2509,6 +2365,103 @@ var createDefaultDeepLinkAdapter = (paramNames) => ({
|
|
|
2509
2365
|
}
|
|
2510
2366
|
});
|
|
2511
2367
|
|
|
2368
|
+
// src/components/RecordsAdmin/data/postMessageDeepLinkAdapter.ts
|
|
2369
|
+
var CONTEXT_KEYS = [
|
|
2370
|
+
"dark",
|
|
2371
|
+
"appId",
|
|
2372
|
+
"collectionId",
|
|
2373
|
+
"productId",
|
|
2374
|
+
"proofId",
|
|
2375
|
+
"lang",
|
|
2376
|
+
"theme"
|
|
2377
|
+
];
|
|
2378
|
+
var findQueryString = (loc) => {
|
|
2379
|
+
if (loc.search && loc.search.length > 1) {
|
|
2380
|
+
return loc.search.startsWith("?") ? loc.search.slice(1) : loc.search;
|
|
2381
|
+
}
|
|
2382
|
+
if (loc.hash && loc.hash.includes("?")) {
|
|
2383
|
+
return loc.hash.slice(loc.hash.indexOf("?") + 1);
|
|
2384
|
+
}
|
|
2385
|
+
return "";
|
|
2386
|
+
};
|
|
2387
|
+
var findPath = (loc) => {
|
|
2388
|
+
if (loc.hash && loc.hash.startsWith("#")) {
|
|
2389
|
+
const hash = loc.hash.slice(1);
|
|
2390
|
+
const qIdx = hash.indexOf("?");
|
|
2391
|
+
const hashPath = qIdx >= 0 ? hash.slice(0, qIdx) : hash;
|
|
2392
|
+
if (hashPath) return hashPath;
|
|
2393
|
+
}
|
|
2394
|
+
return loc.pathname || "/";
|
|
2395
|
+
};
|
|
2396
|
+
var isInSmartLinksIframe = () => {
|
|
2397
|
+
if (typeof window === "undefined") return false;
|
|
2398
|
+
try {
|
|
2399
|
+
return window.parent != null && window.parent !== window;
|
|
2400
|
+
} catch {
|
|
2401
|
+
return true;
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
var createPostMessageDeepLinkAdapter = (paramNames) => {
|
|
2405
|
+
const lastShellState = {};
|
|
2406
|
+
const post = (path) => {
|
|
2407
|
+
if (typeof window === "undefined") return;
|
|
2408
|
+
const params = new URLSearchParams(findQueryString(window.location));
|
|
2409
|
+
const context = {};
|
|
2410
|
+
const state = {};
|
|
2411
|
+
params.forEach((value, key) => {
|
|
2412
|
+
if (key === "theme") return;
|
|
2413
|
+
if (CONTEXT_KEYS.includes(key)) context[key] = value;
|
|
2414
|
+
else state[key] = value;
|
|
2415
|
+
});
|
|
2416
|
+
const overlay = (key, paramKey) => {
|
|
2417
|
+
const v = lastShellState[key];
|
|
2418
|
+
if (v == null || v === "") delete state[paramKey];
|
|
2419
|
+
else state[paramKey] = v;
|
|
2420
|
+
};
|
|
2421
|
+
overlay("recordId", paramNames.recordId);
|
|
2422
|
+
overlay("scope", paramNames.scope);
|
|
2423
|
+
overlay("view", paramNames.view);
|
|
2424
|
+
try {
|
|
2425
|
+
window.parent.postMessage({
|
|
2426
|
+
type: "smartlinks-route-change",
|
|
2427
|
+
path,
|
|
2428
|
+
context,
|
|
2429
|
+
state,
|
|
2430
|
+
appId: context.appId
|
|
2431
|
+
}, "*");
|
|
2432
|
+
} catch {
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
return {
|
|
2436
|
+
read() {
|
|
2437
|
+
if (typeof window === "undefined") return {};
|
|
2438
|
+
const params = new URLSearchParams(findQueryString(window.location));
|
|
2439
|
+
return {
|
|
2440
|
+
recordId: params.get(paramNames.recordId),
|
|
2441
|
+
scope: params.get(paramNames.scope),
|
|
2442
|
+
view: params.get(paramNames.view)
|
|
2443
|
+
};
|
|
2444
|
+
},
|
|
2445
|
+
write(partial) {
|
|
2446
|
+
if (typeof window === "undefined") return;
|
|
2447
|
+
if ("recordId" in partial) lastShellState.recordId = partial.recordId ?? null;
|
|
2448
|
+
if ("scope" in partial) lastShellState.scope = partial.scope ?? null;
|
|
2449
|
+
if ("view" in partial) lastShellState.view = partial.view ?? null;
|
|
2450
|
+
post(findPath(window.location));
|
|
2451
|
+
},
|
|
2452
|
+
subscribe(listener) {
|
|
2453
|
+
if (typeof window === "undefined") return () => {
|
|
2454
|
+
};
|
|
2455
|
+
window.addEventListener("popstate", listener);
|
|
2456
|
+
window.addEventListener("hashchange", listener);
|
|
2457
|
+
return () => {
|
|
2458
|
+
window.removeEventListener("popstate", listener);
|
|
2459
|
+
window.removeEventListener("hashchange", listener);
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
};
|
|
2464
|
+
|
|
2512
2465
|
// src/components/RecordsAdmin/hooks/useDeepLinkState.ts
|
|
2513
2466
|
var SMART_PUSH = ["record.open", "record.close", "scope"];
|
|
2514
2467
|
var classify = (mode, kind) => {
|
|
@@ -2528,7 +2481,7 @@ function useDeepLinkState(options) {
|
|
|
2528
2481
|
if (!enabled) return null;
|
|
2529
2482
|
if (options?.adapter) return options.adapter;
|
|
2530
2483
|
if (!defaultAdapterRef.current) {
|
|
2531
|
-
defaultAdapterRef.current = createDefaultDeepLinkAdapter(paramNames);
|
|
2484
|
+
defaultAdapterRef.current = isInSmartLinksIframe() ? createPostMessageDeepLinkAdapter(paramNames) : createDefaultDeepLinkAdapter(paramNames);
|
|
2532
2485
|
}
|
|
2533
2486
|
return defaultAdapterRef.current;
|
|
2534
2487
|
}, [enabled, options?.adapter, paramNames]);
|
|
@@ -2902,6 +2855,49 @@ function RecordEditor({
|
|
|
2902
2855
|
)
|
|
2903
2856
|
] });
|
|
2904
2857
|
}
|
|
2858
|
+
var isFacetRuleValid = (rule) => {
|
|
2859
|
+
if (!rule || !Array.isArray(rule.all) || rule.all.length === 0) return false;
|
|
2860
|
+
return rule.all.every(
|
|
2861
|
+
(c) => !!c.facetKey && Array.isArray(c.anyOf) && c.anyOf.length > 0
|
|
2862
|
+
);
|
|
2863
|
+
};
|
|
2864
|
+
function useRulePreview(args) {
|
|
2865
|
+
const {
|
|
2866
|
+
SL,
|
|
2867
|
+
collectionId,
|
|
2868
|
+
appId,
|
|
2869
|
+
rule,
|
|
2870
|
+
limit = 20,
|
|
2871
|
+
debounceMs = 350,
|
|
2872
|
+
enabled = true
|
|
2873
|
+
} = args;
|
|
2874
|
+
const [debouncedRule, setDebouncedRule] = useState(rule);
|
|
2875
|
+
const timer = useRef(null);
|
|
2876
|
+
useEffect(() => {
|
|
2877
|
+
if (timer.current) clearTimeout(timer.current);
|
|
2878
|
+
timer.current = setTimeout(() => setDebouncedRule(rule), debounceMs);
|
|
2879
|
+
return () => {
|
|
2880
|
+
if (timer.current) clearTimeout(timer.current);
|
|
2881
|
+
};
|
|
2882
|
+
}, [rule, debounceMs]);
|
|
2883
|
+
const valid = isFacetRuleValid(debouncedRule);
|
|
2884
|
+
const query = useQuery({
|
|
2885
|
+
queryKey: ["records-admin", "preview-rule", collectionId, appId, debouncedRule, limit],
|
|
2886
|
+
enabled: enabled && !!collectionId && !!appId && valid,
|
|
2887
|
+
staleTime: 3e4,
|
|
2888
|
+
queryFn: () => SL.app.records.previewRule(collectionId, appId, {
|
|
2889
|
+
facetRule: debouncedRule,
|
|
2890
|
+
limit
|
|
2891
|
+
})
|
|
2892
|
+
});
|
|
2893
|
+
return {
|
|
2894
|
+
totalMatches: query.data?.total ?? null,
|
|
2895
|
+
sampleProductIds: (query.data?.matchingProducts ?? []).map((p) => p.productId),
|
|
2896
|
+
isLoading: query.isFetching,
|
|
2897
|
+
isStale: rule !== debouncedRule,
|
|
2898
|
+
error: query.error ?? null
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2905
2901
|
function summariseRule(clauses, facets) {
|
|
2906
2902
|
let valueCount = 0;
|
|
2907
2903
|
const parts = clauses.map((c) => {
|
|
@@ -4298,11 +4294,11 @@ var statusDot = (status) => {
|
|
|
4298
4294
|
}
|
|
4299
4295
|
};
|
|
4300
4296
|
var UnsavedTray = ({
|
|
4301
|
-
|
|
4297
|
+
items,
|
|
4302
4298
|
isSaving,
|
|
4303
4299
|
onSaveAll,
|
|
4304
4300
|
onDiscardAll,
|
|
4305
|
-
|
|
4301
|
+
onOpenItem,
|
|
4306
4302
|
onJumpToError,
|
|
4307
4303
|
saveLabel,
|
|
4308
4304
|
discardLabel,
|
|
@@ -4318,27 +4314,8 @@ var UnsavedTray = ({
|
|
|
4318
4314
|
const wrapRef = useRef(null);
|
|
4319
4315
|
const SI = SaveIcon ?? Save;
|
|
4320
4316
|
const DI = DiscardIcon ?? Undo2;
|
|
4321
|
-
const
|
|
4322
|
-
|
|
4323
|
-
for (const d of drafts) {
|
|
4324
|
-
const bucket = d.recordId || d.scopeRaw || d.key;
|
|
4325
|
-
const existing = byBucket.get(bucket);
|
|
4326
|
-
if (!existing) {
|
|
4327
|
-
byBucket.set(bucket, d);
|
|
4328
|
-
continue;
|
|
4329
|
-
}
|
|
4330
|
-
const existingSynthetic = existing.key.startsWith("draft:");
|
|
4331
|
-
const incomingSynthetic = d.key.startsWith("draft:");
|
|
4332
|
-
if (existingSynthetic && !incomingSynthetic) byBucket.set(bucket, d);
|
|
4333
|
-
}
|
|
4334
|
-
return Array.from(byBucket.values()).sort((a, b) => a.order - b.order);
|
|
4335
|
-
}, [drafts]);
|
|
4336
|
-
const liveDrafts = useMemo(
|
|
4337
|
-
() => uniqueDrafts.filter((d) => d.status !== "saved"),
|
|
4338
|
-
[uniqueDrafts]
|
|
4339
|
-
);
|
|
4340
|
-
const total = liveDrafts.length;
|
|
4341
|
-
const errors = liveDrafts.filter((d) => d.status === "error").length;
|
|
4317
|
+
const total = items.length;
|
|
4318
|
+
const errors = items.filter((i) => i.status === "error").length;
|
|
4342
4319
|
const isSingle = total === 1;
|
|
4343
4320
|
useEffect(() => {
|
|
4344
4321
|
if (!open) return;
|
|
@@ -4349,7 +4326,7 @@ var UnsavedTray = ({
|
|
|
4349
4326
|
return () => window.removeEventListener("mousedown", onDoc);
|
|
4350
4327
|
}, [open]);
|
|
4351
4328
|
if (total === 0) return null;
|
|
4352
|
-
const countLabel = isSingle ?
|
|
4329
|
+
const countLabel = isSingle ? items[0].label || "this record" : countTemplate.replace("{n}", String(total));
|
|
4353
4330
|
return /* @__PURE__ */ jsxs(
|
|
4354
4331
|
"div",
|
|
4355
4332
|
{
|
|
@@ -4419,14 +4396,14 @@ var UnsavedTray = ({
|
|
|
4419
4396
|
}
|
|
4420
4397
|
)
|
|
4421
4398
|
] }),
|
|
4422
|
-
open && !isSingle && /* @__PURE__ */ jsx("div", { className: "ra-unsaved-popover", role: "menu", children:
|
|
4399
|
+
open && !isSingle && /* @__PURE__ */ jsx("div", { className: "ra-unsaved-popover", role: "menu", children: items.map((it) => /* @__PURE__ */ jsxs(
|
|
4423
4400
|
"button",
|
|
4424
4401
|
{
|
|
4425
4402
|
type: "button",
|
|
4426
4403
|
className: "ra-unsaved-popover-row",
|
|
4427
4404
|
onClick: () => {
|
|
4428
4405
|
setOpen(false);
|
|
4429
|
-
|
|
4406
|
+
onOpenItem?.(it);
|
|
4430
4407
|
},
|
|
4431
4408
|
role: "menuitem",
|
|
4432
4409
|
children: [
|
|
@@ -4434,31 +4411,29 @@ var UnsavedTray = ({
|
|
|
4434
4411
|
"span",
|
|
4435
4412
|
{
|
|
4436
4413
|
className: "ra-unsaved-popover-dot",
|
|
4437
|
-
style: { background: statusDot(
|
|
4414
|
+
style: { background: statusDot(it.status) },
|
|
4438
4415
|
"aria-hidden": "true"
|
|
4439
4416
|
}
|
|
4440
4417
|
),
|
|
4441
|
-
/* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-label", children:
|
|
4442
|
-
|
|
4443
|
-
d.status === "error" && /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-err", children: "failed" })
|
|
4418
|
+
/* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-label", children: it.label || "Default" }),
|
|
4419
|
+
it.status === "error" && /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-err", children: "failed" })
|
|
4444
4420
|
]
|
|
4445
4421
|
},
|
|
4446
|
-
|
|
4422
|
+
it.editorId
|
|
4447
4423
|
)) })
|
|
4448
4424
|
]
|
|
4449
4425
|
}
|
|
4450
4426
|
);
|
|
4451
4427
|
};
|
|
4452
4428
|
var HEADER_PILL_OVERFLOW_THRESHOLD = 5;
|
|
4453
|
-
function canRenderInHeader(
|
|
4454
|
-
|
|
4455
|
-
if (
|
|
4456
|
-
if (
|
|
4457
|
-
if (live.some((d) => d.status === "error")) return false;
|
|
4429
|
+
function canRenderInHeader(items) {
|
|
4430
|
+
if (items.length === 0) return false;
|
|
4431
|
+
if (items.length >= HEADER_PILL_OVERFLOW_THRESHOLD) return false;
|
|
4432
|
+
if (items.some((i) => i.status === "error")) return false;
|
|
4458
4433
|
return true;
|
|
4459
4434
|
}
|
|
4460
4435
|
var HeaderUnsavedPill = ({
|
|
4461
|
-
|
|
4436
|
+
items,
|
|
4462
4437
|
isSaving,
|
|
4463
4438
|
onSaveAll,
|
|
4464
4439
|
onDiscardAll,
|
|
@@ -4472,24 +4447,8 @@ var HeaderUnsavedPill = ({
|
|
|
4472
4447
|
}) => {
|
|
4473
4448
|
const SI = SaveIcon ?? Save;
|
|
4474
4449
|
const DI = DiscardIcon ?? Undo2;
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
for (const d of drafts) {
|
|
4478
|
-
if (d.status === "saved") continue;
|
|
4479
|
-
const bucket = d.recordId || d.scopeRaw || d.key;
|
|
4480
|
-
const existing = byBucket.get(bucket);
|
|
4481
|
-
if (!existing) {
|
|
4482
|
-
byBucket.set(bucket, d);
|
|
4483
|
-
continue;
|
|
4484
|
-
}
|
|
4485
|
-
const existingSynthetic = existing.key.startsWith("draft:");
|
|
4486
|
-
const incomingSynthetic = d.key.startsWith("draft:");
|
|
4487
|
-
if (existingSynthetic && !incomingSynthetic) byBucket.set(bucket, d);
|
|
4488
|
-
}
|
|
4489
|
-
return Array.from(byBucket.values());
|
|
4490
|
-
}, [drafts]);
|
|
4491
|
-
if (liveDrafts.length === 0) return null;
|
|
4492
|
-
const total = liveDrafts.length;
|
|
4450
|
+
if (items.length === 0) return null;
|
|
4451
|
+
const total = items.length;
|
|
4493
4452
|
const isSingle = total === 1;
|
|
4494
4453
|
return /* @__PURE__ */ jsxs(
|
|
4495
4454
|
"div",
|
|
@@ -4537,8 +4496,8 @@ var HeaderUnsavedPill = ({
|
|
|
4537
4496
|
};
|
|
4538
4497
|
var SaveAllProgress = ({
|
|
4539
4498
|
open,
|
|
4540
|
-
|
|
4541
|
-
|
|
4499
|
+
items,
|
|
4500
|
+
saveOne,
|
|
4542
4501
|
onClose,
|
|
4543
4502
|
onJumpToError,
|
|
4544
4503
|
i18n
|
|
@@ -4546,42 +4505,44 @@ var SaveAllProgress = ({
|
|
|
4546
4505
|
const [running, setRunning] = useState(false);
|
|
4547
4506
|
const [done, setDone] = useState(false);
|
|
4548
4507
|
const stopRef = useRef(false);
|
|
4549
|
-
const [
|
|
4508
|
+
const [batchIds, setBatchIds] = useState([]);
|
|
4550
4509
|
useEffect(() => {
|
|
4551
4510
|
if (!open) {
|
|
4552
4511
|
setRunning(false);
|
|
4553
4512
|
setDone(false);
|
|
4554
4513
|
stopRef.current = false;
|
|
4555
|
-
|
|
4514
|
+
setBatchIds([]);
|
|
4556
4515
|
return;
|
|
4557
4516
|
}
|
|
4558
|
-
const initial =
|
|
4559
|
-
|
|
4517
|
+
const initial = items.map((i) => i.editorId);
|
|
4518
|
+
setBatchIds(initial);
|
|
4560
4519
|
void runBatch(initial);
|
|
4561
4520
|
}, [open]);
|
|
4562
|
-
const runBatch = async (
|
|
4521
|
+
const runBatch = async (ids) => {
|
|
4563
4522
|
setRunning(true);
|
|
4564
4523
|
setDone(false);
|
|
4565
4524
|
stopRef.current = false;
|
|
4566
|
-
for (const
|
|
4525
|
+
for (const id of ids) {
|
|
4567
4526
|
if (stopRef.current) break;
|
|
4568
|
-
const live = store.get(item.key);
|
|
4569
|
-
if (!live) continue;
|
|
4570
|
-
if (live.status === "saved") continue;
|
|
4571
|
-
store.setStatus(live.key, "saving");
|
|
4572
4527
|
try {
|
|
4573
|
-
await
|
|
4574
|
-
|
|
4575
|
-
} catch (err) {
|
|
4576
|
-
store.setStatus(live.key, "error", err);
|
|
4528
|
+
await saveOne(id);
|
|
4529
|
+
} catch {
|
|
4577
4530
|
}
|
|
4578
4531
|
}
|
|
4579
4532
|
setRunning(false);
|
|
4580
4533
|
setDone(true);
|
|
4581
4534
|
};
|
|
4582
|
-
const visible =
|
|
4535
|
+
const visible = useMemo(() => batchIds.map((id) => {
|
|
4536
|
+
const live = items.find((i) => i.editorId === id);
|
|
4537
|
+
return live ?? {
|
|
4538
|
+
editorId: id,
|
|
4539
|
+
label: "",
|
|
4540
|
+
status: "saved",
|
|
4541
|
+
scope: { kind: "collection", raw: "" }
|
|
4542
|
+
};
|
|
4543
|
+
}), [batchIds, items]);
|
|
4583
4544
|
const total = visible.length;
|
|
4584
|
-
const completed = visible.filter((d) => d.status
|
|
4545
|
+
const completed = visible.filter((d) => d.status !== "dirty" && d.status !== "saving").length;
|
|
4585
4546
|
const errors = visible.filter((d) => d.status === "error");
|
|
4586
4547
|
const successAll = done && errors.length === 0 && completed === total;
|
|
4587
4548
|
useEffect(() => {
|
|
@@ -4630,7 +4591,7 @@ var SaveAllProgress = ({
|
|
|
4630
4591
|
] }),
|
|
4631
4592
|
/* @__PURE__ */ jsx("span", { className: "ra-saveall-label", children: d.label || "Default" }),
|
|
4632
4593
|
d.status === "error" && /* @__PURE__ */ jsx("span", { className: "ra-saveall-err", title: String(d.error?.message ?? d.error ?? ""), children: d.error?.message ?? "Save failed" })
|
|
4633
|
-
] }, d.
|
|
4594
|
+
] }, d.editorId)) }),
|
|
4634
4595
|
/* @__PURE__ */ jsxs("div", { className: "ra-saveall-actions", children: [
|
|
4635
4596
|
running && /* @__PURE__ */ jsx(
|
|
4636
4597
|
"button",
|
|
@@ -4661,7 +4622,7 @@ var SaveAllProgress = ({
|
|
|
4661
4622
|
{
|
|
4662
4623
|
type: "button",
|
|
4663
4624
|
className: "ra-unsaved-btn ra-unsaved-btn-primary",
|
|
4664
|
-
onClick: () => void runBatch(errors),
|
|
4625
|
+
onClick: () => void runBatch(errors.map((e) => e.editorId)),
|
|
4665
4626
|
children: i18n.retryFailed
|
|
4666
4627
|
}
|
|
4667
4628
|
)
|
|
@@ -4682,81 +4643,734 @@ var SaveAllProgress = ({
|
|
|
4682
4643
|
}
|
|
4683
4644
|
);
|
|
4684
4645
|
};
|
|
4685
|
-
var ClipboardToast = ({ message, variant = "copy", onDismiss }) => {
|
|
4686
|
-
useEffect(() => {
|
|
4687
|
-
const t = window.setTimeout(onDismiss, 2500);
|
|
4688
|
-
return () => window.clearTimeout(t);
|
|
4689
|
-
}, [message, onDismiss]);
|
|
4690
|
-
const Icon = variant === "paste" ? ClipboardPaste : Copy;
|
|
4691
|
-
return /* @__PURE__ */ jsxs(
|
|
4692
|
-
"div",
|
|
4693
|
-
{
|
|
4694
|
-
role: "status",
|
|
4695
|
-
"aria-live": "polite",
|
|
4696
|
-
className: "ra-clipboard-toast",
|
|
4697
|
-
children: [
|
|
4698
|
-
/* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5 shrink-0", "aria-hidden": "true" }),
|
|
4699
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: message })
|
|
4700
|
-
]
|
|
4701
|
-
}
|
|
4702
|
-
);
|
|
4703
|
-
};
|
|
4704
4646
|
|
|
4705
|
-
// src/components/RecordsAdmin/
|
|
4706
|
-
var
|
|
4707
|
-
if (
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
let cur = "";
|
|
4713
|
-
let inQuotes = false;
|
|
4714
|
-
for (let i = 0; i < line.length; i += 1) {
|
|
4715
|
-
const ch = line[i];
|
|
4716
|
-
if (inQuotes) {
|
|
4717
|
-
if (ch === '"' && line[i + 1] === '"') {
|
|
4718
|
-
cur += '"';
|
|
4719
|
-
i += 1;
|
|
4720
|
-
} else if (ch === '"') inQuotes = false;
|
|
4721
|
-
else cur += ch;
|
|
4722
|
-
} else if (ch === '"') inQuotes = true;
|
|
4723
|
-
else if (ch === ",") {
|
|
4724
|
-
out.push(cur);
|
|
4725
|
-
cur = "";
|
|
4726
|
-
} else cur += ch;
|
|
4647
|
+
// src/components/RecordsAdmin/editor-session/editorStore.ts
|
|
4648
|
+
var isEqual = (a, b) => {
|
|
4649
|
+
if (a === b) return true;
|
|
4650
|
+
try {
|
|
4651
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
4652
|
+
} catch {
|
|
4653
|
+
return false;
|
|
4727
4654
|
}
|
|
4728
|
-
out.push(cur);
|
|
4729
|
-
return out;
|
|
4730
|
-
};
|
|
4731
|
-
var exportCsv = (records, schema) => {
|
|
4732
|
-
const headers = ["ref", ...schema.columns.map((c) => c.header)];
|
|
4733
|
-
const rows = records.filter((r) => r.data != null).map((r) => [r.ref, ...schema.columns.map((c) => c.toCell(r.data))]);
|
|
4734
|
-
const csv = [headers, ...rows].map((row) => row.map((cell) => escapeCell(String(cell ?? ""))).join(",")).join("\n");
|
|
4735
|
-
return new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
4736
4655
|
};
|
|
4737
|
-
var
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4656
|
+
var cloneDeep = (v) => {
|
|
4657
|
+
if (v == null) return v;
|
|
4658
|
+
try {
|
|
4659
|
+
return structuredClone(v);
|
|
4660
|
+
} catch {
|
|
4661
|
+
return JSON.parse(JSON.stringify(v));
|
|
4742
4662
|
}
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
const
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
errorRows.push({ row: i, error: "Missing ref" });
|
|
4753
|
-
continue;
|
|
4663
|
+
};
|
|
4664
|
+
var TITLE_KEYS = ["title", "name", "label", "heading", "question", "slug"];
|
|
4665
|
+
var TITLE_WRAPPERS = ["display", "content", "meta", "data"];
|
|
4666
|
+
var deriveDefaultLabel = (value, spec) => {
|
|
4667
|
+
const pickString = (obj) => {
|
|
4668
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
4669
|
+
for (const key of TITLE_KEYS) {
|
|
4670
|
+
const raw = obj[key];
|
|
4671
|
+
if (typeof raw === "string" && raw.trim()) return raw.trim();
|
|
4754
4672
|
}
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4673
|
+
return void 0;
|
|
4674
|
+
};
|
|
4675
|
+
const v = value;
|
|
4676
|
+
const top = pickString(v);
|
|
4677
|
+
if (top) return top;
|
|
4678
|
+
if (v && typeof v === "object") {
|
|
4679
|
+
for (const wrapper of TITLE_WRAPPERS) {
|
|
4680
|
+
const nested = pickString(v[wrapper]);
|
|
4681
|
+
if (nested) return nested;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
if (spec.scope.raw?.startsWith("item:")) return "Untitled item";
|
|
4685
|
+
if (spec.scope.kind === "rule") return "Rule";
|
|
4686
|
+
if (spec.scope.kind && spec.scope.kind !== "collection") {
|
|
4687
|
+
return spec.scope.kind.charAt(0).toUpperCase() + spec.scope.kind.slice(1);
|
|
4688
|
+
}
|
|
4689
|
+
return "Default";
|
|
4690
|
+
};
|
|
4691
|
+
var editorIdCounter = 0;
|
|
4692
|
+
var mintEditorId = () => {
|
|
4693
|
+
editorIdCounter += 1;
|
|
4694
|
+
return `ed_${Date.now().toString(36)}_${editorIdCounter.toString(36)}`;
|
|
4695
|
+
};
|
|
4696
|
+
var recomputePinned = (e) => {
|
|
4697
|
+
if (e.status === "saving" || e.status === "error") return true;
|
|
4698
|
+
if (e.status === "dirty") return true;
|
|
4699
|
+
return false;
|
|
4700
|
+
};
|
|
4701
|
+
var recomputeStatus = (e) => {
|
|
4702
|
+
if (e.status === "saving") return "saving";
|
|
4703
|
+
if (e.status === "error") return "error";
|
|
4704
|
+
const valueDiff = !isEqual(e.value, e.baseline);
|
|
4705
|
+
const ruleDiff = !isEqual(e.facetRule, e.baselineFacetRule);
|
|
4706
|
+
if (valueDiff || ruleDiff) return "dirty";
|
|
4707
|
+
return e.status === "saved" ? "saved" : "idle";
|
|
4708
|
+
};
|
|
4709
|
+
var createEditorStore = () => {
|
|
4710
|
+
const map = /* @__PURE__ */ new Map();
|
|
4711
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4712
|
+
let cachedList = [];
|
|
4713
|
+
let nextOrder = 0;
|
|
4714
|
+
const recompute = () => {
|
|
4715
|
+
cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
|
|
4716
|
+
};
|
|
4717
|
+
const emit = () => {
|
|
4718
|
+
recompute();
|
|
4719
|
+
listeners.forEach((l) => l());
|
|
4720
|
+
};
|
|
4721
|
+
const update = (editorId, mut) => {
|
|
4722
|
+
const prev = map.get(editorId);
|
|
4723
|
+
if (!prev) return false;
|
|
4724
|
+
const draft = { ...prev };
|
|
4725
|
+
const result = mut(draft);
|
|
4726
|
+
const next = result ?? draft;
|
|
4727
|
+
next.status = recomputeStatus(next);
|
|
4728
|
+
next.pinned = recomputePinned(next);
|
|
4729
|
+
map.set(editorId, next);
|
|
4730
|
+
emit();
|
|
4731
|
+
return true;
|
|
4732
|
+
};
|
|
4733
|
+
return {
|
|
4734
|
+
list: () => cachedList,
|
|
4735
|
+
get: (editorId) => map.get(editorId),
|
|
4736
|
+
findExistingEditorIdFor(spec) {
|
|
4737
|
+
if (spec.recordId) {
|
|
4738
|
+
for (const entry of map.values()) {
|
|
4739
|
+
if (entry.recordId === spec.recordId) return entry.editorId;
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
const wantCreate = !!spec.createMode;
|
|
4743
|
+
for (const entry of map.values()) {
|
|
4744
|
+
if (entry.recordId) continue;
|
|
4745
|
+
if (entry.spec.scope.raw !== spec.scope.raw) continue;
|
|
4746
|
+
if (!!entry.spec.createMode !== wantCreate) continue;
|
|
4747
|
+
return entry.editorId;
|
|
4748
|
+
}
|
|
4749
|
+
return void 0;
|
|
4750
|
+
},
|
|
4751
|
+
open(input) {
|
|
4752
|
+
const editorId = input.editorId ?? mintEditorId();
|
|
4753
|
+
if (map.has(editorId)) return editorId;
|
|
4754
|
+
const seedValue = input.seed?.value ?? input.defaultValue;
|
|
4755
|
+
const seedFacetRule = input.seed?.facetRule ?? input.spec.initialFacetRule ?? null;
|
|
4756
|
+
const source = input.seed?.source ?? "empty";
|
|
4757
|
+
const value = cloneDeep(seedValue);
|
|
4758
|
+
const baseline = cloneDeep(seedValue);
|
|
4759
|
+
const entry = {
|
|
4760
|
+
editorId,
|
|
4761
|
+
spec: input.spec,
|
|
4762
|
+
saveSpec: input.saveSpec,
|
|
4763
|
+
value,
|
|
4764
|
+
baseline,
|
|
4765
|
+
facetRule: seedFacetRule,
|
|
4766
|
+
baselineFacetRule: seedFacetRule,
|
|
4767
|
+
status: "idle",
|
|
4768
|
+
source,
|
|
4769
|
+
recordId: input.seed?.recordId ?? input.spec.recordId,
|
|
4770
|
+
parentValue: input.seed?.parentValue ?? null,
|
|
4771
|
+
label: input.label ?? input.spec.label ?? deriveDefaultLabel(value, input.spec),
|
|
4772
|
+
order: nextOrder++,
|
|
4773
|
+
lastActiveAt: Date.now(),
|
|
4774
|
+
pinned: false,
|
|
4775
|
+
mounted: false
|
|
4776
|
+
};
|
|
4777
|
+
map.set(editorId, entry);
|
|
4778
|
+
emit();
|
|
4779
|
+
return editorId;
|
|
4780
|
+
},
|
|
4781
|
+
close(editorId) {
|
|
4782
|
+
const entry = map.get(editorId);
|
|
4783
|
+
if (!entry) return false;
|
|
4784
|
+
if (entry.pinned) return false;
|
|
4785
|
+
map.delete(editorId);
|
|
4786
|
+
emit();
|
|
4787
|
+
return true;
|
|
4788
|
+
},
|
|
4789
|
+
clearUnpinned() {
|
|
4790
|
+
let changed = false;
|
|
4791
|
+
for (const [id, entry] of map.entries()) {
|
|
4792
|
+
if (!entry.pinned) {
|
|
4793
|
+
map.delete(id);
|
|
4794
|
+
changed = true;
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
if (changed) emit();
|
|
4798
|
+
},
|
|
4799
|
+
setValue(editorId, next) {
|
|
4800
|
+
update(editorId, (e) => {
|
|
4801
|
+
const prev = e.value;
|
|
4802
|
+
const resolved = typeof next === "function" ? next(prev) : next;
|
|
4803
|
+
e.value = resolved;
|
|
4804
|
+
if (e.status === "saved" || e.status === "error") {
|
|
4805
|
+
e.status = "idle";
|
|
4806
|
+
e.error = void 0;
|
|
4807
|
+
}
|
|
4808
|
+
if (!e.spec.label) {
|
|
4809
|
+
e.label = deriveDefaultLabel(e.value, e.spec);
|
|
4810
|
+
}
|
|
4811
|
+
});
|
|
4812
|
+
},
|
|
4813
|
+
setFacetRule(editorId, next) {
|
|
4814
|
+
update(editorId, (e) => {
|
|
4815
|
+
e.facetRule = next;
|
|
4816
|
+
if (e.status === "saved" || e.status === "error") {
|
|
4817
|
+
e.status = "idle";
|
|
4818
|
+
e.error = void 0;
|
|
4819
|
+
}
|
|
4820
|
+
});
|
|
4821
|
+
},
|
|
4822
|
+
hydrateFromResolver(editorId, resolved) {
|
|
4823
|
+
const entry = map.get(editorId);
|
|
4824
|
+
if (!entry) return;
|
|
4825
|
+
if (entry.status === "dirty" || entry.status === "saving" || entry.status === "error") return;
|
|
4826
|
+
if (entry.status === "saved") {
|
|
4827
|
+
update(editorId, (e) => {
|
|
4828
|
+
e.source = resolved.source;
|
|
4829
|
+
if (resolved.recordId) e.recordId = resolved.recordId;
|
|
4830
|
+
e.parentValue = resolved.parentValue ?? null;
|
|
4831
|
+
});
|
|
4832
|
+
return;
|
|
4833
|
+
}
|
|
4834
|
+
update(editorId, (e) => {
|
|
4835
|
+
const fresh = resolved.data == null ? e.value : cloneDeep(resolved.data);
|
|
4836
|
+
e.value = fresh;
|
|
4837
|
+
e.baseline = cloneDeep(fresh);
|
|
4838
|
+
const rule = resolved.facetRule ?? e.facetRule;
|
|
4839
|
+
e.facetRule = rule;
|
|
4840
|
+
e.baselineFacetRule = rule;
|
|
4841
|
+
e.source = resolved.source;
|
|
4842
|
+
if (resolved.recordId) e.recordId = resolved.recordId;
|
|
4843
|
+
e.parentValue = resolved.parentValue ?? null;
|
|
4844
|
+
if (!e.spec.label) e.label = deriveDefaultLabel(fresh, e.spec);
|
|
4845
|
+
});
|
|
4846
|
+
},
|
|
4847
|
+
markMounted(editorId) {
|
|
4848
|
+
update(editorId, (e) => {
|
|
4849
|
+
e.mounted = true;
|
|
4850
|
+
});
|
|
4851
|
+
},
|
|
4852
|
+
markActive(editorId) {
|
|
4853
|
+
update(editorId, (e) => {
|
|
4854
|
+
e.lastActiveAt = Date.now();
|
|
4855
|
+
});
|
|
4856
|
+
},
|
|
4857
|
+
async save(editorId) {
|
|
4858
|
+
const entry = map.get(editorId);
|
|
4859
|
+
if (!entry) return;
|
|
4860
|
+
if (entry.status !== "dirty" && entry.status !== "error") return;
|
|
4861
|
+
const persistedValue = entry.value;
|
|
4862
|
+
const persistedFacetRule = entry.facetRule;
|
|
4863
|
+
const { ctx, anchors } = entry.saveSpec;
|
|
4864
|
+
const spec = entry.spec;
|
|
4865
|
+
update(editorId, (e) => {
|
|
4866
|
+
e.status = "saving";
|
|
4867
|
+
e.error = void 0;
|
|
4868
|
+
});
|
|
4869
|
+
try {
|
|
4870
|
+
let nextRecordId = entry.recordId;
|
|
4871
|
+
if (entry.recordId && entry.source === "self") {
|
|
4872
|
+
await updateRecord(ctx, entry.recordId, {
|
|
4873
|
+
data: persistedValue,
|
|
4874
|
+
facetRule: persistedFacetRule
|
|
4875
|
+
});
|
|
4876
|
+
} else if (spec.createMode) {
|
|
4877
|
+
const created = await createRecord(ctx, {
|
|
4878
|
+
ref: spec.scope.kind === "rule" && spec.scope.raw ? spec.scope.raw : spec.ref,
|
|
4879
|
+
scope: anchors,
|
|
4880
|
+
data: persistedValue,
|
|
4881
|
+
facetRule: persistedFacetRule
|
|
4882
|
+
});
|
|
4883
|
+
nextRecordId = created?.id ?? nextRecordId;
|
|
4884
|
+
} else {
|
|
4885
|
+
const upserted = await upsertRecord(ctx, {
|
|
4886
|
+
ref: spec.scope.kind === "rule" && spec.scope.raw ? spec.scope.raw : spec.ref,
|
|
4887
|
+
scope: anchors,
|
|
4888
|
+
data: persistedValue,
|
|
4889
|
+
facetRule: persistedFacetRule
|
|
4890
|
+
});
|
|
4891
|
+
nextRecordId = upserted.record?.id ?? nextRecordId;
|
|
4892
|
+
}
|
|
4893
|
+
update(editorId, (e) => {
|
|
4894
|
+
e.baseline = cloneDeep(persistedValue);
|
|
4895
|
+
e.baselineFacetRule = persistedFacetRule;
|
|
4896
|
+
e.recordId = nextRecordId;
|
|
4897
|
+
e.source = "self";
|
|
4898
|
+
e.status = "saved";
|
|
4899
|
+
e.error = void 0;
|
|
4900
|
+
});
|
|
4901
|
+
} catch (err) {
|
|
4902
|
+
update(editorId, (e) => {
|
|
4903
|
+
e.status = "error";
|
|
4904
|
+
e.error = err;
|
|
4905
|
+
});
|
|
4906
|
+
throw err;
|
|
4907
|
+
}
|
|
4908
|
+
},
|
|
4909
|
+
reset(editorId) {
|
|
4910
|
+
update(editorId, (e) => {
|
|
4911
|
+
e.value = cloneDeep(e.baseline);
|
|
4912
|
+
e.facetRule = e.baselineFacetRule;
|
|
4913
|
+
e.status = "idle";
|
|
4914
|
+
e.error = void 0;
|
|
4915
|
+
});
|
|
4916
|
+
},
|
|
4917
|
+
async remove(editorId) {
|
|
4918
|
+
const entry = map.get(editorId);
|
|
4919
|
+
if (!entry) return;
|
|
4920
|
+
if (entry.source !== "self") return;
|
|
4921
|
+
if (!entry.recordId) return;
|
|
4922
|
+
await removeRecord(entry.saveSpec.ctx, entry.recordId);
|
|
4923
|
+
map.delete(editorId);
|
|
4924
|
+
emit();
|
|
4925
|
+
},
|
|
4926
|
+
enforcePoolLimit(max, exceptEditorId) {
|
|
4927
|
+
const alive = Array.from(map.values());
|
|
4928
|
+
if (alive.length <= max) return;
|
|
4929
|
+
const evictable = alive.filter((e) => !e.pinned && e.editorId !== exceptEditorId).sort((a, b) => a.lastActiveAt - b.lastActiveAt);
|
|
4930
|
+
let toEvict = alive.length - max;
|
|
4931
|
+
let changed = false;
|
|
4932
|
+
for (const entry of evictable) {
|
|
4933
|
+
if (toEvict <= 0) break;
|
|
4934
|
+
map.delete(entry.editorId);
|
|
4935
|
+
toEvict -= 1;
|
|
4936
|
+
changed = true;
|
|
4937
|
+
}
|
|
4938
|
+
if (changed) emit();
|
|
4939
|
+
},
|
|
4940
|
+
subscribe(listener) {
|
|
4941
|
+
listeners.add(listener);
|
|
4942
|
+
return () => {
|
|
4943
|
+
listeners.delete(listener);
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
};
|
|
4947
|
+
};
|
|
4948
|
+
var buildSaveSpec = (ctx, spec) => ({
|
|
4949
|
+
ctx,
|
|
4950
|
+
anchors: parsedRefToScope(spec.scope)
|
|
4951
|
+
});
|
|
4952
|
+
var EditorSessionContext = createContext(null);
|
|
4953
|
+
var useEditorSessionContext = () => {
|
|
4954
|
+
const ctx = useContext(EditorSessionContext);
|
|
4955
|
+
if (!ctx) {
|
|
4956
|
+
throw new Error(
|
|
4957
|
+
"[smartlinks-ui] useEditorSession / useEditorSelection / useDirtyOverview must be used inside <EditorSessionProvider> (RecordsAdminShell mounts one for you)."
|
|
4958
|
+
);
|
|
4959
|
+
}
|
|
4960
|
+
return ctx;
|
|
4961
|
+
};
|
|
4962
|
+
var EditorSessionProvider = ({
|
|
4963
|
+
ctx,
|
|
4964
|
+
children,
|
|
4965
|
+
maxOpenEditors = 8,
|
|
4966
|
+
defaultValueFactory
|
|
4967
|
+
}) => {
|
|
4968
|
+
const storeRef = useRef(null);
|
|
4969
|
+
if (!storeRef.current) storeRef.current = createEditorStore();
|
|
4970
|
+
const currentRef = useRef(void 0);
|
|
4971
|
+
const listenersRef = useRef(/* @__PURE__ */ new Set());
|
|
4972
|
+
const subscribeCurrent = useCallback((cb) => {
|
|
4973
|
+
listenersRef.current.add(cb);
|
|
4974
|
+
return () => {
|
|
4975
|
+
listenersRef.current.delete(cb);
|
|
4976
|
+
};
|
|
4977
|
+
}, []);
|
|
4978
|
+
const getCurrent = useCallback(() => currentRef.current, []);
|
|
4979
|
+
const setCurrentEditorId = useCallback((id) => {
|
|
4980
|
+
if (currentRef.current === id) return;
|
|
4981
|
+
currentRef.current = id;
|
|
4982
|
+
listenersRef.current.forEach((l) => l());
|
|
4983
|
+
}, []);
|
|
4984
|
+
const currentEditorId = useSyncExternalStore(subscribeCurrent, getCurrent, getCurrent);
|
|
4985
|
+
const value = useMemo(() => ({
|
|
4986
|
+
store: storeRef.current,
|
|
4987
|
+
ctx,
|
|
4988
|
+
currentEditorId,
|
|
4989
|
+
setCurrentEditorId,
|
|
4990
|
+
maxOpenEditors,
|
|
4991
|
+
defaultValueFactory
|
|
4992
|
+
}), [ctx, currentEditorId, setCurrentEditorId, maxOpenEditors, defaultValueFactory]);
|
|
4993
|
+
return /* @__PURE__ */ jsx(EditorSessionContext.Provider, { value, children });
|
|
4994
|
+
};
|
|
4995
|
+
var useEntry = (editorId) => {
|
|
4996
|
+
const { store } = useEditorSessionContext();
|
|
4997
|
+
const subscribe = useCallback((cb) => store.subscribe(cb), [store]);
|
|
4998
|
+
const getSnapshot = useCallback(
|
|
4999
|
+
() => editorId ? store.get(editorId) : void 0,
|
|
5000
|
+
[store, editorId]
|
|
5001
|
+
);
|
|
5002
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
5003
|
+
};
|
|
5004
|
+
var useStoreList = () => {
|
|
5005
|
+
const { store } = useEditorSessionContext();
|
|
5006
|
+
const subscribe = useCallback((cb) => store.subscribe(cb), [store]);
|
|
5007
|
+
const getSnapshot = useCallback(() => store.list(), [store]);
|
|
5008
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
5009
|
+
};
|
|
5010
|
+
function useEditorSession(editorId) {
|
|
5011
|
+
const { store, currentEditorId, ctx } = useEditorSessionContext();
|
|
5012
|
+
const queryClient = useQueryClient();
|
|
5013
|
+
const entry = useEntry(editorId);
|
|
5014
|
+
const onChange = useCallback((next) => {
|
|
5015
|
+
if (!editorId) return;
|
|
5016
|
+
store.setValue(editorId, next);
|
|
5017
|
+
}, [store, editorId]);
|
|
5018
|
+
const onFacetRuleChange = useCallback((next) => {
|
|
5019
|
+
if (!editorId) return;
|
|
5020
|
+
store.setFacetRule(editorId, next);
|
|
5021
|
+
}, [store, editorId]);
|
|
5022
|
+
const save = useCallback(async () => {
|
|
5023
|
+
if (!editorId) return;
|
|
5024
|
+
try {
|
|
5025
|
+
await store.save(editorId);
|
|
5026
|
+
const e = store.get(editorId);
|
|
5027
|
+
if (e) {
|
|
5028
|
+
const key = resolvedRecordQueryKey({
|
|
5029
|
+
collectionId: ctx.collectionId,
|
|
5030
|
+
appId: ctx.appId,
|
|
5031
|
+
recordType: ctx.recordType,
|
|
5032
|
+
productId: e.spec.scope.productId,
|
|
5033
|
+
variantId: e.spec.scope.variantId,
|
|
5034
|
+
batchId: e.spec.scope.batchId,
|
|
5035
|
+
facetId: e.spec.scope.facetId,
|
|
5036
|
+
facetValue: e.spec.scope.facetValue,
|
|
5037
|
+
proofId: e.spec.scope.proofId,
|
|
5038
|
+
recordId: e.recordId,
|
|
5039
|
+
withParent: true
|
|
5040
|
+
});
|
|
5041
|
+
queryClient.invalidateQueries({ queryKey: key });
|
|
5042
|
+
}
|
|
5043
|
+
} catch {
|
|
5044
|
+
}
|
|
5045
|
+
}, [store, editorId, ctx, queryClient]);
|
|
5046
|
+
const reset = useCallback(() => {
|
|
5047
|
+
if (!editorId) return;
|
|
5048
|
+
store.reset(editorId);
|
|
5049
|
+
}, [store, editorId]);
|
|
5050
|
+
const remove = useCallback(async () => {
|
|
5051
|
+
if (!editorId) return;
|
|
5052
|
+
await store.remove(editorId);
|
|
5053
|
+
}, [store, editorId]);
|
|
5054
|
+
if (!editorId || !entry) return null;
|
|
5055
|
+
const isRuleScope = entry.spec.scope.kind === "rule";
|
|
5056
|
+
const ruleValid = !isRuleScope || isFacetRuleValid(entry.facetRule);
|
|
5057
|
+
const canSave = ruleValid;
|
|
5058
|
+
const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
|
|
5059
|
+
return {
|
|
5060
|
+
editorId,
|
|
5061
|
+
value: entry.value,
|
|
5062
|
+
onChange,
|
|
5063
|
+
source: entry.source,
|
|
5064
|
+
recordId: entry.recordId,
|
|
5065
|
+
parentValue: entry.parentValue,
|
|
5066
|
+
scope: entry.spec.scope,
|
|
5067
|
+
isDirty: entry.status === "dirty" || entry.status === "saving" || entry.status === "error",
|
|
5068
|
+
status: entry.status,
|
|
5069
|
+
error: entry.error ?? null,
|
|
5070
|
+
save,
|
|
5071
|
+
reset,
|
|
5072
|
+
remove,
|
|
5073
|
+
canRemove: entry.source === "self" && !!entry.recordId,
|
|
5074
|
+
canSave,
|
|
5075
|
+
cannotSaveReason,
|
|
5076
|
+
facetRule: entry.facetRule,
|
|
5077
|
+
onFacetRuleChange,
|
|
5078
|
+
isActive: editorId === currentEditorId
|
|
5079
|
+
};
|
|
5080
|
+
}
|
|
5081
|
+
var useEditorSelection = () => {
|
|
5082
|
+
const { store, ctx, currentEditorId, setCurrentEditorId, maxOpenEditors, defaultValueFactory } = useEditorSessionContext();
|
|
5083
|
+
const openEditors = useStoreList();
|
|
5084
|
+
const selectTarget = useCallback((input) => {
|
|
5085
|
+
const existing = store.findExistingEditorIdFor(input.spec);
|
|
5086
|
+
if (existing) {
|
|
5087
|
+
store.markActive(existing);
|
|
5088
|
+
setCurrentEditorId(existing);
|
|
5089
|
+
store.enforcePoolLimit(maxOpenEditors, existing);
|
|
5090
|
+
return existing;
|
|
5091
|
+
}
|
|
5092
|
+
const editorId = store.open({
|
|
5093
|
+
spec: input.spec,
|
|
5094
|
+
saveSpec: buildSaveSpec(ctx, input.spec),
|
|
5095
|
+
seed: input.seed ?? null,
|
|
5096
|
+
defaultValue: input.defaultValue ?? defaultValueFactory?.() ?? {},
|
|
5097
|
+
label: input.label
|
|
5098
|
+
});
|
|
5099
|
+
setCurrentEditorId(editorId);
|
|
5100
|
+
store.enforcePoolLimit(maxOpenEditors, editorId);
|
|
5101
|
+
return editorId;
|
|
5102
|
+
}, [store, ctx, setCurrentEditorId, maxOpenEditors, defaultValueFactory]);
|
|
5103
|
+
const focus = useCallback((editorId) => {
|
|
5104
|
+
store.markActive(editorId);
|
|
5105
|
+
setCurrentEditorId(editorId);
|
|
5106
|
+
}, [store, setCurrentEditorId]);
|
|
5107
|
+
const blur = useCallback(() => setCurrentEditorId(void 0), [setCurrentEditorId]);
|
|
5108
|
+
const close = useCallback((editorId) => {
|
|
5109
|
+
const closed = store.close(editorId);
|
|
5110
|
+
if (closed && currentEditorId === editorId) setCurrentEditorId(void 0);
|
|
5111
|
+
return closed;
|
|
5112
|
+
}, [store, currentEditorId, setCurrentEditorId]);
|
|
5113
|
+
const hydrate = useCallback((editorId, resolved) => {
|
|
5114
|
+
store.hydrateFromResolver(editorId, resolved);
|
|
5115
|
+
}, [store]);
|
|
5116
|
+
return { currentEditorId, openEditors, selectTarget, focus, blur, close, hydrate };
|
|
5117
|
+
};
|
|
5118
|
+
var overviewItemFromEntry = (e) => ({
|
|
5119
|
+
editorId: e.editorId,
|
|
5120
|
+
label: e.label,
|
|
5121
|
+
status: e.status,
|
|
5122
|
+
error: e.error,
|
|
5123
|
+
scope: e.spec.scope,
|
|
5124
|
+
recordId: e.recordId
|
|
5125
|
+
});
|
|
5126
|
+
var useDirtyOverview = () => {
|
|
5127
|
+
const { store } = useEditorSessionContext();
|
|
5128
|
+
const list = useStoreList();
|
|
5129
|
+
const items = useMemo(
|
|
5130
|
+
() => list.filter((e) => e.status === "dirty" || e.status === "saving" || e.status === "error").map(overviewItemFromEntry),
|
|
5131
|
+
[list]
|
|
5132
|
+
);
|
|
5133
|
+
const count = items.length;
|
|
5134
|
+
const errorCount = useMemo(() => items.filter((i) => i.status === "error").length, [items]);
|
|
5135
|
+
const saveAll = useCallback(async () => {
|
|
5136
|
+
const ids = list.filter((e) => e.status === "dirty" || e.status === "error").map((e) => e.editorId);
|
|
5137
|
+
for (const id of ids) {
|
|
5138
|
+
try {
|
|
5139
|
+
await store.save(id);
|
|
5140
|
+
} catch {
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
}, [store, list]);
|
|
5144
|
+
const discardAll = useCallback(() => {
|
|
5145
|
+
const ids = list.filter((e) => e.status === "dirty" || e.status === "error").map((e) => e.editorId);
|
|
5146
|
+
ids.forEach((id) => store.reset(id));
|
|
5147
|
+
}, [store, list]);
|
|
5148
|
+
const saveOne = useCallback(async (editorId) => {
|
|
5149
|
+
await store.save(editorId);
|
|
5150
|
+
}, [store]);
|
|
5151
|
+
return { items, count, errorCount, saveAll, discardAll, saveOne };
|
|
5152
|
+
};
|
|
5153
|
+
var EditorMountPool = ({
|
|
5154
|
+
renderSlot,
|
|
5155
|
+
keepMountedHidden = true,
|
|
5156
|
+
fallback = null,
|
|
5157
|
+
className
|
|
5158
|
+
}) => {
|
|
5159
|
+
const { openEditors, currentEditorId } = useEditorSelection();
|
|
5160
|
+
const ids = useMemo(
|
|
5161
|
+
() => openEditors.map((e) => e.editorId).sort(),
|
|
5162
|
+
[openEditors]
|
|
5163
|
+
);
|
|
5164
|
+
if (!currentEditorId && ids.length === 0) {
|
|
5165
|
+
return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
5166
|
+
}
|
|
5167
|
+
const visibleIds = keepMountedHidden ? ids : currentEditorId ? [currentEditorId] : [];
|
|
5168
|
+
return /* @__PURE__ */ jsx("div", { className, style: { display: "contents" }, children: visibleIds.map((id) => {
|
|
5169
|
+
const isActive = id === currentEditorId;
|
|
5170
|
+
return /* @__PURE__ */ jsx(EditorPoolSlot, { editorId: id, isActive, children: renderSlot(id) }, id);
|
|
5171
|
+
}) });
|
|
5172
|
+
};
|
|
5173
|
+
var EditorPoolSlot = ({ editorId, isActive, children }) => {
|
|
5174
|
+
const inertProps = isActive ? {} : { inert: "" };
|
|
5175
|
+
return /* @__PURE__ */ jsx(
|
|
5176
|
+
"div",
|
|
5177
|
+
{
|
|
5178
|
+
"data-editor-slot": editorId,
|
|
5179
|
+
"data-active": isActive ? "true" : "false",
|
|
5180
|
+
"aria-hidden": isActive ? void 0 : true,
|
|
5181
|
+
style: {
|
|
5182
|
+
display: isActive ? "contents" : "none"
|
|
5183
|
+
},
|
|
5184
|
+
...inertProps,
|
|
5185
|
+
children
|
|
5186
|
+
}
|
|
5187
|
+
);
|
|
5188
|
+
};
|
|
5189
|
+
function useEditorSlotContext(editorId) {
|
|
5190
|
+
const session = useEditorSession(editorId);
|
|
5191
|
+
return useMemo(() => {
|
|
5192
|
+
if (!session) return null;
|
|
5193
|
+
return {
|
|
5194
|
+
value: session.value,
|
|
5195
|
+
onChange: session.onChange,
|
|
5196
|
+
source: session.source,
|
|
5197
|
+
recordId: session.recordId,
|
|
5198
|
+
parentValue: session.parentValue,
|
|
5199
|
+
scope: session.scope,
|
|
5200
|
+
isDirty: session.isDirty,
|
|
5201
|
+
save: session.save,
|
|
5202
|
+
reset: session.reset,
|
|
5203
|
+
remove: session.remove,
|
|
5204
|
+
canRemove: session.canRemove,
|
|
5205
|
+
canSave: session.canSave,
|
|
5206
|
+
cannotSaveReason: session.cannotSaveReason,
|
|
5207
|
+
isSaving: session.status === "saving",
|
|
5208
|
+
saveError: session.error,
|
|
5209
|
+
facetRule: session.facetRule,
|
|
5210
|
+
onFacetRuleChange: session.onFacetRuleChange
|
|
5211
|
+
};
|
|
5212
|
+
}, [session]);
|
|
5213
|
+
}
|
|
5214
|
+
var isEqualSpec = (a, b) => {
|
|
5215
|
+
if (a === b) return true;
|
|
5216
|
+
if (!a || !b) return false;
|
|
5217
|
+
if (a.scope.raw !== b.scope.raw) return false;
|
|
5218
|
+
if ((a.recordId ?? null) !== (b.recordId ?? null)) return false;
|
|
5219
|
+
if (!!a.createMode !== !!b.createMode) return false;
|
|
5220
|
+
return true;
|
|
5221
|
+
};
|
|
5222
|
+
function useEditorBridge(args) {
|
|
5223
|
+
const { target, resolved, defaultData, onSaved, onDeleted } = args;
|
|
5224
|
+
const selection = useEditorSelection();
|
|
5225
|
+
const lastTargetRef = useRef(null);
|
|
5226
|
+
const editorIdRef = useRef(void 0);
|
|
5227
|
+
useEffect(() => {
|
|
5228
|
+
if (!target) {
|
|
5229
|
+
selection.blur();
|
|
5230
|
+
lastTargetRef.current = null;
|
|
5231
|
+
editorIdRef.current = void 0;
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
5234
|
+
if (isEqualSpec(target, lastTargetRef.current)) return;
|
|
5235
|
+
lastTargetRef.current = target;
|
|
5236
|
+
const id = selection.selectTarget({
|
|
5237
|
+
spec: target,
|
|
5238
|
+
seed: resolved.source === "empty" ? void 0 : {
|
|
5239
|
+
value: resolved.data ?? (defaultData?.() ?? null),
|
|
5240
|
+
facetRule: resolved.facetRule ?? null,
|
|
5241
|
+
source: resolved.source,
|
|
5242
|
+
parentValue: resolved.parentValue ?? null,
|
|
5243
|
+
recordId: resolved.recordId
|
|
5244
|
+
},
|
|
5245
|
+
defaultValue: defaultData?.() ?? {}
|
|
5246
|
+
});
|
|
5247
|
+
editorIdRef.current = id;
|
|
5248
|
+
}, [target?.scope.raw, target?.recordId, target?.createMode]);
|
|
5249
|
+
useEffect(() => {
|
|
5250
|
+
const id = editorIdRef.current;
|
|
5251
|
+
if (!id) return;
|
|
5252
|
+
if (resolved.source === "empty" && !resolved.recordId) return;
|
|
5253
|
+
selection.hydrate(id, resolved);
|
|
5254
|
+
}, [resolved.source, resolved.recordId, resolved.sourceRef, resolved.data, resolved.facetRule]);
|
|
5255
|
+
const editorId = selection.currentEditorId ?? editorIdRef.current;
|
|
5256
|
+
const session = useEditorSession(editorId);
|
|
5257
|
+
const prevStatusRef = useRef(null);
|
|
5258
|
+
useEffect(() => {
|
|
5259
|
+
const prev = prevStatusRef.current;
|
|
5260
|
+
const next = session?.status ?? null;
|
|
5261
|
+
prevStatusRef.current = next;
|
|
5262
|
+
if (next === "saved" && prev !== "saved") {
|
|
5263
|
+
onSaved?.();
|
|
5264
|
+
}
|
|
5265
|
+
}, [session?.status]);
|
|
5266
|
+
const remove = useMemo(() => async () => {
|
|
5267
|
+
if (!session) return;
|
|
5268
|
+
await session.remove();
|
|
5269
|
+
onDeleted?.();
|
|
5270
|
+
}, [session?.remove]);
|
|
5271
|
+
if (!session) {
|
|
5272
|
+
const sentinelScope = target?.scope ?? { kind: "collection", raw: "" };
|
|
5273
|
+
return {
|
|
5274
|
+
value: defaultData?.() ?? {},
|
|
5275
|
+
onChange: () => {
|
|
5276
|
+
},
|
|
5277
|
+
source: "empty",
|
|
5278
|
+
recordId: void 0,
|
|
5279
|
+
parentValue: null,
|
|
5280
|
+
scope: sentinelScope,
|
|
5281
|
+
isDirty: false,
|
|
5282
|
+
save: async () => {
|
|
5283
|
+
},
|
|
5284
|
+
reset: () => {
|
|
5285
|
+
},
|
|
5286
|
+
remove: async () => {
|
|
5287
|
+
},
|
|
5288
|
+
canRemove: false,
|
|
5289
|
+
canSave: false,
|
|
5290
|
+
cannotSaveReason: void 0,
|
|
5291
|
+
isSaving: false,
|
|
5292
|
+
saveError: null,
|
|
5293
|
+
facetRule: null,
|
|
5294
|
+
onFacetRuleChange: () => {
|
|
5295
|
+
}
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5298
|
+
return {
|
|
5299
|
+
value: session.value,
|
|
5300
|
+
onChange: session.onChange,
|
|
5301
|
+
source: session.source,
|
|
5302
|
+
recordId: session.recordId,
|
|
5303
|
+
parentValue: session.parentValue,
|
|
5304
|
+
scope: session.scope,
|
|
5305
|
+
isDirty: session.isDirty,
|
|
5306
|
+
save: session.save,
|
|
5307
|
+
reset: session.reset,
|
|
5308
|
+
remove,
|
|
5309
|
+
canRemove: session.canRemove,
|
|
5310
|
+
canSave: session.canSave,
|
|
5311
|
+
cannotSaveReason: session.cannotSaveReason,
|
|
5312
|
+
isSaving: session.status === "saving",
|
|
5313
|
+
saveError: session.error,
|
|
5314
|
+
facetRule: session.facetRule,
|
|
5315
|
+
onFacetRuleChange: session.onFacetRuleChange
|
|
5316
|
+
};
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5319
|
+
// src/components/RecordsAdmin/data/csv.ts
|
|
5320
|
+
var escapeCell = (s) => {
|
|
5321
|
+
if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
5322
|
+
return s;
|
|
5323
|
+
};
|
|
5324
|
+
var parseLine = (line) => {
|
|
5325
|
+
const out = [];
|
|
5326
|
+
let cur = "";
|
|
5327
|
+
let inQuotes = false;
|
|
5328
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
5329
|
+
const ch = line[i];
|
|
5330
|
+
if (inQuotes) {
|
|
5331
|
+
if (ch === '"' && line[i + 1] === '"') {
|
|
5332
|
+
cur += '"';
|
|
5333
|
+
i += 1;
|
|
5334
|
+
} else if (ch === '"') inQuotes = false;
|
|
5335
|
+
else cur += ch;
|
|
5336
|
+
} else if (ch === '"') inQuotes = true;
|
|
5337
|
+
else if (ch === ",") {
|
|
5338
|
+
out.push(cur);
|
|
5339
|
+
cur = "";
|
|
5340
|
+
} else cur += ch;
|
|
5341
|
+
}
|
|
5342
|
+
out.push(cur);
|
|
5343
|
+
return out;
|
|
5344
|
+
};
|
|
5345
|
+
var exportCsv = (records, schema) => {
|
|
5346
|
+
const headers = ["ref", ...schema.columns.map((c) => c.header)];
|
|
5347
|
+
const rows = records.filter((r) => r.data != null).map((r) => [r.ref, ...schema.columns.map((c) => c.toCell(r.data))]);
|
|
5348
|
+
const csv = [headers, ...rows].map((row) => row.map((cell) => escapeCell(String(cell ?? ""))).join(",")).join("\n");
|
|
5349
|
+
return new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
5350
|
+
};
|
|
5351
|
+
var importCsv = async (file, schema, ctx) => {
|
|
5352
|
+
const text = await file.text();
|
|
5353
|
+
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
5354
|
+
if (lines.length === 0) {
|
|
5355
|
+
return { total: 0, saved: 0, failed: 0, errorRows: [], annotatedCsv: "" };
|
|
5356
|
+
}
|
|
5357
|
+
const headers = parseLine(lines[0]);
|
|
5358
|
+
const refIdx = headers.indexOf("ref");
|
|
5359
|
+
const colMap = schema.columns.map((c) => ({ col: c, idx: headers.indexOf(c.header) }));
|
|
5360
|
+
const errorRows = [];
|
|
5361
|
+
let saved = 0;
|
|
5362
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
5363
|
+
const cells = parseLine(lines[i]);
|
|
5364
|
+
const ref = refIdx >= 0 ? cells[refIdx] : "";
|
|
5365
|
+
if (!ref) {
|
|
5366
|
+
errorRows.push({ row: i, error: "Missing ref" });
|
|
5367
|
+
continue;
|
|
5368
|
+
}
|
|
5369
|
+
const data = {};
|
|
5370
|
+
for (const { col, idx } of colMap) {
|
|
5371
|
+
if (idx < 0) continue;
|
|
5372
|
+
try {
|
|
5373
|
+
data[col.key] = col.fromCell(cells[idx] ?? "");
|
|
4760
5374
|
} catch (e) {
|
|
4761
5375
|
errorRows.push({ row: i, error: e.message });
|
|
4762
5376
|
continue;
|
|
@@ -4806,11 +5420,58 @@ var downloadBlob = (blob, filename) => {
|
|
|
4806
5420
|
URL.revokeObjectURL(url);
|
|
4807
5421
|
};
|
|
4808
5422
|
|
|
5423
|
+
// src/components/RecordsAdmin/hooks/useShellCsvBulk.ts
|
|
5424
|
+
function useShellCsvBulk(args) {
|
|
5425
|
+
const { csvSchema, recordType, label, items, ctx, refetchAll, onTelemetry } = args;
|
|
5426
|
+
const fileBase = useMemo(
|
|
5427
|
+
() => recordType ?? (label.toLowerCase().replace(/\s+/g, "-") || "records"),
|
|
5428
|
+
[recordType, label]
|
|
5429
|
+
);
|
|
5430
|
+
const handleExport = useCallback(() => {
|
|
5431
|
+
if (!csvSchema) return;
|
|
5432
|
+
const blob = exportCsv(items, csvSchema);
|
|
5433
|
+
downloadBlob(blob, `${fileBase}.csv`);
|
|
5434
|
+
onTelemetry?.({ type: "csv.export", recordType, rows: items.length });
|
|
5435
|
+
}, [csvSchema, items, fileBase, onTelemetry, recordType]);
|
|
5436
|
+
const handleImport = useCallback(() => {
|
|
5437
|
+
if (!csvSchema) return;
|
|
5438
|
+
const inp = document.createElement("input");
|
|
5439
|
+
inp.type = "file";
|
|
5440
|
+
inp.accept = ".csv,text/csv";
|
|
5441
|
+
inp.onchange = async () => {
|
|
5442
|
+
const file = inp.files?.[0];
|
|
5443
|
+
if (!file) return;
|
|
5444
|
+
const report = await importCsv(file, csvSchema, ctx);
|
|
5445
|
+
onTelemetry?.({ type: "csv.import", recordType, rows: report.total, errors: report.failed });
|
|
5446
|
+
if (report.failed > 0) {
|
|
5447
|
+
downloadBlob(
|
|
5448
|
+
new Blob([report.annotatedCsv], { type: "text/csv" }),
|
|
5449
|
+
`${fileBase}-errors.csv`
|
|
5450
|
+
);
|
|
5451
|
+
}
|
|
5452
|
+
refetchAll();
|
|
5453
|
+
};
|
|
5454
|
+
inp.click();
|
|
5455
|
+
}, [csvSchema, ctx, fileBase, onTelemetry, recordType, refetchAll]);
|
|
5456
|
+
return useMemo(
|
|
5457
|
+
() => csvSchema ? { onImportCsv: handleImport, onExportCsv: handleExport } : {},
|
|
5458
|
+
[csvSchema, handleImport, handleExport]
|
|
5459
|
+
);
|
|
5460
|
+
}
|
|
5461
|
+
|
|
4809
5462
|
// src/components/RecordsAdmin/shell/tokens.css
|
|
4810
5463
|
styleInject(':root {\n --ra-status-own: var(--ra-emerald, 142 71% 45%);\n --ra-status-shared: var(--ra-amber, 38 92% 50%);\n --ra-status-missing: var(--muted-foreground, 220 9% 46%);\n --ra-accent: var(--primary, 222 47% 11%);\n --ra-surface: var(--card, 0 0% 100%);\n --ra-border: var(--border, 220 13% 91%);\n --ra-text: var(--foreground, 222 47% 11%);\n --ra-muted: var(--muted, 220 14% 96%);\n --ra-muted-text: var(--muted-foreground, 220 9% 46%);\n --ra-radius: var(--radius, 0.625rem);\n --ra-dot-size: 0.5rem;\n --ra-page-bg: var(--background, 220 14% 98%);\n --ra-card-shadow: 0 1px 2px hsl(var(--ra-accent) / 0.04), 0 4px 12px hsl(var(--ra-accent) / 0.05);\n --ra-card-shadow-hover: 0 2px 4px hsl(var(--ra-accent) / 0.06), 0 8px 24px hsl(var(--ra-accent) / 0.08);\n --ra-row-hover: hsl(var(--ra-accent) / 0.05);\n --ra-row-active-bg: hsl(var(--ra-accent) / 0.10);\n --ra-row-active-bd: hsl(var(--ra-accent) / 0.45);\n --ra-focus-ring: hsl(var(--ra-accent) / 0.35);\n --ra-font-display: var(--font-display, var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));\n --ra-font-ui: var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);\n --ra-title-weight: 600;\n --ra-display-weight: 700;\n --ra-info: var(--ra-blue, 214 95% 55%);\n --ra-success: var(--ra-emerald, 142 71% 45%);\n --ra-warning: var(--ra-amber, 38 92% 50%);\n --ra-danger: var(--destructive, 0 72% 51%);\n}\n:root {\n --sl-control-bg: var(--muted, 220 14% 96%);\n --sl-control-fg: var(--muted-foreground, 220 9% 40%);\n --sl-control-border: var(--border, 220 13% 88%);\n --sl-control-active-bg: var(--primary, 222 47% 11%);\n --sl-control-active-fg: var(--primary-foreground, 0 0% 100%);\n --sl-control-active-bd: var(--primary, 222 47% 11%);\n --sl-control-hover-bg: var(--sl-control-active-bg, 222 47% 11%);\n --sl-control-hover-fg: var(--foreground, 222 47% 11%);\n --sl-control-focus-ring: var(--sl-control-active-bg, 222 47% 11%);\n --sl-control-radius: var(--radius, 0.5rem);\n --sl-control-weight: 500;\n --sl-control-active-weight: 600;\n}\n.ra-status-dot {\n display: inline-block;\n width: var(--ra-dot-size);\n height: var(--ra-dot-size);\n border-radius: 9999px;\n flex-shrink: 0;\n}\n.ra-status-own {\n background: hsl(var(--ra-status-own));\n}\n.ra-status-shared {\n background: hsl(var(--ra-status-shared));\n}\n.ra-status-missing {\n background: hsl(var(--ra-status-missing) / 0.4);\n border: 1px solid hsl(var(--ra-status-missing) / 0.6);\n}\n.ra-row-active {\n background: var(--ra-row-active-bg);\n border-color: var(--ra-row-active-bd) !important;\n}\n');
|
|
4811
5464
|
|
|
4812
5465
|
// src/components/RecordsAdmin/shell/shell.css
|
|
4813
5466
|
styleInject(".ra-shell {\n color: hsl(var(--ra-text));\n background: hsl(var(--ra-page-bg));\n font-family: var(--ra-font-ui);\n}\n.ra-shell *,\n.ra-shell *::before,\n.ra-shell *::after {\n box-sizing: border-box;\n}\n.ra-shell .ra-card {\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-card-hover {\n transition:\n box-shadow .18s ease,\n transform .18s ease,\n border-color .18s ease;\n}\n.ra-shell .ra-card-hover:hover {\n box-shadow: var(--ra-card-shadow-hover);\n}\n.ra-shell .ra-display {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n letter-spacing: -0.01em;\n}\n.ra-shell .ra-title {\n font-weight: var(--ra-title-weight);\n}\n.ra-shell :where(button, [role=button], input, select, textarea, a):focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n border-radius: calc(var(--ra-radius) * 0.6);\n}\n.ra-shell .ra-header {\n display: block;\n width: 100%;\n}\n.ra-shell .ra-header__main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: flex-start;\n gap: 0.55rem;\n}\n.ra-shell .ra-header-aside {\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-header-icon {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n background: transparent;\n color: hsl(var(--ra-text));\n border: 0;\n padding: 0;\n margin-top: 0.1rem;\n}\n.ra-shell .ra-header-icon > svg {\n width: 1.05rem;\n height: 1.05rem;\n}\n.ra-shell .ra-header-text {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-header-title {\n font-family: var(--ra-font-display);\n font-weight: 700;\n font-size: 1.2rem;\n line-height: 1.2;\n color: hsl(var(--ra-text));\n letter-spacing: -0.015em;\n margin: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.ra-shell .ra-header-subtitle {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.1rem;\n line-height: 1.3;\n}\n.ra-shell .ra-header-stats {\n display: flex;\n align-items: stretch;\n gap: 0.15rem;\n padding: 0.15rem 0.4rem;\n border-radius: calc(var(--ra-radius) * 0.75);\n background: hsl(var(--ra-surface) / 0.7);\n border: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-header-stats--titled {\n flex-direction: column;\n align-items: stretch;\n padding: 0.4rem 0.55rem;\n gap: 0.3rem;\n}\n.ra-shell .ra-header-stats .ra-stats-items {\n display: flex;\n align-items: stretch;\n gap: 0.15rem;\n}\n.ra-shell .ra-header-stats .ra-stats-heading {\n display: flex;\n align-items: center;\n gap: 0.35rem;\n color: hsl(var(--ra-muted-text));\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n.ra-shell .ra-header-stats .ra-stats-heading-icon {\n display: inline-flex;\n align-items: center;\n color: hsl(var(--ra-text));\n opacity: 0.75;\n}\n.ra-shell .ra-header-stats .ra-stats-heading-icon > svg {\n width: 0.85rem;\n height: 0.85rem;\n}\n.ra-shell .ra-stat {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 0.15rem 0.45rem;\n min-width: 2.5rem;\n}\n.ra-shell .ra-stat-value {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 0.85rem;\n color: hsl(var(--ra-text));\n line-height: 1;\n}\n.ra-shell .ra-stat-label {\n font-size: 0.6rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n}\n.ra-shell .ra-stat-divider {\n width: 1px;\n background: hsl(var(--ra-border));\n margin: 0.25rem 0;\n}\n.ra-shell .ra-header-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n.ra-shell .ra-tabs {\n display: flex;\n gap: 0.25rem;\n padding: 0.25rem;\n background: hsl(var(--sl-control-bg));\n border-radius: var(--sl-control-radius);\n border: 1px solid hsl(var(--sl-control-border));\n}\n.ra-shell .ra-tab {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.4rem 0.7rem;\n border-radius: calc(var(--sl-control-radius) - 2px);\n font-size: 0.78rem;\n font-weight: var(--sl-control-weight);\n color: hsl(var(--sl-control-fg));\n background: transparent;\n border: 0;\n cursor: pointer;\n transition:\n background .15s ease,\n color .15s ease,\n transform .15s ease;\n white-space: nowrap;\n}\n.ra-shell .ra-tab:hover {\n background: hsl(var(--sl-control-hover-bg) / 0.10);\n color: hsl(var(--sl-control-hover-fg));\n}\n.ra-shell .ra-tab:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px hsl(var(--sl-control-focus-ring) / 0.45);\n}\n.ra-shell .ra-tab[aria-selected=true] {\n background: hsl(var(--sl-control-active-bg));\n color: hsl(var(--sl-control-active-fg));\n border-color: hsl(var(--sl-control-active-bd));\n font-weight: var(--sl-control-active-weight);\n box-shadow: 0 1px 2px hsl(var(--sl-control-active-bg) / 0.25);\n}\n.ra-shell .ra-tab[aria-selected=true]:hover {\n background: hsl(var(--sl-control-active-bg) / 0.92);\n color: hsl(var(--sl-control-active-fg));\n}\n.ra-shell .ra-tab[aria-selected=true] .ra-tab-icon {\n color: hsl(var(--sl-control-active-fg));\n}\n.ra-shell .ra-tab[disabled] {\n opacity: .5;\n cursor: not-allowed;\n}\n.ra-shell .ra-tab-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.25rem;\n padding: 0 0.35rem;\n height: 1.1rem;\n border-radius: 999px;\n background: hsl(var(--sl-control-active-fg) / 0.20);\n color: hsl(var(--sl-control-active-fg));\n font-size: 0.625rem;\n font-weight: 600;\n line-height: 1;\n}\n.ra-shell .ra-tab[aria-selected=false] .ra-tab-count {\n background: hsl(var(--sl-control-fg) / 0.15);\n color: hsl(var(--sl-control-fg));\n}\n.ra-shell[data-density=compact] .ra-row {\n padding-block: 0.4rem;\n}\n.ra-shell .ra-row {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n width: 100%;\n text-align: left;\n padding: 0.45rem 0.75rem;\n border-left: 3px solid transparent;\n background: transparent;\n border-bottom: 1px solid transparent;\n transition: background .12s ease, border-color .12s ease;\n cursor: pointer;\n color: hsl(var(--ra-text));\n font-family: inherit;\n}\n.ra-shell .ra-row + .ra-row {\n border-top: 1px solid hsl(var(--ra-border) / 0.6);\n}\n.ra-shell .ra-row:hover {\n background: var(--ra-row-hover);\n}\n.ra-shell .ra-row[data-selected=true] {\n background: var(--ra-row-active-bg);\n border-left-color: var(--ra-row-active-bd);\n}\n.ra-shell .ra-row-compact {\n padding-block: 0.3rem;\n}\n.ra-shell .ra-row-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: calc(var(--ra-radius) * 0.6);\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n flex-shrink: 0;\n}\n.ra-shell .ra-row[data-selected=true] .ra-row-icon {\n background: hsl(var(--ra-accent) / 0.15);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-row-body {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-row-title {\n font-weight: var(--ra-title-weight);\n font-size: 0.8125rem;\n line-height: 1.2;\n color: hsl(var(--ra-text));\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-row-sub {\n font-size: 0.6875rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.05rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-row-rule-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 0.2rem;\n margin-top: 0.2rem;\n}\n.ra-shell .ra-rule-chip {\n display: inline-flex;\n align-items: center;\n max-width: 100%;\n padding: 0.05rem 0.4rem;\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 500;\n line-height: 1.4;\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-accent));\n border: 1px solid hsl(var(--ra-accent) / 0.20);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-rule-chip-more {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-shell[data-density=compact] .ra-row-rule-chips {\n margin-top: 0.15rem;\n gap: 0.15rem;\n}\n.ra-shell[data-density=compact] .ra-rule-chip {\n font-size: 0.6rem;\n padding: 0.02rem 0.35rem;\n}\n.ra-shell .ra-rule-filters {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n.ra-shell .ra-rule-filters-row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n}\n.ra-shell .ra-rule-filter-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n font-size: 0.65rem;\n font-weight: 500;\n line-height: 1.4;\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n cursor: pointer;\n transition:\n background .12s ease,\n color .12s ease,\n border-color .12s ease;\n max-width: 100%;\n}\n.ra-shell .ra-rule-filter-chip:hover {\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-text));\n border-color: hsl(var(--ra-accent) / 0.25);\n}\n.ra-shell .ra-rule-filter-chip[data-active=true] {\n background: hsl(var(--ra-accent) / 0.15);\n color: hsl(var(--ra-accent));\n border-color: hsl(var(--ra-accent) / 0.40);\n}\n.ra-shell .ra-rule-filter-chip[data-tone=complexity][data-active=true] {\n background: hsl(var(--ra-info) / 0.15);\n color: hsl(var(--ra-info));\n border-color: hsl(var(--ra-info) / 0.40);\n}\n.ra-shell .ra-rule-filter-chip-label {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 9rem;\n}\n.ra-shell .ra-rule-filter-chip-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.1rem;\n height: 1rem;\n padding: 0 0.3rem;\n border-radius: 999px;\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-muted-text));\n font-size: 0.6rem;\n font-weight: 600;\n}\n.ra-shell .ra-rule-filter-chip[data-active=true] .ra-rule-filter-chip-count {\n background: hsl(var(--ra-accent) / 0.18);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-rule-filter-clear {\n align-self: flex-start;\n background: transparent;\n border: 0;\n padding: 0;\n color: hsl(var(--ra-muted-text));\n font-size: 0.65rem;\n cursor: pointer;\n text-decoration: underline;\n text-decoration-style: dotted;\n}\n.ra-shell .ra-rule-filter-clear:hover {\n color: hsl(var(--ra-text));\n}\n.ra-shell[data-density=compact] .ra-row {\n padding-block: 0.3rem;\n gap: 0.45rem;\n}\n.ra-shell[data-density=compact] .ra-row-title {\n font-size: 0.78125rem;\n}\n.ra-shell .ra-row-actions {\n display: inline-flex;\n align-items: center;\n gap: 0.15rem;\n margin-left: auto;\n opacity: 0;\n transition: opacity .15s ease;\n}\n.ra-shell .ra-row:hover .ra-row-actions,\n.ra-shell .ra-row:focus-within .ra-row-actions {\n opacity: 1;\n}\n.ra-shell .ra-row-action {\n width: 1.6rem;\n height: 1.6rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 999px;\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border: 0;\n cursor: pointer;\n transition: background .15s ease, color .15s ease;\n}\n.ra-shell .ra-row-action:hover {\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-row-action[data-tone=danger]:hover {\n background: hsl(var(--ra-danger) / 0.12);\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n font-size: 0.6875rem;\n font-weight: 500;\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n white-space: nowrap;\n max-width: 14rem;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-chip[data-tone=success] {\n background: hsl(var(--ra-success) / 0.12);\n color: hsl(var(--ra-success));\n border-color: hsl(var(--ra-success) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=warning] {\n background: hsl(var(--ra-warning) / 0.14);\n color: hsl(var(--ra-warning));\n border-color: hsl(var(--ra-warning) / 0.35);\n}\n.ra-shell .ra-chip[data-tone=info] {\n background: hsl(var(--ra-info) / 0.10);\n color: hsl(var(--ra-info));\n border-color: hsl(var(--ra-info) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=danger] {\n background: hsl(var(--ra-danger) / 0.10);\n color: hsl(var(--ra-danger));\n border-color: hsl(var(--ra-danger) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=muted] {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-style: dashed;\n}\n.ra-shell .ra-group {\n border-bottom: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-group:last-child {\n border-bottom: 0;\n}\n.ra-shell .ra-group-summary {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 0.85rem;\n background: hsl(var(--ra-muted) / 0.6);\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n border: 0;\n cursor: pointer;\n transition: background .12s ease;\n}\n.ra-shell .ra-group-summary:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-group-summary .ra-group-chevron {\n transition: transform .15s ease;\n}\n.ra-shell .ra-group[data-open=false] .ra-group-chevron {\n transform: rotate(-90deg);\n}\n.ra-shell .ra-group-name {\n flex: 1;\n text-align: left;\n}\n.ra-shell .ra-group-count {\n font-size: 0.65rem;\n font-weight: 600;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n padding: 0.05rem 0.4rem;\n}\n.ra-shell .ra-group[data-open=false] .ra-group-body {\n display: none;\n}\n.ra-shell .ra-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 2.5rem 1.5rem;\n gap: 0.75rem;\n}\n.ra-shell .ra-empty-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 3.25rem;\n height: 3.25rem;\n border-radius: 999px;\n background: hsl(var(--ra-accent) / 0.08);\n color: hsl(var(--ra-accent));\n margin-bottom: 0.25rem;\n}\n.ra-shell .ra-empty-title {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 1rem;\n color: hsl(var(--ra-text));\n margin: 0;\n letter-spacing: -0.01em;\n}\n.ra-shell .ra-empty-body {\n font-size: 0.8125rem;\n color: hsl(var(--ra-muted-text));\n max-width: 22rem;\n line-height: 1.45;\n}\n.ra-shell .ra-empty-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-top: 0.25rem;\n flex-wrap: wrap;\n justify-content: center;\n}\n.ra-shell .ra-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.45rem 0.85rem;\n border-radius: calc(var(--ra-radius) * 0.7);\n font-size: 0.8125rem;\n font-weight: 500;\n border: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n cursor: pointer;\n transition:\n background .15s ease,\n border-color .15s ease,\n box-shadow .15s ease,\n transform .1s ease;\n}\n.ra-shell .ra-btn:hover {\n background: hsl(var(--ra-muted));\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-btn:active {\n transform: translateY(1px);\n}\n.ra-shell .ra-btn[data-variant=primary] {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-surface));\n border-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-btn[data-variant=primary]:hover {\n background: hsl(var(--ra-accent) / 0.92);\n}\n.ra-shell .ra-btn[data-variant=ghost] {\n background: transparent;\n border-color: transparent;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-btn[data-variant=ghost]:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-btn[data-variant=danger] {\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-btn[data-variant=danger]:hover {\n background: hsl(var(--ra-danger) / 0.10);\n border-color: hsl(var(--ra-danger) / 0.40);\n}\n.ra-shell .ra-intro {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.55rem;\n padding: 0.4rem 2rem 0.4rem 0.5rem;\n border-radius: var(--ra-radius);\n border: 1px solid hsl(var(--ra-info) / 0.30);\n background: hsl(var(--ra-info) / 0.08);\n}\n.ra-shell .ra-intro[data-tone=success] {\n border-color: hsl(var(--ra-success) / 0.30);\n background: hsl(var(--ra-success) / 0.08);\n}\n.ra-shell .ra-intro[data-tone=warning] {\n border-color: hsl(var(--ra-warning) / 0.35);\n background: hsl(var(--ra-warning) / 0.10);\n}\n.ra-shell .ra-intro-icon {\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: hsl(var(--ra-info) / 0.18);\n color: hsl(var(--ra-info));\n}\n.ra-shell .ra-intro[data-tone=success] .ra-intro-icon {\n background: hsl(var(--ra-success) / 0.18);\n color: hsl(var(--ra-success));\n}\n.ra-shell .ra-intro[data-tone=warning] .ra-intro-icon {\n background: hsl(var(--ra-warning) / 0.20);\n color: hsl(var(--ra-warning));\n}\n.ra-shell .ra-intro-body {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-intro-title {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-title-weight);\n font-size: 0.8rem;\n color: hsl(var(--ra-text));\n margin: 0;\n line-height: 1.2;\n display: inline;\n}\n.ra-shell .ra-intro-text {\n font-size: 0.78rem;\n color: hsl(var(--ra-text) / 0.85);\n line-height: 1.35;\n display: inline;\n margin-left: 0.4rem;\n}\n.ra-shell .ra-intro-dismiss {\n position: absolute;\n top: 50%;\n right: 0.35rem;\n transform: translateY(-50%);\n width: 1.4rem;\n height: 1.4rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 0;\n color: hsl(var(--ra-muted-text));\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n}\n.ra-shell .ra-intro-dismiss:hover {\n background: hsl(var(--ra-text) / 0.06);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-bulk-menu {\n min-width: 12rem;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: calc(var(--ra-radius) * 0.85);\n box-shadow: var(--ra-card-shadow-hover);\n padding: 0.3rem;\n z-index: 60;\n}\n.ra-shell .ra-bulk-item {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n width: 100%;\n padding: 0.45rem 0.6rem;\n border-radius: calc(var(--ra-radius) * 0.6);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n background: transparent;\n border: 0;\n cursor: pointer;\n text-align: left;\n transition: background .12s ease, color .12s ease;\n}\n.ra-shell .ra-bulk-item:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-bulk-item[data-tone=danger] {\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-bulk-item[data-tone=danger]:hover {\n background: hsl(var(--ra-danger) / 0.10);\n}\n.ra-shell .ra-bulk-divider {\n height: 1px;\n background: hsl(var(--ra-border));\n margin: 0.25rem 0;\n}\n.ra-shell .ra-preview-rail {\n background: hsl(var(--ra-surface));\n border-left: 1px solid hsl(var(--ra-border));\n box-shadow: -4px 0 16px hsl(var(--ra-accent) / 0.04);\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ra-shell .ra-preview-rail-header {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n background:\n linear-gradient(\n 180deg,\n hsl(var(--ra-surface)) 0%,\n hsl(var(--ra-surface) / 0.92) 100%);\n border-bottom: 1px solid hsl(var(--ra-border));\n backdrop-filter: blur(6px);\n}\n.ra-shell .ra-preview-rail-title {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-preview-rail-body {\n flex: 1;\n overflow-y: auto;\n padding: 1rem;\n}\n.ra-confirm-root {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n background: transparent;\n}\n.ra-confirm-root .ra-confirm-backdrop {\n position: absolute;\n inset: 0;\n background: hsl(0 0% 0% / 0.45);\n backdrop-filter: blur(2px);\n animation: ra-confirm-fade .12s ease-out;\n}\n.ra-confirm-root .ra-confirm-card {\n position: relative;\n width: min(440px, 100%);\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 1px 2px hsl(0 0% 0% / 0.08), 0 24px 48px -12px hsl(0 0% 0% / 0.32);\n padding: 1.25rem;\n animation: ra-confirm-pop .14s ease-out;\n}\n.ra-confirm-root .ra-confirm-header {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n margin-bottom: 0.5rem;\n}\n.ra-confirm-root .ra-confirm-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 999px;\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.12);\n color: hsl(var(--ra-warning, 38 92% 50%));\n}\n.ra-confirm-root .ra-confirm-title {\n font-family: var(--ra-font-display);\n font-weight: 600;\n font-size: 1rem;\n margin: 0;\n}\n.ra-confirm-root .ra-confirm-body {\n font-size: 0.875rem;\n color: hsl(var(--ra-muted-text));\n margin: 0 0 1.1rem;\n line-height: 1.45;\n}\n.ra-confirm-root .ra-confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n.ra-confirm-root .ra-confirm-btn {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius) - 2px);\n padding: 0.45rem 0.85rem;\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease;\n}\n.ra-confirm-root .ra-confirm-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n}\n.ra-confirm-root .ra-confirm-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-confirm-root .ra-confirm-btn-ghost:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-confirm-root .ra-confirm-btn-danger {\n background: transparent;\n color: hsl(var(--ra-danger, 0 72% 51%));\n border-color: hsl(var(--ra-danger, 0 72% 51%) / 0.45);\n}\n.ra-confirm-root .ra-confirm-btn-danger:hover {\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.08);\n border-color: hsl(var(--ra-danger, 0 72% 51%));\n}\n.ra-confirm-root .ra-confirm-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.ra-confirm-root .ra-confirm-btn-primary:hover {\n filter: brightness(0.95);\n}\n@keyframes ra-confirm-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n@keyframes ra-confirm-pop {\n from {\n opacity: 0;\n transform: translateY(4px) scale(.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n.ra-shell .ra-unsaved-banner {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: var(--ra-radius);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-warning, 38 92% 50%));\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ra-shell .ra-unsaved-context {\n color: hsl(var(--ra-muted-text));\n font-weight: 400;\n}\n.ra-shell .ra-unsaved-error {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-weight: 500;\n}\n.ra-shell .ra-unsaved-actions {\n display: inline-flex;\n gap: 0.4rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius) - 2px);\n padding: 0.3rem 0.6rem;\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease,\n opacity .12s ease;\n}\n.ra-shell .ra-unsaved-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ra-shell .ra-unsaved-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n}\n.ra-shell .ra-unsaved-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-shell .ra-unsaved-btn-ghost:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-unsaved-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-unsaved-btn-primary:hover:not(:disabled) {\n filter: brightness(0.95);\n}\n.sl-aph .ra-unsaved-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius, 8px) - 2px);\n padding: 0.3rem 0.6rem;\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease,\n opacity .12s ease;\n}\n.sl-aph .ra-unsaved-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.sl-aph .ra-unsaved-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring, 0 0 0 3px hsl(var(--ra-accent) / 0.35));\n}\n.sl-aph .ra-unsaved-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.sl-aph .ra-unsaved-btn-ghost:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.sl-aph .ra-unsaved-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.sl-aph .ra-unsaved-btn-primary:hover:not(:disabled) {\n filter: brightness(0.95);\n}\n.sl-aph .ra-unsaved-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-warning, 38 92% 50%));\n flex-shrink: 0;\n}\n@keyframes ra-unsaved-slide {\n from {\n opacity: 0;\n transform: translateY(-3px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ra-shell .ra-unsaved-pill,\n.sl-aph .ra-unsaved-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.5rem 0.25rem 0.6rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: 999px;\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-pill .ra-unsaved-pill-text,\n.sl-aph .ra-unsaved-pill .ra-unsaved-pill-text {\n font-size: 0.75rem;\n font-weight: 500;\n color: hsl(var(--ra-text));\n white-space: nowrap;\n margin-right: 0.15rem;\n}\n.ra-shell .ra-clipboard-toast {\n position: fixed;\n bottom: 1.25rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 90;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n max-width: min(28rem, calc(100vw - 2rem));\n padding: 0.55rem 0.85rem;\n border-radius: 999px;\n background: hsl(var(--ra-text));\n color: hsl(var(--ra-surface));\n font-size: 0.75rem;\n line-height: 1;\n box-shadow: 0 8px 24px -10px hsl(0 0% 0% / 0.45);\n animation: ra-clipboard-pop 0.18s ease-out both;\n pointer-events: none;\n}\n@keyframes ra-clipboard-pop {\n from {\n opacity: 0;\n transform: translate(-50%, 6px);\n }\n to {\n opacity: 1;\n transform: translate(-50%, 0);\n }\n}\n.ra-shell .ra-row-menu-wrap {\n display: inline-flex;\n align-items: center;\n margin-left: 0.25rem;\n}\n.ra-shell .ra-row-menu-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 0.35rem;\n background: transparent;\n color: hsl(var(--ra-muted-text));\n opacity: 0;\n transition:\n opacity .15s ease,\n background .15s ease,\n color .15s ease;\n border: 1px solid transparent;\n}\n.ra-shell .ra-row:hover .ra-row-menu-trigger,\n.ra-shell .ra-card-hover:hover .ra-row-menu-trigger,\n.ra-shell .ra-row-menu-trigger:focus-visible,\n.ra-shell .ra-row-menu-trigger[aria-expanded=true] {\n opacity: 1;\n}\n.ra-shell .ra-row-menu-trigger:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-row-menu {\n position: absolute;\n right: 0;\n top: calc(100% + 4px);\n z-index: 50;\n min-width: 11rem;\n padding: 0.25rem;\n border-radius: 0.5rem;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n box-shadow: 0 12px 28px -10px hsl(0 0% 0% / 0.25);\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n.ra-shell .ra-row-menu-item {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 0.55rem;\n border-radius: 0.35rem;\n font-size: 0.75rem;\n color: hsl(var(--ra-text));\n background: transparent;\n border: 0;\n text-align: left;\n width: 100%;\n cursor: pointer;\n}\n.ra-shell .ra-row-menu-item:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-row-menu-item:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ra-shell .ra-item-list {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n.ra-shell .ra-item-list-body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 1rem 1.25rem 1.5rem;\n}\n.ra-shell .ra-item-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.75rem 1.25rem;\n border-bottom: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-item-toolbar-title {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n min-width: 0;\n}\n.ra-shell .ra-item-toolbar-count {\n font-size: 0.7rem;\n font-weight: 600;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-muted));\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n padding: 0.05rem 0.45rem;\n}\n.ra-shell .ra-item-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-item-table-wrap {\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n background: hsl(var(--ra-surface));\n overflow: hidden;\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-item-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.85rem;\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-item-table thead th {\n text-align: left;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n padding: 0.65rem 0.85rem;\n background: hsl(var(--ra-muted) / 0.55);\n border-bottom: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-item-table tbody td {\n padding: 0.65rem 0.85rem;\n border-bottom: 1px solid hsl(var(--ra-border) / 0.7);\n vertical-align: middle;\n}\n.ra-shell .ra-item-table tbody tr:last-child td {\n border-bottom: 0;\n}\n.ra-shell .ra-item-row {\n cursor: pointer;\n transition: background .12s ease;\n}\n.ra-shell .ra-item-row:hover {\n background: var(--ra-row-hover);\n}\n.ra-shell .ra-item-row[data-selected=true] {\n background: var(--ra-row-active-bg);\n}\n.ra-shell .ra-item-row-title {\n font-weight: var(--ra-title-weight);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-item-row-sub {\n font-size: 0.75rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n}\n.ra-shell .ra-item-row-meta {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-item-row-actions {\n text-align: right;\n white-space: nowrap;\n}\n.ra-shell .ra-item-row-actions .ra-row-action + .ra-row-action {\n margin-left: 0.15rem;\n}\n.ra-shell .ra-item-cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--ra-item-card-min, 240px), 1fr));\n gap: 0.85rem;\n}\n.ra-shell .ra-item-gallery {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--ra-item-gallery-min, 320px), 1fr));\n gap: 1rem;\n}\n.ra-shell .ra-item-cards[data-card-size=sm] {\n --ra-item-card-min: 180px;\n}\n.ra-shell .ra-item-cards[data-card-size=md] {\n --ra-item-card-min: 240px;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] {\n --ra-item-card-min: 320px;\n gap: 1rem;\n}\n.ra-shell .ra-item-gallery[data-card-size=sm] {\n --ra-item-gallery-min: 240px;\n}\n.ra-shell .ra-item-gallery[data-card-size=md] {\n --ra-item-gallery-min: 320px;\n}\n.ra-shell .ra-item-gallery[data-card-size=lg] {\n --ra-item-gallery-min: 420px;\n gap: 1.25rem;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] .ra-item-card-title,\n.ra-shell .ra-item-gallery[data-card-size=lg] .ra-item-card-title {\n font-size: 0.95rem;\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] .ra-item-card-body,\n.ra-shell .ra-item-gallery[data-card-size=lg] .ra-item-card-body {\n padding: 0.85rem 1rem 1rem;\n}\n.ra-shell .ra-item-card {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n text-align: left;\n padding: 0;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n overflow: hidden;\n cursor: pointer;\n transition:\n box-shadow .18s ease,\n transform .12s ease,\n border-color .15s ease;\n box-shadow: var(--ra-card-shadow);\n font-family: inherit;\n color: inherit;\n}\n.ra-shell .ra-item-card:hover {\n box-shadow: var(--ra-card-shadow-hover);\n border-color: hsl(var(--ra-accent) / 0.30);\n}\n.ra-shell .ra-item-card[data-selected=true] {\n border-color: hsl(var(--ra-accent) / 0.55);\n box-shadow: var(--ra-card-shadow-hover);\n}\n.ra-shell .ra-item-card-thumb {\n width: 100%;\n aspect-ratio: 1 / 1;\n background:\n linear-gradient(\n 135deg,\n hsl(var(--ra-accent) / 0.12),\n hsl(var(--ra-accent) / 0.04));\n display: flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-accent));\n overflow: hidden;\n}\n.ra-shell .ra-item-card-thumb--gallery {\n aspect-ratio: 16 / 9;\n}\n.ra-shell .ra-item-card-thumb img {\n width: 100%;\n height: 100%;\n -o-object-fit: cover;\n object-fit: cover;\n}\n.ra-shell .ra-item-card-initials {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 1.5rem;\n letter-spacing: 0.02em;\n}\n.ra-shell .ra-item-card-body {\n padding: 0.65rem 0.8rem 0.85rem;\n min-width: 0;\n}\n.ra-shell .ra-item-card-title {\n font-weight: var(--ra-title-weight);\n font-size: 0.85rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-item-card-sub {\n font-size: 0.75rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-item-card-delete {\n position: absolute;\n top: 0.4rem;\n right: 0.4rem;\n background: hsl(var(--ra-surface) / 0.85);\n backdrop-filter: blur(4px);\n opacity: 0;\n transition: opacity .15s ease;\n}\n.ra-shell .ra-item-card:hover .ra-item-card-delete,\n.ra-shell .ra-item-card:focus-within .ra-item-card-delete {\n opacity: 1;\n}\n.ra-shell .ra-item-nav {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 1.25rem;\n border-bottom: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-item-nav-position {\n font-size: 0.72rem;\n color: hsl(var(--ra-muted-text));\n font-variant-numeric: tabular-nums;\n}\n.ra-shell .ra-item-nav-arrows {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 0.15rem;\n}\n.ra-shell .ra-item-nav-arrows .ra-row-action[disabled] {\n opacity: 0.35;\n cursor: not-allowed;\n}\n.ra-shell .ra-sibling-rail {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n.ra-shell .ra-sibling-back {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.6rem 0.85rem;\n font-size: 0.75rem;\n font-weight: 500;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-muted) / 0.5);\n border: 0;\n border-bottom: 1px solid hsl(var(--ra-border));\n cursor: pointer;\n text-align: left;\n transition: background .12s ease, color .12s ease;\n}\n.ra-shell .ra-sibling-back:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-sibling-heading {\n padding: 0.6rem 0.85rem 0.4rem;\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-sibling-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n.ra-shell .ra-sibling-list {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n.ra-shell .ra-status-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: 9999px;\n}\n.ra-shell .ra-status-icon > svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n.ra-shell .ra-status-icon--own {\n color: hsl(var(--ra-status-own));\n}\n.ra-shell .ra-status-icon--shared {\n color: hsl(var(--ra-status-shared));\n}\n.ra-shell .ra-status-icon--missing {\n color: hsl(var(--ra-status-missing) / 0.7);\n}\n.ra-shell .ra-row-status {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-row-scope {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: calc(var(--ra-radius) * 0.5);\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n flex-shrink: 0;\n margin-left: auto;\n opacity: 0.55;\n transition:\n opacity .12s ease,\n color .12s ease,\n background .12s ease;\n}\n.ra-shell .ra-row:hover .ra-row-scope {\n opacity: 0.85;\n}\n.ra-shell .ra-row[data-selected=true] .ra-row-scope {\n opacity: 1;\n background: hsl(var(--ra-accent) / 0.12);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-row[data-tone=own] .ra-row-sub {\n color: hsl(var(--ra-status-own));\n}\n.ra-shell .ra-row[data-tone=shared] .ra-row-sub {\n color: hsl(var(--ra-status-shared));\n}\n.ra-shell .ra-row[data-selected=true] {\n background:\n linear-gradient(\n 90deg,\n hsl(var(--ra-accent) / 0.10) 0%,\n hsl(var(--ra-accent) / 0.04) 100%);\n border-left-width: 3px;\n border-left-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-dirty-pip {\n display: inline-block;\n width: 0.45rem;\n height: 0.45rem;\n border-radius: 9999px;\n background: hsl(var(--ra-warning));\n box-shadow: 0 0 0 2px hsl(var(--ra-warning) / 0.18);\n flex-shrink: 0;\n}\n.ra-shell .ra-error-pip {\n display: inline-block;\n width: 0.45rem;\n height: 0.45rem;\n border-radius: 9999px;\n background: hsl(var(--ra-danger, 0 72% 51%));\n box-shadow: 0 0 0 2px hsl(var(--ra-danger, 0 72% 51%) / 0.22);\n flex-shrink: 0;\n}\n.ra-shell .ra-group-summary {\n background: transparent;\n}\n.ra-shell {\n position: relative;\n}\n.ra-shell .ra-help-float {\n position: absolute;\n top: 0.65rem;\n right: 0.85rem;\n z-index: 5;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.6rem;\n height: 1.6rem;\n padding: 0;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-surface) / 0.85);\n backdrop-filter: blur(6px);\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n cursor: pointer;\n transition:\n color .12s ease,\n background .12s ease,\n border-color .12s ease;\n}\n.ra-shell .ra-help-float:hover {\n color: hsl(var(--ra-accent));\n border-color: hsl(var(--ra-accent) / 0.4);\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-help-float svg {\n width: 0.95rem;\n height: 0.95rem;\n}\n.ra-shell .ra-help-float > span {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n.ra-shell .ra-preview-reopen {\n position: absolute;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 4;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.4rem;\n padding: 0.65rem 0.45rem;\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n border-right: 0;\n border-radius: calc(var(--ra-radius) * 0.85) 0 0 calc(var(--ra-radius) * 0.85);\n box-shadow: var(--ra-card-shadow);\n cursor: pointer;\n transition:\n color .12s ease,\n background .12s ease,\n padding-right .15s ease;\n writing-mode: vertical-rl;\n font-size: 0.7rem;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.ra-shell .ra-preview-reopen:hover {\n color: hsl(var(--ra-accent));\n background: hsl(var(--ra-accent) / 0.04);\n padding-right: 0.6rem;\n}\n.ra-shell .ra-preview-reopen svg {\n width: 0.85rem;\n height: 0.85rem;\n writing-mode: horizontal-tb;\n}\n.ra-shell .ra-unsaved-tray {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: var(--ra-radius);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-count {\n flex: 1;\n min-width: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.4rem;\n margin: -0.15rem -0.4rem;\n background: transparent;\n border: 0;\n color: inherit;\n font: inherit;\n font-weight: 500;\n text-align: left;\n cursor: pointer;\n border-radius: calc(var(--ra-radius) - 4px);\n}\n.ra-shell .ra-unsaved-count:hover {\n background: hsl(var(--ra-muted) / 0.6);\n}\n.ra-shell .ra-unsaved-error-chip {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid hsl(var(--ra-danger, 0 72% 51%) / 0.35);\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.08);\n color: hsl(var(--ra-danger, 0 72% 51%));\n border-radius: 999px;\n padding: 0.15rem 0.55rem;\n font-size: 0.7rem;\n font-weight: 500;\n cursor: pointer;\n}\n.ra-shell .ra-unsaved-error-chip:hover {\n filter: brightness(0.97);\n}\n.ra-shell .ra-unsaved-popover {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 60;\n min-width: 18rem;\n max-height: 18rem;\n overflow: auto;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 12px 30px -10px hsl(0 0% 0% / 0.25);\n padding: 0.3rem;\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n}\n.ra-shell .ra-unsaved-popover-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 0.55rem;\n background: transparent;\n border: 0;\n border-radius: calc(var(--ra-radius) - 2px);\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-unsaved-popover-row:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-unsaved-popover-dot {\n width: 0.5rem;\n height: 0.5rem;\n border-radius: 999px;\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-popover-label {\n flex: 1;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 0.8125rem;\n}\n.ra-shell .ra-unsaved-popover-ctx {\n color: hsl(var(--ra-muted-text));\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n.ra-shell .ra-unsaved-popover-err {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.7rem;\n font-weight: 500;\n}\n.ra-saveall-overlay {\n position: fixed;\n inset: 0;\n z-index: 100;\n background: hsl(0 0% 0% / 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n animation: ra-confirm-fade .12s ease-out;\n}\n.ra-saveall-card {\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 24px 48px -16px hsl(0 0% 0% / 0.45);\n width: min(28rem, 100%);\n max-height: min(80vh, 36rem);\n display: flex;\n flex-direction: column;\n animation: ra-confirm-pop .14s ease-out;\n}\n.ra-saveall-header {\n padding: 1rem 1rem 0.5rem;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.ra-saveall-title {\n font-weight: 600;\n font-size: 0.95rem;\n}\n.ra-saveall-progress {\n height: 4px;\n background: hsl(var(--ra-muted));\n border-radius: 999px;\n overflow: hidden;\n}\n.ra-saveall-progress-bar {\n height: 100%;\n background: hsl(var(--ra-accent));\n transition: width .2s ease;\n}\n.ra-saveall-counter {\n color: hsl(var(--ra-muted-text));\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n}\n.ra-saveall-list {\n list-style: none;\n margin: 0;\n padding: 0.25rem 0.5rem;\n overflow: auto;\n flex: 1;\n}\n.ra-saveall-row {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.45rem 0.5rem;\n border-radius: calc(var(--ra-radius) - 4px);\n font-size: 0.8125rem;\n}\n.ra-saveall-row[data-status=saving] {\n background: hsl(var(--ra-accent) / 0.06);\n}\n.ra-saveall-row[data-status=saved] {\n color: hsl(var(--ra-muted-text));\n}\n.ra-saveall-row[data-status=error] {\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.06);\n}\n.ra-saveall-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: hsl(var(--ra-muted-text));\n}\n.ra-saveall-row[data-status=saved] .ra-saveall-icon {\n color: hsl(var(--ra-success, 142 71% 45%));\n}\n.ra-saveall-row[data-status=saving] .ra-saveall-icon {\n color: hsl(var(--ra-accent));\n}\n.ra-saveall-row[data-status=error] .ra-saveall-icon {\n color: hsl(var(--ra-danger, 0 72% 51%));\n}\n.ra-saveall-label {\n flex: 1;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-saveall-err {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.7rem;\n max-width: 12rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-saveall-actions {\n padding: 0.75rem 1rem 1rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.4rem;\n border-top: 1px solid hsl(var(--ra-border));\n}\n.ra-spin {\n animation: ra-spin 1s linear infinite;\n}\n@keyframes ra-spin {\n to {\n transform: rotate(360deg);\n }\n}\n");
|
|
5467
|
+
var EditorPoolBody = ({
|
|
5468
|
+
editorId,
|
|
5469
|
+
renderEditor
|
|
5470
|
+
}) => {
|
|
5471
|
+
const ctx = useEditorSlotContext(editorId);
|
|
5472
|
+
if (!ctx) return null;
|
|
5473
|
+
return renderEditor(ctx);
|
|
5474
|
+
};
|
|
4814
5475
|
var TOP_LEVEL_SCOPES = ["collection", "rule", "product"];
|
|
4815
5476
|
var WARNED_FACET_DEPRECATED = false;
|
|
4816
5477
|
var DRAFT_ID = "__draft__";
|
|
@@ -4836,12 +5497,28 @@ var productItemToSummary = (p) => {
|
|
|
4836
5497
|
};
|
|
4837
5498
|
};
|
|
4838
5499
|
function RecordsAdminShell(props) {
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5500
|
+
const ctx = useMemo(
|
|
5501
|
+
() => ({
|
|
5502
|
+
SL: props.SL,
|
|
5503
|
+
collectionId: props.collectionId,
|
|
5504
|
+
appId: props.appId,
|
|
5505
|
+
recordType: props.recordType
|
|
5506
|
+
}),
|
|
5507
|
+
[props.SL, props.collectionId, props.appId, props.recordType]
|
|
5508
|
+
);
|
|
5509
|
+
return /* @__PURE__ */ jsx(
|
|
5510
|
+
EditorSessionProvider,
|
|
5511
|
+
{
|
|
5512
|
+
ctx,
|
|
5513
|
+
defaultValueFactory: props.defaultData,
|
|
5514
|
+
children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props })
|
|
5515
|
+
}
|
|
5516
|
+
);
|
|
5517
|
+
}
|
|
5518
|
+
function RecordsAdminShellInner(props) {
|
|
5519
|
+
const {
|
|
5520
|
+
SL,
|
|
5521
|
+
appId,
|
|
4845
5522
|
collectionId,
|
|
4846
5523
|
recordType,
|
|
4847
5524
|
label,
|
|
@@ -4995,10 +5672,6 @@ function RecordsAdminShellInner(props) {
|
|
|
4995
5672
|
useEffect(() => {
|
|
4996
5673
|
setActiveScope(initialScope);
|
|
4997
5674
|
}, [initialScope]);
|
|
4998
|
-
const [search, setSearch] = useState("");
|
|
4999
|
-
const [filter, setFilter] = useState("all");
|
|
5000
|
-
const [ruleFilters, setRuleFilters] = useState(EMPTY_RULE_FILTERS);
|
|
5001
|
-
const [facetBrowseFilter, setFacetBrowseFilter] = useState(null);
|
|
5002
5675
|
const [selectedRecordId, setSelectedRecordId] = useState(null);
|
|
5003
5676
|
const [draftKind, setDraftKind] = useState(null);
|
|
5004
5677
|
const [ruleWizardStep, setRuleWizardStep] = useState(null);
|
|
@@ -5040,40 +5713,32 @@ function RecordsAdminShellInner(props) {
|
|
|
5040
5713
|
if (requested === "header" && !headerWillRender) return "footer";
|
|
5041
5714
|
return requested;
|
|
5042
5715
|
}, [intro?.reopenAffordance, headerWillRender]);
|
|
5043
|
-
const
|
|
5716
|
+
const browser = useShellBrowser({
|
|
5717
|
+
ctx,
|
|
5044
5718
|
SL,
|
|
5045
5719
|
collectionId,
|
|
5046
|
-
|
|
5047
|
-
|
|
5720
|
+
activeScope,
|
|
5721
|
+
contextScope,
|
|
5722
|
+
probeIsLoading: probe.isLoading,
|
|
5723
|
+
selectedProductId,
|
|
5724
|
+
drillTab,
|
|
5725
|
+
classify: classify2
|
|
5048
5726
|
});
|
|
5049
|
-
const
|
|
5050
|
-
const recordList = useRecordList({
|
|
5051
|
-
ctx,
|
|
5052
|
-
scopeKind: activeScope,
|
|
5727
|
+
const {
|
|
5053
5728
|
search,
|
|
5729
|
+
setSearch,
|
|
5054
5730
|
filter,
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
SL,
|
|
5067
|
-
collectionId,
|
|
5068
|
-
productId: selectedProductId,
|
|
5069
|
-
kind: drillTab === "variant" ? "variant" : null
|
|
5070
|
-
});
|
|
5071
|
-
const batchChildren = useProductChildren({
|
|
5072
|
-
SL,
|
|
5073
|
-
collectionId,
|
|
5074
|
-
productId: selectedProductId,
|
|
5075
|
-
kind: drillTab === "batch" ? "batch" : null
|
|
5076
|
-
});
|
|
5731
|
+
setFilter,
|
|
5732
|
+
ruleFilters,
|
|
5733
|
+
setRuleFilters,
|
|
5734
|
+
facetBrowseFilter,
|
|
5735
|
+
setFacetBrowseFilter,
|
|
5736
|
+
productBrowse,
|
|
5737
|
+
recordList,
|
|
5738
|
+
facetBrowse,
|
|
5739
|
+
variantChildren,
|
|
5740
|
+
batchChildren
|
|
5741
|
+
} = browser;
|
|
5077
5742
|
useEffect(() => {
|
|
5078
5743
|
if (activeScope !== "product") return;
|
|
5079
5744
|
if (selectedProductId) return;
|
|
@@ -5090,10 +5755,6 @@ function RecordsAdminShellInner(props) {
|
|
|
5090
5755
|
if (first?.id) setSelectedRecordId(first.id);
|
|
5091
5756
|
}, [activeScope, selectedRecordId, recordList.items, cardinality]);
|
|
5092
5757
|
useEffect(() => {
|
|
5093
|
-
setSearch("");
|
|
5094
|
-
setFilter("all");
|
|
5095
|
-
setRuleFilters(EMPTY_RULE_FILTERS);
|
|
5096
|
-
setFacetBrowseFilter(null);
|
|
5097
5758
|
setSelectedRecordId(null);
|
|
5098
5759
|
setDraftKind(null);
|
|
5099
5760
|
}, [activeScope]);
|
|
@@ -5230,9 +5891,35 @@ function RecordsAdminShellInner(props) {
|
|
|
5230
5891
|
batchChildren.refetch();
|
|
5231
5892
|
if (isCollection) collectionItems.refetch();
|
|
5232
5893
|
}, [productBrowse, recordList, facetBrowse, variantChildren, batchChildren, isCollection, collectionItems]);
|
|
5233
|
-
const
|
|
5234
|
-
|
|
5235
|
-
|
|
5894
|
+
const editorTargetSpec = useMemo(() => {
|
|
5895
|
+
if (!editingTargetScope) return null;
|
|
5896
|
+
const initialFacetRule = editingTargetScope.kind === "rule" ? ruleWizardStep === 2 && ruleWizardRule ? ruleWizardRule : resolved.facetRule ?? { all: [] } : ruleWizardStep === 2 && ruleWizardRule && isCollection && !!selectedItemId ? ruleWizardRule : null;
|
|
5897
|
+
const label2 = deriveDraftLabel ? deriveDraftLabel(resolved.data, editingTargetScope) : void 0;
|
|
5898
|
+
return {
|
|
5899
|
+
scope: editingTargetScope,
|
|
5900
|
+
recordId: resolved.recordId,
|
|
5901
|
+
// Collection-cardinality item drafts must `create` (insert a new
|
|
5902
|
+
// row) instead of upsert — multiple items can share the same scope
|
|
5903
|
+
// + recordType, so an upsert would collide on the server's dedupe
|
|
5904
|
+
// key. Flip on whenever the right pane shows an item that hasn't
|
|
5905
|
+
// been saved yet (no resolved.recordId).
|
|
5906
|
+
createMode: isCollection && !!selectedItemId && !resolved.recordId,
|
|
5907
|
+
ref: editingTargetScope.kind === "rule" ? editingTargetScope.raw : void 0,
|
|
5908
|
+
initialFacetRule,
|
|
5909
|
+
label: label2
|
|
5910
|
+
};
|
|
5911
|
+
}, [
|
|
5912
|
+
editingTargetScope?.raw,
|
|
5913
|
+
editingTargetScope?.kind,
|
|
5914
|
+
resolved.recordId,
|
|
5915
|
+
resolved.facetRule,
|
|
5916
|
+
isCollection,
|
|
5917
|
+
selectedItemId,
|
|
5918
|
+
ruleWizardStep,
|
|
5919
|
+
ruleWizardRule
|
|
5920
|
+
]);
|
|
5921
|
+
const editorCtx = useEditorBridge({
|
|
5922
|
+
target: editorTargetSpec,
|
|
5236
5923
|
resolved: {
|
|
5237
5924
|
data: resolved.data,
|
|
5238
5925
|
source: resolved.source,
|
|
@@ -5242,18 +5929,6 @@ function RecordsAdminShellInner(props) {
|
|
|
5242
5929
|
facetRule: resolved.facetRule
|
|
5243
5930
|
},
|
|
5244
5931
|
defaultData,
|
|
5245
|
-
reseed: dirtyStrategy === "keep" ? "preserve-dirty" : "always",
|
|
5246
|
-
// Collection-cardinality item drafts must `create` (insert a new row)
|
|
5247
|
-
// instead of `upsert` — multiple items can share the same scope +
|
|
5248
|
-
// recordType, so an upsert would collide on the server's dedupe key.
|
|
5249
|
-
// Flip on whenever the right pane shows an item that hasn't been
|
|
5250
|
-
// saved yet (no resolved.recordId).
|
|
5251
|
-
createMode: isCollection && !!selectedItemId && !resolved.recordId,
|
|
5252
|
-
// Seed an empty rule for freshly-minted `rule:{id}` refs so the Targeting
|
|
5253
|
-
// section can render its empty-state picker. For existing rule records
|
|
5254
|
-
// pull the saved rule off the resolved record. Pinned scopes get `null`
|
|
5255
|
-
// and the Targeting section stays hidden.
|
|
5256
|
-
initialFacetRule: editingTargetScope?.kind === "rule" ? ruleWizardStep === 2 && ruleWizardRule ? ruleWizardRule : resolved.facetRule ?? { all: [] } : ruleWizardStep === 2 && ruleWizardRule && isCollection && !!selectedItemId ? ruleWizardRule : null,
|
|
5257
5932
|
onSaved: () => {
|
|
5258
5933
|
onTelemetry?.({ type: "record.save", recordType, ref: editingTargetScope?.raw ?? "", isCreate: resolved.source !== "self" });
|
|
5259
5934
|
if (ruleWizardStep !== null) {
|
|
@@ -5273,8 +5948,7 @@ function RecordsAdminShellInner(props) {
|
|
|
5273
5948
|
setSelectedBatchId(void 0);
|
|
5274
5949
|
}
|
|
5275
5950
|
refetchAll();
|
|
5276
|
-
}
|
|
5277
|
-
deriveDraftLabel
|
|
5951
|
+
}
|
|
5278
5952
|
});
|
|
5279
5953
|
useUnsavedGuard({
|
|
5280
5954
|
isDirty: editorCtx.isDirty,
|
|
@@ -5286,32 +5960,41 @@ function RecordsAdminShellInner(props) {
|
|
|
5286
5960
|
disableParentMessage: dirtyStrategy === "keep"
|
|
5287
5961
|
});
|
|
5288
5962
|
const dirtyConfirm = useConfirmDialog();
|
|
5289
|
-
const
|
|
5290
|
-
const
|
|
5291
|
-
const
|
|
5292
|
-
const dirtyKeys = useMemo(
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5963
|
+
const dirtyOverview = useDirtyOverview();
|
|
5964
|
+
const editorSelection = useEditorSelection();
|
|
5965
|
+
const dirtyItems = dirtyOverview.items;
|
|
5966
|
+
const dirtyKeys = useMemo(() => {
|
|
5967
|
+
const s = /* @__PURE__ */ new Set();
|
|
5968
|
+
for (const it of dirtyItems) {
|
|
5969
|
+
if (it.status === "saved") continue;
|
|
5970
|
+
if (it.recordId) s.add(it.recordId);
|
|
5971
|
+
if (it.scope?.raw) s.add(it.scope.raw);
|
|
5972
|
+
}
|
|
5973
|
+
return s;
|
|
5974
|
+
}, [dirtyItems]);
|
|
5975
|
+
const errorKeys = useMemo(() => {
|
|
5976
|
+
const s = /* @__PURE__ */ new Set();
|
|
5977
|
+
for (const it of dirtyItems) {
|
|
5978
|
+
if (it.status !== "error") continue;
|
|
5979
|
+
if (it.recordId) s.add(it.recordId);
|
|
5980
|
+
if (it.scope?.raw) s.add(it.scope.raw);
|
|
5981
|
+
}
|
|
5982
|
+
return s;
|
|
5983
|
+
}, [dirtyItems]);
|
|
5300
5984
|
const [saveAllOpen, setSaveAllOpen] = useState(false);
|
|
5301
5985
|
const handleSaveAll = () => setSaveAllOpen(true);
|
|
5302
5986
|
const handleDiscardAll = () => {
|
|
5303
|
-
|
|
5304
|
-
draftStore.clearAll();
|
|
5987
|
+
dirtyOverview.discardAll();
|
|
5305
5988
|
};
|
|
5306
|
-
const showHeaderUnsavedPill = dirtyStrategy === "keep" &&
|
|
5307
|
-
const showTrayBanner = dirtyStrategy === "keep" && !showHeaderUnsavedPill &&
|
|
5989
|
+
const showHeaderUnsavedPill = dirtyStrategy === "keep" && canRenderInHeader(dirtyItems);
|
|
5990
|
+
const showTrayBanner = dirtyStrategy === "keep" && !showHeaderUnsavedPill && dirtyItems.length > 0;
|
|
5308
5991
|
const composedHeaderActions = useMemo(() => {
|
|
5309
5992
|
if (!showHeaderUnsavedPill) return headerActions;
|
|
5310
5993
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5311
5994
|
/* @__PURE__ */ jsx(
|
|
5312
5995
|
HeaderUnsavedPill,
|
|
5313
5996
|
{
|
|
5314
|
-
|
|
5997
|
+
items: dirtyItems,
|
|
5315
5998
|
isSaving: saveAllOpen,
|
|
5316
5999
|
onSaveAll: handleSaveAll,
|
|
5317
6000
|
onDiscardAll: handleDiscardAll,
|
|
@@ -5329,17 +6012,13 @@ function RecordsAdminShellInner(props) {
|
|
|
5329
6012
|
}, [
|
|
5330
6013
|
showHeaderUnsavedPill,
|
|
5331
6014
|
headerActions,
|
|
5332
|
-
|
|
6015
|
+
dirtyItems,
|
|
5333
6016
|
saveAllOpen,
|
|
5334
6017
|
actionLabels,
|
|
5335
6018
|
actionIcons,
|
|
5336
6019
|
i18n
|
|
5337
6020
|
]);
|
|
5338
|
-
const
|
|
5339
|
-
appId,
|
|
5340
|
-
recordType: recordType ?? "__default"
|
|
5341
|
-
});
|
|
5342
|
-
const [clipboardNotice, setClipboardNotice] = useState(null);
|
|
6021
|
+
const onLeftSelectRef = useRef(null);
|
|
5343
6022
|
const { runWithGuard } = useDirtyNavigation({
|
|
5344
6023
|
strategy: dirtyStrategy,
|
|
5345
6024
|
isDirty: editorCtx.isDirty,
|
|
@@ -5354,32 +6033,15 @@ function RecordsAdminShellInner(props) {
|
|
|
5354
6033
|
cancel: i18n.unsavedPromptCancel
|
|
5355
6034
|
}
|
|
5356
6035
|
});
|
|
5357
|
-
const
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
const inp = document.createElement("input");
|
|
5367
|
-
inp.type = "file";
|
|
5368
|
-
inp.accept = ".csv,text/csv";
|
|
5369
|
-
inp.onchange = async () => {
|
|
5370
|
-
const file = inp.files?.[0];
|
|
5371
|
-
if (!file) return;
|
|
5372
|
-
const report = await importCsv(file, csvSchema, ctx);
|
|
5373
|
-
onTelemetry?.({ type: "csv.import", recordType, rows: report.total, errors: report.failed });
|
|
5374
|
-
if (report.failed > 0) {
|
|
5375
|
-
const fileBase = recordType ?? (label.toLowerCase().replace(/\s+/g, "-") || "records");
|
|
5376
|
-
downloadBlob(new Blob([report.annotatedCsv], { type: "text/csv" }), `${fileBase}-errors.csv`);
|
|
5377
|
-
}
|
|
5378
|
-
refetchAll();
|
|
5379
|
-
};
|
|
5380
|
-
inp.click();
|
|
5381
|
-
};
|
|
5382
|
-
const csvBulk = csvSchema ? { onImportCsv: handleImport, onExportCsv: handleExport } : {};
|
|
6036
|
+
const csvBulk = useShellCsvBulk({
|
|
6037
|
+
csvSchema,
|
|
6038
|
+
recordType,
|
|
6039
|
+
label,
|
|
6040
|
+
items: recordList.items,
|
|
6041
|
+
ctx,
|
|
6042
|
+
refetchAll,
|
|
6043
|
+
onTelemetry
|
|
6044
|
+
});
|
|
5383
6045
|
const [previewScope, setPreviewScope] = useState(null);
|
|
5384
6046
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
5385
6047
|
const [sidePreviewOpen, setSidePreviewOpen] = useState(true);
|
|
@@ -5408,158 +6070,25 @@ function RecordsAdminShellInner(props) {
|
|
|
5408
6070
|
}
|
|
5409
6071
|
return void 0;
|
|
5410
6072
|
}, [activeScope, editingScope, selectedProductId, productBrowse.items]);
|
|
5411
|
-
const
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
sourceRecordId: resolved.recordId,
|
|
5418
|
-
sourceLabel: editorHeaderLabel
|
|
5419
|
-
});
|
|
5420
|
-
onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: editingScope.raw });
|
|
5421
|
-
setClipboardNotice({
|
|
5422
|
-
message: i18n.copyToast.replace("{name}", editorHeaderLabel ?? editingScope.raw),
|
|
5423
|
-
variant: "copy"
|
|
5424
|
-
});
|
|
5425
|
-
}, [
|
|
5426
|
-
enableClipboard,
|
|
6073
|
+
const shellClipboard = useShellClipboard({
|
|
6074
|
+
enabled: !!enableClipboard,
|
|
6075
|
+
appId,
|
|
6076
|
+
recordType,
|
|
6077
|
+
i18n,
|
|
6078
|
+
editorCtx,
|
|
5427
6079
|
editingScope,
|
|
5428
|
-
onCopyOverride,
|
|
5429
|
-
editorCtx.value,
|
|
5430
|
-
clipboard,
|
|
5431
6080
|
editorHeaderLabel,
|
|
6081
|
+
isCollection,
|
|
6082
|
+
selectedItemId,
|
|
6083
|
+
selectedRecordId,
|
|
6084
|
+
resolved: { source: resolved.source, recordId: resolved.recordId },
|
|
5432
6085
|
onTelemetry,
|
|
5433
|
-
|
|
5434
|
-
i18n.copyToast
|
|
5435
|
-
]);
|
|
5436
|
-
const pasteCurrent = useCallback(async () => {
|
|
5437
|
-
if (!enableClipboard || !editingScope || !clipboard.entry) return;
|
|
5438
|
-
const compat = checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
|
|
5439
|
-
if (compat.status === "denied") {
|
|
5440
|
-
setClipboardNotice({ message: compat.reason ?? "Paste not allowed here", variant: "paste" });
|
|
5441
|
-
return;
|
|
5442
|
-
}
|
|
5443
|
-
if (compat.status === "warn") {
|
|
5444
|
-
const ok = await pasteConfirm.confirm({
|
|
5445
|
-
title: i18n.pasteWarnTitle,
|
|
5446
|
-
body: compat.reason ?? "",
|
|
5447
|
-
confirmLabel: i18n.pasteWarnContinue,
|
|
5448
|
-
cancelLabel: i18n.pasteConfirmCancel
|
|
5449
|
-
});
|
|
5450
|
-
if (!ok) return;
|
|
5451
|
-
}
|
|
5452
|
-
const willReplace = resolved.source === "self";
|
|
5453
|
-
if (willReplace) {
|
|
5454
|
-
const ok = await pasteConfirm.confirm({
|
|
5455
|
-
title: i18n.pasteConfirmTitle,
|
|
5456
|
-
body: i18n.pasteConfirmBody.replace(
|
|
5457
|
-
"{destination}",
|
|
5458
|
-
editorHeaderLabel ?? editingScope.raw
|
|
5459
|
-
),
|
|
5460
|
-
confirmLabel: i18n.pasteConfirmReplace,
|
|
5461
|
-
cancelLabel: i18n.pasteConfirmCancel
|
|
5462
|
-
});
|
|
5463
|
-
if (!ok) return;
|
|
5464
|
-
}
|
|
5465
|
-
const sourceParsed = clipboard.entry.sourceScope;
|
|
5466
|
-
const transformed = onPasteOverride ? onPasteOverride(
|
|
5467
|
-
{ value: clipboard.entry.value, sourceScope: sourceParsed },
|
|
5468
|
-
{ scope: editingScope, currentValue: editorCtx.value ?? null }
|
|
5469
|
-
) : cloneValue(clipboard.entry.value);
|
|
5470
|
-
if (transformed === null) return;
|
|
5471
|
-
editorCtx.onChange(transformed);
|
|
5472
|
-
onTelemetry?.({
|
|
5473
|
-
type: "clipboard.paste",
|
|
5474
|
-
recordType,
|
|
5475
|
-
sourceRef: clipboard.entry.sourceScope.raw,
|
|
5476
|
-
destinationRef: editingScope.raw,
|
|
5477
|
-
replaced: willReplace
|
|
5478
|
-
});
|
|
5479
|
-
setClipboardNotice({
|
|
5480
|
-
message: i18n.pasteToast.replace("{name}", clipboard.entry.sourceLabel ?? clipboard.entry.sourceScope.raw),
|
|
5481
|
-
variant: "paste"
|
|
5482
|
-
});
|
|
5483
|
-
}, [
|
|
5484
|
-
enableClipboard,
|
|
5485
|
-
editingScope,
|
|
5486
|
-
clipboard.entry,
|
|
5487
|
-
pasteConfirm,
|
|
5488
|
-
resolved.source,
|
|
6086
|
+
onCopyOverride,
|
|
5489
6087
|
onPasteOverride,
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
i18n.pasteWarnTitle,
|
|
5495
|
-
i18n.pasteWarnContinue,
|
|
5496
|
-
i18n.pasteConfirmCancel,
|
|
5497
|
-
i18n.pasteConfirmTitle,
|
|
5498
|
-
i18n.pasteConfirmBody,
|
|
5499
|
-
i18n.pasteConfirmReplace,
|
|
5500
|
-
i18n.pasteToast
|
|
5501
|
-
]);
|
|
5502
|
-
const editorPasteCompat = useMemo(() => {
|
|
5503
|
-
if (!enableClipboard || !clipboard.entry || !editingScope) return null;
|
|
5504
|
-
const sameRecord = clipboard.entry.sourceRecordId && resolved.recordId && clipboard.entry.sourceRecordId === resolved.recordId;
|
|
5505
|
-
const sameAnchor = !clipboard.entry.sourceRecordId && clipboard.entry.sourceScope.raw === editingScope.raw;
|
|
5506
|
-
if (sameRecord || sameAnchor) return { status: "denied" };
|
|
5507
|
-
return checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
|
|
5508
|
-
}, [enableClipboard, clipboard.entry, editingScope]);
|
|
5509
|
-
const editorClipboard = enableClipboard && editingScope ? {
|
|
5510
|
-
onCopy: copyCurrent,
|
|
5511
|
-
onPaste: () => {
|
|
5512
|
-
void pasteCurrent();
|
|
5513
|
-
},
|
|
5514
|
-
canCopy: true,
|
|
5515
|
-
canPaste: !!clipboard.entry && editorPasteCompat?.status !== "denied",
|
|
5516
|
-
pasteSourceLabel: clipboard.entry?.sourceLabel,
|
|
5517
|
-
pasteWillReplace: resolved.source === "self" && !!clipboard.entry
|
|
5518
|
-
} : void 0;
|
|
5519
|
-
const [pendingPasteTarget, setPendingPasteTarget] = useState(null);
|
|
5520
|
-
useEffect(() => {
|
|
5521
|
-
if (!pendingPasteTarget) return;
|
|
5522
|
-
if (!editingScope) return;
|
|
5523
|
-
const matched = pendingPasteTarget.kind === "record" ? (isCollection ? selectedItemId : selectedRecordId) === pendingPasteTarget.recordId : editingScope.raw === pendingPasteTarget.ref;
|
|
5524
|
-
if (!matched) return;
|
|
5525
|
-
const t = window.setTimeout(() => {
|
|
5526
|
-
setPendingPasteTarget(null);
|
|
5527
|
-
void pasteCurrent();
|
|
5528
|
-
}, 0);
|
|
5529
|
-
return () => window.clearTimeout(t);
|
|
5530
|
-
}, [pendingPasteTarget, editingScope, isCollection, selectedItemId, selectedRecordId, pasteCurrent]);
|
|
5531
|
-
const rowClipboard = enableClipboard ? (record) => {
|
|
5532
|
-
const summaryHasData = record.data != null;
|
|
5533
|
-
const sourceParsed = record.scope;
|
|
5534
|
-
const compat = clipboard.entry ? checkPasteCompatibility(clipboard.entry.sourceScope, sourceParsed) : null;
|
|
5535
|
-
const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === record.ref : false;
|
|
5536
|
-
return {
|
|
5537
|
-
onCopy: summaryHasData ? () => {
|
|
5538
|
-
const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
|
|
5539
|
-
clipboard.set({
|
|
5540
|
-
value,
|
|
5541
|
-
sourceScope: sourceParsed,
|
|
5542
|
-
sourceRecordId: record.id ?? void 0,
|
|
5543
|
-
sourceLabel: record.label
|
|
5544
|
-
});
|
|
5545
|
-
onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: record.ref });
|
|
5546
|
-
setClipboardNotice({
|
|
5547
|
-
message: i18n.copyToast.replace("{name}", record.label),
|
|
5548
|
-
variant: "copy"
|
|
5549
|
-
});
|
|
5550
|
-
} : void 0,
|
|
5551
|
-
onPaste: () => {
|
|
5552
|
-
onLeftSelectRef.current?.(record);
|
|
5553
|
-
setPendingPasteTarget(
|
|
5554
|
-
record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: record.ref }
|
|
5555
|
-
);
|
|
5556
|
-
},
|
|
5557
|
-
canPaste: !!clipboard.entry && !sameTarget && compat?.status !== "denied",
|
|
5558
|
-
pasteWillReplace: record.status === "configured",
|
|
5559
|
-
clipboardSourceLabel: clipboard.entry?.sourceLabel
|
|
5560
|
-
};
|
|
5561
|
-
} : void 0;
|
|
5562
|
-
const onLeftSelectRef = useRef(null);
|
|
6088
|
+
onLeftSelectRef
|
|
6089
|
+
});
|
|
6090
|
+
const editorClipboard = shellClipboard.editorClipboard;
|
|
6091
|
+
const rowClipboard = shellClipboard.rowClipboard;
|
|
5563
6092
|
const baseScopeRef = editingScope?.raw ?? "";
|
|
5564
6093
|
const itemNounLabel = itemNoun || "item";
|
|
5565
6094
|
const buildItemUrlValue = useCallback((id) => {
|
|
@@ -5750,7 +6279,19 @@ function RecordsAdminShellInner(props) {
|
|
|
5750
6279
|
clipboard: editorClipboard,
|
|
5751
6280
|
actionLabels,
|
|
5752
6281
|
actionIcons,
|
|
5753
|
-
children:
|
|
6282
|
+
children: /* @__PURE__ */ jsx(
|
|
6283
|
+
EditorMountPool,
|
|
6284
|
+
{
|
|
6285
|
+
renderSlot: (slotId) => /* @__PURE__ */ jsx(
|
|
6286
|
+
EditorPoolBody,
|
|
6287
|
+
{
|
|
6288
|
+
editorId: slotId,
|
|
6289
|
+
renderEditor
|
|
6290
|
+
}
|
|
6291
|
+
),
|
|
6292
|
+
fallback: renderEditor(editorCtx)
|
|
6293
|
+
}
|
|
6294
|
+
)
|
|
5754
6295
|
}
|
|
5755
6296
|
);
|
|
5756
6297
|
const withNav = (node) => itemNav ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
|
|
@@ -5984,15 +6525,8 @@ function RecordsAdminShellInner(props) {
|
|
|
5984
6525
|
"data-density": density,
|
|
5985
6526
|
children: [
|
|
5986
6527
|
dirtyConfirm.dialog,
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
ClipboardToast,
|
|
5990
|
-
{
|
|
5991
|
-
message: clipboardNotice.message,
|
|
5992
|
-
variant: clipboardNotice.variant,
|
|
5993
|
-
onDismiss: () => setClipboardNotice(null)
|
|
5994
|
-
}
|
|
5995
|
-
),
|
|
6528
|
+
shellClipboard.confirmDialog,
|
|
6529
|
+
shellClipboard.toast,
|
|
5996
6530
|
(() => {
|
|
5997
6531
|
const showFloatHelp = !!intro && dismissed && resolvedReopenAffordance === "footer" && !headerWillRender;
|
|
5998
6532
|
if (!showFloatHelp) return null;
|
|
@@ -6074,19 +6608,22 @@ function RecordsAdminShellInner(props) {
|
|
|
6074
6608
|
showTrayBanner && /* @__PURE__ */ jsx(
|
|
6075
6609
|
UnsavedTray,
|
|
6076
6610
|
{
|
|
6077
|
-
|
|
6611
|
+
items: dirtyItems,
|
|
6078
6612
|
isSaving: saveAllOpen,
|
|
6079
6613
|
onSaveAll: handleSaveAll,
|
|
6080
6614
|
onDiscardAll: handleDiscardAll,
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6615
|
+
onOpenItem: (it) => {
|
|
6616
|
+
editorSelection.focus(it.editorId);
|
|
6617
|
+
if (it.recordId) {
|
|
6618
|
+
setSelectedRecordId(it.recordId);
|
|
6084
6619
|
setDraftKind(null);
|
|
6085
6620
|
}
|
|
6086
6621
|
},
|
|
6087
6622
|
onJumpToError: () => {
|
|
6088
|
-
const first =
|
|
6089
|
-
if (first
|
|
6623
|
+
const first = dirtyItems.find((it) => it.status === "error");
|
|
6624
|
+
if (!first) return;
|
|
6625
|
+
editorSelection.focus(first.editorId);
|
|
6626
|
+
if (first.recordId) {
|
|
6090
6627
|
setSelectedRecordId(first.recordId);
|
|
6091
6628
|
setDraftKind(null);
|
|
6092
6629
|
}
|
|
@@ -6106,12 +6643,13 @@ function RecordsAdminShellInner(props) {
|
|
|
6106
6643
|
SaveAllProgress,
|
|
6107
6644
|
{
|
|
6108
6645
|
open: saveAllOpen,
|
|
6109
|
-
|
|
6110
|
-
|
|
6646
|
+
items: dirtyItems,
|
|
6647
|
+
saveOne: dirtyOverview.saveOne,
|
|
6111
6648
|
onClose: () => setSaveAllOpen(false),
|
|
6112
|
-
onJumpToError: (
|
|
6113
|
-
|
|
6114
|
-
|
|
6649
|
+
onJumpToError: (it) => {
|
|
6650
|
+
editorSelection.focus(it.editorId);
|
|
6651
|
+
if (it.recordId) {
|
|
6652
|
+
setSelectedRecordId(it.recordId);
|
|
6115
6653
|
setDraftKind(null);
|
|
6116
6654
|
}
|
|
6117
6655
|
},
|
|
@@ -6621,6 +7159,414 @@ var ResolvedPreview = ({ children }) => /* @__PURE__ */ jsxs("div", { className:
|
|
|
6621
7159
|
/* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wide mb-2", style: { color: "hsl(var(--ra-muted-text))" }, children: "Public preview" }),
|
|
6622
7160
|
children
|
|
6623
7161
|
] });
|
|
7162
|
+
var createStore = () => {
|
|
7163
|
+
const map = /* @__PURE__ */ new Map();
|
|
7164
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7165
|
+
let nextOrder = 0;
|
|
7166
|
+
let cachedList = [];
|
|
7167
|
+
const recompute = () => {
|
|
7168
|
+
cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
|
|
7169
|
+
};
|
|
7170
|
+
const emit = () => {
|
|
7171
|
+
recompute();
|
|
7172
|
+
listeners.forEach((l) => l());
|
|
7173
|
+
};
|
|
7174
|
+
return {
|
|
7175
|
+
list: () => cachedList,
|
|
7176
|
+
get: (key) => map.get(key),
|
|
7177
|
+
has: (key) => map.has(key),
|
|
7178
|
+
upsertDraft(input) {
|
|
7179
|
+
const existing = map.get(input.key);
|
|
7180
|
+
const order = existing?.order ?? input.order ?? nextOrder++;
|
|
7181
|
+
const status = input.status ?? existing?.status ?? "dirty";
|
|
7182
|
+
map.set(input.key, {
|
|
7183
|
+
...input,
|
|
7184
|
+
order,
|
|
7185
|
+
status
|
|
7186
|
+
});
|
|
7187
|
+
emit();
|
|
7188
|
+
},
|
|
7189
|
+
setStatus(key, status, error) {
|
|
7190
|
+
const existing = map.get(key);
|
|
7191
|
+
if (!existing) return;
|
|
7192
|
+
map.set(key, { ...existing, status, error });
|
|
7193
|
+
emit();
|
|
7194
|
+
},
|
|
7195
|
+
clearDraft(key) {
|
|
7196
|
+
if (map.delete(key)) emit();
|
|
7197
|
+
},
|
|
7198
|
+
clearAll() {
|
|
7199
|
+
if (map.size === 0) return;
|
|
7200
|
+
map.clear();
|
|
7201
|
+
emit();
|
|
7202
|
+
},
|
|
7203
|
+
subscribe(listener) {
|
|
7204
|
+
listeners.add(listener);
|
|
7205
|
+
return () => {
|
|
7206
|
+
listeners.delete(listener);
|
|
7207
|
+
};
|
|
7208
|
+
}
|
|
7209
|
+
};
|
|
7210
|
+
};
|
|
7211
|
+
var DirtyDraftContext = createContext(null);
|
|
7212
|
+
var DirtyDraftProvider = ({ children }) => {
|
|
7213
|
+
const storeRef = useRef(null);
|
|
7214
|
+
if (!storeRef.current) storeRef.current = createStore();
|
|
7215
|
+
return /* @__PURE__ */ jsx(DirtyDraftContext.Provider, { value: storeRef.current, children });
|
|
7216
|
+
};
|
|
7217
|
+
var useDirtyDraftStore = () => {
|
|
7218
|
+
const ctx = useContext(DirtyDraftContext);
|
|
7219
|
+
const fallbackRef = useRef(null);
|
|
7220
|
+
if (!ctx && !fallbackRef.current) fallbackRef.current = createStore();
|
|
7221
|
+
return ctx ?? fallbackRef.current;
|
|
7222
|
+
};
|
|
7223
|
+
var useDirtyDrafts = () => {
|
|
7224
|
+
const store = useDirtyDraftStore();
|
|
7225
|
+
return useSyncExternalStore(
|
|
7226
|
+
useCallback((cb) => store.subscribe(cb), [store]),
|
|
7227
|
+
useCallback(() => store.list(), [store]),
|
|
7228
|
+
useCallback(() => store.list(), [store])
|
|
7229
|
+
);
|
|
7230
|
+
};
|
|
7231
|
+
var useDirtyDraft = (key) => {
|
|
7232
|
+
const store = useDirtyDraftStore();
|
|
7233
|
+
const getSnapshot = useCallback(
|
|
7234
|
+
() => key ? store.get(key) : void 0,
|
|
7235
|
+
[store, key]
|
|
7236
|
+
);
|
|
7237
|
+
return useSyncExternalStore(
|
|
7238
|
+
useCallback((cb) => store.subscribe(cb), [store]),
|
|
7239
|
+
getSnapshot,
|
|
7240
|
+
getSnapshot
|
|
7241
|
+
);
|
|
7242
|
+
};
|
|
7243
|
+
var buildDraftKey = (opts) => {
|
|
7244
|
+
if (opts.recordId) return opts.recordId;
|
|
7245
|
+
const kind = opts.draftKind ?? "rec";
|
|
7246
|
+
return `draft:${kind}:${opts.scopeRaw}`;
|
|
7247
|
+
};
|
|
7248
|
+
var useDirtyDraftActions = () => {
|
|
7249
|
+
const store = useDirtyDraftStore();
|
|
7250
|
+
const drafts = useDirtyDrafts();
|
|
7251
|
+
const count = useMemo(() => drafts.filter((d) => d.status !== "saved").length, [drafts]);
|
|
7252
|
+
const errorCount = useMemo(() => drafts.filter((d) => d.status === "error").length, [drafts]);
|
|
7253
|
+
return { drafts, count, errorCount, store };
|
|
7254
|
+
};
|
|
7255
|
+
|
|
7256
|
+
// src/components/RecordsAdmin/hooks/useRecordEditor.ts
|
|
7257
|
+
var isEqual2 = (a, b) => {
|
|
7258
|
+
try {
|
|
7259
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
7260
|
+
} catch {
|
|
7261
|
+
return false;
|
|
7262
|
+
}
|
|
7263
|
+
};
|
|
7264
|
+
var cloneSeed = (resolved, defaultData) => {
|
|
7265
|
+
if (resolved.source === "self") return resolved.data;
|
|
7266
|
+
if (resolved.source === "inherited" && resolved.data != null) {
|
|
7267
|
+
try {
|
|
7268
|
+
return structuredClone(resolved.data);
|
|
7269
|
+
} catch {
|
|
7270
|
+
return JSON.parse(JSON.stringify(resolved.data));
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
return defaultData?.() ?? {};
|
|
7274
|
+
};
|
|
7275
|
+
var getScopedResolvedRecordId = (scope, resolved) => {
|
|
7276
|
+
if (!resolved.recordId) return void 0;
|
|
7277
|
+
if (scope.itemId && scope.itemId === resolved.recordId) return resolved.recordId;
|
|
7278
|
+
const scopeRaw = scope.raw || "";
|
|
7279
|
+
const sourceRef = resolved.sourceRef || "";
|
|
7280
|
+
if (scopeRaw === sourceRef) return resolved.recordId;
|
|
7281
|
+
return void 0;
|
|
7282
|
+
};
|
|
7283
|
+
function useRecordEditor(args) {
|
|
7284
|
+
const {
|
|
7285
|
+
ctx,
|
|
7286
|
+
scope,
|
|
7287
|
+
resolved,
|
|
7288
|
+
defaultData,
|
|
7289
|
+
onSaved,
|
|
7290
|
+
onDeleted,
|
|
7291
|
+
onSaveError,
|
|
7292
|
+
reseed = "always",
|
|
7293
|
+
initialFacetRule = null,
|
|
7294
|
+
createMode = false,
|
|
7295
|
+
deriveDraftLabel
|
|
7296
|
+
} = args;
|
|
7297
|
+
const queryClient = useQueryClient();
|
|
7298
|
+
const draftStore = useDirtyDraftStore();
|
|
7299
|
+
const scopedResolvedRecordId = getScopedResolvedRecordId(scope, resolved);
|
|
7300
|
+
const draftKey = buildDraftKey({
|
|
7301
|
+
// Scope identity must win during record switches. `resolved.recordId`
|
|
7302
|
+
// can briefly lag behind the newly-selected target while the resolver is
|
|
7303
|
+
// catching up, and keying drafts off that stale id lets one edited item
|
|
7304
|
+
// borrow another item's draft. Once the resolver confirms the matching
|
|
7305
|
+
// record for this scope, we transparently migrate onto the concrete UUID.
|
|
7306
|
+
recordId: scopedResolvedRecordId,
|
|
7307
|
+
scopeRaw: scope.raw,
|
|
7308
|
+
draftKind: scope.kind
|
|
7309
|
+
});
|
|
7310
|
+
const initialDraft = draftStore.get(draftKey);
|
|
7311
|
+
const cleanSeed = cloneSeed(resolved, defaultData);
|
|
7312
|
+
const seed = initialDraft && reseed === "preserve-dirty" ? initialDraft.value : cleanSeed;
|
|
7313
|
+
const seedFacetRule = initialDraft && reseed === "preserve-dirty" ? initialDraft.facetRule : initialFacetRule;
|
|
7314
|
+
const [value, setValue] = useState(seed);
|
|
7315
|
+
const [savedSnapshot, setSavedSnapshot] = useState(
|
|
7316
|
+
initialDraft ? initialDraft.baseline : cleanSeed
|
|
7317
|
+
);
|
|
7318
|
+
const [facetRule, setFacetRule] = useState(seedFacetRule);
|
|
7319
|
+
const [savedFacetRule, setSavedFacetRule] = useState(
|
|
7320
|
+
initialDraft ? initialDraft.baselineFacetRule : initialFacetRule
|
|
7321
|
+
);
|
|
7322
|
+
const [userInteracted, setUserInteracted] = useState(!!initialDraft);
|
|
7323
|
+
const [optimisticSource, setOptimisticSource] = useState(null);
|
|
7324
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
7325
|
+
const [saveError, setSaveError] = useState(null);
|
|
7326
|
+
const valueRef = useRef(seed);
|
|
7327
|
+
useEffect(() => {
|
|
7328
|
+
valueRef.current = value;
|
|
7329
|
+
}, [value]);
|
|
7330
|
+
const prevTargetRef = useRef({
|
|
7331
|
+
scopeRaw: scope.raw,
|
|
7332
|
+
recordId: resolved.recordId
|
|
7333
|
+
});
|
|
7334
|
+
useEffect(() => {
|
|
7335
|
+
const prev = prevTargetRef.current;
|
|
7336
|
+
const targetChanged = prev.scopeRaw !== scope.raw || (prev.recordId ?? null) !== (resolved.recordId ?? null);
|
|
7337
|
+
prevTargetRef.current = { scopeRaw: scope.raw, recordId: resolved.recordId };
|
|
7338
|
+
const fresh = cloneSeed(resolved, defaultData);
|
|
7339
|
+
if (targetChanged) {
|
|
7340
|
+
const incomingKey = buildDraftKey({
|
|
7341
|
+
recordId: getScopedResolvedRecordId(scope, resolved),
|
|
7342
|
+
scopeRaw: scope.raw,
|
|
7343
|
+
draftKind: scope.kind
|
|
7344
|
+
});
|
|
7345
|
+
const incomingDraft = draftStore.get(incomingKey);
|
|
7346
|
+
if (incomingDraft && reseed === "preserve-dirty") {
|
|
7347
|
+
setValue(incomingDraft.value);
|
|
7348
|
+
setSavedSnapshot(incomingDraft.baseline);
|
|
7349
|
+
setFacetRule(incomingDraft.facetRule);
|
|
7350
|
+
setSavedFacetRule(incomingDraft.baselineFacetRule);
|
|
7351
|
+
setUserInteracted(true);
|
|
7352
|
+
} else {
|
|
7353
|
+
setValue(fresh);
|
|
7354
|
+
setSavedSnapshot(fresh);
|
|
7355
|
+
setFacetRule(initialFacetRule);
|
|
7356
|
+
setSavedFacetRule(initialFacetRule);
|
|
7357
|
+
setUserInteracted(false);
|
|
7358
|
+
}
|
|
7359
|
+
} else {
|
|
7360
|
+
if (reseed === "preserve-dirty") {
|
|
7361
|
+
const hasUnsaved = !isEqual2(valueRef.current, savedSnapshot);
|
|
7362
|
+
if (!hasUnsaved) setValue(fresh);
|
|
7363
|
+
} else {
|
|
7364
|
+
setValue(fresh);
|
|
7365
|
+
}
|
|
7366
|
+
setSavedSnapshot(fresh);
|
|
7367
|
+
setFacetRule(initialFacetRule);
|
|
7368
|
+
setSavedFacetRule(initialFacetRule);
|
|
7369
|
+
}
|
|
7370
|
+
setOptimisticSource(null);
|
|
7371
|
+
}, [scope.raw, resolved.recordId, resolved.source, resolved.sourceRef]);
|
|
7372
|
+
const valueDiff = !isEqual2(value, savedSnapshot) || !isEqual2(facetRule, savedFacetRule);
|
|
7373
|
+
const isDirty = userInteracted && valueDiff;
|
|
7374
|
+
const handleChange = useCallback((next) => {
|
|
7375
|
+
setUserInteracted(true);
|
|
7376
|
+
setValue(next);
|
|
7377
|
+
}, []);
|
|
7378
|
+
const handleFacetRuleChange = useCallback((next) => {
|
|
7379
|
+
setUserInteracted(true);
|
|
7380
|
+
setFacetRule(next);
|
|
7381
|
+
}, []);
|
|
7382
|
+
const save = useCallback(async () => {
|
|
7383
|
+
const anchors = parsedRefToScope(scope);
|
|
7384
|
+
const hasAnchors = !!(anchors.productId || anchors.variantId || anchors.batchId || anchors.proofId);
|
|
7385
|
+
const hasRule = !!(facetRule && facetRule.all && facetRule.all.length > 0);
|
|
7386
|
+
const isRuleScope2 = scope.kind === "rule";
|
|
7387
|
+
if (isRuleScope2 && !hasAnchors && !hasRule && !resolved.recordId && !createMode) {
|
|
7388
|
+
console.warn("[useRecordEditor] save() bailed \u2014 rule scope with no clauses, no recordId, not in createMode");
|
|
7389
|
+
return;
|
|
7390
|
+
}
|
|
7391
|
+
const previousSnapshot = savedSnapshot;
|
|
7392
|
+
const previousRuleSnapshot = savedFacetRule;
|
|
7393
|
+
resolved.source;
|
|
7394
|
+
const cacheKey = resolvedRecordQueryKey({
|
|
7395
|
+
collectionId: ctx.collectionId,
|
|
7396
|
+
appId: ctx.appId,
|
|
7397
|
+
recordType: ctx.recordType,
|
|
7398
|
+
productId: scope.productId,
|
|
7399
|
+
variantId: scope.variantId,
|
|
7400
|
+
batchId: scope.batchId,
|
|
7401
|
+
facetId: scope.facetId,
|
|
7402
|
+
facetValue: scope.facetValue,
|
|
7403
|
+
proofId: scope.proofId,
|
|
7404
|
+
recordId: resolved.recordId,
|
|
7405
|
+
withParent: true
|
|
7406
|
+
});
|
|
7407
|
+
const previousCache = queryClient.getQueryData(cacheKey);
|
|
7408
|
+
setSaveError(null);
|
|
7409
|
+
setIsSaving(true);
|
|
7410
|
+
setSavedSnapshot(value);
|
|
7411
|
+
setSavedFacetRule(facetRule);
|
|
7412
|
+
setOptimisticSource("self");
|
|
7413
|
+
queryClient.setQueryData(cacheKey, {
|
|
7414
|
+
data: value,
|
|
7415
|
+
source: "self",
|
|
7416
|
+
sourceRef: scope.raw,
|
|
7417
|
+
recordId: resolved.recordId,
|
|
7418
|
+
parentValue: previousCache?.parentValue ?? resolved.parentValue
|
|
7419
|
+
});
|
|
7420
|
+
try {
|
|
7421
|
+
if (resolved.recordId && resolved.source === "self") {
|
|
7422
|
+
await updateRecord(ctx, resolved.recordId, {
|
|
7423
|
+
data: value,
|
|
7424
|
+
facetRule
|
|
7425
|
+
});
|
|
7426
|
+
} else if (createMode) {
|
|
7427
|
+
await createRecord(ctx, {
|
|
7428
|
+
ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
|
|
7429
|
+
scope: anchors,
|
|
7430
|
+
data: value,
|
|
7431
|
+
facetRule
|
|
7432
|
+
});
|
|
7433
|
+
} else {
|
|
7434
|
+
await upsertRecord(ctx, {
|
|
7435
|
+
// External ref only when the underlying scope already carries
|
|
7436
|
+
// one (e.g. rule:{id} for an existing rule record). Empty for
|
|
7437
|
+
// anchor-only writes — server addresses by anchors.
|
|
7438
|
+
ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
|
|
7439
|
+
scope: anchors,
|
|
7440
|
+
data: value,
|
|
7441
|
+
facetRule
|
|
7442
|
+
});
|
|
7443
|
+
}
|
|
7444
|
+
draftStore.clearDraft(draftKey);
|
|
7445
|
+
onSaved?.();
|
|
7446
|
+
} catch (err) {
|
|
7447
|
+
setSavedSnapshot(previousSnapshot);
|
|
7448
|
+
setSavedFacetRule(previousRuleSnapshot);
|
|
7449
|
+
setOptimisticSource(null);
|
|
7450
|
+
if (previousCache !== void 0) {
|
|
7451
|
+
queryClient.setQueryData(cacheKey, previousCache);
|
|
7452
|
+
} else {
|
|
7453
|
+
queryClient.invalidateQueries({ queryKey: cacheKey });
|
|
7454
|
+
}
|
|
7455
|
+
setSaveError(err);
|
|
7456
|
+
onSaveError?.(err);
|
|
7457
|
+
throw err;
|
|
7458
|
+
} finally {
|
|
7459
|
+
setIsSaving(false);
|
|
7460
|
+
}
|
|
7461
|
+
}, [scope.raw, value, savedSnapshot, facetRule, savedFacetRule, resolved.source, resolved.parentValue, resolved.recordId, createMode, draftKey]);
|
|
7462
|
+
const reset = useCallback(() => {
|
|
7463
|
+
setValue(savedSnapshot);
|
|
7464
|
+
setFacetRule(savedFacetRule);
|
|
7465
|
+
setUserInteracted(false);
|
|
7466
|
+
draftStore.clearDraft(draftKey);
|
|
7467
|
+
}, [savedSnapshot, savedFacetRule, draftKey]);
|
|
7468
|
+
const remove = useCallback(async () => {
|
|
7469
|
+
if (resolved.source !== "self") return;
|
|
7470
|
+
if (!resolved.recordId) return;
|
|
7471
|
+
await removeRecord(ctx, resolved.recordId);
|
|
7472
|
+
draftStore.clearDraft(draftKey);
|
|
7473
|
+
onDeleted?.();
|
|
7474
|
+
}, [resolved.source, resolved.recordId, draftKey]);
|
|
7475
|
+
const prevDraftKeyRef = useRef(draftKey);
|
|
7476
|
+
const prevScopeRawRef = useRef(scope.raw);
|
|
7477
|
+
useEffect(() => {
|
|
7478
|
+
const prevKey = prevDraftKeyRef.current;
|
|
7479
|
+
const prevScopeRaw = prevScopeRawRef.current;
|
|
7480
|
+
if (prevKey && prevKey !== draftKey && prevScopeRaw === scope.raw) {
|
|
7481
|
+
const stale = draftStore.get(prevKey);
|
|
7482
|
+
if (stale) draftStore.clearDraft(prevKey);
|
|
7483
|
+
}
|
|
7484
|
+
prevDraftKeyRef.current = draftKey;
|
|
7485
|
+
prevScopeRawRef.current = scope.raw;
|
|
7486
|
+
}, [draftKey, scope.raw]);
|
|
7487
|
+
useEffect(() => {
|
|
7488
|
+
if (!isDirty) {
|
|
7489
|
+
return;
|
|
7490
|
+
}
|
|
7491
|
+
const anchors = parsedRefToScope(scope);
|
|
7492
|
+
const saveKind = resolved.recordId && resolved.source === "self" ? "update" : createMode ? "create" : "upsert";
|
|
7493
|
+
const deriveLabel = () => {
|
|
7494
|
+
if (deriveDraftLabel) {
|
|
7495
|
+
try {
|
|
7496
|
+
const custom = deriveDraftLabel(value, scope);
|
|
7497
|
+
if (typeof custom === "string" && custom.trim()) return custom.trim();
|
|
7498
|
+
} catch {
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
const KEYS = ["title", "name", "label", "heading", "question", "slug"];
|
|
7502
|
+
const pickString = (obj) => {
|
|
7503
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
7504
|
+
for (const key of KEYS) {
|
|
7505
|
+
const raw = obj[key];
|
|
7506
|
+
if (typeof raw === "string" && raw.trim()) return raw.trim();
|
|
7507
|
+
}
|
|
7508
|
+
return void 0;
|
|
7509
|
+
};
|
|
7510
|
+
const v = value;
|
|
7511
|
+
const top = pickString(v);
|
|
7512
|
+
if (top) return top;
|
|
7513
|
+
if (v && typeof v === "object") {
|
|
7514
|
+
for (const wrapper of ["display", "content", "meta", "data"]) {
|
|
7515
|
+
const nested = pickString(v[wrapper]);
|
|
7516
|
+
if (nested) return nested;
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7519
|
+
if (scope.raw?.startsWith("item:")) return "Untitled item";
|
|
7520
|
+
if (scope.kind === "rule") return "Rule";
|
|
7521
|
+
if (scope.kind && scope.kind !== "collection") {
|
|
7522
|
+
return scope.kind.charAt(0).toUpperCase() + scope.kind.slice(1);
|
|
7523
|
+
}
|
|
7524
|
+
return "Default";
|
|
7525
|
+
};
|
|
7526
|
+
draftStore.upsertDraft({
|
|
7527
|
+
key: draftKey,
|
|
7528
|
+
label: deriveLabel(),
|
|
7529
|
+
context: scope.kind,
|
|
7530
|
+
scopeRaw: scope.raw,
|
|
7531
|
+
recordId: resolved.recordId,
|
|
7532
|
+
value,
|
|
7533
|
+
facetRule,
|
|
7534
|
+
baseline: savedSnapshot,
|
|
7535
|
+
baselineFacetRule: savedFacetRule,
|
|
7536
|
+
createMode,
|
|
7537
|
+
scopeAnchors: anchors,
|
|
7538
|
+
ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
|
|
7539
|
+
saveKind,
|
|
7540
|
+
save: async () => {
|
|
7541
|
+
await save();
|
|
7542
|
+
}
|
|
7543
|
+
});
|
|
7544
|
+
}, [isDirty, value, facetRule, savedSnapshot, savedFacetRule, scope.raw, resolved.recordId, resolved.source, createMode, save]);
|
|
7545
|
+
const effectiveSource = optimisticSource ?? resolved.source;
|
|
7546
|
+
const isRuleScope = scope.kind === "rule";
|
|
7547
|
+
const ruleValid = !isRuleScope || isFacetRuleValid(facetRule);
|
|
7548
|
+
const canSave = ruleValid;
|
|
7549
|
+
const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
|
|
7550
|
+
return {
|
|
7551
|
+
value,
|
|
7552
|
+
onChange: handleChange,
|
|
7553
|
+
source: effectiveSource,
|
|
7554
|
+
recordId: resolved.recordId,
|
|
7555
|
+
parentValue: resolved.parentValue,
|
|
7556
|
+
scope,
|
|
7557
|
+
isDirty,
|
|
7558
|
+
save,
|
|
7559
|
+
reset,
|
|
7560
|
+
remove,
|
|
7561
|
+
canRemove: effectiveSource === "self",
|
|
7562
|
+
canSave,
|
|
7563
|
+
cannotSaveReason,
|
|
7564
|
+
isSaving,
|
|
7565
|
+
saveError,
|
|
7566
|
+
facetRule,
|
|
7567
|
+
onFacetRuleChange: handleFacetRuleChange
|
|
7568
|
+
};
|
|
7569
|
+
}
|
|
6624
7570
|
var resolveAllQueryKey = (args) => [
|
|
6625
7571
|
"records-admin",
|
|
6626
7572
|
"resolve-all",
|
|
@@ -6861,6 +7807,6 @@ function useMergedRecord(args) {
|
|
|
6861
7807
|
};
|
|
6862
7808
|
}
|
|
6863
7809
|
|
|
6864
|
-
export { ALL_ITEM_VIEWS, ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_DEEP_LINK_PARAM_NAMES, DEFAULT_I18N, DEFAULT_ICONS, DefaultItemCards, DefaultItemTable, DefaultRecordCard, DefaultRecordRow, DeleteButton, DirtyDraftProvider, DrawerPreview, EditorItemNav, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, ItemListView, ItemViewSwitcher, LoadingState, PresentationSwitcher, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SiblingRail, SidePreview, StatusDot, StatusFilterPills, StatusIcon, TabbedPreview, UtilityRow, VariantList, buildDraftKey, buildRef, checkPasteCompatibility, cloneValue, createDefaultDeepLinkAdapter, downloadBlob, exportCsv, importCsv, mergeIcons, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, statusToneLabel, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeProbe, useUnsavedGuard };
|
|
7810
|
+
export { ALL_ITEM_VIEWS, ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_DEEP_LINK_PARAM_NAMES, DEFAULT_I18N, DEFAULT_ICONS, DefaultItemCards, DefaultItemTable, DefaultRecordCard, DefaultRecordRow, DeleteButton, DirtyDraftProvider, DrawerPreview, EditorItemNav, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, ItemListView, ItemViewSwitcher, LoadingState, PresentationSwitcher, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SiblingRail, SidePreview, StatusDot, StatusFilterPills, StatusIcon, TabbedPreview, UtilityRow, VariantList, buildDraftKey, buildRef, checkPasteCompatibility, cloneValue, createDefaultDeepLinkAdapter, createPostMessageDeepLinkAdapter, downloadBlob, exportCsv, importCsv, isInSmartLinksIframe, mergeIcons, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, statusToneLabel, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeProbe, useUnsavedGuard };
|
|
6865
7811
|
//# sourceMappingURL=index.js.map
|
|
6866
7812
|
//# sourceMappingURL=index.js.map
|