@expofp/config 3.22.0 → 3.23.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/README.md +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/lib/deprecated-url-params.d.ts +33 -0
- package/dist/lib/deprecated-url-params.js +72 -0
- package/dist/lib/legacy-url.d.ts +5 -24
- package/dist/lib/legacy-url.js +169 -121
- package/dist/lib/load-config.js +16 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -27,7 +27,9 @@ always spoken — `?<slug>`, `?route:B-12:A-3:false`, `?tour=t1`, `?planner=a:b&
|
|
|
27
27
|
params like `?blue-dot=…`. `parseLegacyQuery` turns a raw `location.search` into typed commands and
|
|
28
28
|
`serializeSelection` writes a selection back in the same grammar (round-trip covered by tests), so
|
|
29
29
|
shared links stay bidirectional. The floor plan's `url-dispatch` service binds the commands to
|
|
30
|
-
stores and public methods; this codec stays pure — no DOM, no stores.
|
|
30
|
+
stores and public methods; this codec stays pure — no DOM, no stores. Keys whose capability has
|
|
31
|
+
been retired (`?hide=`, `?centerxy=`, `?lang=`, …) are listed in `retiredKeys` and swallowed, so an
|
|
32
|
+
old link opens a clean plan instead of replaying its query as search text.
|
|
31
33
|
|
|
32
34
|
Also: `applyIntents` (dispatch `selectBooth` / `changeLanguage` / … to the floor plan),
|
|
33
35
|
`serializeConfigResources` (collect schema-tagged assets + payloads for offline copies), and
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { applyIntents } from './lib/apply-intents.js';
|
|
2
2
|
export { getConfig, setConfig } from './lib/config-store.js';
|
|
3
3
|
export { type DebugSettingDescriptor, toDebugSettings } from './lib/debug-settings.js';
|
|
4
|
+
export { parseDeprecatedUrlParams } from './lib/deprecated-url-params.js';
|
|
4
5
|
export { type LegacyOneShot, type LegacyPrimary, type LegacyUrlQuery, parseLegacyQuery, serializeSelection, splitPlatformParams, type UrlSelection, } from './lib/legacy-url.js';
|
|
5
6
|
export { loadConfig } from './lib/load-config.js';
|
|
6
7
|
export { type StorageLike } from './lib/local-storage-codec.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { applyIntents } from './lib/apply-intents.js';
|
|
2
2
|
export { getConfig, setConfig } from './lib/config-store.js';
|
|
3
3
|
export { toDebugSettings } from './lib/debug-settings.js';
|
|
4
|
+
// TEMPORARY, with the shim itself — the floor plan needs it for the runtime
|
|
5
|
+
// `applyParameters` path, which parses the query a second time
|
|
6
|
+
export { parseDeprecatedUrlParams } from './lib/deprecated-url-params.js';
|
|
4
7
|
export { parseLegacyQuery, serializeSelection, splitPlatformParams, } from './lib/legacy-url.js';
|
|
5
8
|
export { loadConfig } from './lib/load-config.js';
|
|
6
9
|
export { serializeConfigResources, } from './lib/serialize-config-resources.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEMPORARY compat shim for the deprecated `?noOverlay=`, `?allowConsent=` and
|
|
3
|
+
* `?agenda`, translated into config fields while integrators move to the
|
|
4
|
+
* canonical `?visibility[overlay]=`, `?consent=` and `?showList=sessions`.
|
|
5
|
+
*
|
|
6
|
+
* A module of its own, outside `UrlConfigSchema`, so the whole shim retires in
|
|
7
|
+
* one move: delete this file and the compiler points at every consumer; the
|
|
8
|
+
* keys then join `retiredKeys` in the legacy codec. `?agenda` broke when the
|
|
9
|
+
* panel was renamed to sessions with no URL alias left (b227ed255, Oct 2025) —
|
|
10
|
+
* installed mobile-SDK apps emit the old spelling, and no SDK release can fix
|
|
11
|
+
* them; this entry is the missing alias.
|
|
12
|
+
*
|
|
13
|
+
* Precedence: an alias is dropped when the same query carries its canonical
|
|
14
|
+
* key, resolved HERE rather than by merge order (`noOverlay` lands in a
|
|
15
|
+
* different field than `visibility[overlay]`, one the FloorPlan latches at
|
|
16
|
+
* load), so both consumers apply the slice without precedence logic.
|
|
17
|
+
*/
|
|
18
|
+
import { type Config, type Consent, type Intent, type Visibility } from '@expofp/schema';
|
|
19
|
+
/** The deprecated keys for the legacy codec's partition — derived, the schema is the only list. */
|
|
20
|
+
export declare const DEPRECATED_URL_KEYS: string[];
|
|
21
|
+
/**
|
|
22
|
+
* The canonical readings the aliases defer to — callers that already parsed
|
|
23
|
+
* the query pass theirs in (`intents`: every canonical intent, bracketed
|
|
24
|
+
* entries before shortcut keys); otherwise the shim reads the query itself.
|
|
25
|
+
*/
|
|
26
|
+
export interface CanonicalAliasTargets {
|
|
27
|
+
visibility?: Visibility;
|
|
28
|
+
consent?: Consent;
|
|
29
|
+
intents?: Intent[];
|
|
30
|
+
}
|
|
31
|
+
/** The deprecated query params as a config slice; `{}` when the URL has none. */
|
|
32
|
+
export declare function parseDeprecatedUrlParams(url: string | URL, canonical?: CanonicalAliasTargets): Partial<Config>;
|
|
33
|
+
//# sourceMappingURL=deprecated-url-params.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEMPORARY compat shim for the deprecated `?noOverlay=`, `?allowConsent=` and
|
|
3
|
+
* `?agenda`, translated into config fields while integrators move to the
|
|
4
|
+
* canonical `?visibility[overlay]=`, `?consent=` and `?showList=sessions`.
|
|
5
|
+
*
|
|
6
|
+
* A module of its own, outside `UrlConfigSchema`, so the whole shim retires in
|
|
7
|
+
* one move: delete this file and the compiler points at every consumer; the
|
|
8
|
+
* keys then join `retiredKeys` in the legacy codec. `?agenda` broke when the
|
|
9
|
+
* panel was renamed to sessions with no URL alias left (b227ed255, Oct 2025) —
|
|
10
|
+
* installed mobile-SDK apps emit the old spelling, and no SDK release can fix
|
|
11
|
+
* them; this entry is the missing alias.
|
|
12
|
+
*
|
|
13
|
+
* Precedence: an alias is dropped when the same query carries its canonical
|
|
14
|
+
* key, resolved HERE rather than by merge order (`noOverlay` lands in a
|
|
15
|
+
* different field than `visibility[overlay]`, one the FloorPlan latches at
|
|
16
|
+
* load), so both consumers apply the slice without precedence logic.
|
|
17
|
+
*/
|
|
18
|
+
import { UrlConfigSchema, } from '@expofp/schema';
|
|
19
|
+
import * as z from 'zod';
|
|
20
|
+
import { parseFromUrlTolerant } from './url-codec.js';
|
|
21
|
+
import { parseIntentsFromUrl } from './url-intents.js';
|
|
22
|
+
/**
|
|
23
|
+
* Parsed through the same tolerant codec as the canonical URL slice, so the
|
|
24
|
+
* accepted spellings stay exactly what these params always accepted.
|
|
25
|
+
*/
|
|
26
|
+
const DeprecatedUrlSchema = z.object({
|
|
27
|
+
/** → `visibility.overlay` (inverted). */
|
|
28
|
+
noOverlay: z.boolean().optional(),
|
|
29
|
+
/** → `consent`: `true` → `'granted'`, `false` → `'denied'`. */
|
|
30
|
+
allowConsent: z.boolean().optional(),
|
|
31
|
+
/** → the `showList` intent on `sessions`. A string: the SDKs emit a bare `?agenda`. */
|
|
32
|
+
agenda: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
/** The deprecated keys for the legacy codec's partition — derived, the schema is the only list. */
|
|
35
|
+
export const DEPRECATED_URL_KEYS = Object.keys(DeprecatedUrlSchema.shape);
|
|
36
|
+
/** The deprecated query params as a config slice; `{}` when the URL has none. */
|
|
37
|
+
export function parseDeprecatedUrlParams(url, canonical = parseCanonicalTargets(url)) {
|
|
38
|
+
const { value } = parseFromUrlTolerant(DeprecatedUrlSchema, url);
|
|
39
|
+
const slice = {};
|
|
40
|
+
// stays the `noOverlay` config field: routing through it leaves the
|
|
41
|
+
// visibility node untouched — exactly what this param always did
|
|
42
|
+
if (value.noOverlay !== undefined && canonical.visibility?.overlay === undefined) {
|
|
43
|
+
slice.noOverlay = value.noOverlay;
|
|
44
|
+
}
|
|
45
|
+
if (value.allowConsent !== undefined && canonical.consent === undefined) {
|
|
46
|
+
slice.consent = value.allowConsent ? 'granted' : 'denied';
|
|
47
|
+
}
|
|
48
|
+
// the panel is `sessions` — and any canonical list panel wins: two
|
|
49
|
+
// `showList` intents would play in sequence, the alias taking the screen
|
|
50
|
+
if (value.agenda !== undefined && !opensList(canonical.intents)) {
|
|
51
|
+
slice.intents = [{ name: 'showList', args: ['sessions'] }];
|
|
52
|
+
}
|
|
53
|
+
return slice;
|
|
54
|
+
}
|
|
55
|
+
function opensList(intents) {
|
|
56
|
+
return (intents ?? []).some((intent) => intent.name === 'showList');
|
|
57
|
+
}
|
|
58
|
+
/** The canonical keys each alias defers to, read from the same query. */
|
|
59
|
+
const CanonicalUrlSchema = UrlConfigSchema.pick({
|
|
60
|
+
visibility: true,
|
|
61
|
+
consent: true,
|
|
62
|
+
intents: true,
|
|
63
|
+
});
|
|
64
|
+
/** Self-parse fallback: the same canonical readings, from the query itself. */
|
|
65
|
+
function parseCanonicalTargets(url) {
|
|
66
|
+
const { value } = parseFromUrlTolerant(CanonicalUrlSchema, url);
|
|
67
|
+
return {
|
|
68
|
+
visibility: value.visibility,
|
|
69
|
+
consent: value.consent,
|
|
70
|
+
intents: [...(value.intents ?? []), ...parseIntentsFromUrl(url).intents],
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/lib/legacy-url.d.ts
CHANGED
|
@@ -27,8 +27,12 @@
|
|
|
27
27
|
* side stays alive for links already shared in the wild, but only as a
|
|
28
28
|
* translator into those two channels; the sole legitimate consumer is
|
|
29
29
|
* `packages/floorplan/src/services/url-dispatch.ts` (lint-enforced).
|
|
30
|
+
*
|
|
31
|
+
* Keys whose capability has been retired keep one line each in
|
|
32
|
+
* {@link retiredKeys} — no translation, just enough for an old link to degrade
|
|
33
|
+
* to a clean plan instead of replaying as search text.
|
|
30
34
|
*/
|
|
31
|
-
import { type
|
|
35
|
+
import { type ListPanel } from '@expofp/schema';
|
|
32
36
|
/** The list panels addressable by keyword (`?bookmarks`, `?sessions`, …). */
|
|
33
37
|
export type LegacyListPanel = ListPanel;
|
|
34
38
|
/**
|
|
@@ -57,22 +61,6 @@ export type LegacyPrimary = {
|
|
|
57
61
|
accessible: boolean;
|
|
58
62
|
waypoints: string[];
|
|
59
63
|
title?: string;
|
|
60
|
-
} | {
|
|
61
|
-
type: 'language';
|
|
62
|
-
langId?: string;
|
|
63
|
-
} | {
|
|
64
|
-
type: 'printPdf';
|
|
65
|
-
} | {
|
|
66
|
-
type: 'visibility';
|
|
67
|
-
hidden: string[];
|
|
68
|
-
} | {
|
|
69
|
-
type: 'buildRoute';
|
|
70
|
-
} | {
|
|
71
|
-
type: 'pathway';
|
|
72
|
-
pathwayId: string;
|
|
73
|
-
boothIds: string[];
|
|
74
|
-
exhibitorIds: string[];
|
|
75
|
-
tourId?: string;
|
|
76
64
|
} | {
|
|
77
65
|
type: 'selectExhibitors';
|
|
78
66
|
values: string[];
|
|
@@ -112,13 +100,6 @@ export type LegacyOneShot = {
|
|
|
112
100
|
raw: string;
|
|
113
101
|
} | {
|
|
114
102
|
type: 'resetUiScale';
|
|
115
|
-
} | {
|
|
116
|
-
type: 'legacyBookmarks';
|
|
117
|
-
appendExhibitorId: number | null;
|
|
118
|
-
} | {
|
|
119
|
-
type: 'camera';
|
|
120
|
-
camera: Camera;
|
|
121
|
-
roll?: number;
|
|
122
103
|
};
|
|
123
104
|
export interface LegacyUrlQuery {
|
|
124
105
|
primary: LegacyPrimary;
|
package/dist/lib/legacy-url.js
CHANGED
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
* side stays alive for links already shared in the wild, but only as a
|
|
28
28
|
* translator into those two channels; the sole legitimate consumer is
|
|
29
29
|
* `packages/floorplan/src/services/url-dispatch.ts` (lint-enforced).
|
|
30
|
+
*
|
|
31
|
+
* Keys whose capability has been retired keep one line each in
|
|
32
|
+
* {@link retiredKeys} — no translation, just enough for an old link to degrade
|
|
33
|
+
* to a clean plan instead of replaying as search text.
|
|
30
34
|
*/
|
|
31
35
|
import { IntentsSchema, UrlConfigSchema } from '@expofp/schema';
|
|
32
36
|
import { safeDecode } from '@expofp/utils';
|
|
37
|
+
import { DEPRECATED_URL_KEYS } from './deprecated-url-params.js';
|
|
33
38
|
/* ── Parse: location.search → commands ─────────────────────────────────────── */
|
|
34
39
|
/** Query keys consumed by `loadConfig` before dispatch: config slice + intent shortcuts. */
|
|
35
40
|
const configOwnedKeys = new Set([
|
|
@@ -39,11 +44,86 @@ const configOwnedKeys = new Set([
|
|
|
39
44
|
/** One-shot keys the parser consumes into commands (never part of the slug). */
|
|
40
45
|
const oneShotKeys = new Set(['blue-dot', 'viewermode', 'previewMode', 'uiscale', 'resetuiscale']);
|
|
41
46
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
47
|
+
* RETIRED keys — capability gone, only swallowed (in `key=value` form: a bare
|
|
48
|
+
* slug that merely spells one of these words still resolves via the catalog),
|
|
49
|
+
* so an old link opens a clean plan instead of replaying as search text. The
|
|
50
|
+
* list only ever shrinks; a parameter that still DOES something belongs in
|
|
51
|
+
* `UrlConfigSchema` or `IntentsSchema`, never here.
|
|
52
|
+
*
|
|
53
|
+
* | retired | canonical form today |
|
|
54
|
+
* | ---------------------- | ------------------------------- |
|
|
55
|
+
* | `centerxy=x,y` | `camera[x]` / `camera[y]` |
|
|
56
|
+
* | `center=lat,lng` | `camera[lat]` / `camera[lng]` |
|
|
57
|
+
* | `z=` | `camera[floor]` |
|
|
58
|
+
* | `bearing=` | `camera[bearing]` |
|
|
59
|
+
* | `zoomtime=` | `camera[zoomTime]` |
|
|
60
|
+
* | `roll=` | — internal renderer knob |
|
|
61
|
+
* | `hide=a,b` | `visibility[a]=false` |
|
|
62
|
+
* | `lang=` | `changeLanguage=` |
|
|
63
|
+
* | `preview=` | — the preview link needs none |
|
|
64
|
+
* | `copy_exh=` | — a removed development seed |
|
|
65
|
+
* | `pathway=id` | `showPathway[0]=id` |
|
|
66
|
+
* | `booths=a,b` | — part of `showPathway` |
|
|
67
|
+
* | `b=` / `ba=` | — bookmark import, removed |
|
|
68
|
+
* | `-pdf` | — a print mode that never was |
|
|
69
|
+
* | `build-route` | `startBuildRoute` |
|
|
70
|
+
* | `sw=` / `yah=` / `k=` | — readers live on in legacy |
|
|
71
|
+
* | `kiosk_<framing>=` | `kiosk[…]` — the anchor node |
|
|
72
|
+
*
|
|
73
|
+
* `sw`/`yah`/`k` still have readers in the legacy runtime (service-worker
|
|
74
|
+
* toggle, YAH-marker command, saved-kiosk id) but none here — the mobile SDK
|
|
75
|
+
* enums emit them and they die out with app installs; their sibling `?agenda`
|
|
76
|
+
* is translated by the shim instead. `kiosk_<framing>` is the 11 fields the
|
|
77
|
+
* old 14-key QR channel spoke beyond the surviving `kiosk_x/y/z` (those
|
|
78
|
+
* partition via {@link directReadKeys}); unswallowed, one replayed as search
|
|
79
|
+
* text — and beside a route slug its tail fused into the last colon-part,
|
|
80
|
+
* silently flipping the accessible flag. `tour=`
|
|
81
|
+
* keeps its live meaning (`selectTour` covers what the pathway link used it
|
|
82
|
+
* for), and retiring `b=`/`ba=` as plain keys drops their emulated query wipe:
|
|
83
|
+
* one-shots beside them now apply.
|
|
84
|
+
*/
|
|
85
|
+
const retiredKeys = new Set([
|
|
86
|
+
'centerxy',
|
|
87
|
+
'center',
|
|
88
|
+
'z',
|
|
89
|
+
'bearing',
|
|
90
|
+
'zoomtime',
|
|
91
|
+
'roll',
|
|
92
|
+
'hide',
|
|
93
|
+
'lang',
|
|
94
|
+
'preview',
|
|
95
|
+
'copy_exh',
|
|
96
|
+
'pathway',
|
|
97
|
+
'booths',
|
|
98
|
+
'b',
|
|
99
|
+
'ba',
|
|
100
|
+
'sw',
|
|
101
|
+
'yah',
|
|
102
|
+
'k',
|
|
103
|
+
// the framing/cosmetic 11 of the old 14-key kiosk QR channel — `kiosk_x/y/z`
|
|
104
|
+
// live on in directReadKeys, and the field names mirror KioskSchema
|
|
105
|
+
'kiosk_heading',
|
|
106
|
+
'kiosk_lat',
|
|
107
|
+
'kiosk_lng',
|
|
108
|
+
'kiosk_iconSizePercent',
|
|
109
|
+
'kiosk_uiScale',
|
|
110
|
+
'kiosk_mapRollDegrees',
|
|
111
|
+
'kiosk_mapInitialPtScale',
|
|
112
|
+
'kiosk_mapPtScale',
|
|
113
|
+
'kiosk_mapCenterX',
|
|
114
|
+
'kiosk_mapCenterY',
|
|
115
|
+
'kiosk_mapPitch',
|
|
116
|
+
]);
|
|
117
|
+
/** Retired keywords: a whole-slug word rather than a `key=value` pair. */
|
|
118
|
+
const retiredKeywords = new Set(['-pdf', 'build-route']);
|
|
119
|
+
/**
|
|
120
|
+
* The deprecated aliases the `loadConfig` compat shim still consumes — config
|
|
121
|
+
* keys as far as this codec is concerned, so they partition with
|
|
122
|
+
* `configOwnedKeys` and match in both spellings (which is what catches the
|
|
123
|
+
* SDKs' valueless `?agenda`, at the cost of a booth slugged `agenda`).
|
|
124
|
+
* TEMPORARY: when the shim goes, move these into `retiredKeys`.
|
|
45
125
|
*/
|
|
46
|
-
const
|
|
126
|
+
const deprecatedKeys = new Set(DEPRECATED_URL_KEYS);
|
|
47
127
|
/**
|
|
48
128
|
* Query keys owned by the DELIVERY PLATFORM, not the floor plan: `?expo=` is
|
|
49
129
|
* the routing param the sha-delivery platform materializes for expo-aware
|
|
@@ -61,31 +141,40 @@ const legacyCameraKeys = new Set(['centerxy', 'center', 'z', 'bearing', 'zoomtim
|
|
|
61
141
|
* whereas config keys are instructions riding alongside the selection state.
|
|
62
142
|
*/
|
|
63
143
|
const platformParamKeys = new Set(['expo']);
|
|
144
|
+
/**
|
|
145
|
+
* Query keys the floor plan reads STRAIGHT from `location.search`: `?layer=`
|
|
146
|
+
* (init-layers) and `?kiosk_x/y/z` (the anchor a kiosk's route QR hands the
|
|
147
|
+
* scanning phone). Spelled out here — this package must not depend on the
|
|
148
|
+
* floor plan — so a reader change has to be mirrored. They partition like the
|
|
149
|
+
* platform's params: stripped in `key=value` form only, never `ownedByConfig`.
|
|
150
|
+
* Unpartitioned they replayed as search text, and the route QR's own link
|
|
151
|
+
* (`?route:…:true&kiosk_x=…`) lost its accessible flag — the 4th colon-part of
|
|
152
|
+
* a slug that had swallowed the whole `&kiosk_*` tail.
|
|
153
|
+
*/
|
|
154
|
+
const directReadKeys = new Set(['layer', 'kiosk_x', 'kiosk_y', 'kiosk_z']);
|
|
155
|
+
/**
|
|
156
|
+
* Companion keys of a primary form — read from `params` by its classifier,
|
|
157
|
+
* never part of the slug: `&title=` names a shared route (the route branch
|
|
158
|
+
* reads it; no canonical form exists). Stripped in `key=value` form only, like
|
|
159
|
+
* the partitions above — left in place, the tail fused into the route slug's
|
|
160
|
+
* last colon-part, where it silently flipped `:true` to false or ate a
|
|
161
|
+
* trailing FROM. A bare slug that merely spells `title` still resolves via
|
|
162
|
+
* the catalog. (The planner's `&source=`/`&from=` companions need no entry:
|
|
163
|
+
* that branch classifies on `params` before the slug is consulted, and the
|
|
164
|
+
* slug of a params-classified query is never replayed as search text.)
|
|
165
|
+
*/
|
|
166
|
+
const companionKeys = new Set(['title']);
|
|
64
167
|
export function parseLegacyQuery(search) {
|
|
65
168
|
const rawQuery = search.startsWith('?') ? search.slice(1) : search;
|
|
66
169
|
// the legacy contract: the payload is one encodeURIComponent unit
|
|
67
170
|
const decoded = safeDecode(rawQuery);
|
|
68
171
|
const params = new URLSearchParams(decoded);
|
|
69
|
-
const oneShots = collectOneShots(rawQuery, params);
|
|
70
|
-
// `?preview=`, `?b=`/`?ba=` (leading param) and `?copy_exh` historically
|
|
71
|
-
// wiped the whole query before the other one-shots could see it — only the
|
|
72
|
-
// heatmap flags (processed first) and the bookmark redirect itself survive
|
|
73
|
-
const wiped = rawQuery.startsWith('preview=') ||
|
|
74
|
-
rawQuery.startsWith('b=') ||
|
|
75
|
-
rawQuery.startsWith('ba=') ||
|
|
76
|
-
decoded.includes('copy_exh');
|
|
77
|
-
if (wiped) {
|
|
78
|
-
return {
|
|
79
|
-
primary: { type: 'select', slug: '' },
|
|
80
|
-
oneShots: oneShots.filter((s) => s.type === 'heatmap' || s.type === 'legacyBookmarks'),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
172
|
return {
|
|
84
|
-
primary: classifyPrimary(residualSlug(decoded), params,
|
|
85
|
-
oneShots,
|
|
173
|
+
primary: classifyPrimary(residualSlug(decoded), params, hasOwnedParts(decoded)),
|
|
174
|
+
oneShots: collectOneShots(params),
|
|
86
175
|
};
|
|
87
176
|
}
|
|
88
|
-
function classifyPrimary(slug, params,
|
|
177
|
+
function classifyPrimary(slug, params, owned) {
|
|
89
178
|
if (params.has('kiosk')) {
|
|
90
179
|
const value = params.get('kiosk');
|
|
91
180
|
return { type: 'kiosk', enabled: value === '1' ? true : value === '0' ? false : null };
|
|
@@ -119,50 +208,36 @@ function classifyPrimary(slug, params, configOwned) {
|
|
|
119
208
|
return { type: 'list', list: 'visited' };
|
|
120
209
|
if (slug === 'language')
|
|
121
210
|
return { type: 'list', list: 'language' };
|
|
122
|
-
if (params.has('lang')) {
|
|
123
|
-
return { type: 'language', langId: params.get('lang')?.toLowerCase() };
|
|
124
|
-
}
|
|
125
211
|
if (slug === 'sessions')
|
|
126
212
|
return { type: 'list', list: 'sessions' };
|
|
127
213
|
if (slug === 'exhibitors')
|
|
128
214
|
return { type: 'list', list: 'exhibitors' };
|
|
129
215
|
if (slug === 'speakers')
|
|
130
216
|
return { type: 'list', list: 'speakers' };
|
|
131
|
-
if (slug
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (pathwayId) {
|
|
141
|
-
return {
|
|
142
|
-
type: 'pathway',
|
|
143
|
-
pathwayId,
|
|
144
|
-
boothIds: params.get('booths')?.split(',') ?? [],
|
|
145
|
-
exhibitorIds: params.get('exhibitors')?.split(',') ?? [],
|
|
146
|
-
tourId: params.get('tour') ?? undefined,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (slug.startsWith('exhibitors') && slug.includes('=')) {
|
|
150
|
-
return { type: 'selectExhibitors', values: slug.split('=')[1].split(',') };
|
|
217
|
+
if (slug.startsWith('exhibitors=')) {
|
|
218
|
+
const value = slug.slice('exhibitors='.length);
|
|
219
|
+
// `?exhibitors=` and `?exhibitors=true` are exactly the two values the
|
|
220
|
+
// FILTER manager reads as "open the exhibitors list" — classified as a
|
|
221
|
+
// selection, the link also selected an exhibitor named "true". Anything
|
|
222
|
+
// else after `exhibitors=` is a display-name list.
|
|
223
|
+
if (value === '' || value === 'true')
|
|
224
|
+
return { type: 'ownedByFilters' };
|
|
225
|
+
return { type: 'selectExhibitors', values: value.split(',') };
|
|
151
226
|
}
|
|
152
227
|
if (slug.includes('=') && (slug.startsWith('categories=') || /^poiTypes?=/.test(slug))) {
|
|
153
228
|
return { type: 'ownedByFilters' };
|
|
154
229
|
}
|
|
155
|
-
// Last, after every params-based branch above
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
if (!slug &&
|
|
230
|
+
// Last, after every params-based branch above: a slug emptied because
|
|
231
|
+
// config/intent keys (or the deprecated aliases) owned the whole query is
|
|
232
|
+
// NOT the clear-selection command an empty query is — `select('')` would
|
|
233
|
+
// close whatever is open. Retired-only queries deliberately ARE (see
|
|
234
|
+
// hasOwnedParts).
|
|
235
|
+
if (!slug && owned) {
|
|
161
236
|
return { type: 'ownedByConfig' };
|
|
162
237
|
}
|
|
163
238
|
return { type: 'select', slug };
|
|
164
239
|
}
|
|
165
|
-
function collectOneShots(
|
|
240
|
+
function collectOneShots(params) {
|
|
166
241
|
const oneShots = [];
|
|
167
242
|
if (params.get('heatmap') === 'true') {
|
|
168
243
|
oneShots.push({
|
|
@@ -171,14 +246,6 @@ function collectOneShots(rawQuery, params) {
|
|
|
171
246
|
kiosk: params.get('subtype') === 'kiosk',
|
|
172
247
|
});
|
|
173
248
|
}
|
|
174
|
-
if (rawQuery.startsWith('b=') || rawQuery.startsWith('ba=')) {
|
|
175
|
-
const appendRaw = new URLSearchParams(rawQuery).get('ba');
|
|
176
|
-
const appendExhibitorId = appendRaw === null ? null : parseInt(appendRaw, 10);
|
|
177
|
-
oneShots.push({
|
|
178
|
-
type: 'legacyBookmarks',
|
|
179
|
-
appendExhibitorId: Number.isFinite(appendExhibitorId) ? appendExhibitorId : null,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
249
|
const blueDot = params.get('blue-dot');
|
|
183
250
|
if (blueDot !== null) {
|
|
184
251
|
const p = blueDot.split(',');
|
|
@@ -218,87 +285,68 @@ function collectOneShots(rawQuery, params) {
|
|
|
218
285
|
const valid = !Number.isNaN(scale) && scale >= 0.8 && scale <= 1.4;
|
|
219
286
|
oneShots.push({ type: 'uiScale', scale: valid ? scale : null, raw: uiScaleRaw });
|
|
220
287
|
}
|
|
221
|
-
const camera = legacyCamera(params);
|
|
222
|
-
if (camera) {
|
|
223
|
-
oneShots.push(camera);
|
|
224
|
-
}
|
|
225
288
|
return oneShots;
|
|
226
289
|
}
|
|
227
|
-
/**
|
|
228
|
-
function
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
[camera.x, camera.y] = centerxy;
|
|
233
|
-
const center = numberPair(params.get('center'));
|
|
234
|
-
if (center)
|
|
235
|
-
[camera.lat, camera.lng] = center;
|
|
236
|
-
const floor = params.get('z');
|
|
237
|
-
if (floor)
|
|
238
|
-
camera.floor = floor;
|
|
239
|
-
const bearing = finiteNumber(params.get('bearing'));
|
|
240
|
-
if (bearing !== undefined)
|
|
241
|
-
camera.bearing = bearing;
|
|
242
|
-
const zoomTime = finiteNumber(params.get('zoomtime'));
|
|
243
|
-
if (zoomTime !== undefined)
|
|
244
|
-
camera.zoomTime = zoomTime;
|
|
245
|
-
const roll = finiteNumber(params.get('roll'));
|
|
246
|
-
if (!Object.keys(camera).length && roll === undefined)
|
|
247
|
-
return null;
|
|
248
|
-
return roll === undefined ? { type: 'camera', camera } : { type: 'camera', camera, roll };
|
|
290
|
+
/** A retired part, matched in `key=value` form only (see {@link retiredKeys}). */
|
|
291
|
+
function isRetiredPart(part) {
|
|
292
|
+
if (retiredKeywords.has(part))
|
|
293
|
+
return true;
|
|
294
|
+
return retiredKeys.has(part.split('=')[0]) && part.includes('=');
|
|
249
295
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
296
|
+
/**
|
|
297
|
+
* A part `loadConfig` consumes: a config-slice key, an intent shortcut, or a
|
|
298
|
+
* deprecated alias — bracketed sub-keys and the valueless spelling included.
|
|
299
|
+
*/
|
|
300
|
+
function isConsumedByConfig(part) {
|
|
301
|
+
const rawKey = part.split('=')[0];
|
|
302
|
+
// the legacy scalar `?heatmap=true` is slug-resident (see residualSlug)
|
|
303
|
+
if (rawKey === 'heatmap')
|
|
304
|
+
return false;
|
|
305
|
+
const key = rawKey.split('[')[0];
|
|
306
|
+
return configOwnedKeys.has(key) || deprecatedKeys.has(key);
|
|
257
307
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Whether the query holds a part the config layer consumes. Retired and
|
|
310
|
+
* one-shot keys deliberately do NOT count: a query made only of those means
|
|
311
|
+
* nothing, keeping the legacy empty select (which closes the open panel).
|
|
312
|
+
*/
|
|
313
|
+
function hasOwnedParts(decoded) {
|
|
314
|
+
return decoded.split('&').some((part) => !!part && isConsumedByConfig(part));
|
|
263
315
|
}
|
|
264
316
|
/**
|
|
265
317
|
* The slug is the decoded query minus every key another consumer owns: the
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* `heatmap=true` as search
|
|
318
|
+
* retired keys swallowed above, the platform's routing params, the keys the
|
|
319
|
+
* floor plan reads straight from the query ({@link directReadKeys}), the
|
|
320
|
+
* grammar's own companion keys ({@link companionKeys}), the config layer's
|
|
321
|
+
* keys and intent shortcuts (read by `loadConfig`), and the one-shot keys. `heatmap`/`type`/`subtype` stay in the slug — heatmap mode historically
|
|
322
|
+
* froze the URL with them in place (its data loader re-reads them), and the
|
|
323
|
+
* select fallback knows not to treat a query carrying `heatmap=true` as search
|
|
324
|
+
* text.
|
|
272
325
|
*/
|
|
273
|
-
function hasConfigOwnedParts(decoded) {
|
|
274
|
-
return decoded.split('&').some((part) => {
|
|
275
|
-
const rawKey = part.split('=')[0];
|
|
276
|
-
// the legacy scalar `?heatmap=true` is slug-resident (see residualSlug)
|
|
277
|
-
if (rawKey === 'heatmap')
|
|
278
|
-
return false;
|
|
279
|
-
return configOwnedKeys.has(rawKey.split('[')[0]);
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
326
|
function residualSlug(decoded) {
|
|
283
327
|
return decoded
|
|
284
328
|
.split('&')
|
|
285
329
|
.filter((part) => {
|
|
286
330
|
if (!part)
|
|
287
331
|
return false;
|
|
288
|
-
|
|
289
|
-
if (legacyCameraKeys.has(key) && part.includes('='))
|
|
332
|
+
if (isRetiredPart(part))
|
|
290
333
|
return false;
|
|
291
|
-
|
|
334
|
+
const key = part.split('=')[0].split('[')[0];
|
|
335
|
+
// all stripped in `key=value` form only, so a booth named "expo",
|
|
336
|
+
// "layer" or "title" still resolves via the catalog
|
|
337
|
+
if ((platformParamKeys.has(key) || directReadKeys.has(key) || companionKeys.has(key)) &&
|
|
338
|
+
part.includes('=')) {
|
|
292
339
|
return false;
|
|
340
|
+
}
|
|
293
341
|
// Carve-out (retirement plan §4.8): `heatmap` is a UrlConfigSchema key
|
|
294
342
|
// now, so the canonical bracketed `heatmap[…]=` form is config-owned and
|
|
295
343
|
// strips like any other — but the legacy scalar `?heatmap=true` must
|
|
296
|
-
// KEEP
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
if (part
|
|
300
|
-
return
|
|
301
|
-
return !
|
|
344
|
+
// KEEP its slug residency (the select fallback recognizes the literal
|
|
345
|
+
// `heatmap=true`; the frozen URL carries it for the data loader).
|
|
346
|
+
// `isConsumedByConfig` discriminates on the raw key: no bracket ⇒ legacy.
|
|
347
|
+
if (isConsumedByConfig(part))
|
|
348
|
+
return false;
|
|
349
|
+
return !oneShotKeys.has(key);
|
|
302
350
|
})
|
|
303
351
|
.join('&');
|
|
304
352
|
}
|
package/dist/lib/load-config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from '@expofp/resolve';
|
|
|
3
3
|
import { ConfigDefaults, DataJsSchema, FpSvgJsSchema, FpSvgLayerJsSchema, LocalStorageConfigSchema, ManifestSchema, OptionsSchema, UrlConfigSchema, WfDataJsSchema, } from '@expofp/schema';
|
|
4
4
|
import { deepClone, deepFreeze } from '@expofp/utils';
|
|
5
5
|
import debug from 'debug';
|
|
6
|
+
import { parseDeprecatedUrlParams } from './deprecated-url-params.js';
|
|
6
7
|
import { parseFromStorage } from './local-storage-codec.js';
|
|
7
8
|
import { normalizeFpSvgLayerAliases } from './normalize-fp-svg.js';
|
|
8
9
|
import { normalizeLegacyData } from './normalize-legacy-data.js';
|
|
@@ -162,13 +163,24 @@ export async function loadConfig(manifest, options, url, storage) {
|
|
|
162
163
|
// shortcut intents (`?selectBooth=A-12`) append after the bracketed
|
|
163
164
|
// `intents[…]` entries — both play, canonical form first
|
|
164
165
|
const shortcuts = parseIntentsFromUrl(url);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
const canonicalIntents = [...(urlConfig.intents ?? []), ...shortcuts.intents];
|
|
167
|
+
// TEMPORARY: the deprecated shim, handed the canonical readings parsed
|
|
168
|
+
// above so it does not re-parse the query. Its fields merge first so the
|
|
169
|
+
// canonical keys below win; its `?agenda` intent appends, since assigning
|
|
170
|
+
// the slice would drop what the canonical keys put there. Delete with the
|
|
171
|
+
// module.
|
|
172
|
+
const { intents: aliasIntents = [], ...deprecatedFields } = parseDeprecatedUrlParams(url, {
|
|
173
|
+
visibility: urlConfig.visibility,
|
|
174
|
+
consent: urlConfig.consent,
|
|
175
|
+
intents: canonicalIntents,
|
|
176
|
+
});
|
|
177
|
+
const intents = [...canonicalIntents, ...aliasIntents];
|
|
178
|
+
if (intents.length)
|
|
179
|
+
urlConfig.intents = intents;
|
|
168
180
|
invalidKeys.push(...shortcuts.invalidKeys);
|
|
169
181
|
if (invalidKeys.length)
|
|
170
182
|
log('loadConfig', 'ignoring invalid URL config params:', invalidKeys);
|
|
171
|
-
|
|
183
|
+
assignDefined(config, deprecatedFields);
|
|
172
184
|
assignDefined(config, urlConfig);
|
|
173
185
|
}
|
|
174
186
|
normalizeLegacyData(config, legacyDataUrlBase);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/config",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ExpoFP SDK internal: config layer and schemas",
|
|
6
6
|
"homepage": "https://developer.expofp.com/",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"debug": "^4.4.3",
|
|
30
30
|
"tslib": "^2.3.0",
|
|
31
31
|
"zod": "4.4.3",
|
|
32
|
-
"@expofp/resolve": "3.
|
|
33
|
-
"@expofp/
|
|
34
|
-
"@expofp/
|
|
32
|
+
"@expofp/resolve": "3.23.0",
|
|
33
|
+
"@expofp/schema": "3.23.0",
|
|
34
|
+
"@expofp/utils": "3.23.0"
|
|
35
35
|
}
|
|
36
36
|
}
|