@phis/ui 0.4.1 → 0.4.3
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/components/cms/phi-cms-layout-renderer.js +4 -3
- package/components/cms/phi-cms-page-render-scope.d.ts +4 -5
- package/components/cms/phi-cms-page-render-scope.js +43 -22
- package/components/cms/phi-cms-render-diagnostic-client.js +7 -3
- package/components/cms/phi-cms-render-error-boundary.js +2 -1
- package/components/controls/phi-bullet-list-control.js +31 -2
- package/components/controls/phi-ribbon-control.d.ts +29 -0
- package/components/controls/phi-ribbon-control.js +53 -0
- package/components/controls/phi-table-control.d.ts +3 -0
- package/components/controls/phi-table-control.js +13 -1
- package/components/controls/phi-tree-control.js +3 -3
- package/components/forms/shared-form-provider-registry.js +1 -4
- package/components/media/asset-collection-runtime.js +6 -16
- package/components/media/media-font-metrics.d.ts +3 -0
- package/components/media/media-font-metrics.js +43 -0
- package/components/media/phi-area-upload-widget.js +53 -97
- package/components/media/phi-image-preview-data.d.ts +13 -0
- package/components/media/phi-image-preview-data.js +184 -9
- package/components/media/phi-media-picker-binding.js +16 -13
- package/components/media/phi-media-upload.d.ts +15 -8
- package/components/media/phi-media-upload.js +18 -9
- package/components/media/phi-site-font-assets.js +7 -8
- package/components/overlays/phi-overlay-container-client.js +16 -1
- package/components/regions/presets/phi-settings-page-shell-tree.js +48 -1
- package/components/runtime/runtime-controller-materialization.js +12 -1
- package/components/state/scoped-state-store.js +20 -3
- package/components/tables/client/phi-table-binding.d.ts +2 -1
- package/components/tables/client/phi-table-binding.js +45 -21
- package/components/trees/client/phi-tree-binding.js +19 -11
- package/components/widgets/client/bullet-list-entries-editor.d.ts +26 -0
- package/components/widgets/client/bullet-list-entries-editor.js +103 -0
- package/components/widgets/config/choice-shared.js +12 -13
- package/components/widgets/config/command-buttons.d.ts +5 -1
- package/components/widgets/config/command-buttons.js +38 -81
- package/components/widgets/config/control-signal-config.d.ts +3 -1
- package/components/widgets/config/control-signal-config.js +19 -17
- package/components/widgets/config/image-source-parser.d.ts +1 -0
- package/components/widgets/config/image-source-parser.js +7 -8
- package/components/widgets/config/mask.d.ts +6 -1
- package/components/widgets/config/mask.js +24 -1
- package/components/widgets/config/parser-primitives.js +72 -0
- package/components/widgets/config/stack-tabs.d.ts +1 -1
- package/components/widgets/config/stack-tabs.js +2 -3
- package/components/widgets/config/strict-config-reader.d.ts +40 -0
- package/components/widgets/config/strict-config-reader.js +174 -0
- package/components/widgets/label-sets/effects.js +2 -0
- package/components/widgets/label-types/effects.d.ts +1 -0
- package/components/widgets/label-types/effects.js +1 -0
- package/components/widgets/navigation-descriptor-resolver.server.js +20 -8
- package/components/widgets/shared/bullet-list-vocabulary.d.ts +17 -1
- package/components/widgets/shared/bullet-list-vocabulary.js +19 -2
- package/components/widgets/shared/card-body-client.d.ts +16 -4
- package/components/widgets/shared/card-body-client.js +81 -24
- package/components/widgets/shared/card-vocabulary.d.ts +21 -4
- package/components/widgets/shared/card-vocabulary.js +28 -2
- package/components/widgets/shared/markdown-body-client.d.ts +2 -1
- package/components/widgets/shared/markdown-body-client.js +3 -1
- package/components/widgets/shared/markdown-render.js +8 -1
- package/components/widgets/signals/control-signal-capabilities.d.ts +0 -18
- package/components/widgets/signals/control-signal-capabilities.js +10 -22
- package/components/workspace/catalog-store.d.ts +4 -3
- package/components/workspace/catalog-store.js +6 -5
- package/gateway/dashboard-cards-route.js +1 -0
- package/gateway/internal-references.d.ts +6 -1
- package/gateway/internal-references.js +48 -23
- package/gateway/site-cms-write-validation.d.ts +9 -0
- package/gateway/site-cms-write-validation.js +78 -0
- package/gateway/site-proxy.js +19 -3
- package/helpers/renderable-block-serialization.js +1 -1
- package/next/config.d.mts +3 -0
- package/next/config.mjs +28 -1
- package/package.json +2 -2
- package/plugins/runtime/phi-slot-child-viewport-effects-observer.js +5 -1
- package/plugins/runtime-modules/accounting/presets.d.ts +9 -0
- package/plugins/runtime-modules/accounting/presets.js +37 -0
- package/plugins/runtime-modules/accounting/server.d.ts +1 -4
- package/plugins/runtime-modules/accounting/server.js +4 -7
- package/plugins/runtime-modules/app/presets.d.ts +18 -0
- package/plugins/runtime-modules/app/presets.js +67 -0
- package/plugins/runtime-modules/app/server.d.ts +1 -4
- package/plugins/runtime-modules/app/server.js +4 -7
- package/plugins/runtime-modules/asset/controller/runtime.js +2 -2
- package/plugins/runtime-modules/asset/services/collection.js +9 -1
- package/plugins/runtime-modules/asset/services/options.js +2 -2
- package/plugins/runtime-modules/builder/area-settings-answer.js +2 -1
- package/plugins/runtime-modules/builder/authoring-canvas.js +6 -6
- package/plugins/runtime-modules/builder/cards.js +3 -5
- package/plugins/runtime-modules/builder/clients/builder-region-overrides.js +3 -3
- package/plugins/runtime-modules/builder/clients/canvas-widget.js +15 -54
- package/plugins/runtime-modules/builder/clients/inspector-config-field.d.ts +4 -1
- package/plugins/runtime-modules/builder/clients/inspector-config-field.js +5 -34
- package/plugins/runtime-modules/builder/clients/inspector-link-target-field.js +1 -1
- package/plugins/runtime-modules/builder/clients/inspector-section-widget.js +8 -8
- package/plugins/runtime-modules/builder/clients/inspector-title.js +6 -6
- package/plugins/runtime-modules/builder/clients/layout-inspector.d.ts +1 -1
- package/plugins/runtime-modules/builder/clients/layout-inspector.js +11 -25
- package/plugins/runtime-modules/builder/clients/mode-switch.js +1 -1
- package/plugins/runtime-modules/builder/clients/pages-header.js +11 -11
- package/plugins/runtime-modules/builder/clients/pages-workspace.js +1 -1
- package/plugins/runtime-modules/builder/clients/region-visibility-fields.js +1 -1
- package/plugins/runtime-modules/builder/clients/runtime-module-authoring-boundary.js +1 -1
- package/plugins/runtime-modules/builder/clients/shells-workspace.js +15 -31
- package/plugins/runtime-modules/builder/clients/structure-canvas.js +1 -1
- package/plugins/runtime-modules/builder/clients/use-region-draft-hydration.d.ts +26 -0
- package/plugins/runtime-modules/builder/clients/use-region-draft-hydration.js +36 -0
- package/plugins/runtime-modules/builder/clients/widget-effects-editor.js +1 -1
- package/plugins/runtime-modules/builder/clients/widget-inspector.js +1 -0
- package/plugins/runtime-modules/builder/clients/workspace-scope-boundary.js +2 -2
- package/plugins/runtime-modules/builder/cms-instance-allocator.js +8 -28
- package/plugins/runtime-modules/builder/controller/client.d.ts +0 -15
- package/plugins/runtime-modules/builder/controller/definition.d.ts +0 -15
- package/plugins/runtime-modules/builder/controller/definition.js +0 -8
- package/plugins/runtime-modules/builder/controller/draft-command-controller.d.ts +1 -2
- package/plugins/runtime-modules/builder/controller/draft-command-controller.js +11 -30
- package/plugins/runtime-modules/builder/controller/draft-status-controller.js +15 -2
- package/plugins/runtime-modules/builder/controller/inspector-controller.d.ts +1 -2
- package/plugins/runtime-modules/builder/controller/inspector-controller.js +4 -7
- package/plugins/runtime-modules/builder/controller/page-controller.d.ts +1 -2
- package/plugins/runtime-modules/builder/controller/page-controller.js +16 -68
- package/plugins/runtime-modules/builder/controller/server-definition.d.ts +0 -15
- package/plugins/runtime-modules/builder/controller/signal-wiring-controller.d.ts +2 -5
- package/plugins/runtime-modules/builder/controller/signal-wiring-controller.js +6 -50
- package/plugins/runtime-modules/builder/controller/workspace-controller.d.ts +2 -3
- package/plugins/runtime-modules/builder/controller/workspace-controller.js +89 -153
- package/plugins/runtime-modules/builder/data-providers.d.ts +1 -0
- package/plugins/runtime-modules/builder/data-providers.js +1 -0
- package/plugins/runtime-modules/builder/definition.d.ts +1 -0
- package/plugins/runtime-modules/builder/developer-workspace-store.d.ts +64 -30
- package/plugins/runtime-modules/builder/developer-workspace-store.js +255 -61
- package/plugins/runtime-modules/builder/edit-scaffold-drawer.js +1 -1
- package/plugins/runtime-modules/builder/history.d.ts +3 -1
- package/plugins/runtime-modules/builder/options-providers.js +3 -2
- package/plugins/runtime-modules/builder/page-catalog-client.js +19 -7
- package/plugins/runtime-modules/builder/page-meta-form.js +1 -0
- package/plugins/runtime-modules/builder/page-references-provider.js +1 -1
- package/plugins/runtime-modules/builder/persistence.js +33 -69
- package/plugins/runtime-modules/builder/public-route-collisions.d.ts +2 -2
- package/plugins/runtime-modules/builder/public-route-collisions.js +6 -19
- package/plugins/runtime-modules/builder/render-root-node-preview.server.js +20 -3
- package/plugins/runtime-modules/builder/render-root-node-scaffold.js +3 -3
- package/plugins/runtime-modules/builder/runtime-module-selection.d.ts +2 -12
- package/plugins/runtime-modules/builder/runtime-module-selection.js +16 -42
- package/plugins/runtime-modules/builder/services/navigation-table.js +9 -15
- package/plugins/runtime-modules/builder/services/page-source-tree.js +8 -8
- package/plugins/runtime-modules/builder/services/runtime-modules-table.js +10 -10
- package/plugins/runtime-modules/builder/services/signal-routes-table.js +3 -3
- package/plugins/runtime-modules/builder/signal-route-selection.d.ts +1 -2
- package/plugins/runtime-modules/builder/signal-route-selection.js +2 -2
- package/plugins/runtime-modules/builder/signal-wiring-options.js +3 -2
- package/plugins/runtime-modules/builder/structure-dnd.js +8 -61
- package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +13 -10
- package/plugins/runtime-modules/builder/widgets/structure-region/built-in.js +12 -18
- package/plugins/runtime-modules/builder/workspace-catalog-actions.d.ts +55 -0
- package/plugins/runtime-modules/builder/workspace-catalog-actions.js +142 -0
- package/plugins/runtime-modules/client-manifests/common.js +1 -1
- package/plugins/runtime-modules/{runtime-calendar-adapters.js → core/calendar-adapters.js} +2 -2
- package/plugins/runtime-modules/{client-calendar-adapters/core.js → core/client-calendar-adapters.js} +1 -1
- package/plugins/runtime-modules/core/content-entries/table.js +1 -1
- package/plugins/runtime-modules/core/data-providers.d.ts +3 -0
- package/plugins/runtime-modules/core/data-providers.js +2 -1
- package/plugins/runtime-modules/core/definition.d.ts +3 -0
- package/plugins/runtime-modules/core/definition.js +1 -1
- package/plugins/runtime-modules/core/messaging/data-providers.d.ts +1 -0
- package/plugins/runtime-modules/core/messaging/data-providers.js +1 -0
- package/plugins/runtime-modules/core/messaging/thread-inbox-table.js +1 -1
- package/plugins/runtime-modules/core/server.js +1 -1
- package/plugins/runtime-modules/core/themes.d.ts +44 -0
- package/plugins/runtime-modules/core/themes.js +12 -0
- package/plugins/runtime-modules/core/widgets/card/collection-view-binding.js +25 -9
- package/plugins/runtime-modules/core/widgets/card/config.d.ts +296 -254
- package/plugins/runtime-modules/core/widgets/card/config.js +52 -3
- package/plugins/runtime-modules/core/widgets/card/editor.js +30 -3
- package/plugins/runtime-modules/core/widgets/card/server.d.ts +3 -0
- package/plugins/runtime-modules/core/widgets/card/server.js +31 -3
- package/plugins/runtime-modules/core/widgets/collection-view/asset-binding.js +7 -6
- package/plugins/runtime-modules/core/widgets/collection-view/config.js +9 -2
- package/plugins/runtime-modules/core/widgets/description/config.d.ts +1 -25
- package/plugins/runtime-modules/core/widgets/description/editor.js +4 -61
- package/plugins/runtime-modules/core/widgets/entry-list/config.d.ts +1 -25
- package/plugins/runtime-modules/core/widgets/gallery/config.d.ts +1 -25
- package/plugins/runtime-modules/core/widgets/image/config.js +2 -2
- package/plugins/runtime-modules/core/widgets/markdown/config.d.ts +3 -1
- package/plugins/runtime-modules/core/widgets/markdown/config.js +14 -0
- package/plugins/runtime-modules/core/widgets/markdown/editor.js +2 -3
- package/plugins/runtime-modules/core/widgets/result/config.d.ts +4 -25
- package/plugins/runtime-modules/core/widgets/result/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/result/server.js +25 -19
- package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +1 -25
- package/plugins/runtime-modules/core/widgets/slot-upload/client.js +11 -2
- package/plugins/runtime-modules/core/widgets/table/client.js +8 -4
- package/plugins/runtime-modules/core/widgets/table/config.d.ts +3 -83
- package/plugins/runtime-modules/core/widgets/table/config.js +302 -452
- package/plugins/runtime-modules/core/widgets/tree/config.js +5 -2
- package/plugins/runtime-modules/dashboard/card-rows.d.ts +5 -0
- package/plugins/runtime-modules/dashboard/card-rows.js +59 -0
- package/plugins/runtime-modules/dashboard/clients/dashboard-card-view.js +4 -6
- package/plugins/runtime-modules/dashboard/services/collection.js +17 -7
- package/plugins/runtime-modules/groups/data-providers.d.ts +2 -0
- package/plugins/runtime-modules/groups/data-providers.js +3 -3
- package/plugins/runtime-modules/groups/definition.d.ts +2 -0
- package/plugins/runtime-modules/localization/data-providers.d.ts +3 -0
- package/plugins/runtime-modules/localization/data-providers.js +2 -0
- package/plugins/runtime-modules/localization/definition.d.ts +3 -0
- package/plugins/runtime-modules/localization/services/table.js +3 -7
- package/plugins/runtime-modules/observability/data-providers.d.ts +1 -0
- package/plugins/runtime-modules/observability/data-providers.js +1 -0
- package/plugins/runtime-modules/observability/definition.d.ts +1 -0
- package/plugins/runtime-modules/observability/module.d.ts +1 -0
- package/plugins/runtime-modules/observability/services/table.js +1 -1
- package/plugins/runtime-modules/phi-default-site-area-preset-tree.js +2 -105
- package/plugins/runtime-modules/public/presets.d.ts +115 -0
- package/plugins/runtime-modules/{area-base-presets.js → public/presets.js} +19 -119
- package/plugins/runtime-modules/public/server.d.ts +1 -4
- package/plugins/runtime-modules/public/server.js +4 -7
- package/plugins/runtime-modules/public/trees/phi-default-pub-error-page-tree.js +7 -8
- package/plugins/runtime-modules/public-base-page-references.d.ts +1 -1
- package/plugins/runtime-modules/public-base-page-references.js +1 -1
- package/plugins/runtime-modules/revisions/controller/client.js +11 -11
- package/plugins/runtime-modules/revisions/services/table.js +3 -3
- package/plugins/runtime-modules/theme/trees/phi-default-builder-theme-page-tree.js +22 -0
- package/plugins/runtime-modules/theme/widgets/brand-controls/client.d.ts +2 -1
- package/plugins/runtime-modules/theme/widgets/brand-controls/client.js +93 -58
- package/plugins/runtime-modules/theme/widgets/brand-controls/config.d.ts +2 -1
- package/plugins/runtime-modules/theme/widgets/brand-controls/config.js +23 -22
- package/plugins/runtime-modules/theme/widgets/brand-controls/plugin.js +1 -1
- package/plugins/runtime-modules/theme-descriptors.js +1 -1
- package/plugins/runtime-modules/user-management/data-providers.d.ts +2 -0
- package/plugins/runtime-modules/user-management/data-providers.js +1 -0
- package/plugins/runtime-modules/user-management/definition.d.ts +2 -0
- package/styles/control-shape.css +11 -11
- package/styles/controls.css +29 -3
- package/styles/layout.css +16 -7
- package/types/cms-config.js +142 -185
- package/types/cms-plugins.d.ts +4 -11
- package/types/core-widget-placements.d.ts +17 -3
- package/types/dashboard-cards.d.ts +9 -0
- package/types/media.d.ts +0 -3
- package/types/renderable-block.d.ts +6 -1
- package/types/table-widget.d.ts +9 -0
- package/types/table-widget.js +20 -0
- package/types.d.ts +1 -1
- package/widget-config.d.ts +2 -0
- package/widget-config.js +6 -0
- package/plugins/runtime-modules/area-base-presets.d.ts +0 -139
- /package/plugins/runtime-modules/{runtime-calendar-adapters.d.ts → core/calendar-adapters.d.ts} +0 -0
- /package/plugins/runtime-modules/{client-calendar-adapters/core.d.ts → core/client-calendar-adapters.d.ts} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { isPhiRecord } from "../../helpers/is-record";
|
|
3
2
|
import { useEffect, useMemo, useState } from "react";
|
|
4
3
|
import { PhiMediaKind } from "../../constants/media";
|
|
5
4
|
import { createPhiAssetUri } from "../../types/references";
|
|
6
5
|
import { usePhiCollectionProvider } from "../widgets/client/shared/phi-collection-provider";
|
|
7
6
|
import { PHI_ASSET_COLLECTION_DATA_SOURCE } from "./asset-collection-runtime";
|
|
7
|
+
import { readPhiFontMetricsFields } from "./media-font-metrics";
|
|
8
|
+
import { readPhiMediaAssetTiles } from "./phi-image-preview-data";
|
|
9
|
+
import { createPhiResponseReader } from "../widgets/config/strict-config-reader";
|
|
8
10
|
/**
|
|
9
11
|
* The typefaces a Site owns, as something a font slot can be set to.
|
|
10
12
|
*
|
|
@@ -19,12 +21,9 @@ import { PHI_ASSET_COLLECTION_DATA_SOURCE } from "./asset-collection-runtime";
|
|
|
19
21
|
/** Enough for any Site's own lettering; a ceiling rather than a page size. */
|
|
20
22
|
const PHI_SITE_FONT_ASSET_LIMIT = 100;
|
|
21
23
|
function readFontAssetMetrics(asset) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const record = meta;
|
|
26
|
-
// The management tile carries `meta` as stored; only a shape with its unit grid is worth drawing from.
|
|
27
|
-
return typeof record.unitsPerEm === "number" && record.unitsPerEm > 0 ? record : null;
|
|
24
|
+
// The management tile carries `meta` as stored, and the server stored every field of the metrics.
|
|
25
|
+
return createPhiResponseReader(asset.meta ?? {}, `The stored metrics of Asset ${asset.id}`, "meta")
|
|
26
|
+
.object("font", readPhiFontMetricsFields) ?? null;
|
|
28
27
|
}
|
|
29
28
|
function readFontAssetLabel(asset) {
|
|
30
29
|
const family = readFontAssetMetrics(asset)?.familyName;
|
|
@@ -68,7 +67,7 @@ export function usePhiSiteFontAssets(enabled = true) {
|
|
|
68
67
|
setError(data.error);
|
|
69
68
|
return;
|
|
70
69
|
}
|
|
71
|
-
setAssets(data.items
|
|
70
|
+
setAssets(readPhiMediaAssetTiles(data.items));
|
|
72
71
|
}).catch((cause) => {
|
|
73
72
|
if (abortController.signal.aborted)
|
|
74
73
|
return;
|
|
@@ -60,6 +60,13 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
|
|
|
60
60
|
const [runtimeControlSizeOverride, setRuntimeControlSizeOverride] = useState(null);
|
|
61
61
|
const [runtimeSizeOverride, setRuntimeSizeOverride] = useState(null);
|
|
62
62
|
const [runtimeTitleOverride, setRuntimeTitleOverride] = useState(null);
|
|
63
|
+
/*
|
|
64
|
+
* Whether work the Overlay holds is under way -- a Form in the Body saving. While it is, a reader's
|
|
65
|
+
* dismissal (close button, mask, Escape) is not taken: closing would abandon the save's answer. A
|
|
66
|
+
* `close` sent on purpose still closes, which is how a Form that went through ends its dialog. Told
|
|
67
|
+
* through the `busy` input by whoever knows, so a dialog needs no Controller to refuse a close.
|
|
68
|
+
*/
|
|
69
|
+
const [busy, setBusy] = useState(false);
|
|
63
70
|
const runtimeControlSize = runtimeControlSizeOverride?.config === config
|
|
64
71
|
? runtimeControlSizeOverride.value
|
|
65
72
|
: null;
|
|
@@ -106,6 +113,9 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
|
|
|
106
113
|
// A failed load is forgotten on close, so the next open asks the server again.
|
|
107
114
|
if (!nextOpen)
|
|
108
115
|
setLoadedZones((current) => current && "failure" in current ? null : current);
|
|
116
|
+
// A closed Overlay holds no work; a busy state left behind would refuse the next dismissal.
|
|
117
|
+
if (!nextOpen)
|
|
118
|
+
setBusy(false);
|
|
109
119
|
setOpen(nextOpen);
|
|
110
120
|
}, []);
|
|
111
121
|
/*
|
|
@@ -200,6 +210,9 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
|
|
|
200
210
|
else if (route.capabilityId === "close") {
|
|
201
211
|
updateOpen(false);
|
|
202
212
|
}
|
|
213
|
+
else if (route.capabilityId === "busy" && signal.valueType === "boolean") {
|
|
214
|
+
setBusy(signal.value === true);
|
|
215
|
+
}
|
|
203
216
|
else if (route.capabilityId === "toggle") {
|
|
204
217
|
updateOpen(typeof signal.value === "boolean" ? signal.value : !open);
|
|
205
218
|
}
|
|
@@ -229,12 +242,14 @@ export function PhiOverlayContainerClient({ overlayId, overlayType, config: rawC
|
|
|
229
242
|
receiver,
|
|
230
243
|
}), [receiver, signalScope]), receiver);
|
|
231
244
|
const requestClose = useCallback((source) => {
|
|
245
|
+
if (busy)
|
|
246
|
+
return;
|
|
232
247
|
if (config.closeMode === "request") {
|
|
233
248
|
emitCapability("closeRequest", { source });
|
|
234
249
|
return;
|
|
235
250
|
}
|
|
236
251
|
updateOpen(false);
|
|
237
|
-
}, [config.closeMode, emitCapability, updateOpen]);
|
|
252
|
+
}, [busy, config.closeMode, emitCapability, updateOpen]);
|
|
238
253
|
// An Overlay's window is simply "open", which is why it can share the rule with a Carousel whose
|
|
239
254
|
// window is several slots wide.
|
|
240
255
|
const shouldRenderContent = shouldPhiCmsContentStayMounted({
|
|
@@ -4,7 +4,7 @@ import { createPhiCmsPresetNodes } from "../../../helpers/cms-preset-nodes";
|
|
|
4
4
|
import { PHI_SPACE } from "../../../theme/antd-css-var-contract";
|
|
5
5
|
import { PHI_LAYOUT } from "../../../theme/phi-tokens";
|
|
6
6
|
import { createPhiPresetCmsInstanceIdMap } from "../../../types/cms-instance-id";
|
|
7
|
-
import { PHI_SIGNAL_VALUE_SCHEMAS, createPhiSignalAddress } from "../../../types/signals";
|
|
7
|
+
import { PHI_SIGNAL_VALUE_SCHEMAS, createPhiSignalAddress, createPhiSignalSubcontrolAddress, } from "../../../types/signals";
|
|
8
8
|
import { buildPhiBasePageContentScaffold, PHI_BASE_PAGE_LAYOUT_NODE_ID } from "./phi-base-page-layout";
|
|
9
9
|
import { PHI_DIALOG_BODY_CONFIG, PHI_DIALOG_COMMANDS_CONFIG } from "../../../helpers/cms-dialog";
|
|
10
10
|
/**
|
|
@@ -252,6 +252,29 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
|
|
|
252
252
|
valueType: "none",
|
|
253
253
|
receiver: overlayAddress,
|
|
254
254
|
},
|
|
255
|
+
/*
|
|
256
|
+
* While the save is under way the dialog refuses a reader's dismissal, and the Save
|
|
257
|
+
* button shows that it is working -- the two things a Controller would otherwise be
|
|
258
|
+
* mounted for (OVERLAYS.md, "Runtime and signaling").
|
|
259
|
+
*/
|
|
260
|
+
{
|
|
261
|
+
routeKey: `${overlay.nodeKey}-busy`,
|
|
262
|
+
capabilityId: "submitting",
|
|
263
|
+
scope: "page",
|
|
264
|
+
channel: "busy",
|
|
265
|
+
action: "change",
|
|
266
|
+
valueType: "boolean",
|
|
267
|
+
receiver: overlayAddress,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
routeKey: `${overlay.nodeKey}-submit-loading`,
|
|
271
|
+
capabilityId: "submitting",
|
|
272
|
+
scope: "page",
|
|
273
|
+
channel: "loading",
|
|
274
|
+
action: "change",
|
|
275
|
+
valueType: "boolean",
|
|
276
|
+
receiver: createPhiSignalSubcontrolAddress("cms", widgets[`${overlay.nodeKey}Commands`], "submit"),
|
|
277
|
+
},
|
|
255
278
|
...(extraSignalRoutes?.emits ?? []),
|
|
256
279
|
],
|
|
257
280
|
listens: [
|
|
@@ -315,6 +338,17 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
|
|
|
315
338
|
receiver: formAddress,
|
|
316
339
|
},
|
|
317
340
|
],
|
|
341
|
+
listens: [
|
|
342
|
+
{
|
|
343
|
+
routeKey: `${overlay.nodeKey}-commands-submit-loading`,
|
|
344
|
+
capabilityId: "loading",
|
|
345
|
+
scope: "page",
|
|
346
|
+
channel: "loading",
|
|
347
|
+
action: "change",
|
|
348
|
+
valueType: "boolean",
|
|
349
|
+
receiver: createPhiSignalSubcontrolAddress("cms", widgets[`${overlay.nodeKey}Commands`], "submit"),
|
|
350
|
+
},
|
|
351
|
+
],
|
|
318
352
|
},
|
|
319
353
|
},
|
|
320
354
|
}),
|
|
@@ -344,6 +378,10 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
|
|
|
344
378
|
* the provider somebody looked at before.
|
|
345
379
|
*/
|
|
346
380
|
mountPolicy: "remount",
|
|
381
|
+
/*
|
|
382
|
+
* `immediate`, because nothing is mounted to answer a close request; while the Form saves, its
|
|
383
|
+
* `submitting` route sets this Overlay `busy`, and a busy Overlay does not take a dismissal.
|
|
384
|
+
*/
|
|
347
385
|
closeMode: "immediate",
|
|
348
386
|
openActionKey: overlay.openActionKey,
|
|
349
387
|
signalRoutes: {
|
|
@@ -371,6 +409,15 @@ export function buildPhiSettingsPageShellTree({ page, ownerModuleId, presetKey,
|
|
|
371
409
|
valueType: "none",
|
|
372
410
|
receiver: createPhiSignalAddress("cms", overlayIds[overlay.nodeKey]),
|
|
373
411
|
},
|
|
412
|
+
{
|
|
413
|
+
routeKey: `${overlay.nodeKey}-busy`,
|
|
414
|
+
capabilityId: "busy",
|
|
415
|
+
scope: "page",
|
|
416
|
+
channel: "busy",
|
|
417
|
+
action: "change",
|
|
418
|
+
valueType: "boolean",
|
|
419
|
+
receiver: createPhiSignalAddress("cms", overlayIds[overlay.nodeKey]),
|
|
420
|
+
},
|
|
374
421
|
],
|
|
375
422
|
},
|
|
376
423
|
},
|
|
@@ -39,7 +39,18 @@ export function materializePhiWidgetRuntimeControllerSettings({ widget, tree, ow
|
|
|
39
39
|
if (!plugin.requiredRuntimeControllers) {
|
|
40
40
|
return [];
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
/*
|
|
43
|
+
* A Widget whose config cannot be read asks for no Controller. It is not rendered either: the renderer
|
|
44
|
+
* meets the same error in the Widget's own place, leaves it out of a live page and shows it in the
|
|
45
|
+
* Builder. Thrown here, outside any block, it took the whole page with it.
|
|
46
|
+
*/
|
|
47
|
+
let config;
|
|
48
|
+
try {
|
|
49
|
+
config = plugin.parseConfig(widget.config);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
43
54
|
const settingsByKey = new Map();
|
|
44
55
|
for (const requirement of plugin.requiredRuntimeControllers({ widget, tree, config })) {
|
|
45
56
|
const setting = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
const scopedStateStoreKey = "__phiScopedStateStores__";
|
|
4
4
|
function normalizeScopedStateKey(scopeKey) {
|
|
5
5
|
return scopeKey.trim() || "default";
|
|
@@ -92,7 +92,12 @@ export function createScopedStateStore(storeId, createDefaultState) {
|
|
|
92
92
|
}
|
|
93
93
|
function useStore(scopeKey) {
|
|
94
94
|
const normalizedScopeKey = normalizeScopedStateKey(scopeKey);
|
|
95
|
-
|
|
95
|
+
/*
|
|
96
|
+
* Held per scope. An inline `subscribe` is a new function every render, and `useSyncExternalStore`
|
|
97
|
+
* unsubscribes and subscribes again for each new one.
|
|
98
|
+
*/
|
|
99
|
+
const subscribeToScope = useCallback((listener) => subscribe(normalizedScopeKey, listener), [normalizedScopeKey]);
|
|
100
|
+
return useSyncExternalStore(subscribeToScope, () => getSnapshot(normalizedScopeKey), () => getHydrationSnapshot(normalizedScopeKey));
|
|
96
101
|
}
|
|
97
102
|
function useStoreSelector(scopeKey, selector) {
|
|
98
103
|
const normalizedScopeKey = normalizeScopedStateKey(scopeKey);
|
|
@@ -124,14 +129,26 @@ export function createScopedStateStore(storeId, createDefaultState) {
|
|
|
124
129
|
selectedHydrationSnapshotRef.current = { source, selector, selected };
|
|
125
130
|
return selected;
|
|
126
131
|
};
|
|
127
|
-
|
|
132
|
+
const subscribeToScope = useCallback((listener) => subscribe(normalizedScopeKey, listener), [normalizedScopeKey]);
|
|
133
|
+
return useSyncExternalStore(subscribeToScope, getSelectedSnapshot, getSelectedHydrationSnapshot);
|
|
128
134
|
}
|
|
129
135
|
function reset(scopeKey) {
|
|
130
136
|
patch(scopeKey, () => createDefaultState(normalizeScopedStateKey(scopeKey)));
|
|
131
137
|
}
|
|
138
|
+
/*
|
|
139
|
+
* Forgets a scope's state. A component still mounted on it keeps its subscription on the entry it
|
|
140
|
+
* subscribed to, so dropping that entry left it listening to nothing: the next write went into a new
|
|
141
|
+
* entry nobody was told about. With listeners, the entry stays and starts over from the default
|
|
142
|
+
* instead, and they hear it.
|
|
143
|
+
*/
|
|
132
144
|
function deleteScope(scopeKey) {
|
|
133
145
|
const normalizedScopeKey = normalizeScopedStateKey(scopeKey);
|
|
134
146
|
const bucket = getScopedBucket();
|
|
147
|
+
const entry = bucket.get(normalizedScopeKey);
|
|
148
|
+
if (entry && entry.listeners.size > 0) {
|
|
149
|
+
replace(normalizedScopeKey, createDefaultState(normalizedScopeKey));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
135
152
|
bucket.delete(normalizedScopeKey);
|
|
136
153
|
}
|
|
137
154
|
return {
|
|
@@ -22,7 +22,8 @@ export declare function usePhiTableBinding({ source, initialQuery, externalQuery
|
|
|
22
22
|
resource: PhiTableProviderResourceDescriptor | null;
|
|
23
23
|
bindingError: string | null;
|
|
24
24
|
rows: readonly TableRow[];
|
|
25
|
-
total: number;
|
|
25
|
+
total: number | null;
|
|
26
|
+
hasMore: boolean;
|
|
26
27
|
summary: Readonly<Record<string, PhiTableSummaryValue>>;
|
|
27
28
|
contractError: string | null;
|
|
28
29
|
loading: boolean;
|
|
@@ -76,7 +76,14 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
76
76
|
expandedRowIdentities: initialQuery.expandedRowIdentities,
|
|
77
77
|
}));
|
|
78
78
|
const [rows, setRows] = useState([]);
|
|
79
|
-
|
|
79
|
+
/*
|
|
80
|
+
* The number of rows the whole query has, where the Provider counts them (`query.total` on its
|
|
81
|
+
* resource), and `null` where it does not. Counting is a query of its own on a large table, so a
|
|
82
|
+
* Provider answers it only where a Table shows it; the rows on this page are never put in its place.
|
|
83
|
+
*/
|
|
84
|
+
const [total, setTotal] = useState(null);
|
|
85
|
+
/** Whether a page follows this one, for a Table that pages without a count. */
|
|
86
|
+
const [hasMore, setHasMore] = useState(false);
|
|
80
87
|
const [summary, setSummary] = useState({});
|
|
81
88
|
const [queryLoading, setQueryLoading] = useState(Boolean(source));
|
|
82
89
|
const [pendingMutationTargets, setPendingMutationTargets] = useState(() => new Set());
|
|
@@ -102,6 +109,30 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
102
109
|
controller.abort();
|
|
103
110
|
mutationControllers.current.clear();
|
|
104
111
|
}, []);
|
|
112
|
+
/*
|
|
113
|
+
* A new source starts from nothing, and it does so in the render that brought it.
|
|
114
|
+
*
|
|
115
|
+
* The reset used to run in a microtask after the effects, so the query effect below had already
|
|
116
|
+
* fetched the new source with the old page and cursor -- one request thrown away on every source or
|
|
117
|
+
* `bindingParamsChange`. Adjusting state while rendering is React's way to follow a prop: the effects
|
|
118
|
+
* of this render see page 1 already. It also leaves the first render alone, so an `initialQuery` page
|
|
119
|
+
* and `initialExpandedRowIdentities` are no longer reset away on mount.
|
|
120
|
+
*/
|
|
121
|
+
const [renderedSourceKey, setRenderedSourceKey] = useState(sourceKey);
|
|
122
|
+
if (renderedSourceKey !== sourceKey) {
|
|
123
|
+
setRenderedSourceKey(sourceKey);
|
|
124
|
+
setPendingMutationTargets(new Set());
|
|
125
|
+
setRows([]);
|
|
126
|
+
setTotal(null);
|
|
127
|
+
setHasMore(false);
|
|
128
|
+
setSummary({});
|
|
129
|
+
setSelectedRowIdentities([]);
|
|
130
|
+
setExpandedRowIdentities([]);
|
|
131
|
+
setQuery((current) => ({ ...current, page: 1, cursor: null }));
|
|
132
|
+
setQueryLoading(sourceKey !== "null");
|
|
133
|
+
setError(null);
|
|
134
|
+
}
|
|
135
|
+
// What a render cannot reset: requests in flight and the bookkeeping kept beside state.
|
|
105
136
|
useEffect(() => {
|
|
106
137
|
for (const controller of mutationControllers.current)
|
|
107
138
|
controller.abort();
|
|
@@ -109,21 +140,6 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
109
140
|
latestMutationByTarget.current.clear();
|
|
110
141
|
pendingMutationCountsByTarget.current.clear();
|
|
111
142
|
rowsRef.current = [];
|
|
112
|
-
let cancelled = false;
|
|
113
|
-
queueMicrotask(() => {
|
|
114
|
-
if (cancelled)
|
|
115
|
-
return;
|
|
116
|
-
setPendingMutationTargets(new Set());
|
|
117
|
-
setRows([]);
|
|
118
|
-
setTotal(0);
|
|
119
|
-
setSummary({});
|
|
120
|
-
setSelectedRowIdentities([]);
|
|
121
|
-
setExpandedRowIdentities([]);
|
|
122
|
-
setQuery((current) => ({ ...current, page: 1, cursor: null }));
|
|
123
|
-
setQueryLoading(sourceKey !== "null");
|
|
124
|
-
setError(null);
|
|
125
|
-
});
|
|
126
|
-
return () => { cancelled = true; };
|
|
127
143
|
}, [sourceKey]);
|
|
128
144
|
/*
|
|
129
145
|
* Held by content, not identity: a caller that writes the object inline hands in a new one every
|
|
@@ -158,7 +174,8 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
158
174
|
if (data.rows.some((row) => readPhiTableRowIdentity(row, resource.rowIdentityPath) == null)) {
|
|
159
175
|
setRows([]);
|
|
160
176
|
rowsRef.current = [];
|
|
161
|
-
setTotal(
|
|
177
|
+
setTotal(null);
|
|
178
|
+
setHasMore(false);
|
|
162
179
|
setSummary({});
|
|
163
180
|
setQueryLoading(false);
|
|
164
181
|
setError(new PhiTableProviderError("missing-row-identity", `Table Provider row is missing identity field "${resource.rowIdentityPath}".`));
|
|
@@ -168,7 +185,8 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
168
185
|
if (summaryError) {
|
|
169
186
|
setRows([]);
|
|
170
187
|
rowsRef.current = [];
|
|
171
|
-
setTotal(
|
|
188
|
+
setTotal(null);
|
|
189
|
+
setHasMore(false);
|
|
172
190
|
setSummary({});
|
|
173
191
|
setQueryLoading(false);
|
|
174
192
|
setError(new PhiTableProviderError("invalid-summary", summaryError));
|
|
@@ -176,7 +194,11 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
176
194
|
}
|
|
177
195
|
rowsRef.current = data.rows;
|
|
178
196
|
setRows(data.rows);
|
|
179
|
-
|
|
197
|
+
// A resource that does not page answers every row at once, so its rows are its count.
|
|
198
|
+
const pages = resource.query.pagination === "offset" || resource.query.pagination === "cursor";
|
|
199
|
+
setTotal(data.total ?? (pages ? null : data.rows.length));
|
|
200
|
+
const pageSize = data.pageSize ?? resolvedQuery.pageSize;
|
|
201
|
+
setHasMore(data.nextCursor != null || (data.total == null && pageSize != null && data.rows.length >= pageSize));
|
|
180
202
|
setSummary(data.summary ?? {});
|
|
181
203
|
setQueryLoading(false);
|
|
182
204
|
setError(null);
|
|
@@ -188,7 +210,7 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
188
210
|
setSelectedRowIdentities((current) => current.filter((identity) => available.has(String(identity))));
|
|
189
211
|
}
|
|
190
212
|
onDataRef.current?.(data);
|
|
191
|
-
}, [preserveSelectedRowIdentities, resource]);
|
|
213
|
+
}, [preserveSelectedRowIdentities, resolvedQuery.pageSize, resource]);
|
|
192
214
|
useEffect(() => {
|
|
193
215
|
if (!source || !provider || !resource || resourceError)
|
|
194
216
|
return;
|
|
@@ -254,7 +276,8 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
254
276
|
if (!background) {
|
|
255
277
|
setRows([]);
|
|
256
278
|
rowsRef.current = [];
|
|
257
|
-
setTotal(
|
|
279
|
+
setTotal(null);
|
|
280
|
+
setHasMore(false);
|
|
258
281
|
setSummary({});
|
|
259
282
|
}
|
|
260
283
|
setQueryLoading(false);
|
|
@@ -505,6 +528,7 @@ export function usePhiTableBinding({ source, initialQuery = {}, externalQuery, f
|
|
|
505
528
|
bindingError,
|
|
506
529
|
rows,
|
|
507
530
|
total,
|
|
531
|
+
hasMore,
|
|
508
532
|
summary,
|
|
509
533
|
contractError: resourceError,
|
|
510
534
|
loading: queryLoading && !bindingError && !resourceError,
|
|
@@ -104,19 +104,27 @@ export function usePhiTreeBinding({ source, initialQuery = PHI_TREE_DEFAULT_INIT
|
|
|
104
104
|
controllers.clear();
|
|
105
105
|
};
|
|
106
106
|
}, []);
|
|
107
|
+
/*
|
|
108
|
+
* A new source, or new defaults, start the tree over -- in the render that brought them, so the query
|
|
109
|
+
* effect below asks once, with the reset query, instead of first with the old one.
|
|
110
|
+
*
|
|
111
|
+
* The search a reader typed stays. Resetting it to the initial query showed the unfiltered tree for a
|
|
112
|
+
* moment and fetched it, and the Widget's debounce put the search back 250 ms later for a third request:
|
|
113
|
+
* a new binding parameter narrows what the tree is about, not what the reader is looking for.
|
|
114
|
+
*/
|
|
115
|
+
const [renderedResetInputs, setRenderedResetInputs] = useState({ sourceKey, initialQuery, defaultExpandedNodeIdentities });
|
|
116
|
+
if (renderedResetInputs.sourceKey !== sourceKey ||
|
|
117
|
+
renderedResetInputs.initialQuery !== initialQuery ||
|
|
118
|
+
renderedResetInputs.defaultExpandedNodeIdentities !== defaultExpandedNodeIdentities) {
|
|
119
|
+
setRenderedResetInputs({ sourceKey, initialQuery, defaultExpandedNodeIdentities });
|
|
120
|
+
setNodes([]);
|
|
121
|
+
setSelectedNodeIdentities([]);
|
|
122
|
+
setCheckedNodeIdentities([]);
|
|
123
|
+
setExpandedNodeIdentities(defaultExpandedNodeIdentities);
|
|
124
|
+
setQuery((current) => (current.search ? { ...initialQuery, search: current.search } : initialQuery));
|
|
125
|
+
}
|
|
107
126
|
useEffect(() => {
|
|
108
127
|
expandedInitialSnapshot.current = false;
|
|
109
|
-
let cancelled = false;
|
|
110
|
-
queueMicrotask(() => {
|
|
111
|
-
if (cancelled)
|
|
112
|
-
return;
|
|
113
|
-
setNodes([]);
|
|
114
|
-
setSelectedNodeIdentities([]);
|
|
115
|
-
setCheckedNodeIdentities([]);
|
|
116
|
-
setExpandedNodeIdentities(defaultExpandedNodeIdentities);
|
|
117
|
-
setQuery(initialQuery);
|
|
118
|
-
});
|
|
119
|
-
return () => { cancelled = true; };
|
|
120
128
|
}, [defaultExpandedNodeIdentities, initialQuery, sourceKey]);
|
|
121
129
|
useEffect(() => {
|
|
122
130
|
if (!source || !provider || !resource)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
import { type PhiBulletListEntry, type PhiResolvedBulletListStyle } from "../shared/bullet-list-vocabulary";
|
|
3
|
+
export type PhiBulletListEntriesEditorProps = {
|
|
4
|
+
entries: readonly PhiBulletListEntry[];
|
|
5
|
+
listStyle: PhiResolvedBulletListStyle;
|
|
6
|
+
/** Absent, the entries are only shown: a read-only canvas. */
|
|
7
|
+
onCommit?: (entries: PhiBulletListEntry[]) => void;
|
|
8
|
+
/** Whether a click on an entry's marker steps its state on (`PHI_BULLET_LIST_ENTRY_STATES`). */
|
|
9
|
+
states?: boolean;
|
|
10
|
+
/** Each entry's field, apart from what every field here is: borderless, its text's own type. */
|
|
11
|
+
fieldStyle?: CSSProperties;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A list written in place, entry by entry, as the Widgets that carry one edit it.
|
|
15
|
+
*
|
|
16
|
+
* Every entry is a text field standing where the entry stands, so the list is set as it will be read.
|
|
17
|
+
* What is typed stays here until the field is left, and the Builder's history records one change per
|
|
18
|
+
* entry rather than one per keystroke. What changes the list's shape is written at once, with what was
|
|
19
|
+
* typed so far: Enter opens an entry after this one, Backspace in an empty entry removes it, Tab and
|
|
20
|
+
* Shift+Tab move an entry a level down and up, and -- where the list has `states` -- a click on the
|
|
21
|
+
* marker steps the entry from included to excluded to neither. Waiting for the blur would let the entry
|
|
22
|
+
* jump back while the tree is drawn again.
|
|
23
|
+
*
|
|
24
|
+
* A list with no entries still offers one empty field, so there is something to type into.
|
|
25
|
+
*/
|
|
26
|
+
export declare function PhiBulletListEntriesEditor({ entries, listStyle, onCommit, states, fieldStyle, }: PhiBulletListEntriesEditorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { PhiBulletListControl } from "../../controls/phi-bullet-list-control";
|
|
5
|
+
import { PhiTextControl } from "../../controls/phi-text-control";
|
|
6
|
+
import { buildPhiBulletListTree, nextPhiBulletListEntryState, resolvePhiBulletListLevel, } from "../shared/bullet-list-vocabulary";
|
|
7
|
+
/**
|
|
8
|
+
* A list written in place, entry by entry, as the Widgets that carry one edit it.
|
|
9
|
+
*
|
|
10
|
+
* Every entry is a text field standing where the entry stands, so the list is set as it will be read.
|
|
11
|
+
* What is typed stays here until the field is left, and the Builder's history records one change per
|
|
12
|
+
* entry rather than one per keystroke. What changes the list's shape is written at once, with what was
|
|
13
|
+
* typed so far: Enter opens an entry after this one, Backspace in an empty entry removes it, Tab and
|
|
14
|
+
* Shift+Tab move an entry a level down and up, and -- where the list has `states` -- a click on the
|
|
15
|
+
* marker steps the entry from included to excluded to neither. Waiting for the blur would let the entry
|
|
16
|
+
* jump back while the tree is drawn again.
|
|
17
|
+
*
|
|
18
|
+
* A list with no entries still offers one empty field, so there is something to type into.
|
|
19
|
+
*/
|
|
20
|
+
export function PhiBulletListEntriesEditor({ entries, listStyle, onCommit, states = false, fieldStyle, }) {
|
|
21
|
+
const [draftState, setDraftState] = useState(() => ({ source: entries, value: [...entries] }));
|
|
22
|
+
const draft = draftState.source === entries ? draftState.value : [...entries];
|
|
23
|
+
const shown = draft.length > 0 || !onCommit ? draft : [{ text: "" }];
|
|
24
|
+
const listRef = useRef(null);
|
|
25
|
+
function focusEntry(index) {
|
|
26
|
+
requestAnimationFrame(() => {
|
|
27
|
+
const field = listRef.current?.querySelectorAll("textarea")[index];
|
|
28
|
+
field?.focus();
|
|
29
|
+
field?.setSelectionRange(field.value.length, field.value.length);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function commit(next) {
|
|
33
|
+
// The empty field an empty list offers is not an entry until something is typed into it.
|
|
34
|
+
if (entries.length === 0 && next.length === 1 && next[0].text === "" && !next[0].state)
|
|
35
|
+
return;
|
|
36
|
+
setDraftState({ source: entries, value: next });
|
|
37
|
+
if (JSON.stringify(next) !== JSON.stringify(entries))
|
|
38
|
+
onCommit?.(next);
|
|
39
|
+
}
|
|
40
|
+
function updateText(index, text) {
|
|
41
|
+
const next = [...shown];
|
|
42
|
+
next[index] = { ...next[index], text };
|
|
43
|
+
setDraftState({ source: entries, value: next });
|
|
44
|
+
}
|
|
45
|
+
function shiftLevel(index, delta) {
|
|
46
|
+
const entry = shown[index];
|
|
47
|
+
if (!entry)
|
|
48
|
+
return;
|
|
49
|
+
const level = resolvePhiBulletListLevel((entry.level ?? 1) + delta);
|
|
50
|
+
if (level === (entry.level ?? 1))
|
|
51
|
+
return;
|
|
52
|
+
const { level: _level, ...rest } = entry;
|
|
53
|
+
void _level;
|
|
54
|
+
commit(shown.map((item, itemIndex) => (itemIndex === index ? (level > 1 ? { ...rest, level } : rest) : item)));
|
|
55
|
+
focusEntry(index);
|
|
56
|
+
}
|
|
57
|
+
function insertAfter(index) {
|
|
58
|
+
const entry = shown[index];
|
|
59
|
+
const level = entry?.level;
|
|
60
|
+
commit([...shown.slice(0, index + 1), { text: "", ...(level ? { level } : {}) }, ...shown.slice(index + 1)]);
|
|
61
|
+
focusEntry(index + 1);
|
|
62
|
+
}
|
|
63
|
+
function remove(index) {
|
|
64
|
+
commit(shown.filter((_, itemIndex) => itemIndex !== index));
|
|
65
|
+
focusEntry(Math.max(0, index - 1));
|
|
66
|
+
}
|
|
67
|
+
function stepState(index) {
|
|
68
|
+
const entry = shown[index];
|
|
69
|
+
if (!entry)
|
|
70
|
+
return;
|
|
71
|
+
const { state: _state, ...rest } = entry;
|
|
72
|
+
void _state;
|
|
73
|
+
const state = nextPhiBulletListEntryState(entry.state);
|
|
74
|
+
commit(shown.map((item, itemIndex) => (itemIndex === index ? (state === "neutral" ? rest : { ...rest, state }) : item)));
|
|
75
|
+
}
|
|
76
|
+
return (_jsx("div", { ref: listRef, style: { width: "100%", minWidth: 0 }, children: _jsx(PhiBulletListControl, { listStyle: listStyle,
|
|
77
|
+
// Each entry is a borderless text field, whose first line stands at the field's height.
|
|
78
|
+
markerLineHeight: "var(--ant-control-height)", items: buildPhiBulletListTree(shown, (item, index) => (_jsx(PhiTextControl, { presentation: "textarea", value: item.text, readOnly: !onCommit, variant: "borderless", placeholder: `Item ${index + 1}`, autoSize: { minRows: 1, maxRows: 6 }, onChange: (value) => updateText(index, value ?? ""), onBlur: () => commit(shown), onKeyDown: (event) => {
|
|
79
|
+
if (!onCommit)
|
|
80
|
+
return;
|
|
81
|
+
if (event.key === "Tab") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
shiftLevel(index, event.shiftKey ? -1 : 1);
|
|
84
|
+
}
|
|
85
|
+
else if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
insertAfter(index);
|
|
88
|
+
}
|
|
89
|
+
else if (event.key === "Backspace" && item.text === "" && shown.length > 1) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
remove(index);
|
|
92
|
+
}
|
|
93
|
+
}, style: {
|
|
94
|
+
paddingInline: 0,
|
|
95
|
+
paddingBlock: 0,
|
|
96
|
+
borderRadius: 0,
|
|
97
|
+
borderWidth: 0,
|
|
98
|
+
backgroundColor: "transparent",
|
|
99
|
+
resize: "none",
|
|
100
|
+
color: "inherit",
|
|
101
|
+
...fieldStyle,
|
|
102
|
+
} })), states && onCommit ? (_entry, index) => stepState(index) : undefined) }) }));
|
|
103
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parsePhiControlOptionsProviderConfig, readPhiControlOptions, } from "../../controls/phi-control-options";
|
|
2
|
-
import {
|
|
2
|
+
import { createPhiConfigReader } from "./strict-config-reader";
|
|
3
3
|
import { PHI_CONTROL_PRESENTATION_FIELDS, PHI_CONTROL_STATE_FIELDS, parsePhiControlConfig, } from "./control-signal-config";
|
|
4
4
|
export function buildPhiChoiceControlOptionFields(editorPlacement = "inspector") {
|
|
5
5
|
return [{
|
|
@@ -28,9 +28,10 @@ export function buildPhiChoiceControlOptionFields(editorPlacement = "inspector")
|
|
|
28
28
|
}
|
|
29
29
|
export const PHI_CHOICE_CONTROL_OPTION_FIELDS = buildPhiChoiceControlOptionFields();
|
|
30
30
|
export function parsePhiChoiceOptionsConfig(config) {
|
|
31
|
+
const reader = createPhiConfigReader(config);
|
|
31
32
|
return {
|
|
32
|
-
optionsProvider:
|
|
33
|
-
options:
|
|
33
|
+
optionsProvider: reader.value("optionsProvider", "an options provider", (value) => parsePhiControlOptionsProviderConfig(value) ?? undefined) ?? null,
|
|
34
|
+
options: reader.value("options", "a list of options", (value) => (Array.isArray(value) ? readPhiControlOptions(value) : undefined)) ?? readPhiControlOptions(undefined),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
export const PHI_CHOICE_CONTROL_CONFIG_FIELDS = [
|
|
@@ -46,27 +47,25 @@ export const PHI_CHOICE_CONTROL_VALUE_MODE_FIELD = {
|
|
|
46
47
|
{ value: "stack-slot-index", label: "Stack Slot Index" },
|
|
47
48
|
],
|
|
48
49
|
};
|
|
49
|
-
function readPhiChoiceValueMode(value) {
|
|
50
|
-
return readString(value) === "stack-slot-index" ? "stack-slot-index" : "raw";
|
|
51
|
-
}
|
|
52
50
|
export function parsePhiChoiceControlConfig(config) {
|
|
53
51
|
const controlState = parsePhiControlConfig(config, {
|
|
54
52
|
key: "choice",
|
|
55
53
|
});
|
|
54
|
+
const reader = createPhiConfigReader(config);
|
|
56
55
|
return {
|
|
57
56
|
...controlState,
|
|
58
|
-
label:
|
|
59
|
-
description:
|
|
60
|
-
value:
|
|
61
|
-
placeholder:
|
|
62
|
-
presentation:
|
|
63
|
-
allowCustom:
|
|
57
|
+
label: reader.string("label"),
|
|
58
|
+
description: reader.string("description"),
|
|
59
|
+
value: reader.string("value"),
|
|
60
|
+
placeholder: reader.string("placeholder"),
|
|
61
|
+
presentation: reader.oneOf("presentation", ["select", "autocomplete"]) ?? "select",
|
|
62
|
+
allowCustom: reader.boolean("allowCustom"),
|
|
64
63
|
...parsePhiChoiceOptionsConfig(config),
|
|
65
64
|
};
|
|
66
65
|
}
|
|
67
66
|
export function parsePhiStackChoiceControlConfig(config) {
|
|
68
67
|
return {
|
|
69
68
|
...parsePhiChoiceControlConfig(config),
|
|
70
|
-
valueMode:
|
|
69
|
+
valueMode: createPhiConfigReader(config).oneOf("valueMode", ["raw", "stack-slot-index"]) ?? "raw",
|
|
71
70
|
};
|
|
72
71
|
}
|
|
@@ -2,7 +2,11 @@ import type { PhiCommandButtonConfig } from "../../../types/core-widget-placemen
|
|
|
2
2
|
import type { PhiCmsConfigField } from "../../../types";
|
|
3
3
|
import type { PhiCmsConfigFieldVisibilityRule } from "../../../types/cms-plugins";
|
|
4
4
|
import { type PhiAccessViewer, type PhiRoleProviderId } from "../../../types/access";
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A Widget's buttons. Strict (`strict-config-reader.ts`): a button without a key or without an emit is
|
|
7
|
+
* an error naming it, not a button left out.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readPhiCommandButtons(value: unknown, path?: string): PhiCommandButtonConfig[];
|
|
6
10
|
export declare function filterPhiCommandButtonsForViewer(buttons: readonly PhiCommandButtonConfig[], viewer: PhiAccessViewer, ownerProviderId?: PhiRoleProviderId | null): PhiCommandButtonConfig[];
|
|
7
11
|
/**
|
|
8
12
|
* A new button: the first free `buttonN` key, labelled after it, sending its key as the command. The
|