@phis/contracts 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.js +14 -2
- package/dist/cms.d.ts +11 -0
- package/dist/cms.js +20 -0
- package/dist/conditions.d.ts +96 -0
- package/dist/conditions.js +163 -0
- package/dist/controls.d.ts +83 -0
- package/dist/controls.js +133 -0
- package/dist/forms.d.ts +496 -0
- package/dist/forms.js +508 -0
- package/dist/http.d.ts +10 -1
- package/dist/http.js +10 -1
- package/dist/layout.d.ts +22 -0
- package/dist/layout.js +30 -0
- package/dist/record.d.ts +8 -0
- package/dist/record.js +24 -0
- package/dist/references.d.ts +130 -0
- package/dist/references.js +208 -0
- package/dist/signals.d.ts +1 -0
- package/dist/signals.js +3 -0
- package/package.json +27 -2
package/dist/catalog.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PhiCmsAreaKey } from "./cms.js";
|
|
1
2
|
/**
|
|
2
3
|
* What a Module is for, as an operator reads it on the Modules page.
|
|
3
4
|
*
|
|
@@ -64,6 +65,19 @@ export type PhisModulePackageEntry = {
|
|
|
64
65
|
* asks at intake, where refusing costs the publisher a message rather than the operator a page.
|
|
65
66
|
*/
|
|
66
67
|
category: string;
|
|
68
|
+
/**
|
|
69
|
+
* The Areas the Module may serve, and the only place a package states them.
|
|
70
|
+
*
|
|
71
|
+
* Here and not in the Module's definition, because the reader that needs them first cannot run the
|
|
72
|
+
* definition: `phis module` writes one Client projection per Area and must know, without loading a
|
|
73
|
+
* stranger's code, which packages' Client doors belong in which. A definition that stated them as
|
|
74
|
+
* well would be a second answer that can disagree with this one; the generated projection hands
|
|
75
|
+
* these to the definition instead.
|
|
76
|
+
*
|
|
77
|
+
* Membership, unlike the category: an Area is a place the Site renders, and one this build has no
|
|
78
|
+
* place for cannot be served at all.
|
|
79
|
+
*/
|
|
80
|
+
eligibleAreas: PhiCmsAreaKey[];
|
|
67
81
|
};
|
|
68
82
|
/**
|
|
69
83
|
* What a Module package says about itself, without being run.
|
package/dist/catalog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPhiCmsAreaKey } from "./cms.js";
|
|
1
2
|
/**
|
|
2
3
|
* What a Module is for, as an operator reads it on the Modules page.
|
|
3
4
|
*
|
|
@@ -103,14 +104,25 @@ export function readPhisModulePackageDeclaration(packageJson) {
|
|
|
103
104
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
104
105
|
throw new Error(`"${at}" must be an object.`);
|
|
105
106
|
}
|
|
106
|
-
const { moduleId, category } = entry;
|
|
107
|
+
const { moduleId, category, eligibleAreas } = entry;
|
|
107
108
|
if (typeof moduleId !== "string" || !moduleId.trim()) {
|
|
108
109
|
throw new Error(`"${at}.moduleId" must be a non-empty string.`);
|
|
109
110
|
}
|
|
110
111
|
if (typeof category !== "string" || !category.trim()) {
|
|
111
112
|
throw new Error(`"${at}.category" must be a non-empty string.`);
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
if (!Array.isArray(eligibleAreas) || eligibleAreas.length === 0) {
|
|
115
|
+
throw new Error(`"${at}.eligibleAreas" must name at least one Area.`);
|
|
116
|
+
}
|
|
117
|
+
for (const area of eligibleAreas) {
|
|
118
|
+
if (!isPhiCmsAreaKey(area)) {
|
|
119
|
+
throw new Error(`"${at}.eligibleAreas" names "${String(area)}", which is not an Area.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (new Set(eligibleAreas).size !== eligibleAreas.length) {
|
|
123
|
+
throw new Error(`"${at}.eligibleAreas" names an Area twice.`);
|
|
124
|
+
}
|
|
125
|
+
return { moduleId, category, eligibleAreas: [...eligibleAreas] };
|
|
114
126
|
});
|
|
115
127
|
return { sourceLocale: typeof sourceLocale === "string" ? sourceLocale : null, modules };
|
|
116
128
|
}
|
package/dist/cms.d.ts
CHANGED
|
@@ -50,6 +50,17 @@ export type PhiCmsRegionTypeValue = (typeof PhiCmsRegionType)[keyof typeof PhiCm
|
|
|
50
50
|
* Bits 2, 5 and 7 are unassigned. That is what makes the numbers worth stating once: 8 reads like the
|
|
51
51
|
* fourth Area and is the third, and phi-server stores it as `area_id` where nothing spells it out.
|
|
52
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* The Areas a Site has, by name.
|
|
55
|
+
*
|
|
56
|
+
* The numbering below already lived here, because both sides store it and a renumbering neither of
|
|
57
|
+
* them noticed would put a Page in a different Area than it was written into. The names are the same
|
|
58
|
+
* agreement read the other way round and stood in two more copies -- one per repository -- so a Site
|
|
59
|
+
* that gained an Area would have had to gain it three times.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PHI_CMS_AREA_KEYS: readonly ["public", "app", "admin", "builder", "editor", "accounting"];
|
|
62
|
+
export type PhiCmsAreaKey = (typeof PHI_CMS_AREA_KEYS)[number];
|
|
63
|
+
export declare function isPhiCmsAreaKey(value: unknown): value is PhiCmsAreaKey;
|
|
53
64
|
export declare const PhiCmsVisibilityContext: {
|
|
54
65
|
readonly PublicArea: number;
|
|
55
66
|
readonly AppArea: number;
|
package/dist/cms.js
CHANGED
|
@@ -49,6 +49,26 @@ export const PhiCmsRegionType = {
|
|
|
49
49
|
* Bits 2, 5 and 7 are unassigned. That is what makes the numbers worth stating once: 8 reads like the
|
|
50
50
|
* fourth Area and is the third, and phi-server stores it as `area_id` where nothing spells it out.
|
|
51
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* The Areas a Site has, by name.
|
|
54
|
+
*
|
|
55
|
+
* The numbering below already lived here, because both sides store it and a renumbering neither of
|
|
56
|
+
* them noticed would put a Page in a different Area than it was written into. The names are the same
|
|
57
|
+
* agreement read the other way round and stood in two more copies -- one per repository -- so a Site
|
|
58
|
+
* that gained an Area would have had to gain it three times.
|
|
59
|
+
*/
|
|
60
|
+
export const PHI_CMS_AREA_KEYS = [
|
|
61
|
+
"public",
|
|
62
|
+
"app",
|
|
63
|
+
"admin",
|
|
64
|
+
"builder",
|
|
65
|
+
"editor",
|
|
66
|
+
"accounting",
|
|
67
|
+
];
|
|
68
|
+
const PHI_CMS_AREA_KEY_SET = new Set(PHI_CMS_AREA_KEYS);
|
|
69
|
+
export function isPhiCmsAreaKey(value) {
|
|
70
|
+
return typeof value === "string" && PHI_CMS_AREA_KEY_SET.has(value);
|
|
71
|
+
}
|
|
52
72
|
export const PhiCmsVisibilityContext = {
|
|
53
73
|
PublicArea: 1 << 0,
|
|
54
74
|
AppArea: 1 << 1,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grammar of a runtime condition: when a Widget shows, when a Form field is visible or disabled.
|
|
3
|
+
*
|
|
4
|
+
* Stored inside descriptors phis-server keeps, a Form's `visibleWhen` among them, so it is read here and
|
|
5
|
+
* read once -- the renderer evaluates what the server accepted, by the same rules.
|
|
6
|
+
*/
|
|
7
|
+
import { type PhiControllerSignalAddress, type PhiSignalAddress } from "./signals.js";
|
|
8
|
+
/**
|
|
9
|
+
* Where a condition reads the value it judges.
|
|
10
|
+
*
|
|
11
|
+
* `row`, `form`, `controller` and `widget` are values the page produced: a record under the cursor, what
|
|
12
|
+
* has been typed, what a Controller holds, what a neighbouring Widget found out. `feature` is what this
|
|
13
|
+
* Site was configured to have -- a named fact a Module publishes about its own setup, such as whether a
|
|
14
|
+
* sign-in method is switched on, settled before anything renders and never seen by the browser. `page`
|
|
15
|
+
* is the one the visitor arrived with --
|
|
16
|
+
* the address of the page they opened, which is state as much as any of the others and the only kind
|
|
17
|
+
* that is already settled before the first Widget renders. A confirmation link carries its token there,
|
|
18
|
+
* an external login returns with its outcome there, and a Widget that should appear only for one of
|
|
19
|
+
* those can say so without a component of its own to read the query string.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PHI_RUNTIME_CONDITION_SOURCES: readonly ["row", "form", "controller", "page", "widget", "feature"];
|
|
22
|
+
export declare const PHI_RUNTIME_CONDITION_OPERATORS: readonly ["truthy", "falsy", "equals", "contains"];
|
|
23
|
+
export declare const PHI_RUNTIME_CONDITION_GROUP_MATCHES: readonly ["all", "any"];
|
|
24
|
+
export type PhiRuntimeConditionSource = (typeof PHI_RUNTIME_CONDITION_SOURCES)[number];
|
|
25
|
+
export type PhiRuntimeConditionOperator = (typeof PHI_RUNTIME_CONDITION_OPERATORS)[number];
|
|
26
|
+
export type PhiRuntimeConditionGroupMatch = (typeof PHI_RUNTIME_CONDITION_GROUP_MATCHES)[number];
|
|
27
|
+
export type PhiRuntimeConditionValue = string;
|
|
28
|
+
export type PhiRuntimeValueCondition = {
|
|
29
|
+
source: PhiRuntimeConditionSource;
|
|
30
|
+
controllerAddress?: PhiControllerSignalAddress;
|
|
31
|
+
/**
|
|
32
|
+
* Which Widget is being asked, for a `widget` condition.
|
|
33
|
+
*
|
|
34
|
+
* A Controller is state that outlives the Widgets reading it; a Widget reports only what it found out
|
|
35
|
+
* for itself. Both answer on the same channel, so a Widget beside another one -- a preview of what a
|
|
36
|
+
* link will do, next to the form that does it -- needs no Controller of its own to be composed with.
|
|
37
|
+
*/
|
|
38
|
+
widgetAddress?: PhiSignalAddress;
|
|
39
|
+
valuePath: string;
|
|
40
|
+
operator: PhiRuntimeConditionOperator;
|
|
41
|
+
value?: PhiRuntimeConditionValue;
|
|
42
|
+
/**
|
|
43
|
+
* How to read a sender that has not said anything yet. Absent means "not matched".
|
|
44
|
+
*
|
|
45
|
+
* Hiding until told is right for a fact that is being fetched: a form that spends a confirmation link
|
|
46
|
+
* must not appear before the link has been checked. It is wrong for a state that only a later action
|
|
47
|
+
* can bring about -- nobody is halfway through a second authentication factor when the page opens, so
|
|
48
|
+
* a Widget standing back "while a step is under way" would stand back forever, waiting for a report
|
|
49
|
+
* that nothing is happening.
|
|
50
|
+
*/
|
|
51
|
+
whenUnavailable?: PhiRuntimeConditionResult;
|
|
52
|
+
reason?: string;
|
|
53
|
+
};
|
|
54
|
+
export type PhiRuntimeConditionExpression = PhiRuntimeValueCondition | {
|
|
55
|
+
match: PhiRuntimeConditionGroupMatch;
|
|
56
|
+
conditions: readonly PhiRuntimeConditionExpression[];
|
|
57
|
+
};
|
|
58
|
+
export type PhiRuntimeConditionResult = "matched" | "not-matched" | "unavailable";
|
|
59
|
+
/**
|
|
60
|
+
* The address of the page as the visitor opened it, which is what a `page` condition reads.
|
|
61
|
+
*
|
|
62
|
+
* A missing query parameter is an answer rather than a gap: `falsy` on `query.token` matches on the
|
|
63
|
+
* page without one, which is how a form with two stages knows which of them it is on.
|
|
64
|
+
*/
|
|
65
|
+
export type PhiRuntimePageConditionState = {
|
|
66
|
+
path: string;
|
|
67
|
+
query: Readonly<Record<string, string>>;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* What the active Modules report about this Site's configuration, by namespaced name.
|
|
71
|
+
*
|
|
72
|
+
* A Module publishes these the way it publishes signal capabilities: deliberately, under a name it
|
|
73
|
+
* keeps. A condition reads `auth.password`, not a field of somebody's internal context, so renaming
|
|
74
|
+
* something inside a Module cannot silently change which Widgets a page shows.
|
|
75
|
+
*/
|
|
76
|
+
export type PhiRuntimeFeatureState = Readonly<Record<string, unknown>>;
|
|
77
|
+
export type PhiRuntimeConditionSourceValues = {
|
|
78
|
+
row?: Record<string, unknown> | null;
|
|
79
|
+
features?: PhiRuntimeFeatureState | null;
|
|
80
|
+
form?: Record<string, unknown> | null;
|
|
81
|
+
controllers?: Readonly<Record<string, Record<string, unknown>>> | null;
|
|
82
|
+
/** Reported by Widgets, keyed by the address each one sends from. */
|
|
83
|
+
widgets?: Readonly<Record<string, Record<string, unknown>>> | null;
|
|
84
|
+
page?: PhiRuntimePageConditionState | null;
|
|
85
|
+
};
|
|
86
|
+
export type PhiRuntimeConditionStateSignalValue = {
|
|
87
|
+
state: Record<string, unknown>;
|
|
88
|
+
};
|
|
89
|
+
export declare function readPhiRuntimeValueCondition(value: unknown): PhiRuntimeValueCondition | null;
|
|
90
|
+
export declare function readPhiRuntimeConditionExpression(value: unknown): PhiRuntimeConditionExpression | null;
|
|
91
|
+
export declare function collectPhiRuntimeValueConditions(expression: PhiRuntimeConditionExpression | null | undefined): PhiRuntimeValueCondition[];
|
|
92
|
+
export declare function combinePhiRuntimeConditionExpressions(match: PhiRuntimeConditionGroupMatch, expressions: readonly (PhiRuntimeConditionExpression | null | undefined)[]): PhiRuntimeConditionExpression | undefined;
|
|
93
|
+
export declare function readPhiRuntimeConditionStateSignalValue(value: unknown): PhiRuntimeConditionStateSignalValue | null;
|
|
94
|
+
export declare function readPhiRuntimeConditionValue(input: unknown, valuePath: string): unknown;
|
|
95
|
+
export declare function matchesPhiRuntimeValueCondition(condition: PhiRuntimeValueCondition, sources: PhiRuntimeConditionSourceValues): boolean;
|
|
96
|
+
export declare function evaluatePhiRuntimeConditionExpression(expression: PhiRuntimeConditionExpression, sources: PhiRuntimeConditionSourceValues): PhiRuntimeConditionResult;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grammar of a runtime condition: when a Widget shows, when a Form field is visible or disabled.
|
|
3
|
+
*
|
|
4
|
+
* Stored inside descriptors phis-server keeps, a Form's `visibleWhen` among them, so it is read here and
|
|
5
|
+
* read once -- the renderer evaluates what the server accepted, by the same rules.
|
|
6
|
+
*/
|
|
7
|
+
import { readPhiControllerSignalAddress, readPhiSignalAddress, } from "./signals.js";
|
|
8
|
+
import { isPhiRecord, readPhiDotPath } from "./record.js";
|
|
9
|
+
/**
|
|
10
|
+
* Where a condition reads the value it judges.
|
|
11
|
+
*
|
|
12
|
+
* `row`, `form`, `controller` and `widget` are values the page produced: a record under the cursor, what
|
|
13
|
+
* has been typed, what a Controller holds, what a neighbouring Widget found out. `feature` is what this
|
|
14
|
+
* Site was configured to have -- a named fact a Module publishes about its own setup, such as whether a
|
|
15
|
+
* sign-in method is switched on, settled before anything renders and never seen by the browser. `page`
|
|
16
|
+
* is the one the visitor arrived with --
|
|
17
|
+
* the address of the page they opened, which is state as much as any of the others and the only kind
|
|
18
|
+
* that is already settled before the first Widget renders. A confirmation link carries its token there,
|
|
19
|
+
* an external login returns with its outcome there, and a Widget that should appear only for one of
|
|
20
|
+
* those can say so without a component of its own to read the query string.
|
|
21
|
+
*/
|
|
22
|
+
export const PHI_RUNTIME_CONDITION_SOURCES = ["row", "form", "controller", "page", "widget", "feature"];
|
|
23
|
+
export const PHI_RUNTIME_CONDITION_OPERATORS = ["truthy", "falsy", "equals", "contains"];
|
|
24
|
+
export const PHI_RUNTIME_CONDITION_GROUP_MATCHES = ["all", "any"];
|
|
25
|
+
function readConditionValue(value) {
|
|
26
|
+
return typeof value === "string" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function readConditionSource(value) {
|
|
29
|
+
return PHI_RUNTIME_CONDITION_SOURCES.includes(value)
|
|
30
|
+
? value
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
export function readPhiRuntimeValueCondition(value) {
|
|
34
|
+
if (!isPhiRecord(value))
|
|
35
|
+
return null;
|
|
36
|
+
const source = readConditionSource(value.source);
|
|
37
|
+
const operator = value.operator === "truthy" || value.operator === "falsy" ||
|
|
38
|
+
value.operator === "equals" || value.operator === "contains"
|
|
39
|
+
? value.operator
|
|
40
|
+
: null;
|
|
41
|
+
const valuePath = typeof value.valuePath === "string" ? value.valuePath.trim() : "";
|
|
42
|
+
const whenUnavailable = value.whenUnavailable === "matched" || value.whenUnavailable === "not-matched"
|
|
43
|
+
? value.whenUnavailable
|
|
44
|
+
: undefined;
|
|
45
|
+
const controllerAddress = source === "controller"
|
|
46
|
+
? readPhiControllerSignalAddress(value.controllerAddress)
|
|
47
|
+
: undefined;
|
|
48
|
+
const widgetAddress = source === "widget"
|
|
49
|
+
? readPhiSignalAddress(value.widgetAddress)
|
|
50
|
+
: undefined;
|
|
51
|
+
const conditionValue = readConditionValue(value.value);
|
|
52
|
+
if (!source ||
|
|
53
|
+
!operator ||
|
|
54
|
+
!valuePath ||
|
|
55
|
+
(source === "controller" && !controllerAddress) ||
|
|
56
|
+
(source === "widget" && !widgetAddress) ||
|
|
57
|
+
(operator !== "truthy" && operator !== "falsy" && conditionValue === undefined)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
source,
|
|
62
|
+
controllerAddress,
|
|
63
|
+
widgetAddress,
|
|
64
|
+
valuePath,
|
|
65
|
+
operator,
|
|
66
|
+
whenUnavailable,
|
|
67
|
+
value: operator === "truthy" || operator === "falsy" ? undefined : conditionValue,
|
|
68
|
+
reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function readPhiRuntimeConditionExpression(value) {
|
|
72
|
+
const leaf = readPhiRuntimeValueCondition(value);
|
|
73
|
+
if (leaf)
|
|
74
|
+
return leaf;
|
|
75
|
+
if (!isPhiRecord(value) || (value.match !== "all" && value.match !== "any") || !Array.isArray(value.conditions)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const conditions = value.conditions.map(readPhiRuntimeConditionExpression);
|
|
79
|
+
if (conditions.length === 0 || conditions.some((condition) => condition == null)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
match: value.match,
|
|
84
|
+
conditions: conditions,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function collectPhiRuntimeValueConditions(expression) {
|
|
88
|
+
if (!expression)
|
|
89
|
+
return [];
|
|
90
|
+
if ("source" in expression)
|
|
91
|
+
return [expression];
|
|
92
|
+
return expression.conditions.flatMap(collectPhiRuntimeValueConditions);
|
|
93
|
+
}
|
|
94
|
+
export function combinePhiRuntimeConditionExpressions(match, expressions) {
|
|
95
|
+
const conditions = expressions.filter((entry) => Boolean(entry));
|
|
96
|
+
if (conditions.length === 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (conditions.length === 1)
|
|
99
|
+
return conditions[0];
|
|
100
|
+
return { match, conditions };
|
|
101
|
+
}
|
|
102
|
+
export function readPhiRuntimeConditionStateSignalValue(value) {
|
|
103
|
+
return isPhiRecord(value) && isPhiRecord(value.state) ? { state: value.state } : null;
|
|
104
|
+
}
|
|
105
|
+
export function readPhiRuntimeConditionValue(input, valuePath) {
|
|
106
|
+
return readPhiDotPath(input, valuePath);
|
|
107
|
+
}
|
|
108
|
+
function resolvePhiRuntimeConditionSource(condition, sources) {
|
|
109
|
+
if (condition.source === "row")
|
|
110
|
+
return sources.row ?? null;
|
|
111
|
+
if (condition.source === "form")
|
|
112
|
+
return sources.form ?? null;
|
|
113
|
+
if (condition.source === "page")
|
|
114
|
+
return sources.page ?? null;
|
|
115
|
+
if (condition.source === "feature")
|
|
116
|
+
return sources.features ?? null;
|
|
117
|
+
if (condition.source === "widget") {
|
|
118
|
+
return condition.widgetAddress ? sources.widgets?.[condition.widgetAddress] ?? null : null;
|
|
119
|
+
}
|
|
120
|
+
return condition.controllerAddress
|
|
121
|
+
? sources.controllers?.[condition.controllerAddress] ?? null
|
|
122
|
+
: null;
|
|
123
|
+
}
|
|
124
|
+
export function matchesPhiRuntimeValueCondition(condition, sources) {
|
|
125
|
+
const source = resolvePhiRuntimeConditionSource(condition, sources);
|
|
126
|
+
if (source == null)
|
|
127
|
+
return false;
|
|
128
|
+
const current = readPhiRuntimeConditionValue(source, condition.valuePath);
|
|
129
|
+
if ((condition.source === "controller" || condition.source === "widget") && current === undefined)
|
|
130
|
+
return false;
|
|
131
|
+
if (condition.operator === "truthy")
|
|
132
|
+
return Boolean(current);
|
|
133
|
+
if (condition.operator === "falsy")
|
|
134
|
+
return !current;
|
|
135
|
+
if (condition.operator === "equals")
|
|
136
|
+
return Object.is(current, condition.value);
|
|
137
|
+
return Array.isArray(current)
|
|
138
|
+
? current.some((entry) => typeof entry === "string" && entry === condition.value)
|
|
139
|
+
: typeof current === "string" && typeof condition.value === "string"
|
|
140
|
+
? current.includes(condition.value)
|
|
141
|
+
: false;
|
|
142
|
+
}
|
|
143
|
+
export function evaluatePhiRuntimeConditionExpression(expression, sources) {
|
|
144
|
+
if ("source" in expression) {
|
|
145
|
+
const source = resolvePhiRuntimeConditionSource(expression, sources);
|
|
146
|
+
if (source == null)
|
|
147
|
+
return expression.whenUnavailable ?? "unavailable";
|
|
148
|
+
if ((expression.source === "controller" || expression.source === "widget") &&
|
|
149
|
+
readPhiRuntimeConditionValue(source, expression.valuePath) === undefined) {
|
|
150
|
+
return expression.whenUnavailable ?? "unavailable";
|
|
151
|
+
}
|
|
152
|
+
return matchesPhiRuntimeValueCondition(expression, sources) ? "matched" : "not-matched";
|
|
153
|
+
}
|
|
154
|
+
const results = expression.conditions.map((condition) => evaluatePhiRuntimeConditionExpression(condition, sources));
|
|
155
|
+
if (expression.match === "all") {
|
|
156
|
+
if (results.some((result) => result === "not-matched"))
|
|
157
|
+
return "not-matched";
|
|
158
|
+
return results.every((result) => result === "matched") ? "matched" : "unavailable";
|
|
159
|
+
}
|
|
160
|
+
if (results.some((result) => result === "matched"))
|
|
161
|
+
return "matched";
|
|
162
|
+
return results.every((result) => result === "not-matched") ? "not-matched" : "unavailable";
|
|
163
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a choice control offers, and where it asks for its choices when they are not written out.
|
|
3
|
+
*
|
|
4
|
+
* Here because stored descriptors carry both: a Form field lists its options or names the provider
|
|
5
|
+
* that answers for them, and phis-server reads that field as it is stored.
|
|
6
|
+
*/
|
|
7
|
+
export type PhiRuntimeDataProviderKey = `${string}/${string}`;
|
|
8
|
+
/**
|
|
9
|
+
* `<namespace>/<name>`: an identifier that says whose it is.
|
|
10
|
+
*
|
|
11
|
+
* Provider keys, item renderer keys and everything else a Module may contribute to a shared registry
|
|
12
|
+
* share this shape, so they share one check. A second spelling of it is the one that drifts.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isPhiNamespacedRuntimeKey(value: unknown): value is `${string}/${string}`;
|
|
15
|
+
export declare function isPhiRuntimeDataProviderKey(value: unknown): value is PhiRuntimeDataProviderKey;
|
|
16
|
+
export type PhiControlOption<TValue extends string | number = string> = {
|
|
17
|
+
value: TValue;
|
|
18
|
+
label: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
description?: string;
|
|
21
|
+
icon?: string;
|
|
22
|
+
preview?: PhiControlOptionPreview;
|
|
23
|
+
/**
|
|
24
|
+
* The heading this option is listed under, where the Control can group -- a Select shows consecutive
|
|
25
|
+
* options with the same group under one heading. A Control that cannot group lists the options as they
|
|
26
|
+
* come, so the order has to make sense without the headings.
|
|
27
|
+
*/
|
|
28
|
+
group?: string;
|
|
29
|
+
};
|
|
30
|
+
export type PhiControlOptionPreview = PhiControlOptionBackgroundPreview | PhiControlOptionFontPreview;
|
|
31
|
+
/**
|
|
32
|
+
* The option's label set in the typeface it names, so a list of fonts shows the fonts.
|
|
33
|
+
*
|
|
34
|
+
* `fontFamily` is a complete `font-family` value -- a family, its fallback, a generic -- because the
|
|
35
|
+
* option is drawn in a popup outside the page's root, where a variable that names a font is not in
|
|
36
|
+
* scope. Whoever builds the option resolves the variable first.
|
|
37
|
+
*/
|
|
38
|
+
export type PhiControlOptionFontPreview = {
|
|
39
|
+
kind: "font";
|
|
40
|
+
fontFamily: string;
|
|
41
|
+
};
|
|
42
|
+
export type PhiControlOptionBackgroundPreview = {
|
|
43
|
+
kind: "background";
|
|
44
|
+
backgroundColor?: string;
|
|
45
|
+
backgroundImage?: string;
|
|
46
|
+
backgroundSize?: string;
|
|
47
|
+
backgroundPosition?: string;
|
|
48
|
+
backgroundRepeat?: string;
|
|
49
|
+
};
|
|
50
|
+
export type PhiControlOptionsProviderLoadMode = "client" | "server" | "hybrid";
|
|
51
|
+
export type PhiControlOptionsProviderSearchConfig = {
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
minChars?: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* A value this provider needs that another field holds.
|
|
57
|
+
*
|
|
58
|
+
* `source` and `valuePath` are the vocabulary conditions already use, so "the value of a sibling field"
|
|
59
|
+
* is said the same way everywhere. `form` reads the live form values; `config` reads the field or Widget
|
|
60
|
+
* configuration the control was built from, which is what an Inspector has instead of a form.
|
|
61
|
+
*
|
|
62
|
+
* `required` is the difference between narrowing a list and having no list: a provider whose answer is
|
|
63
|
+
* meaningless without the parent value is not asked at all until it has one, rather than answering with
|
|
64
|
+
* everything and looking as if the dependency were ignored.
|
|
65
|
+
*/
|
|
66
|
+
export type PhiControlOptionsProviderDependency = {
|
|
67
|
+
/** The name the provider reads this value under. */
|
|
68
|
+
param: string;
|
|
69
|
+
source: "form" | "config";
|
|
70
|
+
valuePath: string;
|
|
71
|
+
required?: boolean;
|
|
72
|
+
};
|
|
73
|
+
export type PhiControlOptionsProviderConfig = {
|
|
74
|
+
providerKey: PhiRuntimeDataProviderKey;
|
|
75
|
+
scopeKey?: string;
|
|
76
|
+
area?: string;
|
|
77
|
+
params?: Record<string, unknown>;
|
|
78
|
+
loadMode?: PhiControlOptionsProviderLoadMode;
|
|
79
|
+
search?: PhiControlOptionsProviderSearchConfig;
|
|
80
|
+
dependencies?: readonly PhiControlOptionsProviderDependency[];
|
|
81
|
+
};
|
|
82
|
+
export declare function parsePhiControlOptionsProviderConfig(value: unknown): PhiControlOptionsProviderConfig | null;
|
|
83
|
+
export declare function readPhiControlOptions(value: unknown): PhiControlOption[];
|
package/dist/controls.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a choice control offers, and where it asks for its choices when they are not written out.
|
|
3
|
+
*
|
|
4
|
+
* Here because stored descriptors carry both: a Form field lists its options or names the provider
|
|
5
|
+
* that answers for them, and phis-server reads that field as it is stored.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* `<namespace>/<name>`: an identifier that says whose it is.
|
|
9
|
+
*
|
|
10
|
+
* Provider keys, item renderer keys and everything else a Module may contribute to a shared registry
|
|
11
|
+
* share this shape, so they share one check. A second spelling of it is the one that drifts.
|
|
12
|
+
*/
|
|
13
|
+
export function isPhiNamespacedRuntimeKey(value) {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const separatorIndex = value.lastIndexOf("/");
|
|
18
|
+
return separatorIndex > 0 && separatorIndex < value.length - 1;
|
|
19
|
+
}
|
|
20
|
+
export function isPhiRuntimeDataProviderKey(value) {
|
|
21
|
+
return isPhiNamespacedRuntimeKey(value);
|
|
22
|
+
}
|
|
23
|
+
function readString(value) {
|
|
24
|
+
return typeof value === "string" && value.trim().length > 0
|
|
25
|
+
? value.trim()
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
function readRecord(value) {
|
|
29
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
30
|
+
}
|
|
31
|
+
function readLoadMode(value) {
|
|
32
|
+
return value === "client" || value === "server" || value === "hybrid" ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function readSearchConfig(value) {
|
|
35
|
+
const record = readRecord(value);
|
|
36
|
+
if (!record) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const enabled = typeof record.enabled === "boolean" ? record.enabled : undefined;
|
|
40
|
+
const minChars = typeof record.minChars === "number" && Number.isFinite(record.minChars)
|
|
41
|
+
? Math.max(0, Math.trunc(record.minChars))
|
|
42
|
+
: undefined;
|
|
43
|
+
if (enabled === undefined && minChars === undefined) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
enabled,
|
|
48
|
+
minChars,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function readDependencies(value) {
|
|
52
|
+
if (!Array.isArray(value)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const dependencies = value.flatMap((entry) => {
|
|
56
|
+
const record = readRecord(entry);
|
|
57
|
+
const param = readString(record?.param);
|
|
58
|
+
const valuePath = readString(record?.valuePath);
|
|
59
|
+
const source = record?.source === "form" || record?.source === "config" ? record.source : undefined;
|
|
60
|
+
if (!param || !valuePath || !source) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return [{
|
|
64
|
+
param,
|
|
65
|
+
source,
|
|
66
|
+
valuePath,
|
|
67
|
+
required: record?.required === true ? true : undefined,
|
|
68
|
+
}];
|
|
69
|
+
});
|
|
70
|
+
return dependencies.length > 0 ? dependencies : undefined;
|
|
71
|
+
}
|
|
72
|
+
function readOptionPreview(value) {
|
|
73
|
+
const record = readRecord(value);
|
|
74
|
+
if (record?.kind === "font") {
|
|
75
|
+
const fontFamily = readString(record.fontFamily);
|
|
76
|
+
return fontFamily ? { kind: "font", fontFamily } : undefined;
|
|
77
|
+
}
|
|
78
|
+
if (record?.kind !== "background")
|
|
79
|
+
return undefined;
|
|
80
|
+
return {
|
|
81
|
+
kind: "background",
|
|
82
|
+
backgroundColor: readString(record.backgroundColor),
|
|
83
|
+
backgroundImage: readString(record.backgroundImage),
|
|
84
|
+
backgroundSize: readString(record.backgroundSize),
|
|
85
|
+
backgroundPosition: readString(record.backgroundPosition),
|
|
86
|
+
backgroundRepeat: readString(record.backgroundRepeat),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function parsePhiControlOptionsProviderConfig(value) {
|
|
90
|
+
const record = readRecord(value);
|
|
91
|
+
if (!record) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const providerKey = readString(record.providerKey);
|
|
95
|
+
if (!isPhiRuntimeDataProviderKey(providerKey)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
providerKey,
|
|
100
|
+
scopeKey: readString(record.scopeKey),
|
|
101
|
+
area: readString(record.area),
|
|
102
|
+
params: readRecord(record.params),
|
|
103
|
+
loadMode: readLoadMode(record.loadMode),
|
|
104
|
+
search: readSearchConfig(record.search),
|
|
105
|
+
dependencies: readDependencies(record.dependencies),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function readPhiControlOptions(value) {
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
return value
|
|
111
|
+
.map((item) => {
|
|
112
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const record = item;
|
|
116
|
+
const optionValue = readString(record.value);
|
|
117
|
+
if (!optionValue) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
value: optionValue,
|
|
122
|
+
label: readString(record.label) ?? optionValue,
|
|
123
|
+
disabled: typeof record.disabled === "boolean" ? record.disabled : undefined,
|
|
124
|
+
description: readString(record.description),
|
|
125
|
+
icon: readString(record.icon),
|
|
126
|
+
preview: readOptionPreview(record.preview),
|
|
127
|
+
group: readString(record.group),
|
|
128
|
+
};
|
|
129
|
+
})
|
|
130
|
+
.filter((item) => item != null);
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|