@voyant-travel/apps 0.10.3 → 0.11.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/compiler.js
CHANGED
|
@@ -53,7 +53,14 @@ function normalizeManifest(manifest) {
|
|
|
53
53
|
optional: sortedUnique(manifest.scopes.optional),
|
|
54
54
|
},
|
|
55
55
|
admin: {
|
|
56
|
-
|
|
56
|
+
// Resolve the app-level default icon into each page that omits its own,
|
|
57
|
+
// so every normalized/persisted page carries a concrete icon or none.
|
|
58
|
+
pages: [...manifest.admin.pages]
|
|
59
|
+
.map((page) => {
|
|
60
|
+
const icon = page.icon ?? manifest.icon;
|
|
61
|
+
return icon ? { ...page, icon } : page;
|
|
62
|
+
})
|
|
63
|
+
.sort(byKey),
|
|
57
64
|
slotExtensions: [...manifest.admin.slotExtensions]
|
|
58
65
|
.map((extension) => ({ ...extension, slots: sortedUnique(extension.slots) }))
|
|
59
66
|
.sort(byKey),
|
package/dist/compiler.test.js
CHANGED
|
@@ -102,6 +102,54 @@ describe("app manifest compiler", () => {
|
|
|
102
102
|
},
|
|
103
103
|
})).toThrow(/not an external event contract/);
|
|
104
104
|
});
|
|
105
|
+
it("accepts an HTTPS per-page nav icon and rejects a non-HTTPS one", () => {
|
|
106
|
+
const parsed = appManifestSchema.parse({
|
|
107
|
+
...validManifest,
|
|
108
|
+
admin: {
|
|
109
|
+
...validManifest.admin,
|
|
110
|
+
pages: [{ ...validManifest.admin.pages[0], icon: "https://app.example.com/icon.svg" }],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
expect(parsed.admin.pages[0]?.icon).toBe("https://app.example.com/icon.svg");
|
|
114
|
+
expect(() => appManifestSchema.parse({
|
|
115
|
+
...validManifest,
|
|
116
|
+
admin: {
|
|
117
|
+
...validManifest.admin,
|
|
118
|
+
pages: [{ ...validManifest.admin.pages[0], icon: "http://app.example.com/icon.svg" }],
|
|
119
|
+
},
|
|
120
|
+
})).toThrow(/https/i);
|
|
121
|
+
});
|
|
122
|
+
it("resolves the app-level default icon into pages that omit their own", () => {
|
|
123
|
+
const normalized = compileAppManifest({
|
|
124
|
+
...validManifest,
|
|
125
|
+
icon: "https://app.example.com/app-icon.svg",
|
|
126
|
+
admin: {
|
|
127
|
+
...validManifest.admin,
|
|
128
|
+
pages: [
|
|
129
|
+
{
|
|
130
|
+
key: "inherits",
|
|
131
|
+
titleKey: "t",
|
|
132
|
+
path: "/inherits",
|
|
133
|
+
entryUrl: "https://app.example.com/a",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "owns",
|
|
137
|
+
titleKey: "t",
|
|
138
|
+
path: "/owns",
|
|
139
|
+
entryUrl: "https://app.example.com/b",
|
|
140
|
+
icon: "https://app.example.com/own-icon.svg",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
}).normalizedRelease;
|
|
145
|
+
const byKey = new Map(normalized.adminPages.map((page) => [page.key, page.icon]));
|
|
146
|
+
expect(byKey.get("inherits")).toBe("https://app.example.com/app-icon.svg");
|
|
147
|
+
expect(byKey.get("owns")).toBe("https://app.example.com/own-icon.svg");
|
|
148
|
+
});
|
|
149
|
+
it("leaves pages without an icon when neither the page nor the app declares one", () => {
|
|
150
|
+
const normalized = compileAppManifest(validManifest).normalizedRelease;
|
|
151
|
+
expect(normalized.adminPages[0]?.icon).toBeUndefined();
|
|
152
|
+
});
|
|
105
153
|
it("rejects webhook endpoints that target local infrastructure", () => {
|
|
106
154
|
expect(() => compileAppManifest({
|
|
107
155
|
...validManifest,
|
package/dist/contracts.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare const appManifestSchema: z.ZodPipe<z.ZodRecord<z.ZodString, z.Zod
|
|
|
44
44
|
min: z.ZodString;
|
|
45
45
|
max: z.ZodString;
|
|
46
46
|
}, z.core.$strict>;
|
|
47
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
47
48
|
scopes: z.ZodObject<{
|
|
48
49
|
requested: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
49
50
|
optional: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -54,6 +55,7 @@ export declare const appManifestSchema: z.ZodPipe<z.ZodRecord<z.ZodString, z.Zod
|
|
|
54
55
|
titleKey: z.ZodString;
|
|
55
56
|
path: z.ZodString;
|
|
56
57
|
entryUrl: z.ZodString;
|
|
58
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
57
59
|
}, z.core.$strict>>>;
|
|
58
60
|
slotExtensions: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
59
61
|
key: z.ZodString;
|
package/dist/contracts.js
CHANGED
|
@@ -91,6 +91,12 @@ const adminPageSchema = z
|
|
|
91
91
|
.trim()
|
|
92
92
|
.regex(/^\/[a-z0-9-_/]*$/),
|
|
93
93
|
entryUrl: httpsUrlSchema,
|
|
94
|
+
/**
|
|
95
|
+
* App-declared nav icon rendered as a remote `<img>` in admin chrome.
|
|
96
|
+
* HTTPS-only; the app-level {@link appManifestSchema} `icon` is the
|
|
97
|
+
* fallback when a page omits its own. Absent/invalid → generic app icon.
|
|
98
|
+
*/
|
|
99
|
+
icon: httpsUrlSchema.optional(),
|
|
94
100
|
})
|
|
95
101
|
.strict();
|
|
96
102
|
const slotExtensionSchema = z
|
|
@@ -126,6 +132,11 @@ export const appManifestSchema = manifestDisallowedKeySchema.pipe(z
|
|
|
126
132
|
schemaVersion: z.literal(APP_MANIFEST_SCHEMA_VERSION),
|
|
127
133
|
releaseVersion: semverLikeSchema,
|
|
128
134
|
apiCompatibility: z.object({ min: semverLikeSchema, max: semverLikeSchema }).strict(),
|
|
135
|
+
/**
|
|
136
|
+
* App-level default nav icon (HTTPS remote asset). Applied at normalize
|
|
137
|
+
* time to any admin page that omits its own `icon`.
|
|
138
|
+
*/
|
|
139
|
+
icon: httpsUrlSchema.optional(),
|
|
129
140
|
scopes: z
|
|
130
141
|
.object({
|
|
131
142
|
requested: z.array(scopeSchema).default([]),
|
|
@@ -36,6 +36,12 @@ export interface ResolvedAppPage {
|
|
|
36
36
|
title: string;
|
|
37
37
|
/** Host-rendered navigation label for the page's nav entry. */
|
|
38
38
|
navLabel: string;
|
|
39
|
+
/**
|
|
40
|
+
* App-declared nav icon (HTTPS remote asset). Already resolved through the
|
|
41
|
+
* app-level default at manifest-normalize time; absent when neither the page
|
|
42
|
+
* nor the app declared one (the host falls back to a generic app icon).
|
|
43
|
+
*/
|
|
44
|
+
icon?: string;
|
|
39
45
|
appLocale: string;
|
|
40
46
|
direction: AppTextDirection;
|
|
41
47
|
}
|
|
@@ -69,6 +69,7 @@ export function assembleInstalledExtensions(rows, localizationsByRelease, option
|
|
|
69
69
|
entryUrl: page.entryUrl,
|
|
70
70
|
title,
|
|
71
71
|
navLabel,
|
|
72
|
+
...(page.icon ? { icon: page.icon } : {}),
|
|
72
73
|
appLocale: locale.appLocale,
|
|
73
74
|
direction: locale.direction,
|
|
74
75
|
});
|
|
@@ -136,7 +137,8 @@ function parsePageDescriptor(value) {
|
|
|
136
137
|
const entryUrl = stringField(value.entryUrl);
|
|
137
138
|
if (!key || !titleKey || !path || !entryUrl)
|
|
138
139
|
return null;
|
|
139
|
-
|
|
140
|
+
const icon = stringField(value.icon);
|
|
141
|
+
return icon ? { key, titleKey, path, entryUrl, icon } : { key, titleKey, path, entryUrl };
|
|
140
142
|
}
|
|
141
143
|
function parseSlotDescriptor(value) {
|
|
142
144
|
const key = stringField(value.key);
|
|
@@ -67,6 +67,23 @@ describe("assembleInstalledExtensions", () => {
|
|
|
67
67
|
expect(page?.title).toBe("App Settings");
|
|
68
68
|
expect(page?.navLabel).toBe("Settings");
|
|
69
69
|
});
|
|
70
|
+
it("threads a page's icon from the descriptor when present", () => {
|
|
71
|
+
const withIcon = pageRow({
|
|
72
|
+
descriptor: {
|
|
73
|
+
key: "settings",
|
|
74
|
+
titleKey: "settings.title",
|
|
75
|
+
path: "/settings",
|
|
76
|
+
entryUrl: "https://app.example.com/settings",
|
|
77
|
+
icon: "https://app.example.com/settings-icon.svg",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const result = assembleInstalledExtensions([withIcon], localizations, { activeLocale: "en" });
|
|
81
|
+
expect(result.pages[0]?.icon).toBe("https://app.example.com/settings-icon.svg");
|
|
82
|
+
});
|
|
83
|
+
it("leaves icon undefined when the descriptor omits it", () => {
|
|
84
|
+
const result = assembleInstalledExtensions([pageRow()], localizations, { activeLocale: "en" });
|
|
85
|
+
expect(result.pages[0]?.icon).toBeUndefined();
|
|
86
|
+
});
|
|
70
87
|
it("filters out extensionApi-incompatible slot extensions", () => {
|
|
71
88
|
const incompatible = slotRow({
|
|
72
89
|
descriptor: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/apps",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -131,15 +131,15 @@
|
|
|
131
131
|
"drizzle-orm": "^0.45.2",
|
|
132
132
|
"hono": "^4.12.27",
|
|
133
133
|
"zod": "^4.4.3",
|
|
134
|
-
"@voyant-travel/admin": "^0.
|
|
134
|
+
"@voyant-travel/admin": "^0.128.0",
|
|
135
135
|
"@voyant-travel/admin-extension-sdk": "^0.2.0",
|
|
136
136
|
"@voyant-travel/core": "^0.130.0",
|
|
137
|
-
"@voyant-travel/custom-fields": "^0.2.
|
|
137
|
+
"@voyant-travel/custom-fields": "^0.2.9",
|
|
138
138
|
"@voyant-travel/finance-contracts": "^0.107.0",
|
|
139
|
-
"@voyant-travel/
|
|
140
|
-
"@voyant-travel/
|
|
141
|
-
"@voyant-travel/types": "^0.109.
|
|
142
|
-
"@voyant-travel/webhook-delivery": "^0.4.
|
|
139
|
+
"@voyant-travel/hono": "^0.131.2",
|
|
140
|
+
"@voyant-travel/db": "^0.117.0",
|
|
141
|
+
"@voyant-travel/types": "^0.109.8",
|
|
142
|
+
"@voyant-travel/webhook-delivery": "^0.4.7"
|
|
143
143
|
},
|
|
144
144
|
"devDependencies": {
|
|
145
145
|
"@types/node": "^25.5.2",
|