@entifix/next-shell 0.1.0-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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/back-office/account-menu.d.ts +29 -0
- package/dist/lib/back-office/account-menu.d.ts.map +1 -0
- package/dist/lib/back-office/account-menu.js +70 -0
- package/dist/lib/back-office/account-menu.js.map +1 -0
- package/dist/lib/back-office/back-office-shell.d.ts +50 -0
- package/dist/lib/back-office/back-office-shell.d.ts.map +1 -0
- package/dist/lib/back-office/back-office-shell.js +128 -0
- package/dist/lib/back-office/back-office-shell.js.map +1 -0
- package/dist/lib/back-office/breadcrumbs.d.ts +16 -0
- package/dist/lib/back-office/breadcrumbs.d.ts.map +1 -0
- package/dist/lib/back-office/breadcrumbs.js +53 -0
- package/dist/lib/back-office/breadcrumbs.js.map +1 -0
- package/dist/lib/back-office/group-by-screen-type.d.ts +21 -0
- package/dist/lib/back-office/group-by-screen-type.d.ts.map +1 -0
- package/dist/lib/back-office/group-by-screen-type.js +39 -0
- package/dist/lib/back-office/group-by-screen-type.js.map +1 -0
- package/dist/lib/back-office/index.d.ts +6 -0
- package/dist/lib/back-office/index.d.ts.map +1 -0
- package/dist/lib/back-office/index.js +7 -0
- package/dist/lib/back-office/index.js.map +1 -0
- package/dist/lib/back-office/nav.d.ts +39 -0
- package/dist/lib/back-office/nav.d.ts.map +1 -0
- package/dist/lib/back-office/nav.js +3 -0
- package/dist/lib/back-office/nav.js.map +1 -0
- package/dist/lib/back-office/sidebar-nav.d.ts +26 -0
- package/dist/lib/back-office/sidebar-nav.d.ts.map +1 -0
- package/dist/lib/back-office/sidebar-nav.js +181 -0
- package/dist/lib/back-office/sidebar-nav.js.map +1 -0
- package/dist/lib/command-palette/command-palette-host.d.ts +41 -0
- package/dist/lib/command-palette/command-palette-host.d.ts.map +1 -0
- package/dist/lib/command-palette/command-palette-host.js +196 -0
- package/dist/lib/command-palette/command-palette-host.js.map +1 -0
- package/dist/lib/command-palette/index.d.ts +9 -0
- package/dist/lib/command-palette/index.d.ts.map +1 -0
- package/dist/lib/command-palette/index.js +10 -0
- package/dist/lib/command-palette/index.js.map +1 -0
- package/dist/lib/command-palette/palette-command.d.ts +32 -0
- package/dist/lib/command-palette/palette-command.d.ts.map +1 -0
- package/dist/lib/command-palette/palette-command.js +19 -0
- package/dist/lib/command-palette/palette-command.js.map +1 -0
- package/dist/lib/command-palette/use-command-recency.d.ts +28 -0
- package/dist/lib/command-palette/use-command-recency.d.ts.map +1 -0
- package/dist/lib/command-palette/use-command-recency.js +67 -0
- package/dist/lib/command-palette/use-command-recency.js.map +1 -0
- package/dist/lib/command-palette/use-command-source.d.ts +17 -0
- package/dist/lib/command-palette/use-command-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-command-source.js +70 -0
- package/dist/lib/command-palette/use-command-source.js.map +1 -0
- package/dist/lib/command-palette/use-nav-source.d.ts +21 -0
- package/dist/lib/command-palette/use-nav-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-nav-source.js +78 -0
- package/dist/lib/command-palette/use-nav-source.js.map +1 -0
- package/dist/lib/command-palette/use-records-source.d.ts +23 -0
- package/dist/lib/command-palette/use-records-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-records-source.js +199 -0
- package/dist/lib/command-palette/use-records-source.js.map +1 -0
- package/dist/lib/command-palette/use-tabs-source.d.ts +20 -0
- package/dist/lib/command-palette/use-tabs-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-tabs-source.js +52 -0
- package/dist/lib/command-palette/use-tabs-source.js.map +1 -0
- package/dist/lib/command-palette/use-use-cases-source.d.ts +22 -0
- package/dist/lib/command-palette/use-use-cases-source.d.ts.map +1 -0
- package/dist/lib/command-palette/use-use-cases-source.js +93 -0
- package/dist/lib/command-palette/use-use-cases-source.js.map +1 -0
- package/dist/lib/config/config-route.d.ts +51 -0
- package/dist/lib/config/config-route.d.ts.map +1 -0
- package/dist/lib/config/config-route.js +65 -0
- package/dist/lib/config/config-route.js.map +1 -0
- package/dist/lib/config/service-proxy-route.d.ts +28 -0
- package/dist/lib/config/service-proxy-route.d.ts.map +1 -0
- package/dist/lib/config/service-proxy-route.js +125 -0
- package/dist/lib/config/service-proxy-route.js.map +1 -0
- package/dist/lib/config/service-token.d.ts +5 -0
- package/dist/lib/config/service-token.d.ts.map +1 -0
- package/dist/lib/config/service-token.js +18 -0
- package/dist/lib/config/service-token.js.map +1 -0
- package/dist/lib/crud/entity-crud-form.d.ts +49 -0
- package/dist/lib/crud/entity-crud-form.d.ts.map +1 -0
- package/dist/lib/crud/entity-crud-form.js +133 -0
- package/dist/lib/crud/entity-crud-form.js.map +1 -0
- package/dist/lib/crud/hand-off-write.d.ts +35 -0
- package/dist/lib/crud/hand-off-write.d.ts.map +1 -0
- package/dist/lib/crud/hand-off-write.js +27 -0
- package/dist/lib/crud/hand-off-write.js.map +1 -0
- package/dist/lib/crud/index.d.ts +9 -0
- package/dist/lib/crud/index.d.ts.map +1 -0
- package/dist/lib/crud/index.js +10 -0
- package/dist/lib/crud/index.js.map +1 -0
- package/dist/lib/crud/make-entity-crud.d.ts +28 -0
- package/dist/lib/crud/make-entity-crud.d.ts.map +1 -0
- package/dist/lib/crud/make-entity-crud.js +318 -0
- package/dist/lib/crud/make-entity-crud.js.map +1 -0
- package/dist/lib/crud/make-entity-crud.types.d.ts +237 -0
- package/dist/lib/crud/make-entity-crud.types.d.ts.map +1 -0
- package/dist/lib/crud/make-entity-crud.types.js +6 -0
- package/dist/lib/crud/make-entity-crud.types.js.map +1 -0
- package/dist/lib/crud/pending-notice.d.ts +31 -0
- package/dist/lib/crud/pending-notice.d.ts.map +1 -0
- package/dist/lib/crud/pending-notice.js +72 -0
- package/dist/lib/crud/pending-notice.js.map +1 -0
- package/dist/lib/crud/slug.d.ts +13 -0
- package/dist/lib/crud/slug.d.ts.map +1 -0
- package/dist/lib/crud/slug.js +13 -0
- package/dist/lib/crud/slug.js.map +1 -0
- package/dist/lib/crud/use-entity-affordances.d.ts +20 -0
- package/dist/lib/crud/use-entity-affordances.d.ts.map +1 -0
- package/dist/lib/crud/use-entity-affordances.js +22 -0
- package/dist/lib/crud/use-entity-affordances.js.map +1 -0
- package/dist/lib/crud/use-entity-bulk.d.ts +43 -0
- package/dist/lib/crud/use-entity-bulk.d.ts.map +1 -0
- package/dist/lib/crud/use-entity-bulk.js +106 -0
- package/dist/lib/crud/use-entity-bulk.js.map +1 -0
- package/dist/lib/crud/use-entity-link-sources.d.ts +25 -0
- package/dist/lib/crud/use-entity-link-sources.d.ts.map +1 -0
- package/dist/lib/crud/use-entity-link-sources.js +37 -0
- package/dist/lib/crud/use-entity-link-sources.js.map +1 -0
- package/dist/lib/health/health-routes.d.ts +37 -0
- package/dist/lib/health/health-routes.d.ts.map +1 -0
- package/dist/lib/health/health-routes.js +90 -0
- package/dist/lib/health/health-routes.js.map +1 -0
- package/dist/lib/i18n/catalog/en.d.ts +4 -0
- package/dist/lib/i18n/catalog/en.d.ts.map +1 -0
- package/dist/lib/i18n/catalog/en.js +347 -0
- package/dist/lib/i18n/catalog/en.js.map +1 -0
- package/dist/lib/i18n/catalog/es.d.ts +397 -0
- package/dist/lib/i18n/catalog/es.d.ts.map +1 -0
- package/dist/lib/i18n/catalog/es.js +407 -0
- package/dist/lib/i18n/catalog/es.js.map +1 -0
- package/dist/lib/i18n/catalog/index.d.ts +671 -0
- package/dist/lib/i18n/catalog/index.d.ts.map +1 -0
- package/dist/lib/i18n/catalog/index.js +28 -0
- package/dist/lib/i18n/catalog/index.js.map +1 -0
- package/dist/lib/i18n/index.d.ts +2 -0
- package/dist/lib/i18n/index.d.ts.map +1 -0
- package/dist/lib/i18n/index.js +3 -0
- package/dist/lib/i18n/index.js.map +1 -0
- package/dist/lib/i18n/locale-link.d.ts +23 -0
- package/dist/lib/i18n/locale-link.d.ts.map +1 -0
- package/dist/lib/i18n/locale-link.js +43 -0
- package/dist/lib/i18n/locale-link.js.map +1 -0
- package/dist/lib/i18n/spec-catalog.d.ts +2 -0
- package/dist/lib/i18n/spec-catalog.d.ts.map +1 -0
- package/dist/lib/i18n/spec-catalog.js +84 -0
- package/dist/lib/i18n/spec-catalog.js.map +1 -0
- package/dist/lib/search/record-search-route.d.ts +44 -0
- package/dist/lib/search/record-search-route.d.ts.map +1 -0
- package/dist/lib/search/record-search-route.js +158 -0
- package/dist/lib/search/record-search-route.js.map +1 -0
- package/dist/lib/search/record-search-source.d.ts +59 -0
- package/dist/lib/search/record-search-source.d.ts.map +1 -0
- package/dist/lib/search/record-search-source.js +118 -0
- package/dist/lib/search/record-search-source.js.map +1 -0
- package/dist/lib/search/record-search.types.d.ts +85 -0
- package/dist/lib/search/record-search.types.d.ts.map +1 -0
- package/dist/lib/search/record-search.types.js +29 -0
- package/dist/lib/search/record-search.types.js.map +1 -0
- package/dist/lib/search/search-records.d.ts +25 -0
- package/dist/lib/search/search-records.d.ts.map +1 -0
- package/dist/lib/search/search-records.js +36 -0
- package/dist/lib/search/search-records.js.map +1 -0
- package/dist/lib/session/account-links.d.ts +46 -0
- package/dist/lib/session/account-links.d.ts.map +1 -0
- package/dist/lib/session/account-links.js +41 -0
- package/dist/lib/session/account-links.js.map +1 -0
- package/dist/lib/session/bearer.d.ts +25 -0
- package/dist/lib/session/bearer.d.ts.map +1 -0
- package/dist/lib/session/bearer.js +31 -0
- package/dist/lib/session/bearer.js.map +1 -0
- package/dist/lib/session/cookies.d.ts +14 -0
- package/dist/lib/session/cookies.d.ts.map +1 -0
- package/dist/lib/session/cookies.js +33 -0
- package/dist/lib/session/cookies.js.map +1 -0
- package/dist/lib/session/device.d.ts +31 -0
- package/dist/lib/session/device.d.ts.map +1 -0
- package/dist/lib/session/device.js +80 -0
- package/dist/lib/session/device.js.map +1 -0
- package/dist/lib/session/index.d.ts +3 -0
- package/dist/lib/session/index.d.ts.map +1 -0
- package/dist/lib/session/index.js +12 -0
- package/dist/lib/session/index.js.map +1 -0
- package/dist/lib/session/refresh-route.d.ts +19 -0
- package/dist/lib/session/refresh-route.d.ts.map +1 -0
- package/dist/lib/session/refresh-route.js +85 -0
- package/dist/lib/session/refresh-route.js.map +1 -0
- package/dist/lib/session/session-keepalive.d.ts +52 -0
- package/dist/lib/session/session-keepalive.d.ts.map +1 -0
- package/dist/lib/session/session-keepalive.js +72 -0
- package/dist/lib/session/session-keepalive.js.map +1 -0
- package/dist/lib/session/use-session-refresh.d.ts +43 -0
- package/dist/lib/session/use-session-refresh.d.ts.map +1 -0
- package/dist/lib/session/use-session-refresh.js +120 -0
- package/dist/lib/session/use-session-refresh.js.map +1 -0
- package/dist/lib/wizard/index.d.ts +4 -0
- package/dist/lib/wizard/index.d.ts.map +1 -0
- package/dist/lib/wizard/index.js +5 -0
- package/dist/lib/wizard/index.js.map +1 -0
- package/dist/lib/wizard/use-wizard-draft.d.ts +22 -0
- package/dist/lib/wizard/use-wizard-draft.d.ts.map +1 -0
- package/dist/lib/wizard/use-wizard-draft.js +39 -0
- package/dist/lib/wizard/use-wizard-draft.js.map +1 -0
- package/dist/lib/wizard/use-wizard-step-param.d.ts +60 -0
- package/dist/lib/wizard/use-wizard-step-param.d.ts.map +1 -0
- package/dist/lib/wizard/use-wizard-step-param.js +94 -0
- package/dist/lib/wizard/use-wizard-step-param.js.map +1 -0
- package/dist/lib/wizard/wizard-tab-kind.d.ts +31 -0
- package/dist/lib/wizard/wizard-tab-kind.d.ts.map +1 -0
- package/dist/lib/wizard/wizard-tab-kind.js +33 -0
- package/dist/lib/wizard/wizard-tab-kind.js.map +1 -0
- package/dist/lib/workspace/drafts-state.d.ts +88 -0
- package/dist/lib/workspace/drafts-state.d.ts.map +1 -0
- package/dist/lib/workspace/drafts-state.js +128 -0
- package/dist/lib/workspace/drafts-state.js.map +1 -0
- package/dist/lib/workspace/entity-nav.d.ts +26 -0
- package/dist/lib/workspace/entity-nav.d.ts.map +1 -0
- package/dist/lib/workspace/entity-nav.js +56 -0
- package/dist/lib/workspace/entity-nav.js.map +1 -0
- package/dist/lib/workspace/entity-tab-kind.d.ts +51 -0
- package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -0
- package/dist/lib/workspace/entity-tab-kind.js +47 -0
- package/dist/lib/workspace/entity-tab-kind.js.map +1 -0
- package/dist/lib/workspace/idb-state-storage.d.ts +9 -0
- package/dist/lib/workspace/idb-state-storage.d.ts.map +1 -0
- package/dist/lib/workspace/idb-state-storage.js +32 -0
- package/dist/lib/workspace/idb-state-storage.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +13 -0
- package/dist/lib/workspace/index.d.ts.map +1 -0
- package/dist/lib/workspace/index.js +14 -0
- package/dist/lib/workspace/index.js.map +1 -0
- package/dist/lib/workspace/pending-state.d.ts +84 -0
- package/dist/lib/workspace/pending-state.d.ts.map +1 -0
- package/dist/lib/workspace/pending-state.js +155 -0
- package/dist/lib/workspace/pending-state.js.map +1 -0
- package/dist/lib/workspace/pending-transactions.d.ts +44 -0
- package/dist/lib/workspace/pending-transactions.d.ts.map +1 -0
- package/dist/lib/workspace/pending-transactions.js +114 -0
- package/dist/lib/workspace/pending-transactions.js.map +1 -0
- package/dist/lib/workspace/tab-kind.d.ts +48 -0
- package/dist/lib/workspace/tab-kind.d.ts.map +1 -0
- package/dist/lib/workspace/tab-kind.js +47 -0
- package/dist/lib/workspace/tab-kind.js.map +1 -0
- package/dist/lib/workspace/tab-state.d.ts +36 -0
- package/dist/lib/workspace/tab-state.d.ts.map +1 -0
- package/dist/lib/workspace/tab-state.js +81 -0
- package/dist/lib/workspace/tab-state.js.map +1 -0
- package/dist/lib/workspace/tabs-state.d.ts +46 -0
- package/dist/lib/workspace/tabs-state.d.ts.map +1 -0
- package/dist/lib/workspace/tabs-state.js +56 -0
- package/dist/lib/workspace/tabs-state.js.map +1 -0
- package/dist/lib/workspace/use-entity-draft.d.ts +25 -0
- package/dist/lib/workspace/use-entity-draft.d.ts.map +1 -0
- package/dist/lib/workspace/use-entity-draft.js +38 -0
- package/dist/lib/workspace/use-entity-draft.js.map +1 -0
- package/dist/lib/workspace/workspace-scope.d.ts +51 -0
- package/dist/lib/workspace/workspace-scope.d.ts.map +1 -0
- package/dist/lib/workspace/workspace-scope.js +42 -0
- package/dist/lib/workspace/workspace-scope.js.map +1 -0
- package/dist/lib/workspace/workspace-shell.d.ts +33 -0
- package/dist/lib/workspace/workspace-shell.d.ts.map +1 -0
- package/dist/lib/workspace/workspace-shell.js +206 -0
- package/dist/lib/workspace/workspace-shell.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +41 -0
- package/dist/server.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
/** The query parameter a wizard route carries its step in. */ export const WIZARD_STEP_PARAM = 'step';
|
|
5
|
+
/**
|
|
6
|
+
* Builds this route's address with a step in it.
|
|
7
|
+
*
|
|
8
|
+
* Shared by the writer and the follower so the two cannot disagree about the
|
|
9
|
+
* shape — and so neither drops the rest of the query, which on a workspace host
|
|
10
|
+
* is the `?tab=` value naming the tab the wizard is inside.
|
|
11
|
+
*/ function useStepAddress() {
|
|
12
|
+
const pathname = usePathname();
|
|
13
|
+
const searchParams = useSearchParams();
|
|
14
|
+
return useCallback((stepId)=>{
|
|
15
|
+
const next = new URLSearchParams(searchParams);
|
|
16
|
+
next.set(WIZARD_STEP_PARAM, stepId);
|
|
17
|
+
return `${pathname}?${next.toString()}`;
|
|
18
|
+
}, [
|
|
19
|
+
pathname,
|
|
20
|
+
searchParams
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Writes a step into the address — what `useWizard`'s `onStepChange` is wired to.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ **This must be handed to `useWizard` directly, never through a ref.** The
|
|
27
|
+
* push has to land in the same commit as the move: the follower below compares
|
|
28
|
+
* the address with the active step, so a report that arrives a tick late leaves
|
|
29
|
+
* the two disagreeing, and the follower reads that as a Back and moves the
|
|
30
|
+
* wizard *back* to the step it just left. Splitting the writer from the follower
|
|
31
|
+
* is what removes the cycle that made a ref look necessary.
|
|
32
|
+
*
|
|
33
|
+
* It **checks the address before pushing**, and that guard is what stops the
|
|
34
|
+
* loop: a Back drives `goTo`, which reports the move, which would otherwise push
|
|
35
|
+
* the very address Back had just restored — turning one press into an entry the
|
|
36
|
+
* operator has to get past twice.
|
|
37
|
+
*/ export function useWizardStepUrl() {
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
const searchParams = useSearchParams();
|
|
40
|
+
const addressOf = useStepAddress();
|
|
41
|
+
return useCallback((stepId)=>{
|
|
42
|
+
if (searchParams.get(WIZARD_STEP_PARAM) === stepId) return;
|
|
43
|
+
router.push(addressOf(stepId));
|
|
44
|
+
}, [
|
|
45
|
+
router,
|
|
46
|
+
searchParams,
|
|
47
|
+
addressOf
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Moves the wizard when the address moves, so browser-Back moves a step instead
|
|
52
|
+
* of leaving the flow.
|
|
53
|
+
*
|
|
54
|
+
* **Back works; Forward deliberately does not re-advance.** Going back pops the
|
|
55
|
+
* history, so the step Forward names is no longer on the path and `goTo` ignores
|
|
56
|
+
* it — re-entering it would skip the per-step submit that validated everything
|
|
57
|
+
* in between, which is the only thing gating advancement.
|
|
58
|
+
*
|
|
59
|
+
* An address carrying **no** step means the flow's beginning — which is where
|
|
60
|
+
* browser-Back from the second step lands, since the first step is never pushed.
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ **It reacts to the address *changing*, never to what the address says.**
|
|
63
|
+
* Two faults sit on either side of that, and both were measured:
|
|
64
|
+
*
|
|
65
|
+
* - *Rewriting the address to match the wizard* races the advance. The replace
|
|
66
|
+
* is scheduled at mount with the entry step captured, the operator advances
|
|
67
|
+
* before it lands, and the navigation then puts the older step back — this
|
|
68
|
+
* effect reads it and rewinds a step they had already passed. It appeared only
|
|
69
|
+
* under load, which is the kind of race that reaches a user and not a test.
|
|
70
|
+
* - *Acting on the address at mount* breaks resume. A restored wizard takes its
|
|
71
|
+
* position from the persisted draft, so it opens on step four with an address
|
|
72
|
+
* that says nothing — read as "the beginning", that sends it back to step one
|
|
73
|
+
* and the resume is undone.
|
|
74
|
+
*
|
|
75
|
+
* So the first address seen is a **baseline**, and only a move away from it is a
|
|
76
|
+
* navigation. One direction throughout: the writer owns the address, the
|
|
77
|
+
* follower owns the wizard.
|
|
78
|
+
*/ export function useFollowWizardStepUrl({ activeStep, entryStep, goTo }) {
|
|
79
|
+
var _searchParams_get;
|
|
80
|
+
const searchParams = useSearchParams();
|
|
81
|
+
const target = (_searchParams_get = searchParams.get(WIZARD_STEP_PARAM)) != null ? _searchParams_get : entryStep;
|
|
82
|
+
const seen = useRef(target);
|
|
83
|
+
useEffect(()=>{
|
|
84
|
+
if (target === seen.current) return;
|
|
85
|
+
seen.current = target;
|
|
86
|
+
if (target !== activeStep) goTo(target);
|
|
87
|
+
}, [
|
|
88
|
+
target,
|
|
89
|
+
activeStep,
|
|
90
|
+
goTo
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=use-wizard-step-param.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/wizard/use-wizard-step-param.ts"],"sourcesContent":["'use client';\n\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation';\nimport { useCallback, useEffect, useRef } from 'react';\n\n/** The query parameter a wizard route carries its step in. */\nexport const WIZARD_STEP_PARAM = 'step';\n\n/**\n * Builds this route's address with a step in it.\n *\n * Shared by the writer and the follower so the two cannot disagree about the\n * shape — and so neither drops the rest of the query, which on a workspace host\n * is the `?tab=` value naming the tab the wizard is inside.\n */\nfunction useStepAddress(): (stepId: string) => string {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n return useCallback(\n (stepId: string) => {\n const next = new URLSearchParams(searchParams);\n next.set(WIZARD_STEP_PARAM, stepId);\n return `${pathname}?${next.toString()}`;\n },\n [pathname, searchParams],\n );\n}\n\n/**\n * Writes a step into the address — what `useWizard`'s `onStepChange` is wired to.\n *\n * ⚠️ **This must be handed to `useWizard` directly, never through a ref.** The\n * push has to land in the same commit as the move: the follower below compares\n * the address with the active step, so a report that arrives a tick late leaves\n * the two disagreeing, and the follower reads that as a Back and moves the\n * wizard *back* to the step it just left. Splitting the writer from the follower\n * is what removes the cycle that made a ref look necessary.\n *\n * It **checks the address before pushing**, and that guard is what stops the\n * loop: a Back drives `goTo`, which reports the move, which would otherwise push\n * the very address Back had just restored — turning one press into an entry the\n * operator has to get past twice.\n */\nexport function useWizardStepUrl(): (stepId: string) => void {\n const router = useRouter();\n const searchParams = useSearchParams();\n const addressOf = useStepAddress();\n\n return useCallback(\n (stepId: string) => {\n if (searchParams.get(WIZARD_STEP_PARAM) === stepId) return;\n router.push(addressOf(stepId));\n },\n [router, searchParams, addressOf],\n );\n}\n\nexport interface FollowWizardStepUrlOptions {\n /** Where the wizard currently is, from `useWizard`. */\n activeStep: string;\n /** The flow's first step — where an address carrying no step points. */\n entryStep: string;\n /**\n * `useWizard`'s `goTo`. It ignores a step that is not on the path already\n * walked, which is what makes browser-Forward safe to hand straight to it.\n */\n goTo: (stepId: string) => void;\n}\n\n/**\n * Moves the wizard when the address moves, so browser-Back moves a step instead\n * of leaving the flow.\n *\n * **Back works; Forward deliberately does not re-advance.** Going back pops the\n * history, so the step Forward names is no longer on the path and `goTo` ignores\n * it — re-entering it would skip the per-step submit that validated everything\n * in between, which is the only thing gating advancement.\n *\n * An address carrying **no** step means the flow's beginning — which is where\n * browser-Back from the second step lands, since the first step is never pushed.\n *\n * ⚠️ **It reacts to the address *changing*, never to what the address says.**\n * Two faults sit on either side of that, and both were measured:\n *\n * - *Rewriting the address to match the wizard* races the advance. The replace\n * is scheduled at mount with the entry step captured, the operator advances\n * before it lands, and the navigation then puts the older step back — this\n * effect reads it and rewinds a step they had already passed. It appeared only\n * under load, which is the kind of race that reaches a user and not a test.\n * - *Acting on the address at mount* breaks resume. A restored wizard takes its\n * position from the persisted draft, so it opens on step four with an address\n * that says nothing — read as \"the beginning\", that sends it back to step one\n * and the resume is undone.\n *\n * So the first address seen is a **baseline**, and only a move away from it is a\n * navigation. One direction throughout: the writer owns the address, the\n * follower owns the wizard.\n */\nexport function useFollowWizardStepUrl({\n activeStep,\n entryStep,\n goTo,\n}: FollowWizardStepUrlOptions): void {\n const searchParams = useSearchParams();\n const target = searchParams.get(WIZARD_STEP_PARAM) ?? entryStep;\n const seen = useRef(target);\n\n useEffect(() => {\n if (target === seen.current) return;\n seen.current = target;\n if (target !== activeStep) goTo(target);\n }, [target, activeStep, goTo]);\n}\n"],"names":["usePathname","useRouter","useSearchParams","useCallback","useEffect","useRef","WIZARD_STEP_PARAM","useStepAddress","pathname","searchParams","stepId","next","URLSearchParams","set","toString","useWizardStepUrl","router","addressOf","get","push","useFollowWizardStepUrl","activeStep","entryStep","goTo","target","seen","current"],"mappings":"AAAA;AAEA,SAASA,WAAW,EAAEC,SAAS,EAAEC,eAAe,QAAQ,kBAAkB;AAC1E,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,QAAQ,QAAQ;AAEvD,4DAA4D,GAC5D,OAAO,MAAMC,oBAAoB,OAAO;AAExC;;;;;;CAMC,GACD,SAASC;IACP,MAAMC,WAAWR;IACjB,MAAMS,eAAeP;IAErB,OAAOC,YACL,CAACO;QACC,MAAMC,OAAO,IAAIC,gBAAgBH;QACjCE,KAAKE,GAAG,CAACP,mBAAmBI;QAC5B,OAAO,GAAGF,SAAS,CAAC,EAAEG,KAAKG,QAAQ,IAAI;IACzC,GACA;QAACN;QAAUC;KAAa;AAE5B;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASM;IACd,MAAMC,SAASf;IACf,MAAMQ,eAAeP;IACrB,MAAMe,YAAYV;IAElB,OAAOJ,YACL,CAACO;QACC,IAAID,aAAaS,GAAG,CAACZ,uBAAuBI,QAAQ;QACpDM,OAAOG,IAAI,CAACF,UAAUP;IACxB,GACA;QAACM;QAAQP;QAAcQ;KAAU;AAErC;AAcA;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASG,uBAAuB,EACrCC,UAAU,EACVC,SAAS,EACTC,IAAI,EACuB;QAEZd;IADf,MAAMA,eAAeP;IACrB,MAAMsB,UAASf,oBAAAA,aAAaS,GAAG,CAACZ,8BAAjBG,oBAAuCa;IACtD,MAAMG,OAAOpB,OAAOmB;IAEpBpB,UAAU;QACR,IAAIoB,WAAWC,KAAKC,OAAO,EAAE;QAC7BD,KAAKC,OAAO,GAAGF;QACf,IAAIA,WAAWH,YAAYE,KAAKC;IAClC,GAAG;QAACA;QAAQH;QAAYE;KAAK;AAC/B"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { TabKind } from '../workspace/tab-kind.js';
|
|
3
|
+
/** One wizard a workspace may open, as the registry needs it. */
|
|
4
|
+
export interface WizardTabScreen {
|
|
5
|
+
/** The tab caption's catalog key — the wizard's name, never the step's. */
|
|
6
|
+
readonly titleKey: string;
|
|
7
|
+
/** `step` is the third address segment, absent while the flow is at its first. */
|
|
8
|
+
render(step: string | undefined): ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `wizard:` tab kind: `wizard:<key>` opens a flow, `wizard:<key>:<step>`
|
|
12
|
+
* opens it at a step.
|
|
13
|
+
*
|
|
14
|
+
* The third segment is a **step** here and a record under `master:`, which is
|
|
15
|
+
* the one thing [ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)
|
|
16
|
+
* changed about the address grammar — `screenAddress` and `parseScreenPayload`
|
|
17
|
+
* already produced and accepted the shape.
|
|
18
|
+
*
|
|
19
|
+
* **The caption is the wizard's name and not the step's**, deliberately: a tab
|
|
20
|
+
* whose title changed on every Siguiente would reflow the strip mid-flow and
|
|
21
|
+
* make an open wizard hard to find again by the name it was opened under.
|
|
22
|
+
*
|
|
23
|
+
* A factory rather than a constant, for the reason `masterKind` is built in the
|
|
24
|
+
* host: which screens a workspace offers is the host's decision, and the shells
|
|
25
|
+
* that own the wizards may not import each other.
|
|
26
|
+
*/
|
|
27
|
+
export declare function wizardTabKind(screens: Record<string, WizardTabScreen>): TabKind<{
|
|
28
|
+
key: string;
|
|
29
|
+
id?: string;
|
|
30
|
+
}>;
|
|
31
|
+
//# sourceMappingURL=wizard-tab-kind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard-tab-kind.d.ts","sourceRoot":"","sources":["../../../src/lib/wizard/wizard-tab-kind.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAExD,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GACvC,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAavC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { parseScreenPayload } from '@entifix/authz';
|
|
3
|
+
/**
|
|
4
|
+
* The `wizard:` tab kind: `wizard:<key>` opens a flow, `wizard:<key>:<step>`
|
|
5
|
+
* opens it at a step.
|
|
6
|
+
*
|
|
7
|
+
* The third segment is a **step** here and a record under `master:`, which is
|
|
8
|
+
* the one thing [ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)
|
|
9
|
+
* changed about the address grammar — `screenAddress` and `parseScreenPayload`
|
|
10
|
+
* already produced and accepted the shape.
|
|
11
|
+
*
|
|
12
|
+
* **The caption is the wizard's name and not the step's**, deliberately: a tab
|
|
13
|
+
* whose title changed on every Siguiente would reflow the strip mid-flow and
|
|
14
|
+
* make an open wizard hard to find again by the name it was opened under.
|
|
15
|
+
*
|
|
16
|
+
* A factory rather than a constant, for the reason `masterKind` is built in the
|
|
17
|
+
* host: which screens a workspace offers is the host's decision, and the shells
|
|
18
|
+
* that own the wizards may not import each other.
|
|
19
|
+
*/ export function wizardTabKind(screens) {
|
|
20
|
+
return {
|
|
21
|
+
kind: 'wizard',
|
|
22
|
+
match: (payload)=>{
|
|
23
|
+
const parsed = parseScreenPayload(payload);
|
|
24
|
+
if (parsed === null) return null;
|
|
25
|
+
return parsed.key in screens ? parsed : null;
|
|
26
|
+
},
|
|
27
|
+
toParam: (addr)=>addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,
|
|
28
|
+
title: (addr, translate)=>translate(screens[addr.key].titleKey),
|
|
29
|
+
render: (addr)=>screens[addr.key].render(addr.id)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=wizard-tab-kind.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/wizard/wizard-tab-kind.tsx"],"sourcesContent":["'use client';\n\nimport { parseScreenPayload } from '@entifix/authz';\nimport type { ReactNode } from 'react';\n\nimport type { TabKind } from '../workspace/tab-kind.js';\n\n/** One wizard a workspace may open, as the registry needs it. */\nexport interface WizardTabScreen {\n /** The tab caption's catalog key — the wizard's name, never the step's. */\n readonly titleKey: string;\n /** `step` is the third address segment, absent while the flow is at its first. */\n render(step: string | undefined): ReactNode;\n}\n\n/**\n * The `wizard:` tab kind: `wizard:<key>` opens a flow, `wizard:<key>:<step>`\n * opens it at a step.\n *\n * The third segment is a **step** here and a record under `master:`, which is\n * the one thing [ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)\n * changed about the address grammar — `screenAddress` and `parseScreenPayload`\n * already produced and accepted the shape.\n *\n * **The caption is the wizard's name and not the step's**, deliberately: a tab\n * whose title changed on every Siguiente would reflow the strip mid-flow and\n * make an open wizard hard to find again by the name it was opened under.\n *\n * A factory rather than a constant, for the reason `masterKind` is built in the\n * host: which screens a workspace offers is the host's decision, and the shells\n * that own the wizards may not import each other.\n */\nexport function wizardTabKind(\n screens: Record<string, WizardTabScreen>,\n): TabKind<{ key: string; id?: string }> {\n return {\n kind: 'wizard',\n match: payload => {\n const parsed = parseScreenPayload(payload);\n if (parsed === null) return null;\n return parsed.key in screens ? parsed : null;\n },\n toParam: addr =>\n addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,\n title: (addr, translate) => translate(screens[addr.key].titleKey),\n render: addr => screens[addr.key].render(addr.id),\n };\n}\n"],"names":["parseScreenPayload","wizardTabKind","screens","kind","match","payload","parsed","key","toParam","addr","id","undefined","title","translate","titleKey","render"],"mappings":"AAAA;AAEA,SAASA,kBAAkB,QAAQ,iBAAiB;AAapD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,cACdC,OAAwC;IAExC,OAAO;QACLC,MAAM;QACNC,OAAOC,CAAAA;YACL,MAAMC,SAASN,mBAAmBK;YAClC,IAAIC,WAAW,MAAM,OAAO;YAC5B,OAAOA,OAAOC,GAAG,IAAIL,UAAUI,SAAS;QAC1C;QACAE,SAASC,CAAAA,OACPA,KAAKC,EAAE,KAAKC,YAAYF,KAAKF,GAAG,GAAG,GAAGE,KAAKF,GAAG,CAAC,CAAC,EAAEE,KAAKC,EAAE,EAAE;QAC7DE,OAAO,CAACH,MAAMI,YAAcA,UAAUX,OAAO,CAACO,KAAKF,GAAG,CAAC,CAACO,QAAQ;QAChEC,QAAQN,CAAAA,OAAQP,OAAO,CAACO,KAAKF,GAAG,CAAC,CAACQ,MAAM,CAACN,KAAKC,EAAE;IAClD;AACF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type JsonValue } from '@entifix/core';
|
|
2
|
+
/**
|
|
3
|
+
* Per-address edit drafts, persisted to IndexedDB so an in-progress edit
|
|
4
|
+
* survives a refresh. Keyed by the tab's `?tab=` param (its address), a draft is
|
|
5
|
+
* whatever the editor needs to restore its fields; it exists until the user
|
|
6
|
+
* saves (or discards). This is the "continuous autosave" seam — workspace-host
|
|
7
|
+
* only, so a plain route stays ephemeral.
|
|
8
|
+
*
|
|
9
|
+
* **A draft is JSON round-trippable, period.** It is written through
|
|
10
|
+
* `createJSONStorage`, so a class instance, an `EntityLink` or a `Date` does not
|
|
11
|
+
* "mostly survive" — it comes back as something else, silently. {@link JsonValue}
|
|
12
|
+
* is the compile-time half of that rule and {@link mergeDrafts} the runtime half.
|
|
13
|
+
* Note that `UiPreferencesState`, the other persisted client store, writes
|
|
14
|
+
* through IndexedDB's structured clone and *does* keep a `Date`; the two
|
|
15
|
+
* contracts differ and must not be conflated.
|
|
16
|
+
*/
|
|
17
|
+
export interface DraftsState {
|
|
18
|
+
drafts: Record<string, JsonValue>;
|
|
19
|
+
setDraft(address: string, value: JsonValue): void;
|
|
20
|
+
clearDraft(address: string): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function persistedDrafts(store: DraftsState): Pick<DraftsState, 'drafts'>;
|
|
23
|
+
/**
|
|
24
|
+
* Bump when the *envelope* changes — how a draft is keyed or wrapped — and every
|
|
25
|
+
* persisted draft is discarded on the next load.
|
|
26
|
+
*
|
|
27
|
+
* It cannot see a change to one entity's members, because a draft is opaque
|
|
28
|
+
* here: this store holds `JsonValue`, and only the form that wrote one knows
|
|
29
|
+
* which members it should have. That half is `restoreEntityDraft`, which layers
|
|
30
|
+
* a restored draft over a freshly seeded one so a drifted member is dropped or
|
|
31
|
+
* re-seeded rather than rendered.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DRAFTS_VERSION = 2;
|
|
34
|
+
/**
|
|
35
|
+
* Discard, never guess. A draft whose envelope this build does not understand is
|
|
36
|
+
* an unfinished edit, not a record — losing it costs a retype, while migrating
|
|
37
|
+
* it blind risks submitting values whose meaning has changed.
|
|
38
|
+
*
|
|
39
|
+
* Written out rather than left to zustand's default, which discards too but logs
|
|
40
|
+
* `State loaded from storage couldn't be migrated since no migrate function was
|
|
41
|
+
* provided` while doing it — an error message for a decision that was made on
|
|
42
|
+
* purpose.
|
|
43
|
+
*/
|
|
44
|
+
export declare function migrateDrafts(): Pick<DraftsState, 'drafts'>;
|
|
45
|
+
/**
|
|
46
|
+
* Restores only the entries that are actually JSON.
|
|
47
|
+
*
|
|
48
|
+
* The type above says what may be written; this is what happens when something
|
|
49
|
+
* else was, by a build that predates the rule or by a `setDraft` reached through
|
|
50
|
+
* a cast. Dropping the offending entry keeps one bad draft from taking the whole
|
|
51
|
+
* workspace's drafts with it — and it is per entry rather than all-or-nothing
|
|
52
|
+
* for that reason.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mergeDrafts(persistedState: unknown, currentState: DraftsState): DraftsState;
|
|
55
|
+
export declare const useDraftsState: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<DraftsState>, "setState" | "persist"> & {
|
|
56
|
+
setState(partial: DraftsState | Partial<DraftsState> | ((state: DraftsState) => DraftsState | Partial<DraftsState>), replace?: false | undefined): unknown;
|
|
57
|
+
setState(state: DraftsState | ((state: DraftsState) => DraftsState), replace: true): unknown;
|
|
58
|
+
persist: {
|
|
59
|
+
setOptions: (options: Partial<import("zustand/middleware").PersistOptions<DraftsState, unknown, unknown>>) => void;
|
|
60
|
+
clearStorage: () => void;
|
|
61
|
+
rehydrate: () => Promise<void> | void;
|
|
62
|
+
hasHydrated: () => boolean;
|
|
63
|
+
onHydrate: (fn: (state: DraftsState) => void) => () => void;
|
|
64
|
+
onFinishHydration: (fn: (state: DraftsState) => void) => () => void;
|
|
65
|
+
getOptions: () => Partial<import("zustand/middleware").PersistOptions<DraftsState, unknown, unknown>>;
|
|
66
|
+
};
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Bind an editor to its persisted draft. Returns the current draft (restored
|
|
70
|
+
* from IndexedDB on mount), a setter the editor calls as fields change, and a
|
|
71
|
+
* clear for when the edit is committed.
|
|
72
|
+
*
|
|
73
|
+
* `TDraft` is constrained to {@link JsonValue}: an editor that wants to autosave
|
|
74
|
+
* something a JSON round trip would not return is asking for the one thing this
|
|
75
|
+
* store cannot do, and the compile error is the whole point of the constraint.
|
|
76
|
+
*
|
|
77
|
+
* One sharp edge, and the error message does not explain it: **declare a draft
|
|
78
|
+
* type as a `type`, never an `interface`.** TypeScript gives an interface no
|
|
79
|
+
* implicit index signature, so even `interface Draft { name: string }` — as JSON
|
|
80
|
+
* as a value gets — fails the constraint with "Index signature for type 'string'
|
|
81
|
+
* is missing". A `type` alias of the same shape passes.
|
|
82
|
+
*/
|
|
83
|
+
export declare function useDraft<TDraft extends JsonValue>(address: string): {
|
|
84
|
+
draft: TDraft | undefined;
|
|
85
|
+
setDraft: (value: TDraft) => void;
|
|
86
|
+
clearDraft: () => void;
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=drafts-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drafts-state.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/drafts-state.ts"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAQ5D;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IAClD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,WAAW,GACjB,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAE7B;AAID;;;;;;;;;GASG;AAGH,eAAO,MAAM,cAAc,IAAI,CAAC;AAIhC;;;;;;;;;GASG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,cAAc,EAAE,OAAO,EACvB,YAAY,EAAE,WAAW,GACxB,WAAW,CASb;AAED,eAAO,MAAM,cAAc;;;;;;;;;;;;EA6B1B,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,SAAS,SAAS,EAAE,OAAO,EAAE,MAAM;;sBAYtD,MAAM;;EAMjB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
3
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
4
|
+
import { _ as _to_property_key } from "@swc/helpers/_/_to_property_key";
|
|
5
|
+
import { isJsonValue } from '@entifix/core';
|
|
6
|
+
import { useCallback, useEffect } from 'react';
|
|
7
|
+
import { create } from 'zustand';
|
|
8
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
9
|
+
import { makeIndexedDbStateStorage } from './idb-state-storage.js';
|
|
10
|
+
import { WORKSPACE_DB } from './tabs-state.js';
|
|
11
|
+
export function persistedDrafts(store) {
|
|
12
|
+
return {
|
|
13
|
+
drafts: store.drafts
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const DRAFTS_STORE = 'stores';
|
|
17
|
+
/**
|
|
18
|
+
* Bump when the *envelope* changes — how a draft is keyed or wrapped — and every
|
|
19
|
+
* persisted draft is discarded on the next load.
|
|
20
|
+
*
|
|
21
|
+
* It cannot see a change to one entity's members, because a draft is opaque
|
|
22
|
+
* here: this store holds `JsonValue`, and only the form that wrote one knows
|
|
23
|
+
* which members it should have. That half is `restoreEntityDraft`, which layers
|
|
24
|
+
* a restored draft over a freshly seeded one so a drifted member is dropped or
|
|
25
|
+
* re-seeded rather than rendered.
|
|
26
|
+
*/ // 2: a draft is keyed by the tab address, and that grammar changed with
|
|
27
|
+
// ADR 0042 — so every persisted draft is filed under a key nothing will ask for.
|
|
28
|
+
export const DRAFTS_VERSION = 2;
|
|
29
|
+
const emptyDrafts = {
|
|
30
|
+
drafts: {}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Discard, never guess. A draft whose envelope this build does not understand is
|
|
34
|
+
* an unfinished edit, not a record — losing it costs a retype, while migrating
|
|
35
|
+
* it blind risks submitting values whose meaning has changed.
|
|
36
|
+
*
|
|
37
|
+
* Written out rather than left to zustand's default, which discards too but logs
|
|
38
|
+
* `State loaded from storage couldn't be migrated since no migrate function was
|
|
39
|
+
* provided` while doing it — an error message for a decision that was made on
|
|
40
|
+
* purpose.
|
|
41
|
+
*/ export function migrateDrafts() {
|
|
42
|
+
return emptyDrafts;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Restores only the entries that are actually JSON.
|
|
46
|
+
*
|
|
47
|
+
* The type above says what may be written; this is what happens when something
|
|
48
|
+
* else was, by a build that predates the rule or by a `setDraft` reached through
|
|
49
|
+
* a cast. Dropping the offending entry keeps one bad draft from taking the whole
|
|
50
|
+
* workspace's drafts with it — and it is per entry rather than all-or-nothing
|
|
51
|
+
* for that reason.
|
|
52
|
+
*/ export function mergeDrafts(persistedState, currentState) {
|
|
53
|
+
const restored = persistedState == null ? void 0 : persistedState.drafts;
|
|
54
|
+
if (restored === null || typeof restored !== 'object') return currentState;
|
|
55
|
+
const drafts = {};
|
|
56
|
+
for (const [address, value] of Object.entries(restored)){
|
|
57
|
+
if (isJsonValue(value)) drafts[address] = value;
|
|
58
|
+
}
|
|
59
|
+
return _extends({}, currentState, {
|
|
60
|
+
drafts
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export const useDraftsState = create()(persist((set)=>({
|
|
64
|
+
drafts: {},
|
|
65
|
+
setDraft: (address, value)=>set((state)=>({
|
|
66
|
+
drafts: _extends({}, state.drafts, {
|
|
67
|
+
[address]: value
|
|
68
|
+
})
|
|
69
|
+
})),
|
|
70
|
+
clearDraft: (address)=>set((state)=>{
|
|
71
|
+
if (!(address in state.drafts)) return state;
|
|
72
|
+
const _state_drafts = state.drafts, { [address]: _removed } = _state_drafts, rest = _object_without_properties_loose(_state_drafts, [
|
|
73
|
+
_to_property_key(address)
|
|
74
|
+
]);
|
|
75
|
+
return {
|
|
76
|
+
drafts: rest
|
|
77
|
+
};
|
|
78
|
+
})
|
|
79
|
+
}), {
|
|
80
|
+
// Replaced with a scoped name by `WorkspaceShell` before it rehydrates, so
|
|
81
|
+
// two accounts on one browser profile do not share drafts. This literal is
|
|
82
|
+
// only what an unscoped store would write, and nothing rehydrates without
|
|
83
|
+
// passing through that call.
|
|
84
|
+
name: 'drafts',
|
|
85
|
+
version: DRAFTS_VERSION,
|
|
86
|
+
migrate: migrateDrafts,
|
|
87
|
+
merge: mergeDrafts,
|
|
88
|
+
storage: createJSONStorage(()=>makeIndexedDbStateStorage(WORKSPACE_DB, DRAFTS_STORE)),
|
|
89
|
+
partialize: persistedDrafts,
|
|
90
|
+
skipHydration: true
|
|
91
|
+
}));
|
|
92
|
+
/**
|
|
93
|
+
* Bind an editor to its persisted draft. Returns the current draft (restored
|
|
94
|
+
* from IndexedDB on mount), a setter the editor calls as fields change, and a
|
|
95
|
+
* clear for when the edit is committed.
|
|
96
|
+
*
|
|
97
|
+
* `TDraft` is constrained to {@link JsonValue}: an editor that wants to autosave
|
|
98
|
+
* something a JSON round trip would not return is asking for the one thing this
|
|
99
|
+
* store cannot do, and the compile error is the whole point of the constraint.
|
|
100
|
+
*
|
|
101
|
+
* One sharp edge, and the error message does not explain it: **declare a draft
|
|
102
|
+
* type as a `type`, never an `interface`.** TypeScript gives an interface no
|
|
103
|
+
* implicit index signature, so even `interface Draft { name: string }` — as JSON
|
|
104
|
+
* as a value gets — fails the constraint with "Index signature for type 'string'
|
|
105
|
+
* is missing". A `type` alias of the same shape passes.
|
|
106
|
+
*/ export function useDraft(address) {
|
|
107
|
+
const draft = useDraftsState((state)=>state.drafts[address]);
|
|
108
|
+
const set = useDraftsState((state)=>state.setDraft);
|
|
109
|
+
const clear = useDraftsState((state)=>state.clearDraft);
|
|
110
|
+
useEffect(()=>{
|
|
111
|
+
void useDraftsState.persist.rehydrate();
|
|
112
|
+
}, []);
|
|
113
|
+
const setDraft = useCallback((value)=>set(address, value), [
|
|
114
|
+
set,
|
|
115
|
+
address
|
|
116
|
+
]);
|
|
117
|
+
const clearDraft = useCallback(()=>clear(address), [
|
|
118
|
+
clear,
|
|
119
|
+
address
|
|
120
|
+
]);
|
|
121
|
+
return {
|
|
122
|
+
draft,
|
|
123
|
+
setDraft,
|
|
124
|
+
clearDraft
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=drafts-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/workspace/drafts-state.ts"],"sourcesContent":["'use client';\n\nimport { isJsonValue, type JsonValue } from '@entifix/core';\nimport { useCallback, useEffect } from 'react';\nimport { create } from 'zustand';\nimport { createJSONStorage, persist } from 'zustand/middleware';\n\nimport { makeIndexedDbStateStorage } from './idb-state-storage.js';\nimport { WORKSPACE_DB } from './tabs-state.js';\n\n/**\n * Per-address edit drafts, persisted to IndexedDB so an in-progress edit\n * survives a refresh. Keyed by the tab's `?tab=` param (its address), a draft is\n * whatever the editor needs to restore its fields; it exists until the user\n * saves (or discards). This is the \"continuous autosave\" seam — workspace-host\n * only, so a plain route stays ephemeral.\n *\n * **A draft is JSON round-trippable, period.** It is written through\n * `createJSONStorage`, so a class instance, an `EntityLink` or a `Date` does not\n * \"mostly survive\" — it comes back as something else, silently. {@link JsonValue}\n * is the compile-time half of that rule and {@link mergeDrafts} the runtime half.\n * Note that `UiPreferencesState`, the other persisted client store, writes\n * through IndexedDB's structured clone and *does* keep a `Date`; the two\n * contracts differ and must not be conflated.\n */\nexport interface DraftsState {\n drafts: Record<string, JsonValue>;\n setDraft(address: string, value: JsonValue): void;\n clearDraft(address: string): void;\n}\n\nexport function persistedDrafts(\n store: DraftsState,\n): Pick<DraftsState, 'drafts'> {\n return { drafts: store.drafts };\n}\n\nconst DRAFTS_STORE = 'stores';\n\n/**\n * Bump when the *envelope* changes — how a draft is keyed or wrapped — and every\n * persisted draft is discarded on the next load.\n *\n * It cannot see a change to one entity's members, because a draft is opaque\n * here: this store holds `JsonValue`, and only the form that wrote one knows\n * which members it should have. That half is `restoreEntityDraft`, which layers\n * a restored draft over a freshly seeded one so a drifted member is dropped or\n * re-seeded rather than rendered.\n */\n// 2: a draft is keyed by the tab address, and that grammar changed with\n// ADR 0042 — so every persisted draft is filed under a key nothing will ask for.\nexport const DRAFTS_VERSION = 2;\n\nconst emptyDrafts: Pick<DraftsState, 'drafts'> = { drafts: {} };\n\n/**\n * Discard, never guess. A draft whose envelope this build does not understand is\n * an unfinished edit, not a record — losing it costs a retype, while migrating\n * it blind risks submitting values whose meaning has changed.\n *\n * Written out rather than left to zustand's default, which discards too but logs\n * `State loaded from storage couldn't be migrated since no migrate function was\n * provided` while doing it — an error message for a decision that was made on\n * purpose.\n */\nexport function migrateDrafts(): Pick<DraftsState, 'drafts'> {\n return emptyDrafts;\n}\n\n/**\n * Restores only the entries that are actually JSON.\n *\n * The type above says what may be written; this is what happens when something\n * else was, by a build that predates the rule or by a `setDraft` reached through\n * a cast. Dropping the offending entry keeps one bad draft from taking the whole\n * workspace's drafts with it — and it is per entry rather than all-or-nothing\n * for that reason.\n */\nexport function mergeDrafts(\n persistedState: unknown,\n currentState: DraftsState,\n): DraftsState {\n const restored = (persistedState as Partial<DraftsState> | undefined)?.drafts;\n if (restored === null || typeof restored !== 'object') return currentState;\n\n const drafts: Record<string, JsonValue> = {};\n for (const [address, value] of Object.entries(restored)) {\n if (isJsonValue(value)) drafts[address] = value;\n }\n return { ...currentState, drafts };\n}\n\nexport const useDraftsState = create<DraftsState>()(\n persist(\n set => ({\n drafts: {},\n setDraft: (address, value) =>\n set(state => ({ drafts: { ...state.drafts, [address]: value } })),\n clearDraft: address =>\n set(state => {\n if (!(address in state.drafts)) return state;\n const { [address]: _removed, ...rest } = state.drafts;\n return { drafts: rest };\n }),\n }),\n {\n // Replaced with a scoped name by `WorkspaceShell` before it rehydrates, so\n // two accounts on one browser profile do not share drafts. This literal is\n // only what an unscoped store would write, and nothing rehydrates without\n // passing through that call.\n name: 'drafts',\n version: DRAFTS_VERSION,\n migrate: migrateDrafts,\n merge: mergeDrafts,\n storage: createJSONStorage(() =>\n makeIndexedDbStateStorage(WORKSPACE_DB, DRAFTS_STORE),\n ),\n partialize: persistedDrafts,\n skipHydration: true,\n },\n ),\n);\n\n/**\n * Bind an editor to its persisted draft. Returns the current draft (restored\n * from IndexedDB on mount), a setter the editor calls as fields change, and a\n * clear for when the edit is committed.\n *\n * `TDraft` is constrained to {@link JsonValue}: an editor that wants to autosave\n * something a JSON round trip would not return is asking for the one thing this\n * store cannot do, and the compile error is the whole point of the constraint.\n *\n * One sharp edge, and the error message does not explain it: **declare a draft\n * type as a `type`, never an `interface`.** TypeScript gives an interface no\n * implicit index signature, so even `interface Draft { name: string }` — as JSON\n * as a value gets — fails the constraint with \"Index signature for type 'string'\n * is missing\". A `type` alias of the same shape passes.\n */\nexport function useDraft<TDraft extends JsonValue>(address: string) {\n const draft = useDraftsState(\n state => state.drafts[address] as TDraft | undefined,\n );\n const set = useDraftsState(state => state.setDraft);\n const clear = useDraftsState(state => state.clearDraft);\n\n useEffect(() => {\n void useDraftsState.persist.rehydrate();\n }, []);\n\n const setDraft = useCallback(\n (value: TDraft) => set(address, value),\n [set, address],\n );\n const clearDraft = useCallback(() => clear(address), [clear, address]);\n\n return { draft, setDraft, clearDraft };\n}\n"],"names":["isJsonValue","useCallback","useEffect","create","createJSONStorage","persist","makeIndexedDbStateStorage","WORKSPACE_DB","persistedDrafts","store","drafts","DRAFTS_STORE","DRAFTS_VERSION","emptyDrafts","migrateDrafts","mergeDrafts","persistedState","currentState","restored","address","value","Object","entries","useDraftsState","set","setDraft","state","clearDraft","_removed","rest","name","version","migrate","merge","storage","partialize","skipHydration","useDraft","draft","clear","rehydrate"],"mappings":"AAAA;;;;AAEA,SAASA,WAAW,QAAwB,gBAAgB;AAC5D,SAASC,WAAW,EAAEC,SAAS,QAAQ,QAAQ;AAC/C,SAASC,MAAM,QAAQ,UAAU;AACjC,SAASC,iBAAiB,EAAEC,OAAO,QAAQ,qBAAqB;AAEhE,SAASC,yBAAyB,QAAQ,yBAAyB;AACnE,SAASC,YAAY,QAAQ,kBAAkB;AAuB/C,OAAO,SAASC,gBACdC,KAAkB;IAElB,OAAO;QAAEC,QAAQD,MAAMC,MAAM;IAAC;AAChC;AAEA,MAAMC,eAAe;AAErB;;;;;;;;;CASC,GACD,wEAAwE;AACxE,iFAAiF;AACjF,OAAO,MAAMC,iBAAiB,EAAE;AAEhC,MAAMC,cAA2C;IAAEH,QAAQ,CAAC;AAAE;AAE9D;;;;;;;;;CASC,GACD,OAAO,SAASI;IACd,OAAOD;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,YACdC,cAAuB,EACvBC,YAAyB;IAEzB,MAAMC,WAAYF,kCAAD,AAACA,eAAqDN,MAAM;IAC7E,IAAIQ,aAAa,QAAQ,OAAOA,aAAa,UAAU,OAAOD;IAE9D,MAAMP,SAAoC,CAAC;IAC3C,KAAK,MAAM,CAACS,SAASC,MAAM,IAAIC,OAAOC,OAAO,CAACJ,UAAW;QACvD,IAAIlB,YAAYoB,QAAQV,MAAM,CAACS,QAAQ,GAAGC;IAC5C;IACA,OAAO,aAAKH;QAAcP;;AAC5B;AAEA,OAAO,MAAMa,iBAAiBpB,SAC5BE,QACEmB,CAAAA,MAAQ,CAAA;QACNd,QAAQ,CAAC;QACTe,UAAU,CAACN,SAASC,QAClBI,IAAIE,CAAAA,QAAU,CAAA;oBAAEhB,QAAQ,aAAKgB,MAAMhB,MAAM;wBAAE,CAACS,QAAQ,EAAEC;;gBAAQ,CAAA;QAChEO,YAAYR,CAAAA,UACVK,IAAIE,CAAAA;gBACF,IAAI,CAAEP,CAAAA,WAAWO,MAAMhB,MAAM,AAAD,GAAI,OAAOgB;gBACvC,MAAyCA,gBAAAA,MAAMhB,MAAM,EAA/C,EAAE,CAACS,QAAQ,EAAES,QAAQ,EAAW,GAAGF,eAATG,wCAASH;qCAAhCP;;gBACT,OAAO;oBAAET,QAAQmB;gBAAK;YACxB;IACJ,CAAA,GACA;IACE,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,6BAA6B;IAC7BC,MAAM;IACNC,SAASnB;IACToB,SAASlB;IACTmB,OAAOlB;IACPmB,SAAS9B,kBAAkB,IACzBE,0BAA0BC,cAAcI;IAE1CwB,YAAY3B;IACZ4B,eAAe;AACjB,IAEF;AAEF;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,SAAmClB,OAAe;IAChE,MAAMmB,QAAQf,eACZG,CAAAA,QAASA,MAAMhB,MAAM,CAACS,QAAQ;IAEhC,MAAMK,MAAMD,eAAeG,CAAAA,QAASA,MAAMD,QAAQ;IAClD,MAAMc,QAAQhB,eAAeG,CAAAA,QAASA,MAAMC,UAAU;IAEtDzB,UAAU;QACR,KAAKqB,eAAelB,OAAO,CAACmC,SAAS;IACvC,GAAG,EAAE;IAEL,MAAMf,WAAWxB,YACf,CAACmB,QAAkBI,IAAIL,SAASC,QAChC;QAACI;QAAKL;KAAQ;IAEhB,MAAMQ,aAAa1B,YAAY,IAAMsC,MAAMpB,UAAU;QAACoB;QAAOpB;KAAQ;IAErE,OAAO;QAAEmB;QAAOb;QAAUE;IAAW;AACvC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Navigation intent, decoupled from the host. A page says "go to this list" or
|
|
4
|
+
* "open this entity"; the host decides how. In a route the destination is a URL
|
|
5
|
+
* push; in the workspace it is a tab open/focus. A page calls {@link useEntityNav}
|
|
6
|
+
* and never learns which host it is in.
|
|
7
|
+
*/
|
|
8
|
+
export interface EntityNav {
|
|
9
|
+
toList(entityKey: string): void;
|
|
10
|
+
toEntity(entityKey: string, id: string): void;
|
|
11
|
+
}
|
|
12
|
+
/** Route host: navigation is a canonical URL push under `/catalog`. */
|
|
13
|
+
export declare function useRouteEntityNav(basePath?: string): EntityNav;
|
|
14
|
+
/** Tab host: navigation opens or focuses a workspace tab. */
|
|
15
|
+
export declare function useTabEntityNav(): EntityNav;
|
|
16
|
+
export declare function EntityNavProvider({ value, children, }: {
|
|
17
|
+
value: EntityNav;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* The active {@link EntityNav}. Falls back to route navigation when no provider
|
|
22
|
+
* is mounted, so a page rendered as a plain route (its default host) navigates
|
|
23
|
+
* by URL without any wiring; the workspace wraps its tabs in a tab-nav provider.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useEntityNav(): EntityNav;
|
|
26
|
+
//# sourceMappingURL=entity-nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-nav.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/entity-nav.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAI3E;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C;AAID,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,QAAQ,SAAa,GAAG,SAAS,CAUlE;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,IAAI,SAAS,CAiB3C;AAED,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAMA;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAIxC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { screenAddress } from '@entifix/authz';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
6
|
+
import { useTabsState } from './tabs-state.js';
|
|
7
|
+
const EntityNavContext = /*#__PURE__*/ createContext(null);
|
|
8
|
+
/** Route host: navigation is a canonical URL push under `/catalog`. */ export function useRouteEntityNav(basePath = '/catalog') {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
return useMemo(()=>({
|
|
11
|
+
toList: (entityKey)=>router.push(`${basePath}/${entityKey}`),
|
|
12
|
+
toEntity: (entityKey, id)=>router.push(`${basePath}/${entityKey}/${id}`)
|
|
13
|
+
}), [
|
|
14
|
+
router,
|
|
15
|
+
basePath
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
/** Tab host: navigation opens or focuses a workspace tab. */ export function useTabEntityNav() {
|
|
19
|
+
const open = useTabsState((state)=>state.open);
|
|
20
|
+
return useMemo(()=>({
|
|
21
|
+
toList: (entityKey)=>open({
|
|
22
|
+
param: screenAddress({
|
|
23
|
+
type: 'master',
|
|
24
|
+
key: entityKey
|
|
25
|
+
}),
|
|
26
|
+
title: entityKey
|
|
27
|
+
}),
|
|
28
|
+
toEntity: (entityKey, id)=>open({
|
|
29
|
+
param: screenAddress({
|
|
30
|
+
type: 'master',
|
|
31
|
+
key: entityKey,
|
|
32
|
+
id
|
|
33
|
+
}),
|
|
34
|
+
title: `${entityKey} #${id}`
|
|
35
|
+
})
|
|
36
|
+
}), [
|
|
37
|
+
open
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
export function EntityNavProvider({ value, children }) {
|
|
41
|
+
return /*#__PURE__*/ _jsx(EntityNavContext.Provider, {
|
|
42
|
+
value: value,
|
|
43
|
+
children: children
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The active {@link EntityNav}. Falls back to route navigation when no provider
|
|
48
|
+
* is mounted, so a page rendered as a plain route (its default host) navigates
|
|
49
|
+
* by URL without any wiring; the workspace wraps its tabs in a tab-nav provider.
|
|
50
|
+
*/ export function useEntityNav() {
|
|
51
|
+
const provided = useContext(EntityNavContext);
|
|
52
|
+
const routeNav = useRouteEntityNav();
|
|
53
|
+
return provided != null ? provided : routeNav;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=entity-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/workspace/entity-nav.tsx"],"sourcesContent":["'use client';\n\nimport { screenAddress } from '@entifix/authz';\nimport { useRouter } from 'next/navigation';\nimport { createContext, type ReactNode, useContext, useMemo } from 'react';\n\nimport { useTabsState } from './tabs-state.js';\n\n/**\n * Navigation intent, decoupled from the host. A page says \"go to this list\" or\n * \"open this entity\"; the host decides how. In a route the destination is a URL\n * push; in the workspace it is a tab open/focus. A page calls {@link useEntityNav}\n * and never learns which host it is in.\n */\nexport interface EntityNav {\n toList(entityKey: string): void;\n toEntity(entityKey: string, id: string): void;\n}\n\nconst EntityNavContext = createContext<EntityNav | null>(null);\n\n/** Route host: navigation is a canonical URL push under `/catalog`. */\nexport function useRouteEntityNav(basePath = '/catalog'): EntityNav {\n const router = useRouter();\n return useMemo(\n () => ({\n toList: entityKey => router.push(`${basePath}/${entityKey}`),\n toEntity: (entityKey, id) =>\n router.push(`${basePath}/${entityKey}/${id}`),\n }),\n [router, basePath],\n );\n}\n\n/** Tab host: navigation opens or focuses a workspace tab. */\nexport function useTabEntityNav(): EntityNav {\n const open = useTabsState(state => state.open);\n return useMemo(\n () => ({\n toList: entityKey =>\n open({\n param: screenAddress({ type: 'master', key: entityKey }),\n title: entityKey,\n }),\n toEntity: (entityKey, id) =>\n open({\n param: screenAddress({ type: 'master', key: entityKey, id }),\n title: `${entityKey} #${id}`,\n }),\n }),\n [open],\n );\n}\n\nexport function EntityNavProvider({\n value,\n children,\n}: {\n value: EntityNav;\n children: ReactNode;\n}) {\n return (\n <EntityNavContext.Provider value={value}>\n {children}\n </EntityNavContext.Provider>\n );\n}\n\n/**\n * The active {@link EntityNav}. Falls back to route navigation when no provider\n * is mounted, so a page rendered as a plain route (its default host) navigates\n * by URL without any wiring; the workspace wraps its tabs in a tab-nav provider.\n */\nexport function useEntityNav(): EntityNav {\n const provided = useContext(EntityNavContext);\n const routeNav = useRouteEntityNav();\n return provided ?? routeNav;\n}\n"],"names":["screenAddress","useRouter","createContext","useContext","useMemo","useTabsState","EntityNavContext","useRouteEntityNav","basePath","router","toList","entityKey","push","toEntity","id","useTabEntityNav","open","state","param","type","key","title","EntityNavProvider","value","children","Provider","useEntityNav","provided","routeNav"],"mappings":"AAAA;;AAEA,SAASA,aAAa,QAAQ,iBAAiB;AAC/C,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,aAAa,EAAkBC,UAAU,EAAEC,OAAO,QAAQ,QAAQ;AAE3E,SAASC,YAAY,QAAQ,kBAAkB;AAa/C,MAAMC,iCAAmBJ,cAAgC;AAEzD,qEAAqE,GACrE,OAAO,SAASK,kBAAkBC,WAAW,UAAU;IACrD,MAAMC,SAASR;IACf,OAAOG,QACL,IAAO,CAAA;YACLM,QAAQC,CAAAA,YAAaF,OAAOG,IAAI,CAAC,GAAGJ,SAAS,CAAC,EAAEG,WAAW;YAC3DE,UAAU,CAACF,WAAWG,KACpBL,OAAOG,IAAI,CAAC,GAAGJ,SAAS,CAAC,EAAEG,UAAU,CAAC,EAAEG,IAAI;QAChD,CAAA,GACA;QAACL;QAAQD;KAAS;AAEtB;AAEA,2DAA2D,GAC3D,OAAO,SAASO;IACd,MAAMC,OAAOX,aAAaY,CAAAA,QAASA,MAAMD,IAAI;IAC7C,OAAOZ,QACL,IAAO,CAAA;YACLM,QAAQC,CAAAA,YACNK,KAAK;oBACHE,OAAOlB,cAAc;wBAAEmB,MAAM;wBAAUC,KAAKT;oBAAU;oBACtDU,OAAOV;gBACT;YACFE,UAAU,CAACF,WAAWG,KACpBE,KAAK;oBACHE,OAAOlB,cAAc;wBAAEmB,MAAM;wBAAUC,KAAKT;wBAAWG;oBAAG;oBAC1DO,OAAO,GAAGV,UAAU,EAAE,EAAEG,IAAI;gBAC9B;QACJ,CAAA,GACA;QAACE;KAAK;AAEV;AAEA,OAAO,SAASM,kBAAkB,EAChCC,KAAK,EACLC,QAAQ,EAIT;IACC,qBACE,KAAClB,iBAAiBmB,QAAQ;QAACF,OAAOA;kBAC/BC;;AAGP;AAEA;;;;CAIC,GACD,OAAO,SAASE;IACd,MAAMC,WAAWxB,WAAWG;IAC5B,MAAMsB,WAAWrB;IACjB,OAAOoB,mBAAAA,WAAYC;AACrB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ScreenType } from '@entifix/authz';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { TabKind } from './tab-kind.js';
|
|
4
|
+
/** One list a workspace may open — the collection half of a screen. */
|
|
5
|
+
export interface EntityTabList {
|
|
6
|
+
/** The tab caption's catalog key. `@entity({ pluralKey })` for a generated screen. */
|
|
7
|
+
readonly titleKey: string;
|
|
8
|
+
render(): ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/** One record a workspace may open — the same screen, with an id. */
|
|
11
|
+
export interface EntityTabRecord {
|
|
12
|
+
/** The tab caption's catalog key. `@entity({ labelKey })` for a generated screen. */
|
|
13
|
+
readonly labelKey: string;
|
|
14
|
+
render(id: string): ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export interface EntityTabScreens {
|
|
17
|
+
readonly lists: Record<string, EntityTabList>;
|
|
18
|
+
readonly records: Record<string, EntityTabRecord>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An entity tab kind: `<type>:<key>` for a list, `<type>:<key>:<id>` for one
|
|
22
|
+
* record.
|
|
23
|
+
*
|
|
24
|
+
* **The list and the record are not different kinds.** They are the same screen
|
|
25
|
+
* with and without a record, which is exactly what the optional id in the
|
|
26
|
+
* payload says — three kinds (`catalog:`, `entity:`, `system:`) collapsed into
|
|
27
|
+
* one for that reason, and the prefix names the *taxonomy* now
|
|
28
|
+
* ([ADR 0042](../../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).
|
|
29
|
+
*
|
|
30
|
+
* A factory over the {@link ScreenType} rather than a constant, and that is the
|
|
31
|
+
* whole of what makes an Operaciones screen addressable. It was written inline
|
|
32
|
+
* in back-office-app as a `masterKind`, so `master:` was the only entity
|
|
33
|
+
* address any host could resolve and `operation:` matched nothing at all — a
|
|
34
|
+
* second copy of this parser beside it is exactly the drift the registry was
|
|
35
|
+
* consolidated to stop, and the two would have had to agree about a grammar
|
|
36
|
+
* neither owns.
|
|
37
|
+
*
|
|
38
|
+
* Which screens a host offers stays the host's decision — a second host
|
|
39
|
+
* mounting the same shells may want a different set — so the screens arrive as
|
|
40
|
+
* an argument. What no longer varies is how one is addressed.
|
|
41
|
+
*
|
|
42
|
+
* `render` takes the id rather than a component, so the caller decides what
|
|
43
|
+
* hosting a record means: in a workspace that is an editor wired to an
|
|
44
|
+
* address-keyed autosaved draft, and the address it keys on has to carry this
|
|
45
|
+
* same `type`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function entityTabKind(type: ScreenType, screens: EntityTabScreens): TabKind<{
|
|
48
|
+
key: string;
|
|
49
|
+
id?: string;
|
|
50
|
+
}>;
|
|
51
|
+
//# sourceMappingURL=entity-tab-kind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-tab-kind.d.ts","sourceRoot":"","sources":["../../../src/lib/workspace/entity-tab-kind.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,MAAM,IAAI,SAAS,CAAC;CACrB;AAED,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BvC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { parseScreenPayload } from '@entifix/authz';
|
|
3
|
+
/**
|
|
4
|
+
* An entity tab kind: `<type>:<key>` for a list, `<type>:<key>:<id>` for one
|
|
5
|
+
* record.
|
|
6
|
+
*
|
|
7
|
+
* **The list and the record are not different kinds.** They are the same screen
|
|
8
|
+
* with and without a record, which is exactly what the optional id in the
|
|
9
|
+
* payload says — three kinds (`catalog:`, `entity:`, `system:`) collapsed into
|
|
10
|
+
* one for that reason, and the prefix names the *taxonomy* now
|
|
11
|
+
* ([ADR 0042](../../../../../../../docs/adr/0042-the-workspace-address-is-the-taxonomy-serialized.md)).
|
|
12
|
+
*
|
|
13
|
+
* A factory over the {@link ScreenType} rather than a constant, and that is the
|
|
14
|
+
* whole of what makes an Operaciones screen addressable. It was written inline
|
|
15
|
+
* in back-office-app as a `masterKind`, so `master:` was the only entity
|
|
16
|
+
* address any host could resolve and `operation:` matched nothing at all — a
|
|
17
|
+
* second copy of this parser beside it is exactly the drift the registry was
|
|
18
|
+
* consolidated to stop, and the two would have had to agree about a grammar
|
|
19
|
+
* neither owns.
|
|
20
|
+
*
|
|
21
|
+
* Which screens a host offers stays the host's decision — a second host
|
|
22
|
+
* mounting the same shells may want a different set — so the screens arrive as
|
|
23
|
+
* an argument. What no longer varies is how one is addressed.
|
|
24
|
+
*
|
|
25
|
+
* `render` takes the id rather than a component, so the caller decides what
|
|
26
|
+
* hosting a record means: in a workspace that is an editor wired to an
|
|
27
|
+
* address-keyed autosaved draft, and the address it keys on has to carry this
|
|
28
|
+
* same `type`.
|
|
29
|
+
*/ export function entityTabKind(type, screens) {
|
|
30
|
+
return {
|
|
31
|
+
kind: type,
|
|
32
|
+
match: (payload)=>{
|
|
33
|
+
const parsed = parseScreenPayload(payload);
|
|
34
|
+
if (parsed === null) return null;
|
|
35
|
+
// A key is valid for the half it names: a record address for a screen
|
|
36
|
+
// with a list and no record editor resolves to nothing, and the workspace
|
|
37
|
+
// answers with its fallback rather than rendering an empty editor.
|
|
38
|
+
const known = parsed.id === undefined ? parsed.key in screens.lists : parsed.key in screens.records;
|
|
39
|
+
return known ? parsed : null;
|
|
40
|
+
},
|
|
41
|
+
toParam: (addr)=>addr.id === undefined ? addr.key : `${addr.key}:${addr.id}`,
|
|
42
|
+
title: (addr, translate)=>addr.id === undefined ? translate(screens.lists[addr.key].titleKey) : `${translate(screens.records[addr.key].labelKey)} #${addr.id}`,
|
|
43
|
+
render: (addr)=>addr.id === undefined ? screens.lists[addr.key].render() : screens.records[addr.key].render(addr.id)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=entity-tab-kind.js.map
|