@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
- pages: [...manifest.admin.pages].sort(byKey),
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),
@@ -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,
@@ -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
- return { key, titleKey, path, entryUrl };
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.10.3",
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.127.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.8",
137
+ "@voyant-travel/custom-fields": "^0.2.9",
138
138
  "@voyant-travel/finance-contracts": "^0.107.0",
139
- "@voyant-travel/db": "^0.116.0",
140
- "@voyant-travel/hono": "^0.131.1",
141
- "@voyant-travel/types": "^0.109.7",
142
- "@voyant-travel/webhook-delivery": "^0.4.6"
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",