@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/dist/index.html +1 -1
- package/dist/{main.kr19xkv0.js → main.9qvpgp32.js} +43 -43
- package/package.json +1 -1
- package/src/app-context.tsx +6 -1
- package/src/fields.tsx +28 -6
- package/src/routes/_layout.tsx +17 -0
- package/src/types.ts +4 -0
package/package.json
CHANGED
package/src/app-context.tsx
CHANGED
|
@@ -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?: {
|
|
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
|
-
<
|
|
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);
|
package/src/routes/_layout.tsx
CHANGED
|
@@ -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 {
|