@pramen/cms-editor 0.0.32 → 0.0.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms-editor",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "Visual block/page editor for @pramen/cms — a standalone React SPA that talks to the CMS handlers over HTTP.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,7 +10,12 @@ import { Api, clearConfig, isTokenExpired, loadConfig, saveConfig, type Config }
10
10
  declare global {
11
11
  interface Window {
12
12
  /** Runtime config set by the host's /config.js (see @pramen/cms-editor build). */
13
- PRAMEN_CMS_EDITOR?: { signInUrl?: string };
13
+ PRAMEN_CMS_EDITOR?: {
14
+ signInUrl?: string;
15
+ /** Extra top-nav links to companion tools the host serves (e.g. a curation page).
16
+ * Rendered as plain external `<a>` links after the built-in tabs. */
17
+ extraNav?: { label: string; href: string }[];
18
+ };
14
19
  }
15
20
  }
16
21
 
package/src/fields.tsx CHANGED
@@ -73,12 +73,7 @@ function FieldInput({ def, value, onChange, api }: { def: FieldDefinition; value
73
73
  case "select":
74
74
  return (
75
75
  <FieldShell label={label}>
76
- <select className={CONTROL} value={(value as string) ?? ""} onChange={(e) => onChange(e.target.value || null)}>
77
- <option value="">—</option>
78
- {(def.options ?? []).map((o) => (
79
- <option key={o} value={o}>{o}</option>
80
- ))}
81
- </select>
76
+ <SelectField def={def} value={value as string | null} onChange={onChange} api={api} />
82
77
  </FieldShell>
83
78
  );
84
79
  case "media":
@@ -235,6 +230,33 @@ function Repeater({ def, value, onChange, api, label }: { def: FieldDefinition;
235
230
  );
236
231
  }
237
232
 
233
+ // A `select` field. Static `options` render as-is; when `optionsFrom` is set, the options are
234
+ // fetched once from that query handler (returns `{ value, label }[]`) — e.g. a live list of
235
+ // campaigns — so the editor never has to hardcode or copy identifiers by hand.
236
+ function SelectField({ def, value, onChange, api }: { def: FieldDefinition; value: string | null; onChange: (v: unknown) => void; api: Api }) {
237
+ const [dyn, setDyn] = useState<{ value: string; label: string }[] | null>(null);
238
+ const from = def.optionsFrom;
239
+ useEffect(() => {
240
+ if (!from) return;
241
+ let alive = true;
242
+ api
243
+ .call<{ value: string; label: string }[]>(from)
244
+ .then((r) => { if (alive) setDyn(Array.isArray(r) ? r : []); })
245
+ .catch(() => { if (alive) setDyn([]); });
246
+ return () => { alive = false; };
247
+ }, [from, api]);
248
+ const loading = Boolean(from) && dyn === null;
249
+ const opts = from ? dyn ?? [] : (def.options ?? []).map((o) => ({ value: o, label: o }));
250
+ return (
251
+ <select className={CONTROL} value={value ?? ""} onChange={(e) => onChange(e.target.value || null)}>
252
+ <option value="">{loading ? "Načítám…" : "—"}</option>
253
+ {opts.map((o) => (
254
+ <option key={o.value} value={o.value}>{o.label}</option>
255
+ ))}
256
+ </select>
257
+ );
258
+ }
259
+
238
260
  function MediaField({ value, onChange, api }: { value: string | null; onChange: (v: string | null) => void; api: Api }) {
239
261
  const [open, setOpen] = useState(false);
240
262
  const [media, setMedia] = useState<Media | null>(null);
@@ -21,6 +21,9 @@ export default function RootLayout() {
21
21
  const tabCls = (key: string) =>
22
22
  active === key ? "bg-surface-muted text-fg" : "text-fg-muted";
23
23
 
24
+ // Host-configured links to companion tools (e.g. a curation page), from /config.js.
25
+ const extraNav = typeof window !== "undefined" ? window.PRAMEN_CMS_EDITOR?.extraNav ?? [] : [];
26
+
24
27
  return (
25
28
  <>
26
29
  <div className="sticky top-0 z-10 flex items-center gap-4 bg-surface px-7 py-4">
@@ -35,6 +38,20 @@ export default function RootLayout() {
35
38
  <Button variant="ghost" size="sm" className={tabCls("users")} onPress={() => navigate("users")}>Users</Button>
36
39
  ) : null}
37
40
  <Button variant="ghost" size="sm" className={tabCls("settings")} onPress={() => navigate("settings")}>Settings</Button>
41
+ {extraNav.map((l) => (
42
+ // Companion tools live OUTSIDE this SPA (a separate static page/worker route), so
43
+ // open them in a new tab. A same-tab click would be caught by the client router
44
+ // (Navigation API) and fall to the in-app 404, since the path isn't an SPA route.
45
+ <a
46
+ key={l.href}
47
+ href={l.href}
48
+ target="_blank"
49
+ rel="noopener noreferrer"
50
+ className="rounded-md px-2.5 py-1.5 text-sm text-fg-muted transition-colors hover:bg-surface-muted hover:text-fg"
51
+ >
52
+ {l.label}
53
+ </a>
54
+ ))}
38
55
  </nav>
39
56
  <span className="ml-3 text-fg-subtle">{cfg.tenant}</span>
40
57
  <Button variant="ghost" size="sm" onPress={reconfigure}>sign out</Button>
package/src/types.ts CHANGED
@@ -26,6 +26,10 @@ export interface FieldDefinition {
26
26
  min?: number;
27
27
  max?: number;
28
28
  options?: string[];
29
+ /** For `select`: fetch options at edit time from a query handler of this name, which must
30
+ * return `{ value, label }[]`. Lets a select offer live data (e.g. existing campaigns)
31
+ * instead of a static list. Takes precedence over `options`. */
32
+ optionsFrom?: string;
29
33
  }
30
34
 
31
35
  export interface RegionDefinition {