@emdash-cms/admin 0.14.0 → 0.16.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/{LocaleDirectionProvider-D32XwRVp.js → LocaleDirectionProvider-BNEVyvgW.js} +17 -17
- package/dist/{LocaleDirectionProvider-D32XwRVp.js.map → LocaleDirectionProvider-BNEVyvgW.js.map} +1 -1
- package/dist/index.d.ts +58 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1940 -736
- package/dist/index.js.map +1 -1
- package/dist/locales/ar/messages.mjs +1 -1
- package/dist/locales/de/messages.mjs +1 -1
- package/dist/locales/en/messages.mjs +1 -1
- package/dist/locales/es-419/messages.mjs +1 -1
- package/dist/locales/es-ES/messages.mjs +1 -1
- package/dist/locales/eu/messages.mjs +1 -1
- package/dist/locales/fa/messages.mjs +1 -1
- package/dist/locales/fr/messages.mjs +1 -1
- package/dist/locales/id/messages.mjs +1 -1
- package/dist/locales/index.js +1 -1
- package/dist/locales/ja/messages.mjs +1 -1
- package/dist/locales/ko/messages.mjs +1 -1
- package/dist/locales/pl/messages.mjs +1 -1
- package/dist/locales/pseudo/messages.mjs +1 -1
- package/dist/locales/pt-BR/messages.mjs +1 -1
- package/dist/locales/zh-CN/messages.mjs +1 -1
- package/dist/locales/zh-TW/messages.mjs +1 -1
- package/dist/messages-6iBQvP4l.js +6 -0
- package/dist/messages-6iBQvP4l.js.map +1 -0
- package/dist/messages-B3qgKgae.js +6 -0
- package/dist/messages-B3qgKgae.js.map +1 -0
- package/dist/messages-B8kb5Oko.js +6 -0
- package/dist/messages-B8kb5Oko.js.map +1 -0
- package/dist/messages-BhSzAEGL.js +6 -0
- package/dist/messages-BhSzAEGL.js.map +1 -0
- package/dist/messages-C0wfBdmD.js +6 -0
- package/dist/messages-C0wfBdmD.js.map +1 -0
- package/dist/messages-CboPeBM0.js +6 -0
- package/dist/messages-CboPeBM0.js.map +1 -0
- package/dist/messages-CehVxd9q.js +6 -0
- package/dist/messages-CehVxd9q.js.map +1 -0
- package/dist/messages-D2R4PbiQ.js +6 -0
- package/dist/messages-D2R4PbiQ.js.map +1 -0
- package/dist/messages-DHha7f8K.js +6 -0
- package/dist/messages-DHha7f8K.js.map +1 -0
- package/dist/messages-DIehX9BP.js +6 -0
- package/dist/messages-DIehX9BP.js.map +1 -0
- package/dist/messages-DKdkwHEw.js +6 -0
- package/dist/messages-DKdkwHEw.js.map +1 -0
- package/dist/messages-DMJobUj_.js +6 -0
- package/dist/messages-DMJobUj_.js.map +1 -0
- package/dist/messages-Ddgurz-D.js +6 -0
- package/dist/messages-Ddgurz-D.js.map +1 -0
- package/dist/messages-LeijTgr3.js +6 -0
- package/dist/messages-LeijTgr3.js.map +1 -0
- package/dist/messages-S5UPBqIz.js +6 -0
- package/dist/messages-S5UPBqIz.js.map +1 -0
- package/dist/messages-vQ-B-9ct.js +6 -0
- package/dist/messages-vQ-B-9ct.js.map +1 -0
- package/dist/{plugins-qrjvPK4g.js → plugins-BZzztFdK.js} +3 -3
- package/dist/plugins-BZzztFdK.js.map +1 -0
- package/dist/styles.css +2 -2
- package/package.json +10 -9
- package/dist/messages-35lkat0c.js +0 -6
- package/dist/messages-35lkat0c.js.map +0 -1
- package/dist/messages-BSSaBU23.js +0 -6
- package/dist/messages-BSSaBU23.js.map +0 -1
- package/dist/messages-Boy6iMLn.js +0 -6
- package/dist/messages-Boy6iMLn.js.map +0 -1
- package/dist/messages-BwIV9n3i.js +0 -6
- package/dist/messages-BwIV9n3i.js.map +0 -1
- package/dist/messages-CGrAqIHP.js +0 -6
- package/dist/messages-CGrAqIHP.js.map +0 -1
- package/dist/messages-CgLmZ-Xe.js +0 -6
- package/dist/messages-CgLmZ-Xe.js.map +0 -1
- package/dist/messages-Ckz9OJjP.js +0 -6
- package/dist/messages-Ckz9OJjP.js.map +0 -1
- package/dist/messages-CrY9gw3p.js +0 -6
- package/dist/messages-CrY9gw3p.js.map +0 -1
- package/dist/messages-CtrOZaXa.js +0 -6
- package/dist/messages-CtrOZaXa.js.map +0 -1
- package/dist/messages-CxW3nwfb.js +0 -6
- package/dist/messages-CxW3nwfb.js.map +0 -1
- package/dist/messages-D7CyjcVC.js +0 -6
- package/dist/messages-D7CyjcVC.js.map +0 -1
- package/dist/messages-Dhy7Stmm.js +0 -6
- package/dist/messages-Dhy7Stmm.js.map +0 -1
- package/dist/messages-FyJi9vss.js +0 -6
- package/dist/messages-FyJi9vss.js.map +0 -1
- package/dist/messages-JMBMiRyI.js +0 -6
- package/dist/messages-JMBMiRyI.js.map +0 -1
- package/dist/messages-pPTbyAIO.js +0 -6
- package/dist/messages-pPTbyAIO.js.map +0 -1
- package/dist/messages-zl21KaEb.js +0 -6
- package/dist/messages-zl21KaEb.js.map +0 -1
- package/dist/plugins-qrjvPK4g.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-
|
|
2
|
-
import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-
|
|
1
|
+
import { c as fetchAuthMode, d as throwResponseError, i as fetchPlugins, l as fetchManifest, n as enablePlugin, o as API_BASE, r as fetchPlugin, s as apiFetch, t as disablePlugin, u as parseApiResponse } from "./plugins-BZzztFdK.js";
|
|
2
|
+
import { a as SUPPORTED_LOCALES, i as DEFAULT_LOCALE, l as resolveLocale, n as useLocale, o as SUPPORTED_LOCALE_CODES, t as LocaleDirectionProvider } from "./LocaleDirectionProvider-BNEVyvgW.js";
|
|
3
3
|
import "./locales/index.js";
|
|
4
|
-
import { Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
|
|
4
|
+
import { Autocomplete, Badge, Button, Checkbox, CommandPalette, Dialog, Input, InputArea, Label, LinkButton, Loader, Popover, Select, Sidebar as KumoSidebar, Switch, Tabs, Toast, Toasty, Tooltip, buttonVariants, useSidebar } from "@cloudflare/kumo";
|
|
5
5
|
import { i18n } from "@lingui/core";
|
|
6
6
|
import { I18nProvider, Trans, useLingui } from "@lingui/react";
|
|
7
7
|
import { QueryClient, QueryClientProvider, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
@@ -27,17 +27,19 @@ import { TableHeader } from "@tiptap/extension-table-header";
|
|
|
27
27
|
import { TableRow } from "@tiptap/extension-table-row";
|
|
28
28
|
import TextAlign from "@tiptap/extension-text-align";
|
|
29
29
|
import Typography from "@tiptap/extension-typography";
|
|
30
|
-
import { EditorContent, Node as Node$2, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
|
|
30
|
+
import { EditorContent, Node as Node$2, NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes as mergeAttributes$1, useEditor, useEditorState } from "@tiptap/react";
|
|
31
31
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
32
32
|
import StarterKit from "@tiptap/starter-kit";
|
|
33
33
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
34
34
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
35
35
|
import { createPortal } from "react-dom";
|
|
36
|
+
import CodeBlock$1 from "@tiptap/extension-code-block";
|
|
36
37
|
import { DragHandle } from "@tiptap/extension-drag-handle-react";
|
|
37
38
|
import { isAllowedUri } from "@tiptap/extension-link";
|
|
38
39
|
import { BlockRenderer } from "@emdash-cms/blocks";
|
|
39
40
|
import DOMPurify from "dompurify";
|
|
40
41
|
import { Marked, Renderer } from "marked";
|
|
42
|
+
import { checkEnvCompatibility, hostEnvFromVersions } from "@emdash-cms/registry-client/env";
|
|
41
43
|
import { useHotkeys } from "react-hotkeys-hook";
|
|
42
44
|
|
|
43
45
|
//#region src/components/ThemeProvider.tsx
|
|
@@ -1956,6 +1958,7 @@ async function fetchBylines(options) {
|
|
|
1956
1958
|
if (options?.search) params.set("search", options.search);
|
|
1957
1959
|
if (options?.isGuest !== void 0) params.set("isGuest", String(options.isGuest));
|
|
1958
1960
|
if (options?.userId) params.set("userId", options.userId);
|
|
1961
|
+
if (options?.locale) params.set("locale", options.locale);
|
|
1959
1962
|
if (options?.cursor) params.set("cursor", options.cursor);
|
|
1960
1963
|
if (options?.limit) params.set("limit", String(options.limit));
|
|
1961
1964
|
return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines${params.toString() ? `?${params}` : ""}`), "Failed to fetch bylines");
|
|
@@ -1984,6 +1987,26 @@ async function deleteByline(id) {
|
|
|
1984
1987
|
message: "Failed to delete byline"
|
|
1985
1988
|
}));
|
|
1986
1989
|
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Fetch every translation of a byline (siblings sharing the same
|
|
1992
|
+
* translation_group).
|
|
1993
|
+
*/
|
|
1994
|
+
async function fetchBylineTranslations(id) {
|
|
1995
|
+
return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`), "Failed to fetch byline translations");
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Create a new locale variant of a byline. The new row joins the source's
|
|
1999
|
+
* `translation_group`. Body defaults — slug, display name, avatar, website —
|
|
2000
|
+
* inherit from the source when omitted, so editors only have to fill in the
|
|
2001
|
+
* localized bio (and optionally a localized display name).
|
|
2002
|
+
*/
|
|
2003
|
+
async function createBylineTranslation(id, input) {
|
|
2004
|
+
return parseApiResponse(await apiFetch(`${API_BASE}/admin/bylines/${id}/translations`, {
|
|
2005
|
+
method: "POST",
|
|
2006
|
+
headers: { "Content-Type": "application/json" },
|
|
2007
|
+
body: JSON.stringify(input)
|
|
2008
|
+
}), "Failed to create byline translation");
|
|
2009
|
+
}
|
|
1987
2010
|
|
|
1988
2011
|
//#endregion
|
|
1989
2012
|
//#region src/lib/api/menus.ts
|
|
@@ -5525,6 +5548,392 @@ function Suggestion({ pluginKey = SuggestionPluginKey, editor, char = "@", allow
|
|
|
5525
5548
|
}
|
|
5526
5549
|
var index_default = Suggestion;
|
|
5527
5550
|
|
|
5551
|
+
//#endregion
|
|
5552
|
+
//#region src/components/editor/codeBlockLanguages.ts
|
|
5553
|
+
const CODE_BLOCK_LANGUAGES = [
|
|
5554
|
+
{
|
|
5555
|
+
id: "plaintext",
|
|
5556
|
+
label: "Plain text",
|
|
5557
|
+
aliases: [
|
|
5558
|
+
"text",
|
|
5559
|
+
"plain",
|
|
5560
|
+
"txt"
|
|
5561
|
+
]
|
|
5562
|
+
},
|
|
5563
|
+
{
|
|
5564
|
+
id: "astro",
|
|
5565
|
+
label: "Astro"
|
|
5566
|
+
},
|
|
5567
|
+
{
|
|
5568
|
+
id: "bash",
|
|
5569
|
+
label: "Bash",
|
|
5570
|
+
aliases: [
|
|
5571
|
+
"sh",
|
|
5572
|
+
"shell",
|
|
5573
|
+
"zsh"
|
|
5574
|
+
]
|
|
5575
|
+
},
|
|
5576
|
+
{
|
|
5577
|
+
id: "c",
|
|
5578
|
+
label: "C"
|
|
5579
|
+
},
|
|
5580
|
+
{
|
|
5581
|
+
id: "cpp",
|
|
5582
|
+
label: "C++",
|
|
5583
|
+
aliases: ["c++"]
|
|
5584
|
+
},
|
|
5585
|
+
{
|
|
5586
|
+
id: "csharp",
|
|
5587
|
+
label: "C#",
|
|
5588
|
+
aliases: ["cs", "c#"]
|
|
5589
|
+
},
|
|
5590
|
+
{
|
|
5591
|
+
id: "css",
|
|
5592
|
+
label: "CSS"
|
|
5593
|
+
},
|
|
5594
|
+
{
|
|
5595
|
+
id: "diff",
|
|
5596
|
+
label: "Diff",
|
|
5597
|
+
aliases: ["patch"]
|
|
5598
|
+
},
|
|
5599
|
+
{
|
|
5600
|
+
id: "dockerfile",
|
|
5601
|
+
label: "Dockerfile",
|
|
5602
|
+
aliases: ["docker"]
|
|
5603
|
+
},
|
|
5604
|
+
{
|
|
5605
|
+
id: "go",
|
|
5606
|
+
label: "Go",
|
|
5607
|
+
aliases: ["golang"]
|
|
5608
|
+
},
|
|
5609
|
+
{
|
|
5610
|
+
id: "graphql",
|
|
5611
|
+
label: "GraphQL",
|
|
5612
|
+
aliases: ["gql"]
|
|
5613
|
+
},
|
|
5614
|
+
{
|
|
5615
|
+
id: "html",
|
|
5616
|
+
label: "HTML"
|
|
5617
|
+
},
|
|
5618
|
+
{
|
|
5619
|
+
id: "java",
|
|
5620
|
+
label: "Java"
|
|
5621
|
+
},
|
|
5622
|
+
{
|
|
5623
|
+
id: "javascript",
|
|
5624
|
+
label: "JavaScript",
|
|
5625
|
+
aliases: ["js"]
|
|
5626
|
+
},
|
|
5627
|
+
{
|
|
5628
|
+
id: "json",
|
|
5629
|
+
label: "JSON"
|
|
5630
|
+
},
|
|
5631
|
+
{
|
|
5632
|
+
id: "jsx",
|
|
5633
|
+
label: "JSX"
|
|
5634
|
+
},
|
|
5635
|
+
{
|
|
5636
|
+
id: "kotlin",
|
|
5637
|
+
label: "Kotlin",
|
|
5638
|
+
aliases: ["kt"]
|
|
5639
|
+
},
|
|
5640
|
+
{
|
|
5641
|
+
id: "markdown",
|
|
5642
|
+
label: "Markdown",
|
|
5643
|
+
aliases: ["md"]
|
|
5644
|
+
},
|
|
5645
|
+
{
|
|
5646
|
+
id: "mdx",
|
|
5647
|
+
label: "MDX"
|
|
5648
|
+
},
|
|
5649
|
+
{
|
|
5650
|
+
id: "php",
|
|
5651
|
+
label: "PHP"
|
|
5652
|
+
},
|
|
5653
|
+
{
|
|
5654
|
+
id: "python",
|
|
5655
|
+
label: "Python",
|
|
5656
|
+
aliases: ["py"]
|
|
5657
|
+
},
|
|
5658
|
+
{
|
|
5659
|
+
id: "ruby",
|
|
5660
|
+
label: "Ruby",
|
|
5661
|
+
aliases: ["rb"]
|
|
5662
|
+
},
|
|
5663
|
+
{
|
|
5664
|
+
id: "rust",
|
|
5665
|
+
label: "Rust",
|
|
5666
|
+
aliases: ["rs"]
|
|
5667
|
+
},
|
|
5668
|
+
{
|
|
5669
|
+
id: "scss",
|
|
5670
|
+
label: "SCSS",
|
|
5671
|
+
aliases: ["sass"]
|
|
5672
|
+
},
|
|
5673
|
+
{
|
|
5674
|
+
id: "sql",
|
|
5675
|
+
label: "SQL"
|
|
5676
|
+
},
|
|
5677
|
+
{
|
|
5678
|
+
id: "svelte",
|
|
5679
|
+
label: "Svelte"
|
|
5680
|
+
},
|
|
5681
|
+
{
|
|
5682
|
+
id: "swift",
|
|
5683
|
+
label: "Swift"
|
|
5684
|
+
},
|
|
5685
|
+
{
|
|
5686
|
+
id: "toml",
|
|
5687
|
+
label: "TOML"
|
|
5688
|
+
},
|
|
5689
|
+
{
|
|
5690
|
+
id: "tsx",
|
|
5691
|
+
label: "TSX"
|
|
5692
|
+
},
|
|
5693
|
+
{
|
|
5694
|
+
id: "typescript",
|
|
5695
|
+
label: "TypeScript",
|
|
5696
|
+
aliases: ["ts"]
|
|
5697
|
+
},
|
|
5698
|
+
{
|
|
5699
|
+
id: "vue",
|
|
5700
|
+
label: "Vue"
|
|
5701
|
+
},
|
|
5702
|
+
{
|
|
5703
|
+
id: "xml",
|
|
5704
|
+
label: "XML"
|
|
5705
|
+
},
|
|
5706
|
+
{
|
|
5707
|
+
id: "yaml",
|
|
5708
|
+
label: "YAML",
|
|
5709
|
+
aliases: ["yml"]
|
|
5710
|
+
}
|
|
5711
|
+
];
|
|
5712
|
+
/**
|
|
5713
|
+
* Look up a language by id or alias. Case-insensitive.
|
|
5714
|
+
* Returns the canonical entry, or `null` if not found in the curated list.
|
|
5715
|
+
*/
|
|
5716
|
+
function findLanguage(value) {
|
|
5717
|
+
if (!value) return null;
|
|
5718
|
+
const needle = value.trim().toLowerCase();
|
|
5719
|
+
if (!needle) return null;
|
|
5720
|
+
for (const lang of CODE_BLOCK_LANGUAGES) {
|
|
5721
|
+
if (lang.id === needle) return lang;
|
|
5722
|
+
if (lang.aliases?.includes(needle)) return lang;
|
|
5723
|
+
}
|
|
5724
|
+
return null;
|
|
5725
|
+
}
|
|
5726
|
+
/**
|
|
5727
|
+
* Normalize a user-entered language string to a canonical id where possible.
|
|
5728
|
+
* Unknown inputs are sanitized to a single safe class token: lowercased,
|
|
5729
|
+
* trimmed, with any character outside `[a-z0-9_-]` (including whitespace,
|
|
5730
|
+
* dots, slashes, etc.) collapsed to `-`. This keeps the stored value safe
|
|
5731
|
+
* to interpolate into a `language-{id}` CSS class without splitting on
|
|
5732
|
+
* whitespace.
|
|
5733
|
+
*
|
|
5734
|
+
* Examples:
|
|
5735
|
+
* normalizeLanguage("TypeScript") -> "typescript" (canonical id)
|
|
5736
|
+
* normalizeLanguage("ts") -> "typescript" (alias)
|
|
5737
|
+
* normalizeLanguage("Objective C") -> "objective-c" (sanitized)
|
|
5738
|
+
* normalizeLanguage("F#") -> "f-" (sanitized)
|
|
5739
|
+
* normalizeLanguage("") -> undefined
|
|
5740
|
+
*/
|
|
5741
|
+
const DISALLOWED_CHARS_RE = /[^a-z0-9_-]+/g;
|
|
5742
|
+
const LEADING_TRAILING_HYPHENS_RE = /^-+|-+$/g;
|
|
5743
|
+
function normalizeLanguage(value) {
|
|
5744
|
+
if (!value) return void 0;
|
|
5745
|
+
const trimmed = value.trim();
|
|
5746
|
+
if (!trimmed) return void 0;
|
|
5747
|
+
const match = findLanguage(trimmed);
|
|
5748
|
+
if (match) return match.id;
|
|
5749
|
+
return trimmed.toLowerCase().replace(DISALLOWED_CHARS_RE, "-").replace(LEADING_TRAILING_HYPHENS_RE, "") || void 0;
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Human-readable label for a stored language id. Falls back to the id itself
|
|
5753
|
+
* for unknown values so the editor never shows "undefined".
|
|
5754
|
+
*/
|
|
5755
|
+
function languageLabel(value) {
|
|
5756
|
+
if (!value) return "Plain text";
|
|
5757
|
+
const match = findLanguage(value);
|
|
5758
|
+
if (match) return match.label;
|
|
5759
|
+
return value;
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
//#endregion
|
|
5763
|
+
//#region src/components/editor/CodeBlockNode.tsx
|
|
5764
|
+
/**
|
|
5765
|
+
* Code block node with language picker.
|
|
5766
|
+
*
|
|
5767
|
+
* Wraps the base `@tiptap/extension-code-block` with a React node view that
|
|
5768
|
+
* overlays a small language chip in the top-right corner. Clicking the chip
|
|
5769
|
+
* opens a popover with a Kumo Autocomplete: a free-form text input plus a
|
|
5770
|
+
* filtered list of curated language suggestions. The value is persisted on
|
|
5771
|
+
* the node's `language` attribute and round-trips through Portable Text as
|
|
5772
|
+
* `block.language`.
|
|
5773
|
+
*
|
|
5774
|
+
* The picker accepts arbitrary strings (not restricted to the curated list)
|
|
5775
|
+
* so that less common languages can still be used. Free-form input is
|
|
5776
|
+
* sanitized to a single safe CSS class token via `normalizeLanguage` so the
|
|
5777
|
+
* frontend's `language-{id}` class stays well-formed.
|
|
5778
|
+
*/
|
|
5779
|
+
const LANGUAGE_ITEMS = CODE_BLOCK_LANGUAGES.map((l) => l.label);
|
|
5780
|
+
function filterLanguages(item, query) {
|
|
5781
|
+
if (!query) return true;
|
|
5782
|
+
const needle = query.toLowerCase();
|
|
5783
|
+
const lang = CODE_BLOCK_LANGUAGES.find((l) => l.label === item);
|
|
5784
|
+
if (!lang) return false;
|
|
5785
|
+
if (lang.label.toLowerCase().includes(needle)) return true;
|
|
5786
|
+
if (lang.id.toLowerCase().includes(needle)) return true;
|
|
5787
|
+
return lang.aliases?.some((alias) => alias.toLowerCase().includes(needle)) ?? false;
|
|
5788
|
+
}
|
|
5789
|
+
function CodeBlockNodeView({ node, updateAttributes, selected }) {
|
|
5790
|
+
const { _: _t } = useLingui();
|
|
5791
|
+
const [isEditing, setIsEditing] = React$1.useState(false);
|
|
5792
|
+
const storedLanguage = typeof node.attrs.language === "string" ? node.attrs.language : "";
|
|
5793
|
+
const [draft, setDraft] = React$1.useState(() => languageLabel(storedLanguage));
|
|
5794
|
+
const popoverRef = React$1.useRef(null);
|
|
5795
|
+
React$1.useEffect(() => {
|
|
5796
|
+
if (!isEditing) setDraft(languageLabel(storedLanguage));
|
|
5797
|
+
}, [storedLanguage, isEditing]);
|
|
5798
|
+
const openPicker = React$1.useCallback(() => {
|
|
5799
|
+
setDraft(storedLanguage ? languageLabel(storedLanguage) : "");
|
|
5800
|
+
setIsEditing(true);
|
|
5801
|
+
}, [storedLanguage]);
|
|
5802
|
+
const closePicker = React$1.useCallback(() => {
|
|
5803
|
+
setIsEditing(false);
|
|
5804
|
+
setDraft(languageLabel(storedLanguage));
|
|
5805
|
+
}, [storedLanguage]);
|
|
5806
|
+
const commit = React$1.useCallback((value) => {
|
|
5807
|
+
updateAttributes({ language: normalizeLanguage(value ?? draft) ?? null });
|
|
5808
|
+
setIsEditing(false);
|
|
5809
|
+
}, [draft, updateAttributes]);
|
|
5810
|
+
const handleKeyDown = (e) => {
|
|
5811
|
+
if (e.key === "Enter") {
|
|
5812
|
+
e.preventDefault();
|
|
5813
|
+
commit();
|
|
5814
|
+
} else if (e.key === "Escape") {
|
|
5815
|
+
e.preventDefault();
|
|
5816
|
+
closePicker();
|
|
5817
|
+
}
|
|
5818
|
+
};
|
|
5819
|
+
React$1.useEffect(() => {
|
|
5820
|
+
if (!isEditing) return void 0;
|
|
5821
|
+
const onMouseDown = (event) => {
|
|
5822
|
+
const target = event.target instanceof Node ? event.target : null;
|
|
5823
|
+
if (popoverRef.current && target && !popoverRef.current.contains(target)) closePicker();
|
|
5824
|
+
};
|
|
5825
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
5826
|
+
return () => document.removeEventListener("mousedown", onMouseDown);
|
|
5827
|
+
}, [isEditing, closePicker]);
|
|
5828
|
+
const label = languageLabel(storedLanguage);
|
|
5829
|
+
const chipPersistent = isEditing || Boolean(storedLanguage) || selected;
|
|
5830
|
+
return /* @__PURE__ */ jsxs(NodeViewWrapper, {
|
|
5831
|
+
className: "group relative my-4",
|
|
5832
|
+
"data-language": storedLanguage || void 0,
|
|
5833
|
+
children: [/* @__PURE__ */ jsx("pre", {
|
|
5834
|
+
className: "emdash-code-block",
|
|
5835
|
+
children: /* @__PURE__ */ jsx(NodeViewContent, { as: "code" })
|
|
5836
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
5837
|
+
className: "absolute end-2 top-2 select-none",
|
|
5838
|
+
contentEditable: false,
|
|
5839
|
+
children: isEditing ? /* @__PURE__ */ jsxs("div", {
|
|
5840
|
+
ref: popoverRef,
|
|
5841
|
+
className: "flex items-center gap-1 rounded-md border bg-kumo-overlay p-1 shadow-lg",
|
|
5842
|
+
onKeyDown: handleKeyDown,
|
|
5843
|
+
children: [
|
|
5844
|
+
/* @__PURE__ */ jsxs(Autocomplete, {
|
|
5845
|
+
items: LANGUAGE_ITEMS,
|
|
5846
|
+
value: draft,
|
|
5847
|
+
onValueChange: (next) => setDraft(next),
|
|
5848
|
+
filter: filterLanguages,
|
|
5849
|
+
children: [/* @__PURE__ */ jsx(Autocomplete.InputGroup, {
|
|
5850
|
+
size: "sm",
|
|
5851
|
+
placeholder: _t({
|
|
5852
|
+
id: "vXIe7J",
|
|
5853
|
+
message: "Language"
|
|
5854
|
+
})
|
|
5855
|
+
}), /* @__PURE__ */ jsxs(Autocomplete.Content, {
|
|
5856
|
+
sideOffset: 4,
|
|
5857
|
+
children: [/* @__PURE__ */ jsx(Autocomplete.List, { children: (item) => /* @__PURE__ */ jsx(Autocomplete.Item, {
|
|
5858
|
+
value: item,
|
|
5859
|
+
children: item
|
|
5860
|
+
}, item) }), /* @__PURE__ */ jsx(Autocomplete.Empty, { children: _t({
|
|
5861
|
+
id: "4TVVsS",
|
|
5862
|
+
message: "No matches"
|
|
5863
|
+
}) })]
|
|
5864
|
+
})]
|
|
5865
|
+
}),
|
|
5866
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5867
|
+
type: "button",
|
|
5868
|
+
variant: "ghost",
|
|
5869
|
+
shape: "square",
|
|
5870
|
+
className: "h-7 w-7",
|
|
5871
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
5872
|
+
onClick: () => commit(),
|
|
5873
|
+
title: _t({
|
|
5874
|
+
id: "Ek42QX",
|
|
5875
|
+
message: "Apply language"
|
|
5876
|
+
}),
|
|
5877
|
+
"aria-label": _t({
|
|
5878
|
+
id: "Ek42QX",
|
|
5879
|
+
message: "Apply language"
|
|
5880
|
+
}),
|
|
5881
|
+
children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" })
|
|
5882
|
+
}),
|
|
5883
|
+
/* @__PURE__ */ jsx(Button, {
|
|
5884
|
+
type: "button",
|
|
5885
|
+
variant: "ghost",
|
|
5886
|
+
shape: "square",
|
|
5887
|
+
className: "h-7 w-7",
|
|
5888
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
5889
|
+
onClick: closePicker,
|
|
5890
|
+
title: _t({
|
|
5891
|
+
id: "dEgA5A",
|
|
5892
|
+
message: "Cancel"
|
|
5893
|
+
}),
|
|
5894
|
+
"aria-label": _t({
|
|
5895
|
+
id: "dEgA5A",
|
|
5896
|
+
message: "Cancel"
|
|
5897
|
+
}),
|
|
5898
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
5899
|
+
})
|
|
5900
|
+
]
|
|
5901
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
5902
|
+
type: "button",
|
|
5903
|
+
tabIndex: chipPersistent ? 0 : -1,
|
|
5904
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
5905
|
+
onClick: openPicker,
|
|
5906
|
+
className: "rounded-md border bg-kumo-overlay/90 px-2 py-1 text-xs text-kumo-subtle opacity-0 transition-opacity hover:text-kumo-strong focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-kumo-brand group-hover:opacity-100 data-[persistent=true]:opacity-100",
|
|
5907
|
+
"data-persistent": chipPersistent ? "true" : "false",
|
|
5908
|
+
title: _t({
|
|
5909
|
+
id: "9cbOH7",
|
|
5910
|
+
message: "Set language"
|
|
5911
|
+
}),
|
|
5912
|
+
"aria-label": _t({
|
|
5913
|
+
id: "q/A3pF",
|
|
5914
|
+
message: "Set language (current: {label})",
|
|
5915
|
+
values: { label }
|
|
5916
|
+
}),
|
|
5917
|
+
"aria-hidden": chipPersistent ? void 0 : true,
|
|
5918
|
+
children: storedLanguage ? label : _t({
|
|
5919
|
+
id: "9cbOH7",
|
|
5920
|
+
message: "Set language"
|
|
5921
|
+
})
|
|
5922
|
+
})
|
|
5923
|
+
})]
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* TipTap extension: code block with an inline language picker node view.
|
|
5928
|
+
*
|
|
5929
|
+
* Drop-in replacement for StarterKit's default `codeBlock`. Configure
|
|
5930
|
+
* `StarterKit.configure({ codeBlock: false })` and add this extension to
|
|
5931
|
+
* the editor's extensions array.
|
|
5932
|
+
*/
|
|
5933
|
+
const CodeBlockExtension = CodeBlock$1.extend({ addNodeView() {
|
|
5934
|
+
return ReactNodeViewRenderer(CodeBlockNodeView);
|
|
5935
|
+
} });
|
|
5936
|
+
|
|
5528
5937
|
//#endregion
|
|
5529
5938
|
//#region src/components/editor/BlockMenu.tsx
|
|
5530
5939
|
const blockTransforms = [
|
|
@@ -7549,19 +7958,17 @@ function PluginBlockModal({ block, initialValues, onClose, onInsert }) {
|
|
|
7549
7958
|
size: dialogSize,
|
|
7550
7959
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
7551
7960
|
className: "flex items-start justify-between gap-4 mb-4",
|
|
7552
|
-
children: [/* @__PURE__ */
|
|
7961
|
+
children: [/* @__PURE__ */ jsx(Dialog.Title, {
|
|
7553
7962
|
className: "text-lg font-semibold leading-none tracking-tight",
|
|
7554
|
-
children:
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
}
|
|
7562
|
-
|
|
7563
|
-
block?.label || ""
|
|
7564
|
-
]
|
|
7963
|
+
children: isEditing ? _t2({
|
|
7964
|
+
id: "N6j2JH",
|
|
7965
|
+
message: "Edit {0}",
|
|
7966
|
+
values: { 0: block?.label || "" }
|
|
7967
|
+
}) : _t2({
|
|
7968
|
+
id: "yQNfsE",
|
|
7969
|
+
message: "Insert {0}",
|
|
7970
|
+
values: { 0: block?.label || "" }
|
|
7971
|
+
})
|
|
7565
7972
|
}), /* @__PURE__ */ jsx(Dialog.Close, {
|
|
7566
7973
|
"aria-label": _t2({
|
|
7567
7974
|
id: "yz7wBu",
|
|
@@ -8153,6 +8560,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
|
|
|
8153
8560
|
color: "#3b82f6",
|
|
8154
8561
|
width: 2
|
|
8155
8562
|
},
|
|
8563
|
+
codeBlock: false,
|
|
8156
8564
|
link: {
|
|
8157
8565
|
openOnClick: false,
|
|
8158
8566
|
enableClickSelection: true,
|
|
@@ -8160,6 +8568,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
|
|
|
8160
8568
|
},
|
|
8161
8569
|
underline: {}
|
|
8162
8570
|
}),
|
|
8571
|
+
CodeBlockExtension,
|
|
8163
8572
|
ImageExtension,
|
|
8164
8573
|
MarkdownLinkExtension,
|
|
8165
8574
|
PluginBlockExtension,
|
|
@@ -8262,8 +8671,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
|
|
|
8262
8671
|
if (!editor || !pluginBlockModal) return;
|
|
8263
8672
|
const { id, ...data } = values;
|
|
8264
8673
|
const editPos = editingBlockPosRef.current;
|
|
8265
|
-
if (editPos !== null) {
|
|
8266
|
-
const { tr } = editor.state;
|
|
8674
|
+
if (editPos !== null) editor.chain().command(({ tr }) => {
|
|
8267
8675
|
const node = tr.doc.nodeAt(editPos);
|
|
8268
8676
|
if (node?.type.name === "pluginBlock") {
|
|
8269
8677
|
tr.setNodeMarkup(editPos, void 0, {
|
|
@@ -8271,9 +8679,11 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
|
|
|
8271
8679
|
id: typeof id === "string" ? id : node.attrs.id,
|
|
8272
8680
|
data
|
|
8273
8681
|
});
|
|
8274
|
-
|
|
8682
|
+
return true;
|
|
8275
8683
|
}
|
|
8276
|
-
|
|
8684
|
+
return false;
|
|
8685
|
+
}).run();
|
|
8686
|
+
else editor.chain().focus().insertContent({
|
|
8277
8687
|
type: "pluginBlock",
|
|
8278
8688
|
attrs: {
|
|
8279
8689
|
blockType: pluginBlockModal.type,
|
|
@@ -9908,6 +10318,7 @@ function termExactMatches(term, input) {
|
|
|
9908
10318
|
* - Checkbox tree for hierarchical taxonomies (categories)
|
|
9909
10319
|
* - Tag input for flat taxonomies (tags)
|
|
9910
10320
|
*/
|
|
10321
|
+
const EMPTY_TERMS = [];
|
|
9911
10322
|
/**
|
|
9912
10323
|
* Fetch taxonomy definitions
|
|
9913
10324
|
*/
|
|
@@ -9975,11 +10386,13 @@ function CategoryCheckboxTree({ term, level = 0, selectedIds, onToggle }) {
|
|
|
9975
10386
|
function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, label }) {
|
|
9976
10387
|
const { _: _t } = useLingui();
|
|
9977
10388
|
const [input, setInput] = React$1.useState("");
|
|
10389
|
+
const [isOpen, setIsOpen] = React$1.useState(false);
|
|
9978
10390
|
const selectedTerms = terms.filter((term) => selectedIds.has(term.id));
|
|
9979
10391
|
const trimmedInput = input.trim();
|
|
9980
10392
|
const suggestions = React$1.useMemo(() => {
|
|
9981
|
-
|
|
9982
|
-
|
|
10393
|
+
const availableTerms = terms.filter((term) => !selectedIds.has(term.id));
|
|
10394
|
+
if (!trimmedInput) return availableTerms.slice(0, 5);
|
|
10395
|
+
return availableTerms.filter((term) => termMatches(term, trimmedInput)).slice(0, 5);
|
|
9983
10396
|
}, [
|
|
9984
10397
|
trimmedInput,
|
|
9985
10398
|
terms,
|
|
@@ -9993,11 +10406,18 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
|
|
|
9993
10406
|
const handleSelect = (term) => {
|
|
9994
10407
|
onAdd(term.id);
|
|
9995
10408
|
setInput("");
|
|
10409
|
+
setIsOpen(false);
|
|
9996
10410
|
};
|
|
9997
10411
|
const handleCreate = () => {
|
|
9998
10412
|
if (!trimmedInput || isCreating) return;
|
|
9999
10413
|
onCreate(trimmedInput);
|
|
10000
10414
|
setInput("");
|
|
10415
|
+
setIsOpen(false);
|
|
10416
|
+
};
|
|
10417
|
+
const handleBlur = (e) => {
|
|
10418
|
+
const nextFocused = e.relatedTarget;
|
|
10419
|
+
if (nextFocused instanceof Node && e.currentTarget.contains(nextFocused)) return;
|
|
10420
|
+
setIsOpen(false);
|
|
10001
10421
|
};
|
|
10002
10422
|
const handleKeyDown = (e) => {
|
|
10003
10423
|
if (e.key === "Enter") {
|
|
@@ -10026,9 +10446,14 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
|
|
|
10026
10446
|
}, term.id))
|
|
10027
10447
|
}), /* @__PURE__ */ jsxs("div", {
|
|
10028
10448
|
className: "relative",
|
|
10449
|
+
onBlur: handleBlur,
|
|
10029
10450
|
children: [/* @__PURE__ */ jsx(Input, {
|
|
10030
10451
|
value: input,
|
|
10031
|
-
onChange: (e) =>
|
|
10452
|
+
onChange: (e) => {
|
|
10453
|
+
setInput(e.target.value);
|
|
10454
|
+
setIsOpen(true);
|
|
10455
|
+
},
|
|
10456
|
+
onFocus: () => setIsOpen(true),
|
|
10032
10457
|
onKeyDown: handleKeyDown,
|
|
10033
10458
|
placeholder: _t({
|
|
10034
10459
|
id: "SnJVrE",
|
|
@@ -10040,15 +10465,17 @@ function TagInput({ terms, selectedIds, onAdd, onRemove, onCreate, isCreating, l
|
|
|
10040
10465
|
values: { label }
|
|
10041
10466
|
}),
|
|
10042
10467
|
className: "text-sm"
|
|
10043
|
-
}), (suggestions.length > 0 || showCreateOption) && /* @__PURE__ */ jsxs("div", {
|
|
10468
|
+
}), isOpen && (suggestions.length > 0 || showCreateOption) && /* @__PURE__ */ jsxs("div", {
|
|
10044
10469
|
className: "absolute top-full start-0 end-0 mt-1 bg-kumo-overlay border rounded-md shadow-lg z-10",
|
|
10045
10470
|
children: [suggestions.map((term) => /* @__PURE__ */ jsx("button", {
|
|
10046
10471
|
type: "button",
|
|
10472
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
10047
10473
|
onClick: () => handleSelect(term),
|
|
10048
10474
|
className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint",
|
|
10049
10475
|
children: term.label
|
|
10050
10476
|
}, term.id)), showCreateOption && /* @__PURE__ */ jsxs("button", {
|
|
10051
10477
|
type: "button",
|
|
10478
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
10052
10479
|
onClick: handleCreate,
|
|
10053
10480
|
disabled: isCreating,
|
|
10054
10481
|
className: "w-full text-start px-3 py-2 text-sm hover:bg-kumo-tint text-kumo-accent flex items-center gap-1 border-t",
|
|
@@ -10074,11 +10501,11 @@ function TaxonomySection({ taxonomy, collection, entryId, onChange }) {
|
|
|
10074
10501
|
const toastManager = Toast.useToastManager();
|
|
10075
10502
|
const [newCategoryLabel, setNewCategoryLabel] = React$1.useState("");
|
|
10076
10503
|
const [showCategoryInput, setShowCategoryInput] = React$1.useState(false);
|
|
10077
|
-
const { data: terms =
|
|
10504
|
+
const { data: terms = EMPTY_TERMS } = useQuery({
|
|
10078
10505
|
queryKey: ["taxonomy-terms", taxonomy.name],
|
|
10079
10506
|
queryFn: () => fetchTerms$1(taxonomy.name)
|
|
10080
10507
|
});
|
|
10081
|
-
const { data: entryTerms =
|
|
10508
|
+
const { data: entryTerms = EMPTY_TERMS } = useQuery({
|
|
10082
10509
|
queryKey: [
|
|
10083
10510
|
"entry-terms",
|
|
10084
10511
|
collection,
|
|
@@ -10373,7 +10800,7 @@ function formatScheduledDate(dateStr) {
|
|
|
10373
10800
|
/**
|
|
10374
10801
|
* Content editor with dynamic field rendering
|
|
10375
10802
|
*/
|
|
10376
|
-
function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, supportsPreview = false, currentUser, users, onAuthorChange, availableBylines, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
|
|
10803
|
+
function ContentEditor({ collection, collectionLabel, item, fields, isNew, entryLocale, isSaving, onSave, onAutosave, isAutosaving, lastAutosaveAt, onPublish, onUnpublish, onDiscardDraft, onSchedule, onUnschedule, isScheduling, supportsDrafts = false, supportsRevisions = false, supportsPreview = false, currentUser, users, onAuthorChange, availableBylines, availableBylinesLoaded, selectedBylines, onBylinesChange, onQuickCreateByline, onQuickEditByline, onDelete, isDeleting, i18n, translations, onTranslate, pluginBlocks, hasSeo = false, onSeoChange, manifest }) {
|
|
10377
10804
|
const { _: _t } = useLingui();
|
|
10378
10805
|
const navigate = useNavigate$1();
|
|
10379
10806
|
const [formData, setFormData] = React$1.useState(item?.data || {});
|
|
@@ -10384,6 +10811,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10384
10811
|
bylineId: entry.byline.id,
|
|
10385
10812
|
roleLabel: entry.roleLabel
|
|
10386
10813
|
})) ?? []);
|
|
10814
|
+
const [bylinesTouched, setBylinesTouched] = React$1.useState(false);
|
|
10387
10815
|
const [portableTextEditor, setPortableTextEditor] = React$1.useState(null);
|
|
10388
10816
|
const [blockSidebarPanel, setBlockSidebarPanel] = React$1.useState(null);
|
|
10389
10817
|
const handleBlockSidebarOpen = React$1.useCallback((panel) => {
|
|
@@ -10425,6 +10853,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10425
10853
|
bylines: nextBylines
|
|
10426
10854
|
}));
|
|
10427
10855
|
pendingAutosaveStateRef.current = null;
|
|
10856
|
+
setBylinesTouched(false);
|
|
10428
10857
|
}
|
|
10429
10858
|
const itemDataString = React$1.useMemo(() => item ? JSON.stringify(item.data) : "", [item?.data]);
|
|
10430
10859
|
React$1.useEffect(() => {
|
|
@@ -10446,6 +10875,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10446
10875
|
})) ?? []
|
|
10447
10876
|
}));
|
|
10448
10877
|
pendingAutosaveStateRef.current = null;
|
|
10878
|
+
setBylinesTouched(false);
|
|
10449
10879
|
}
|
|
10450
10880
|
}, [
|
|
10451
10881
|
item?.updatedAt,
|
|
@@ -10455,6 +10885,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10455
10885
|
]);
|
|
10456
10886
|
const activeBylines = isNew ? selectedBylines ?? [] : internalBylines;
|
|
10457
10887
|
const handleBylinesChange = React$1.useCallback((next) => {
|
|
10888
|
+
setBylinesTouched(true);
|
|
10458
10889
|
if (isNew) {
|
|
10459
10890
|
onBylinesChange?.(next);
|
|
10460
10891
|
return;
|
|
@@ -10497,13 +10928,13 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10497
10928
|
if (hasInvalidUrls(formDataRef.current)) return;
|
|
10498
10929
|
const payload = {
|
|
10499
10930
|
data: formDataRef.current,
|
|
10500
|
-
slug: slugRef.current || void 0
|
|
10501
|
-
bylines: activeBylines
|
|
10931
|
+
slug: slugRef.current || void 0
|
|
10502
10932
|
};
|
|
10933
|
+
if (bylinesTouched) payload.bylines = activeBylines;
|
|
10503
10934
|
pendingAutosaveStateRef.current = serializeEditorState({
|
|
10504
10935
|
data: payload.data,
|
|
10505
10936
|
slug: payload.slug || "",
|
|
10506
|
-
bylines:
|
|
10937
|
+
bylines: activeBylines
|
|
10507
10938
|
});
|
|
10508
10939
|
onAutosave(payload);
|
|
10509
10940
|
}, AUTOSAVE_DELAY);
|
|
@@ -10519,6 +10950,7 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10519
10950
|
isSaving,
|
|
10520
10951
|
isAutosaving,
|
|
10521
10952
|
activeBylines,
|
|
10953
|
+
bylinesTouched,
|
|
10522
10954
|
hasInvalidUrls
|
|
10523
10955
|
]);
|
|
10524
10956
|
const handleSubmit = (e) => {
|
|
@@ -10528,11 +10960,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
10528
10960
|
clearTimeout(autosaveTimeoutRef.current);
|
|
10529
10961
|
autosaveTimeoutRef.current = null;
|
|
10530
10962
|
}
|
|
10531
|
-
|
|
10963
|
+
const payload = {
|
|
10532
10964
|
data: formData,
|
|
10533
|
-
slug: slug || void 0
|
|
10534
|
-
|
|
10535
|
-
|
|
10965
|
+
slug: slug || void 0
|
|
10966
|
+
};
|
|
10967
|
+
if (isNew || bylinesTouched) payload.bylines = activeBylines;
|
|
10968
|
+
onSave?.(payload);
|
|
10536
10969
|
};
|
|
10537
10970
|
const [isLoadingPreview, setIsLoadingPreview] = React$1.useState(false);
|
|
10538
10971
|
const urlPattern = manifest?.collections[collection]?.urlPattern;
|
|
@@ -11066,9 +11499,12 @@ function ContentEditor({ collection, collectionLabel, item, fields, isNew, isSav
|
|
|
11066
11499
|
}), /* @__PURE__ */ jsx(BylineCreditsEditor, {
|
|
11067
11500
|
credits: activeBylines,
|
|
11068
11501
|
bylines: availableBylines ?? [],
|
|
11502
|
+
bylinesLoaded: availableBylinesLoaded,
|
|
11069
11503
|
onChange: handleBylinesChange,
|
|
11070
11504
|
onQuickCreate: onQuickCreateByline,
|
|
11071
|
-
onQuickEdit: onQuickEditByline
|
|
11505
|
+
onQuickEdit: onQuickEditByline,
|
|
11506
|
+
entryLocale: item?.locale ?? entryLocale,
|
|
11507
|
+
i18n
|
|
11072
11508
|
})]
|
|
11073
11509
|
}),
|
|
11074
11510
|
i18n && item && !isNew && /* @__PURE__ */ jsx("div", {
|
|
@@ -11725,7 +12161,7 @@ function FileFieldRenderer({ id, label, value, onChange, required, allowedMimeTy
|
|
|
11725
12161
|
]
|
|
11726
12162
|
});
|
|
11727
12163
|
}
|
|
11728
|
-
function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit }) {
|
|
12164
|
+
function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuickEdit, entryLocale, i18n, bylinesLoaded = true }) {
|
|
11729
12165
|
const { _: _t7 } = useLingui();
|
|
11730
12166
|
const [selectedBylineId, setSelectedBylineId] = React$1.useState("");
|
|
11731
12167
|
const [quickName, setQuickName] = React$1.useState("");
|
|
@@ -11768,6 +12204,27 @@ function BylineCreditsEditor({ credits, bylines, onChange, onQuickCreate, onQuic
|
|
|
11768
12204
|
return /* @__PURE__ */ jsxs("div", {
|
|
11769
12205
|
className: "space-y-3",
|
|
11770
12206
|
children: [
|
|
12207
|
+
!!i18n && i18n.locales.length > 1 && bylinesLoaded && bylines.length === 0 && !!entryLocale && /* @__PURE__ */ jsxs("div", {
|
|
12208
|
+
className: "rounded border border-dashed p-3 text-sm space-y-2",
|
|
12209
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
12210
|
+
className: "text-kumo-subtle",
|
|
12211
|
+
children: _t7({
|
|
12212
|
+
id: "Tmb8Fw",
|
|
12213
|
+
message: "No bylines available in {entryLocale}. Create a variant from the Bylines page before crediting one on this entry.",
|
|
12214
|
+
values: { entryLocale }
|
|
12215
|
+
})
|
|
12216
|
+
}), /* @__PURE__ */ jsx(RouterLinkButton, {
|
|
12217
|
+
to: "/bylines",
|
|
12218
|
+
search: { locale: entryLocale ?? void 0 },
|
|
12219
|
+
variant: "secondary",
|
|
12220
|
+
size: "sm",
|
|
12221
|
+
children: _t7({
|
|
12222
|
+
id: "yXD9HA",
|
|
12223
|
+
message: "Manage bylines in {entryLocale}",
|
|
12224
|
+
values: { entryLocale }
|
|
12225
|
+
})
|
|
12226
|
+
})]
|
|
12227
|
+
}),
|
|
11771
12228
|
/* @__PURE__ */ jsxs("div", {
|
|
11772
12229
|
className: "flex gap-2",
|
|
11773
12230
|
children: [/* @__PURE__ */ jsx(Select, {
|
|
@@ -17591,6 +18048,85 @@ function formatInstallCount(count) {
|
|
|
17591
18048
|
return String(count);
|
|
17592
18049
|
}
|
|
17593
18050
|
|
|
18051
|
+
//#endregion
|
|
18052
|
+
//#region src/lib/markdown.ts
|
|
18053
|
+
const HTML_ESCAPE_MAP = {
|
|
18054
|
+
"&": "&",
|
|
18055
|
+
"<": "<",
|
|
18056
|
+
">": ">",
|
|
18057
|
+
"\"": """,
|
|
18058
|
+
"'": "'"
|
|
18059
|
+
};
|
|
18060
|
+
const HTML_ESCAPE_RE = /[&<>"']/g;
|
|
18061
|
+
function escapeHtml(str) {
|
|
18062
|
+
return str.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch]);
|
|
18063
|
+
}
|
|
18064
|
+
const renderer = new Renderer();
|
|
18065
|
+
renderer.link = function({ href, tokens }) {
|
|
18066
|
+
const inner = this.parser.parseInline(tokens);
|
|
18067
|
+
if (!SAFE_URL_RE.test(href)) return inner;
|
|
18068
|
+
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
|
|
18069
|
+
};
|
|
18070
|
+
renderer.image = ({ text }) => escapeHtml(text);
|
|
18071
|
+
renderer.html = () => "";
|
|
18072
|
+
const md = new Marked({
|
|
18073
|
+
renderer,
|
|
18074
|
+
async: false
|
|
18075
|
+
});
|
|
18076
|
+
/** Allowed tags and attributes for DOMPurify — only standard markdown output. */
|
|
18077
|
+
const SANITIZE_CONFIG = {
|
|
18078
|
+
ALLOWED_TAGS: [
|
|
18079
|
+
"h1",
|
|
18080
|
+
"h2",
|
|
18081
|
+
"h3",
|
|
18082
|
+
"h4",
|
|
18083
|
+
"h5",
|
|
18084
|
+
"h6",
|
|
18085
|
+
"p",
|
|
18086
|
+
"a",
|
|
18087
|
+
"ul",
|
|
18088
|
+
"ol",
|
|
18089
|
+
"li",
|
|
18090
|
+
"blockquote",
|
|
18091
|
+
"pre",
|
|
18092
|
+
"code",
|
|
18093
|
+
"em",
|
|
18094
|
+
"strong",
|
|
18095
|
+
"del",
|
|
18096
|
+
"br",
|
|
18097
|
+
"hr",
|
|
18098
|
+
"table",
|
|
18099
|
+
"thead",
|
|
18100
|
+
"tbody",
|
|
18101
|
+
"tr",
|
|
18102
|
+
"th",
|
|
18103
|
+
"td",
|
|
18104
|
+
"details",
|
|
18105
|
+
"summary",
|
|
18106
|
+
"sup",
|
|
18107
|
+
"sub"
|
|
18108
|
+
],
|
|
18109
|
+
ALLOWED_ATTR: [
|
|
18110
|
+
"href",
|
|
18111
|
+
"target",
|
|
18112
|
+
"rel"
|
|
18113
|
+
]
|
|
18114
|
+
};
|
|
18115
|
+
/**
|
|
18116
|
+
* Render untrusted Markdown to a sanitized HTML string safe for
|
|
18117
|
+
* `dangerouslySetInnerHTML`.
|
|
18118
|
+
*
|
|
18119
|
+
* Defense in depth: the `marked` renderer drops raw HTML, drops images, and
|
|
18120
|
+
* emits only `https?:` links (escaped, with `rel="noopener noreferrer"`);
|
|
18121
|
+
* DOMPurify then re-sanitizes the output against a tag/attribute allowlist
|
|
18122
|
+
* (no `script`/`style`/`img`/`iframe`, no inline styles or event handlers).
|
|
18123
|
+
*/
|
|
18124
|
+
function renderMarkdown(markdown) {
|
|
18125
|
+
const result = md.parse(markdown);
|
|
18126
|
+
const html = typeof result === "string" ? result : "";
|
|
18127
|
+
return DOMPurify.sanitize(html, SANITIZE_CONFIG);
|
|
18128
|
+
}
|
|
18129
|
+
|
|
17594
18130
|
//#endregion
|
|
17595
18131
|
//#region src/components/CapabilityConsentDialog.tsx
|
|
17596
18132
|
/**
|
|
@@ -17600,10 +18136,10 @@ function formatInstallCount(count) {
|
|
|
17600
18136
|
* Lists each requested capability with a human-readable explanation.
|
|
17601
18137
|
* User must explicitly confirm before the action proceeds.
|
|
17602
18138
|
*/
|
|
17603
|
-
function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
|
|
18139
|
+
function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts, newCapabilities = [], newlyPublicRoutes = [], auditVerdict, isPending = false, error, onConfirm, onCancel }) {
|
|
17604
18140
|
const { _: _t } = useLingui();
|
|
17605
18141
|
const newSet = new Set(newCapabilities);
|
|
17606
|
-
const isUpdate = mode === "update" || newCapabilities.length > 0;
|
|
18142
|
+
const isUpdate = mode === "update" || newCapabilities.length > 0 || newlyPublicRoutes.length > 0;
|
|
17607
18143
|
return /* @__PURE__ */ jsxs("div", {
|
|
17608
18144
|
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
17609
18145
|
role: "dialog",
|
|
@@ -17644,31 +18180,60 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
|
|
|
17644
18180
|
}),
|
|
17645
18181
|
/* @__PURE__ */ jsxs("div", {
|
|
17646
18182
|
className: "px-6 py-4 space-y-3",
|
|
17647
|
-
children: [
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
className: cn(isNew
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17657
|
-
|
|
17658
|
-
|
|
18183
|
+
children: [
|
|
18184
|
+
capabilities.map((cap) => {
|
|
18185
|
+
const isNew = newSet.has(cap);
|
|
18186
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18187
|
+
className: cn("flex items-start gap-3 rounded-md p-2 text-sm", isNew ? "bg-warning/10 border border-warning/30" : "bg-kumo-tint/50"),
|
|
18188
|
+
children: [/* @__PURE__ */ jsx(ShieldCheck, { className: cn("mt-0.5 h-4 w-4 shrink-0", isNew ? "text-warning" : "text-kumo-subtle") }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
18189
|
+
className: cn(isNew && "font-medium"),
|
|
18190
|
+
children: describeCapability(cap, allowedHosts)
|
|
18191
|
+
}), isNew && /* @__PURE__ */ jsx("span", {
|
|
18192
|
+
className: "ms-2 text-xs text-warning font-medium",
|
|
18193
|
+
children: _t({
|
|
18194
|
+
id: "p6NueD",
|
|
18195
|
+
message: "NEW"
|
|
18196
|
+
})
|
|
18197
|
+
})] })]
|
|
18198
|
+
}, cap);
|
|
18199
|
+
}),
|
|
18200
|
+
newlyPublicRoutes.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
18201
|
+
className: "rounded-md border border-warning/30 bg-warning/10 p-3 text-sm",
|
|
18202
|
+
children: [
|
|
18203
|
+
/* @__PURE__ */ jsxs("div", {
|
|
18204
|
+
className: "flex items-center gap-2 font-medium text-warning",
|
|
18205
|
+
children: [/* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }), _t({
|
|
18206
|
+
id: "NXruDe",
|
|
18207
|
+
message: "New public routes"
|
|
18208
|
+
})]
|
|
18209
|
+
}),
|
|
18210
|
+
/* @__PURE__ */ jsx("p", {
|
|
18211
|
+
className: "mt-1 text-xs text-kumo-subtle",
|
|
18212
|
+
children: _t({
|
|
18213
|
+
id: "qCUL4n",
|
|
18214
|
+
message: "This update exposes the following routes without authentication:"
|
|
18215
|
+
})
|
|
18216
|
+
}),
|
|
18217
|
+
/* @__PURE__ */ jsx("ul", {
|
|
18218
|
+
className: "mt-2 space-y-1 ps-5 text-xs",
|
|
18219
|
+
children: newlyPublicRoutes.map((route) => /* @__PURE__ */ jsx("li", {
|
|
18220
|
+
className: "list-disc font-mono",
|
|
18221
|
+
children: route
|
|
18222
|
+
}, route))
|
|
17659
18223
|
})
|
|
17660
|
-
|
|
17661
|
-
},
|
|
17662
|
-
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
|
|
18224
|
+
]
|
|
18225
|
+
}),
|
|
18226
|
+
auditVerdict && auditVerdict !== "pass" && /* @__PURE__ */ jsxs("div", {
|
|
18227
|
+
className: cn("flex items-center gap-2 rounded-md p-3 text-sm mt-2", auditVerdict === "warn" ? "bg-warning/10 text-warning" : "bg-kumo-danger/10 text-kumo-danger"),
|
|
18228
|
+
children: [auditVerdict === "warn" ? /* @__PURE__ */ jsx(Warning, { className: "h-4 w-4 shrink-0" }) : /* @__PURE__ */ jsx(ShieldWarning, { className: "h-4 w-4 shrink-0" }), /* @__PURE__ */ jsx("span", { children: auditVerdict === "warn" ? _t({
|
|
18229
|
+
id: "zPUuMZ",
|
|
18230
|
+
message: "Security audit flagged potential concerns with this plugin."
|
|
18231
|
+
}) : _t({
|
|
18232
|
+
id: "nvVzbM",
|
|
18233
|
+
message: "Security audit flagged this plugin as potentially unsafe."
|
|
18234
|
+
}) })]
|
|
18235
|
+
})
|
|
18236
|
+
]
|
|
17672
18237
|
}),
|
|
17673
18238
|
/* @__PURE__ */ jsx(DialogError, {
|
|
17674
18239
|
message: error,
|
|
@@ -17707,6 +18272,474 @@ function CapabilityConsentDialog({ mode, pluginName, capabilities, allowedHosts,
|
|
|
17707
18272
|
});
|
|
17708
18273
|
}
|
|
17709
18274
|
|
|
18275
|
+
//#endregion
|
|
18276
|
+
//#region src/lib/api/registry.ts
|
|
18277
|
+
let cachedDiscovery = null;
|
|
18278
|
+
async function getDiscoveryClient(config) {
|
|
18279
|
+
if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
|
|
18280
|
+
const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
|
|
18281
|
+
const discovery = new DiscoveryClient({
|
|
18282
|
+
aggregatorUrl: config.aggregatorUrl,
|
|
18283
|
+
acceptLabelers: config.acceptLabelers
|
|
18284
|
+
});
|
|
18285
|
+
const wrapped = {
|
|
18286
|
+
async searchPackages(opts) {
|
|
18287
|
+
return discovery.searchPackages({
|
|
18288
|
+
q: opts.q,
|
|
18289
|
+
cursor: opts.cursor,
|
|
18290
|
+
limit: opts.limit
|
|
18291
|
+
});
|
|
18292
|
+
},
|
|
18293
|
+
async resolvePackage(handle, slug) {
|
|
18294
|
+
return discovery.resolvePackage({
|
|
18295
|
+
handle,
|
|
18296
|
+
slug
|
|
18297
|
+
});
|
|
18298
|
+
},
|
|
18299
|
+
async getPackage(did, slug) {
|
|
18300
|
+
return discovery.getPackage({
|
|
18301
|
+
did,
|
|
18302
|
+
slug
|
|
18303
|
+
});
|
|
18304
|
+
},
|
|
18305
|
+
async getLatestRelease(did, slug) {
|
|
18306
|
+
return discovery.getLatestRelease({
|
|
18307
|
+
did,
|
|
18308
|
+
package: slug
|
|
18309
|
+
});
|
|
18310
|
+
},
|
|
18311
|
+
async listReleases(did, slug, opts) {
|
|
18312
|
+
return discovery.listReleases({
|
|
18313
|
+
did,
|
|
18314
|
+
package: slug,
|
|
18315
|
+
cursor: opts?.cursor,
|
|
18316
|
+
limit: opts?.limit
|
|
18317
|
+
});
|
|
18318
|
+
}
|
|
18319
|
+
};
|
|
18320
|
+
cachedDiscovery = {
|
|
18321
|
+
config,
|
|
18322
|
+
client: wrapped
|
|
18323
|
+
};
|
|
18324
|
+
return wrapped;
|
|
18325
|
+
}
|
|
18326
|
+
/**
|
|
18327
|
+
* Returns whether a release should be considered installable given the
|
|
18328
|
+
* configured policy. Currently implements the minimum-release-age check
|
|
18329
|
+
* described in RFC 0001's "Pre-label gap and launch tempo" section,
|
|
18330
|
+
* plus the `minimumReleaseAgeExclude` allowlist.
|
|
18331
|
+
*
|
|
18332
|
+
* Returns `false` (release blocked) when the policy is configured but
|
|
18333
|
+
* the release is missing a valid `indexedAt` -- we fail closed rather
|
|
18334
|
+
* than silently letting unbounded-age releases through.
|
|
18335
|
+
*/
|
|
18336
|
+
function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
|
|
18337
|
+
if (!policy?.minimumReleaseAgeSeconds) return true;
|
|
18338
|
+
if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
|
|
18339
|
+
const indexedAt = Date.parse(release.indexedAt);
|
|
18340
|
+
if (!Number.isFinite(indexedAt)) return false;
|
|
18341
|
+
return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
|
|
18342
|
+
}
|
|
18343
|
+
/**
|
|
18344
|
+
* Canonicalize a capabilities list for set-style comparison. Mirrors
|
|
18345
|
+
* the server-side helper `canonicalCapabilitiesForDriftCheck` in
|
|
18346
|
+
* `packages/core/src/registry/config.ts` -- both sides must produce
|
|
18347
|
+
* the same canonical shape so the install handler's drift check is
|
|
18348
|
+
* stable across reorderings, duplicates, and junk entries.
|
|
18349
|
+
*
|
|
18350
|
+
* Filters non-strings, deduplicates, and sorts lexically.
|
|
18351
|
+
*/
|
|
18352
|
+
function canonicalCapabilitiesForDriftCheck(value) {
|
|
18353
|
+
if (!Array.isArray(value)) return [];
|
|
18354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18355
|
+
for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
|
|
18356
|
+
return [...seen].toSorted();
|
|
18357
|
+
}
|
|
18358
|
+
/**
|
|
18359
|
+
* Matches a `(publisher_did, slug)` against the
|
|
18360
|
+
* `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
|
|
18361
|
+
* of the same name in `packages/core/src/registry/config.ts`.
|
|
18362
|
+
*
|
|
18363
|
+
* DID-only on purpose: handles are aggregator-supplied envelope data
|
|
18364
|
+
* and accepting them as a trust input would let a compromised
|
|
18365
|
+
* aggregator bypass the holdback by claiming any handle for any
|
|
18366
|
+
* package. DIDs are tied to the AT URI of the record itself.
|
|
18367
|
+
*
|
|
18368
|
+
* Entries from the config list have already been lowercased at
|
|
18369
|
+
* manifest build time, so this only needs to lowercase the runtime
|
|
18370
|
+
* values for comparison.
|
|
18371
|
+
*/
|
|
18372
|
+
function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
|
|
18373
|
+
if (!exclude || exclude.length === 0) return false;
|
|
18374
|
+
const didLower = publisherDid.toLowerCase();
|
|
18375
|
+
const fullDid = `${didLower}/${slug.toLowerCase()}`;
|
|
18376
|
+
for (const entry of exclude) {
|
|
18377
|
+
if (entry === didLower) return true;
|
|
18378
|
+
if (entry === fullDid) return true;
|
|
18379
|
+
}
|
|
18380
|
+
return false;
|
|
18381
|
+
}
|
|
18382
|
+
/**
|
|
18383
|
+
* The FAIR-recognised long-form section keys, in display order. Publishers may
|
|
18384
|
+
* also ship unrecognised keys (the lexicon's `sections` map is open), but the
|
|
18385
|
+
* admin renders only this known set so an aggregator can't inject a section
|
|
18386
|
+
* with an attacker-chosen heading; everything else is ignored.
|
|
18387
|
+
*/
|
|
18388
|
+
const SECTION_ORDER = [
|
|
18389
|
+
"description",
|
|
18390
|
+
"installation",
|
|
18391
|
+
"faq",
|
|
18392
|
+
"changelog",
|
|
18393
|
+
"security"
|
|
18394
|
+
];
|
|
18395
|
+
/**
|
|
18396
|
+
* Select the non-empty long-form sections off a package profile, in
|
|
18397
|
+
* `SECTION_ORDER`. `profile.sections` is a lexicon-validated map of Markdown
|
|
18398
|
+
* strings (or `null` when the aggregator returned a non-conforming record), so
|
|
18399
|
+
* each value is narrowed to a non-whitespace string before inclusion. Empty,
|
|
18400
|
+
* missing, whitespace-only, and non-string entries are dropped, so callers can
|
|
18401
|
+
* suppress the whole sections UI when the result is empty.
|
|
18402
|
+
*/
|
|
18403
|
+
function presentSections(profile) {
|
|
18404
|
+
const sections = profile?.sections;
|
|
18405
|
+
if (!sections || typeof sections !== "object") return [];
|
|
18406
|
+
const map = sections;
|
|
18407
|
+
const out = [];
|
|
18408
|
+
for (const key of SECTION_ORDER) {
|
|
18409
|
+
const value = map[key];
|
|
18410
|
+
if (typeof value === "string" && value.trim().length > 0) out.push({
|
|
18411
|
+
key,
|
|
18412
|
+
markdown: value
|
|
18413
|
+
});
|
|
18414
|
+
}
|
|
18415
|
+
return out;
|
|
18416
|
+
}
|
|
18417
|
+
/**
|
|
18418
|
+
* Narrow a release record's `sbom` field to the fields the admin renders.
|
|
18419
|
+
* Returns `null` unless the value is an object carrying at least one usable
|
|
18420
|
+
* field (`format` or `url`); every field is independently optional per the
|
|
18421
|
+
* lexicon. `sbom` is lexicon-validated at the DiscoveryClient boundary, but the
|
|
18422
|
+
* record is a publisher pass-through, so its inner shape still needs narrowing.
|
|
18423
|
+
*/
|
|
18424
|
+
function extractSbom(value) {
|
|
18425
|
+
if (!value || typeof value !== "object") return null;
|
|
18426
|
+
const v = value;
|
|
18427
|
+
const sbom = {};
|
|
18428
|
+
if (typeof v.format === "string" && v.format.length > 0) sbom.format = v.format;
|
|
18429
|
+
if (typeof v.url === "string" && v.url.length > 0) sbom.url = v.url;
|
|
18430
|
+
if (typeof v.checksum === "string") sbom.checksum = v.checksum;
|
|
18431
|
+
if (!sbom.format && !sbom.url) return null;
|
|
18432
|
+
return sbom;
|
|
18433
|
+
}
|
|
18434
|
+
/**
|
|
18435
|
+
* Validate an SBOM document URL for use in a download `href`. Returns the
|
|
18436
|
+
* normalised URL only when it is an absolute `http(s)` URL; everything else
|
|
18437
|
+
* (relative, `javascript:`, `data:`, non-string) returns `null`. The release
|
|
18438
|
+
* record is a remote pass-through, so an unsanitised SBOM `href` would be
|
|
18439
|
+
* stored XSS in the authenticated admin origin. The browser fetches the SBOM
|
|
18440
|
+
* client-side on click — no server proxy, so SSRF isn't a concern here.
|
|
18441
|
+
*/
|
|
18442
|
+
function sbomDownloadHref(value) {
|
|
18443
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
18444
|
+
let parsed;
|
|
18445
|
+
try {
|
|
18446
|
+
parsed = new URL(value);
|
|
18447
|
+
} catch {
|
|
18448
|
+
return null;
|
|
18449
|
+
}
|
|
18450
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
|
|
18451
|
+
return parsed.href;
|
|
18452
|
+
}
|
|
18453
|
+
async function searchRegistryPackages(config, opts) {
|
|
18454
|
+
return (await getDiscoveryClient(config)).searchPackages(opts);
|
|
18455
|
+
}
|
|
18456
|
+
async function resolveRegistryPackage(config, handle, slug) {
|
|
18457
|
+
return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
|
|
18458
|
+
}
|
|
18459
|
+
async function getRegistryPackage(config, did, slug) {
|
|
18460
|
+
return (await getDiscoveryClient(config)).getPackage(did, slug);
|
|
18461
|
+
}
|
|
18462
|
+
async function listRegistryReleases(config, did, slug, opts) {
|
|
18463
|
+
return (await getDiscoveryClient(config)).listReleases(did, slug, opts);
|
|
18464
|
+
}
|
|
18465
|
+
/**
|
|
18466
|
+
* Derive the host environment versions (`env:emdash`, `env:astro`) the running
|
|
18467
|
+
* EmDash install advertises, so a release's `requires` constraints can be
|
|
18468
|
+
* evaluated client-side before offering install. Reads the already-fetched
|
|
18469
|
+
* admin manifest (`version`, `astroVersion`) rather than issuing a second
|
|
18470
|
+
* request. The dev-skip / astro-omit rule is shared with the server gate via
|
|
18471
|
+
* `hostEnvFromVersions`.
|
|
18472
|
+
*/
|
|
18473
|
+
function hostEnvFromManifest(manifest) {
|
|
18474
|
+
return hostEnvFromVersions(manifest?.version, manifest?.astroVersion);
|
|
18475
|
+
}
|
|
18476
|
+
/**
|
|
18477
|
+
* Resolve a publisher DID to its claimed handle using the same
|
|
18478
|
+
* `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
|
|
18479
|
+
* `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
|
|
18480
|
+
* domain points back to the same DID) is part of the resolver --
|
|
18481
|
+
* `LocalActorResolver` returns the sentinel `"handle.invalid"` when
|
|
18482
|
+
* the `alsoKnownAs` handle is present but doesn't round-trip.
|
|
18483
|
+
*
|
|
18484
|
+
* Three distinct outcomes the UI can render:
|
|
18485
|
+
*
|
|
18486
|
+
* - `{ status: "ok", handle }` — verified handle, round-trip OK.
|
|
18487
|
+
* - `{ status: "invalid" }` — DID claims a handle but it doesn't
|
|
18488
|
+
* resolve back. The publisher's handle setup is broken; the admin
|
|
18489
|
+
* should see a clear "Invalid handle" indicator rather than the
|
|
18490
|
+
* raw DID.
|
|
18491
|
+
* - `{ status: "missing" }` — no handle claimed at all (no
|
|
18492
|
+
* `alsoKnownAs`), or the DID document couldn't be fetched (network
|
|
18493
|
+
* error, unsupported DID method).
|
|
18494
|
+
*/
|
|
18495
|
+
let actorResolver = null;
|
|
18496
|
+
async function getActorResolver() {
|
|
18497
|
+
if (actorResolver) return actorResolver;
|
|
18498
|
+
const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
|
|
18499
|
+
actorResolver = new LocalActorResolver({
|
|
18500
|
+
handleResolver: new CompositeHandleResolver({ methods: {
|
|
18501
|
+
dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
|
|
18502
|
+
http: new WellKnownHandleResolver()
|
|
18503
|
+
} }),
|
|
18504
|
+
didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
|
|
18505
|
+
plc: new PlcDidDocumentResolver(),
|
|
18506
|
+
web: new WebDidDocumentResolver()
|
|
18507
|
+
} })
|
|
18508
|
+
});
|
|
18509
|
+
return actorResolver;
|
|
18510
|
+
}
|
|
18511
|
+
/**
|
|
18512
|
+
* localStorage-backed cache for DID→handle resolutions. Handles are
|
|
18513
|
+
* stable for hours-to-days in practice, but bound the cache so a
|
|
18514
|
+
* compromised handle eventually flips back to "invalid" without a
|
|
18515
|
+
* forced refresh. 24h matches the typical atproto handle TTL.
|
|
18516
|
+
*
|
|
18517
|
+
* Failures (network errors, unsupported DID method) are *not* cached --
|
|
18518
|
+
* those should retry on the next render.
|
|
18519
|
+
*/
|
|
18520
|
+
const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
18521
|
+
const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
|
|
18522
|
+
function isCachedResolution(value) {
|
|
18523
|
+
if (typeof value !== "object" || value === null) return false;
|
|
18524
|
+
const candidate = value;
|
|
18525
|
+
return typeof candidate.expiresAt === "number" && typeof candidate.resolution === "object" && candidate.resolution !== null;
|
|
18526
|
+
}
|
|
18527
|
+
function readHandleCache(did) {
|
|
18528
|
+
if (typeof localStorage === "undefined") return null;
|
|
18529
|
+
try {
|
|
18530
|
+
const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
|
|
18531
|
+
if (!raw) return null;
|
|
18532
|
+
const parsed = JSON.parse(raw);
|
|
18533
|
+
if (!isCachedResolution(parsed) || parsed.expiresAt < Date.now()) return null;
|
|
18534
|
+
return parsed.resolution;
|
|
18535
|
+
} catch {
|
|
18536
|
+
return null;
|
|
18537
|
+
}
|
|
18538
|
+
}
|
|
18539
|
+
function writeHandleCache(did, resolution) {
|
|
18540
|
+
if (typeof localStorage === "undefined") return;
|
|
18541
|
+
try {
|
|
18542
|
+
const entry = {
|
|
18543
|
+
resolution,
|
|
18544
|
+
expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
|
|
18545
|
+
};
|
|
18546
|
+
localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
|
|
18547
|
+
} catch {}
|
|
18548
|
+
}
|
|
18549
|
+
async function resolveDidToHandle(did) {
|
|
18550
|
+
const cached = readHandleCache(did);
|
|
18551
|
+
if (cached) return cached;
|
|
18552
|
+
let result;
|
|
18553
|
+
try {
|
|
18554
|
+
const resolved = await (await getActorResolver()).resolve(did);
|
|
18555
|
+
if (resolved.handle === "handle.invalid") result = { status: "invalid" };
|
|
18556
|
+
else if (resolved.handle) result = {
|
|
18557
|
+
status: "ok",
|
|
18558
|
+
handle: resolved.handle
|
|
18559
|
+
};
|
|
18560
|
+
else result = { status: "missing" };
|
|
18561
|
+
} catch (err) {
|
|
18562
|
+
console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
|
|
18563
|
+
return { status: "missing" };
|
|
18564
|
+
}
|
|
18565
|
+
writeHandleCache(did, result);
|
|
18566
|
+
return result;
|
|
18567
|
+
}
|
|
18568
|
+
const ARTIFACT_PROXY_ENDPOINT = `${API_BASE}/admin/plugins/registry/artifact`;
|
|
18569
|
+
/**
|
|
18570
|
+
* Build the URL of the server-side artifact proxy for an artifact addressed by
|
|
18571
|
+
* its `(did, slug, version, kind, index)` coordinates. The browser never sends
|
|
18572
|
+
* the publisher's URL — the proxy resolves the *declared* URL from the release
|
|
18573
|
+
* record, applies SSRF defences, enforces an image content-type allowlist, and
|
|
18574
|
+
* serves the bytes back same-origin.
|
|
18575
|
+
*
|
|
18576
|
+
* Empty `version` (latest) and `index` (non-screenshot kinds) are omitted.
|
|
18577
|
+
*/
|
|
18578
|
+
function artifactProxyUrl(coords) {
|
|
18579
|
+
const params = new URLSearchParams();
|
|
18580
|
+
params.set("did", coords.did);
|
|
18581
|
+
params.set("slug", coords.slug);
|
|
18582
|
+
params.set("kind", coords.kind);
|
|
18583
|
+
if (coords.version) params.set("version", coords.version);
|
|
18584
|
+
if (coords.kind === "screenshot" && coords.index !== void 0) params.set("index", String(coords.index));
|
|
18585
|
+
return `${ARTIFACT_PROXY_ENDPOINT}?${params.toString()}`;
|
|
18586
|
+
}
|
|
18587
|
+
/**
|
|
18588
|
+
* Narrow one entry of a release's `artifacts` map to the fields we render.
|
|
18589
|
+
* Returns `null` when the value isn't an object carrying a usable `url`
|
|
18590
|
+
* (presence gate), keeping only the dimensions for layout.
|
|
18591
|
+
*
|
|
18592
|
+
* Records are lexicon-validated at the DiscoveryClient boundary, but
|
|
18593
|
+
* `artifacts` is an aggregator pass-through, so each entry still needs
|
|
18594
|
+
* shape-narrowing.
|
|
18595
|
+
*/
|
|
18596
|
+
function asMediaArtifact(value) {
|
|
18597
|
+
if (!value || typeof value !== "object") return null;
|
|
18598
|
+
const v = value;
|
|
18599
|
+
if (typeof v.url !== "string" || v.url.length === 0) return null;
|
|
18600
|
+
const artifact = {};
|
|
18601
|
+
if (typeof v.width === "number") artifact.width = v.width;
|
|
18602
|
+
if (typeof v.height === "number") artifact.height = v.height;
|
|
18603
|
+
return artifact;
|
|
18604
|
+
}
|
|
18605
|
+
/**
|
|
18606
|
+
* Pull icon, banner, and the screenshot gallery out of a release's `artifacts`
|
|
18607
|
+
* map, keeping presence and dimensions only. The lexicon types `screenshots`
|
|
18608
|
+
* as an array of artifacts; entries without a usable `url` are dropped, and
|
|
18609
|
+
* gallery order is preserved so screenshot indices line up with the proxy's.
|
|
18610
|
+
*/
|
|
18611
|
+
function extractMediaArtifacts(artifacts) {
|
|
18612
|
+
const result = { screenshots: [] };
|
|
18613
|
+
if (!artifacts || typeof artifacts !== "object") return result;
|
|
18614
|
+
const map = artifacts;
|
|
18615
|
+
const icon = asMediaArtifact(map.icon);
|
|
18616
|
+
if (icon) result.icon = icon;
|
|
18617
|
+
const banner = asMediaArtifact(map.banner);
|
|
18618
|
+
if (banner) result.banner = banner;
|
|
18619
|
+
if (Array.isArray(map.screenshots)) map.screenshots.forEach((entry, index) => {
|
|
18620
|
+
const artifact = asMediaArtifact(entry);
|
|
18621
|
+
if (artifact) result.screenshots.push({
|
|
18622
|
+
...artifact,
|
|
18623
|
+
index
|
|
18624
|
+
});
|
|
18625
|
+
});
|
|
18626
|
+
return result;
|
|
18627
|
+
}
|
|
18628
|
+
const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
|
|
18629
|
+
/**
|
|
18630
|
+
* Install a plugin from the registry.
|
|
18631
|
+
*
|
|
18632
|
+
* Posts to the EmDash server, which re-resolves the same `(handle,
|
|
18633
|
+
* slug)` against the aggregator, re-verifies the bundle's checksum
|
|
18634
|
+
* against the signed release record, and writes the install. Surfaces
|
|
18635
|
+
* structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
|
|
18636
|
+
* `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
|
|
18637
|
+
* messages.
|
|
18638
|
+
*/
|
|
18639
|
+
async function installRegistryPlugin(body) {
|
|
18640
|
+
return parseApiResponse(await apiFetch(INSTALL_ENDPOINT, {
|
|
18641
|
+
method: "POST",
|
|
18642
|
+
headers: { "Content-Type": "application/json" },
|
|
18643
|
+
body: JSON.stringify(body)
|
|
18644
|
+
}), i18n._({
|
|
18645
|
+
id: "/LWKyx",
|
|
18646
|
+
message: "Failed to install plugin"
|
|
18647
|
+
}));
|
|
18648
|
+
}
|
|
18649
|
+
/**
|
|
18650
|
+
* Server-side escalation gate raised by the update endpoint when the
|
|
18651
|
+
* target version widens the trust contract. Carries the diff the user
|
|
18652
|
+
* needs to see in the consent dialog before the call is retried with the
|
|
18653
|
+
* matching `confirm*` flag.
|
|
18654
|
+
*/
|
|
18655
|
+
var RegistryUpdateEscalationError = class extends Error {
|
|
18656
|
+
code;
|
|
18657
|
+
capabilityChanges;
|
|
18658
|
+
routeVisibilityChanges;
|
|
18659
|
+
constructor(code, message, capabilityChanges, routeVisibilityChanges) {
|
|
18660
|
+
super(message);
|
|
18661
|
+
this.name = "RegistryUpdateEscalationError";
|
|
18662
|
+
this.code = code;
|
|
18663
|
+
this.capabilityChanges = capabilityChanges;
|
|
18664
|
+
this.routeVisibilityChanges = routeVisibilityChanges;
|
|
18665
|
+
}
|
|
18666
|
+
};
|
|
18667
|
+
/**
|
|
18668
|
+
* Update a registry-source plugin to a newer version.
|
|
18669
|
+
* `POST /_emdash/api/admin/plugins/registry/:id/update`
|
|
18670
|
+
*
|
|
18671
|
+
* Called without `confirm*` flags first, this throws
|
|
18672
|
+
* `RegistryUpdateEscalationError` when the target version widens
|
|
18673
|
+
* permissions; the caller renders a consent dialog populated from the
|
|
18674
|
+
* error's diff, then re-calls with the matching `confirm*` flag once
|
|
18675
|
+
* the user agrees.
|
|
18676
|
+
*/
|
|
18677
|
+
async function updateRegistryPlugin(pluginId, opts = {}) {
|
|
18678
|
+
const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/update`, {
|
|
18679
|
+
method: "POST",
|
|
18680
|
+
headers: { "Content-Type": "application/json" },
|
|
18681
|
+
body: JSON.stringify(opts)
|
|
18682
|
+
});
|
|
18683
|
+
if (response.ok) return;
|
|
18684
|
+
const escalation = parseEscalation(await response.clone().json().catch(() => void 0));
|
|
18685
|
+
if (escalation) throw escalation;
|
|
18686
|
+
await throwResponseError(response, i18n._({
|
|
18687
|
+
id: "8m84rO",
|
|
18688
|
+
message: "Failed to update plugin"
|
|
18689
|
+
}));
|
|
18690
|
+
}
|
|
18691
|
+
function parseEscalation(body) {
|
|
18692
|
+
if (!body || typeof body !== "object" || !("error" in body)) return null;
|
|
18693
|
+
const error = body.error;
|
|
18694
|
+
if (!error || typeof error !== "object" || !("code" in error)) return null;
|
|
18695
|
+
const code = error.code;
|
|
18696
|
+
if (code !== "CAPABILITY_ESCALATION" && code !== "ROUTE_VISIBILITY_ESCALATION") return null;
|
|
18697
|
+
const details = "details" in error && error.details && typeof error.details === "object" ? error.details : {};
|
|
18698
|
+
const capabilityChanges = normaliseCapabilityChanges("capabilityChanges" in details ? details.capabilityChanges : void 0);
|
|
18699
|
+
const routeVisibilityChanges = normaliseRouteVisibilityChanges("routeVisibilityChanges" in details ? details.routeVisibilityChanges : void 0);
|
|
18700
|
+
return new RegistryUpdateEscalationError(code, "message" in error && typeof error.message === "string" ? error.message : i18n._({
|
|
18701
|
+
id: "dgD7yY",
|
|
18702
|
+
message: "Plugin update requires re-consent"
|
|
18703
|
+
}), capabilityChanges, routeVisibilityChanges);
|
|
18704
|
+
}
|
|
18705
|
+
function normaliseCapabilityChanges(value) {
|
|
18706
|
+
if (!value || typeof value !== "object") return {
|
|
18707
|
+
added: [],
|
|
18708
|
+
removed: []
|
|
18709
|
+
};
|
|
18710
|
+
const v = value;
|
|
18711
|
+
return {
|
|
18712
|
+
added: Array.isArray(v.added) ? v.added.filter((s) => typeof s === "string") : [],
|
|
18713
|
+
removed: Array.isArray(v.removed) ? v.removed.filter((s) => typeof s === "string") : []
|
|
18714
|
+
};
|
|
18715
|
+
}
|
|
18716
|
+
function normaliseRouteVisibilityChanges(value) {
|
|
18717
|
+
if (!value || typeof value !== "object") return void 0;
|
|
18718
|
+
const v = value;
|
|
18719
|
+
if (!Array.isArray(v.newlyPublic)) return void 0;
|
|
18720
|
+
const newlyPublic = v.newlyPublic.filter((s) => typeof s === "string");
|
|
18721
|
+
return newlyPublic.length > 0 ? { newlyPublic } : void 0;
|
|
18722
|
+
}
|
|
18723
|
+
/**
|
|
18724
|
+
* Uninstall a registry-source plugin.
|
|
18725
|
+
* `POST /_emdash/api/admin/plugins/registry/:id/uninstall`
|
|
18726
|
+
*
|
|
18727
|
+
* The server refuses to uninstall non-registry sources, so calling this
|
|
18728
|
+
* with a marketplace or config plugin id is a no-op error rather than a
|
|
18729
|
+
* destructive cross-source action.
|
|
18730
|
+
*/
|
|
18731
|
+
async function uninstallRegistryPlugin(pluginId, opts = {}) {
|
|
18732
|
+
const response = await apiFetch(`${API_BASE}/admin/plugins/registry/${encodeURIComponent(pluginId)}/uninstall`, {
|
|
18733
|
+
method: "POST",
|
|
18734
|
+
headers: { "Content-Type": "application/json" },
|
|
18735
|
+
body: JSON.stringify(opts)
|
|
18736
|
+
});
|
|
18737
|
+
if (!response.ok) await throwResponseError(response, i18n._({
|
|
18738
|
+
id: "IDxl65",
|
|
18739
|
+
message: "Failed to uninstall plugin"
|
|
18740
|
+
}));
|
|
18741
|
+
}
|
|
18742
|
+
|
|
17710
18743
|
//#endregion
|
|
17711
18744
|
//#region src/components/PluginManager.tsx
|
|
17712
18745
|
/**
|
|
@@ -17796,7 +18829,7 @@ function PluginManager({ manifest }) {
|
|
|
17796
18829
|
if (!updates) return /* @__PURE__ */ new Map();
|
|
17797
18830
|
return new Map(updates.map((u) => [u.pluginId, u]));
|
|
17798
18831
|
}, [updates]);
|
|
17799
|
-
const
|
|
18832
|
+
const hasUpdatableSources = plugins?.some((p) => p.source === "marketplace" || p.source === "registry");
|
|
17800
18833
|
if (isLoading) return /* @__PURE__ */ jsxs("div", {
|
|
17801
18834
|
className: "space-y-6",
|
|
17802
18835
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
@@ -17844,7 +18877,7 @@ function PluginManager({ manifest }) {
|
|
|
17844
18877
|
}), /* @__PURE__ */ jsxs("div", {
|
|
17845
18878
|
className: "flex items-center gap-3",
|
|
17846
18879
|
children: [
|
|
17847
|
-
|
|
18880
|
+
hasUpdatableSources && /* @__PURE__ */ jsx(Button, {
|
|
17848
18881
|
variant: "ghost",
|
|
17849
18882
|
onClick: () => void refetchUpdates(),
|
|
17850
18883
|
disabled: isCheckingUpdates,
|
|
@@ -17939,15 +18972,17 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
17939
18972
|
const [expanded, setExpanded] = React$1.useState(false);
|
|
17940
18973
|
const [showUpdateConsent, setShowUpdateConsent] = React$1.useState(false);
|
|
17941
18974
|
const [showUninstallConfirm, setShowUninstallConfirm] = React$1.useState(false);
|
|
18975
|
+
const [registryEscalation, setRegistryEscalation] = React$1.useState(null);
|
|
17942
18976
|
const queryClient = useQueryClient();
|
|
17943
18977
|
const toastManager = Toast.useToastManager();
|
|
17944
18978
|
const isMarketplace = plugin.source === "marketplace";
|
|
17945
18979
|
const isRegistry = plugin.source === "registry";
|
|
17946
18980
|
const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
|
|
17947
18981
|
const updateMutation = useMutation({
|
|
17948
|
-
mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
|
|
18982
|
+
mutationFn: (opts) => isRegistry ? updateRegistryPlugin(plugin.id, opts) : updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
|
|
17949
18983
|
onSuccess: () => {
|
|
17950
18984
|
setShowUpdateConsent(false);
|
|
18985
|
+
setRegistryEscalation(null);
|
|
17951
18986
|
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
|
17952
18987
|
queryClient.invalidateQueries({ queryKey: ["plugin-updates"] });
|
|
17953
18988
|
queryClient.invalidateQueries({ queryKey: ["manifest"] });
|
|
@@ -17965,10 +19000,29 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
17965
19000
|
}
|
|
17966
19001
|
})
|
|
17967
19002
|
});
|
|
19003
|
+
},
|
|
19004
|
+
onError: (err) => {
|
|
19005
|
+
if (err instanceof RegistryUpdateEscalationError) {
|
|
19006
|
+
setRegistryEscalation(err);
|
|
19007
|
+
setShowUpdateConsent(true);
|
|
19008
|
+
}
|
|
17968
19009
|
}
|
|
17969
19010
|
});
|
|
19011
|
+
const handleUpdateClick = () => {
|
|
19012
|
+
if (isRegistry) {
|
|
19013
|
+
setRegistryEscalation(null);
|
|
19014
|
+
updateMutation.mutate({});
|
|
19015
|
+
} else setShowUpdateConsent(true);
|
|
19016
|
+
};
|
|
19017
|
+
const handleUpdateConfirm = () => {
|
|
19018
|
+
if (isRegistry) {
|
|
19019
|
+
const opts = { confirmCapabilityChanges: true };
|
|
19020
|
+
if (registryEscalation?.code === "ROUTE_VISIBILITY_ESCALATION") opts.confirmRouteVisibilityChanges = true;
|
|
19021
|
+
updateMutation.mutate(opts);
|
|
19022
|
+
} else updateMutation.mutate({});
|
|
19023
|
+
};
|
|
17970
19024
|
const uninstallMutation = useMutation({
|
|
17971
|
-
mutationFn: (deleteData) => uninstallMarketplacePlugin(plugin.id, { deleteData }),
|
|
19025
|
+
mutationFn: (deleteData) => isRegistry ? uninstallRegistryPlugin(plugin.id, { deleteData }) : uninstallMarketplacePlugin(plugin.id, { deleteData }),
|
|
17972
19026
|
onSuccess: () => {
|
|
17973
19027
|
setShowUninstallConfirm(false);
|
|
17974
19028
|
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
|
@@ -18078,13 +19132,10 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18078
19132
|
const label = CAPABILITY_LABELS[c];
|
|
18079
19133
|
return label ? _t2(label) : c;
|
|
18080
19134
|
}).join(", "),
|
|
18081
|
-
children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }),
|
|
18082
|
-
id: "
|
|
18083
|
-
message: "{0}
|
|
18084
|
-
values: {
|
|
18085
|
-
0: plugin.capabilities.length,
|
|
18086
|
-
1: plugin.capabilities.length !== 1 ? "s" : ""
|
|
18087
|
-
}
|
|
19135
|
+
children: [/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3" }), i18n._({
|
|
19136
|
+
id: "dB/Iw4",
|
|
19137
|
+
message: "{0, plural, one {# permission} other {# permissions}}",
|
|
19138
|
+
values: { 0: plugin.capabilities.length }
|
|
18088
19139
|
})]
|
|
18089
19140
|
})
|
|
18090
19141
|
]
|
|
@@ -18097,7 +19148,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18097
19148
|
hasUpdate && /* @__PURE__ */ jsx(Button, {
|
|
18098
19149
|
variant: "outline",
|
|
18099
19150
|
size: "sm",
|
|
18100
|
-
onClick:
|
|
19151
|
+
onClick: handleUpdateClick,
|
|
18101
19152
|
disabled: updateMutation.isPending,
|
|
18102
19153
|
children: updateMutation.isPending ? _t2({
|
|
18103
19154
|
id: "RXbEvi",
|
|
@@ -18261,7 +19312,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18261
19312
|
] })
|
|
18262
19313
|
]
|
|
18263
19314
|
}),
|
|
18264
|
-
isMarketplace && /* @__PURE__ */ jsx("div", {
|
|
19315
|
+
(isMarketplace || isRegistry) && /* @__PURE__ */ jsx("div", {
|
|
18265
19316
|
className: "pt-2 border-t",
|
|
18266
19317
|
children: /* @__PURE__ */ jsx(Button, {
|
|
18267
19318
|
variant: "ghost",
|
|
@@ -18274,13 +19325,6 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18274
19325
|
message: "Uninstall"
|
|
18275
19326
|
})
|
|
18276
19327
|
})
|
|
18277
|
-
}),
|
|
18278
|
-
isRegistry && /* @__PURE__ */ jsx("div", {
|
|
18279
|
-
className: "pt-2 border-t text-xs text-kumo-subtle",
|
|
18280
|
-
children: _t2({
|
|
18281
|
-
id: "YvXVtr",
|
|
18282
|
-
message: "Uninstall is not yet available for registry plugins. Disable the plugin to stop it from running; full uninstall will land in a follow-up."
|
|
18283
|
-
})
|
|
18284
19328
|
})
|
|
18285
19329
|
]
|
|
18286
19330
|
})]
|
|
@@ -18289,12 +19333,14 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18289
19333
|
mode: "update",
|
|
18290
19334
|
pluginName: plugin.name,
|
|
18291
19335
|
capabilities: plugin.capabilities,
|
|
18292
|
-
newCapabilities: [],
|
|
19336
|
+
newCapabilities: registryEscalation?.capabilityChanges.added ?? [],
|
|
19337
|
+
newlyPublicRoutes: registryEscalation?.routeVisibilityChanges?.newlyPublic ?? [],
|
|
18293
19338
|
isPending: updateMutation.isPending,
|
|
18294
|
-
error: getMutationError(updateMutation.error),
|
|
18295
|
-
onConfirm:
|
|
19339
|
+
error: updateMutation.error instanceof RegistryUpdateEscalationError ? null : getMutationError(updateMutation.error),
|
|
19340
|
+
onConfirm: handleUpdateConfirm,
|
|
18296
19341
|
onCancel: () => {
|
|
18297
19342
|
setShowUpdateConsent(false);
|
|
19343
|
+
setRegistryEscalation(null);
|
|
18298
19344
|
updateMutation.reset();
|
|
18299
19345
|
}
|
|
18300
19346
|
}),
|
|
@@ -18850,72 +19896,6 @@ function ScreenshotLightbox({ screenshots, index, isBlurred = false, onClose, on
|
|
|
18850
19896
|
]
|
|
18851
19897
|
});
|
|
18852
19898
|
}
|
|
18853
|
-
const HTML_ESCAPE_MAP = {
|
|
18854
|
-
"&": "&",
|
|
18855
|
-
"<": "<",
|
|
18856
|
-
">": ">",
|
|
18857
|
-
"\"": """,
|
|
18858
|
-
"'": "'"
|
|
18859
|
-
};
|
|
18860
|
-
const HTML_ESCAPE_RE = /[&<>"']/g;
|
|
18861
|
-
function escapeHtml(str) {
|
|
18862
|
-
return str.replace(HTML_ESCAPE_RE, (ch) => HTML_ESCAPE_MAP[ch]);
|
|
18863
|
-
}
|
|
18864
|
-
const renderer = new Renderer();
|
|
18865
|
-
renderer.link = ({ href, text }) => {
|
|
18866
|
-
if (!SAFE_URL_RE.test(href)) return escapeHtml(text);
|
|
18867
|
-
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
|
|
18868
|
-
};
|
|
18869
|
-
renderer.image = ({ text }) => escapeHtml(text);
|
|
18870
|
-
renderer.html = () => "";
|
|
18871
|
-
const md = new Marked({
|
|
18872
|
-
renderer,
|
|
18873
|
-
async: false
|
|
18874
|
-
});
|
|
18875
|
-
/** Allowed tags and attributes for DOMPurify — only standard markdown output. */
|
|
18876
|
-
const SANITIZE_CONFIG = {
|
|
18877
|
-
ALLOWED_TAGS: [
|
|
18878
|
-
"h1",
|
|
18879
|
-
"h2",
|
|
18880
|
-
"h3",
|
|
18881
|
-
"h4",
|
|
18882
|
-
"h5",
|
|
18883
|
-
"h6",
|
|
18884
|
-
"p",
|
|
18885
|
-
"a",
|
|
18886
|
-
"ul",
|
|
18887
|
-
"ol",
|
|
18888
|
-
"li",
|
|
18889
|
-
"blockquote",
|
|
18890
|
-
"pre",
|
|
18891
|
-
"code",
|
|
18892
|
-
"em",
|
|
18893
|
-
"strong",
|
|
18894
|
-
"del",
|
|
18895
|
-
"br",
|
|
18896
|
-
"hr",
|
|
18897
|
-
"table",
|
|
18898
|
-
"thead",
|
|
18899
|
-
"tbody",
|
|
18900
|
-
"tr",
|
|
18901
|
-
"th",
|
|
18902
|
-
"td",
|
|
18903
|
-
"details",
|
|
18904
|
-
"summary",
|
|
18905
|
-
"sup",
|
|
18906
|
-
"sub"
|
|
18907
|
-
],
|
|
18908
|
-
ALLOWED_ATTR: [
|
|
18909
|
-
"href",
|
|
18910
|
-
"target",
|
|
18911
|
-
"rel"
|
|
18912
|
-
]
|
|
18913
|
-
};
|
|
18914
|
-
function renderMarkdown(markdown) {
|
|
18915
|
-
const result = md.parse(markdown);
|
|
18916
|
-
const html = typeof result === "string" ? result : "";
|
|
18917
|
-
return DOMPurify.sanitize(html, SANITIZE_CONFIG);
|
|
18918
|
-
}
|
|
18919
19899
|
function formatBytes(bytes) {
|
|
18920
19900
|
if (bytes < 1024) return `${bytes} B`;
|
|
18921
19901
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -21951,235 +22931,6 @@ function Redirects() {
|
|
|
21951
22931
|
});
|
|
21952
22932
|
}
|
|
21953
22933
|
|
|
21954
|
-
//#endregion
|
|
21955
|
-
//#region src/lib/api/registry.ts
|
|
21956
|
-
let cachedDiscovery = null;
|
|
21957
|
-
async function getDiscoveryClient(config) {
|
|
21958
|
-
if (cachedDiscovery && cachedDiscovery.config.aggregatorUrl === config.aggregatorUrl && cachedDiscovery.config.acceptLabelers === config.acceptLabelers) return cachedDiscovery.client;
|
|
21959
|
-
const DiscoveryClient = (await import("@emdash-cms/registry-client/discovery")).DiscoveryClient;
|
|
21960
|
-
const discovery = new DiscoveryClient({
|
|
21961
|
-
aggregatorUrl: config.aggregatorUrl,
|
|
21962
|
-
acceptLabelers: config.acceptLabelers
|
|
21963
|
-
});
|
|
21964
|
-
const wrapped = {
|
|
21965
|
-
async searchPackages(opts) {
|
|
21966
|
-
return discovery.searchPackages({
|
|
21967
|
-
q: opts.q,
|
|
21968
|
-
cursor: opts.cursor,
|
|
21969
|
-
limit: opts.limit
|
|
21970
|
-
});
|
|
21971
|
-
},
|
|
21972
|
-
async resolvePackage(handle, slug) {
|
|
21973
|
-
return discovery.resolvePackage({
|
|
21974
|
-
handle,
|
|
21975
|
-
slug
|
|
21976
|
-
});
|
|
21977
|
-
},
|
|
21978
|
-
async getPackage(did, slug) {
|
|
21979
|
-
return discovery.getPackage({
|
|
21980
|
-
did,
|
|
21981
|
-
slug
|
|
21982
|
-
});
|
|
21983
|
-
},
|
|
21984
|
-
async getLatestRelease(did, slug) {
|
|
21985
|
-
return discovery.getLatestRelease({
|
|
21986
|
-
did,
|
|
21987
|
-
package: slug
|
|
21988
|
-
});
|
|
21989
|
-
},
|
|
21990
|
-
async listReleases(did, slug, cursor) {
|
|
21991
|
-
return discovery.listReleases({
|
|
21992
|
-
did,
|
|
21993
|
-
package: slug,
|
|
21994
|
-
cursor
|
|
21995
|
-
});
|
|
21996
|
-
}
|
|
21997
|
-
};
|
|
21998
|
-
cachedDiscovery = {
|
|
21999
|
-
config,
|
|
22000
|
-
client: wrapped
|
|
22001
|
-
};
|
|
22002
|
-
return wrapped;
|
|
22003
|
-
}
|
|
22004
|
-
/**
|
|
22005
|
-
* Returns whether a release should be considered installable given the
|
|
22006
|
-
* configured policy. Currently implements the minimum-release-age check
|
|
22007
|
-
* described in RFC 0001's "Pre-label gap and launch tempo" section,
|
|
22008
|
-
* plus the `minimumReleaseAgeExclude` allowlist.
|
|
22009
|
-
*
|
|
22010
|
-
* Returns `false` (release blocked) when the policy is configured but
|
|
22011
|
-
* the release is missing a valid `indexedAt` -- we fail closed rather
|
|
22012
|
-
* than silently letting unbounded-age releases through.
|
|
22013
|
-
*/
|
|
22014
|
-
function releasePassesPolicy(release, pkg, policy, now = Date.now()) {
|
|
22015
|
-
if (!policy?.minimumReleaseAgeSeconds) return true;
|
|
22016
|
-
if (releaseExemptFromMinimumAge(policy.minimumReleaseAgeExclude, pkg.did, pkg.slug)) return true;
|
|
22017
|
-
const indexedAt = Date.parse(release.indexedAt);
|
|
22018
|
-
if (!Number.isFinite(indexedAt)) return false;
|
|
22019
|
-
return (now - indexedAt) / 1e3 >= policy.minimumReleaseAgeSeconds;
|
|
22020
|
-
}
|
|
22021
|
-
/**
|
|
22022
|
-
* Canonicalize a capabilities list for set-style comparison. Mirrors
|
|
22023
|
-
* the server-side helper `canonicalCapabilitiesForDriftCheck` in
|
|
22024
|
-
* `packages/core/src/registry/config.ts` -- both sides must produce
|
|
22025
|
-
* the same canonical shape so the install handler's drift check is
|
|
22026
|
-
* stable across reorderings, duplicates, and junk entries.
|
|
22027
|
-
*
|
|
22028
|
-
* Filters non-strings, deduplicates, and sorts lexically.
|
|
22029
|
-
*/
|
|
22030
|
-
function canonicalCapabilitiesForDriftCheck(value) {
|
|
22031
|
-
if (!Array.isArray(value)) return [];
|
|
22032
|
-
const seen = /* @__PURE__ */ new Set();
|
|
22033
|
-
for (const entry of value) if (typeof entry === "string" && entry.length > 0) seen.add(entry);
|
|
22034
|
-
return [...seen].toSorted();
|
|
22035
|
-
}
|
|
22036
|
-
/**
|
|
22037
|
-
* Matches a `(publisher_did, slug)` against the
|
|
22038
|
-
* `minimumReleaseAgeExclude` allowlist. Mirrors the server-side helper
|
|
22039
|
-
* of the same name in `packages/core/src/registry/config.ts`.
|
|
22040
|
-
*
|
|
22041
|
-
* DID-only on purpose: handles are aggregator-supplied envelope data
|
|
22042
|
-
* and accepting them as a trust input would let a compromised
|
|
22043
|
-
* aggregator bypass the holdback by claiming any handle for any
|
|
22044
|
-
* package. DIDs are tied to the AT URI of the record itself.
|
|
22045
|
-
*
|
|
22046
|
-
* Entries from the config list have already been lowercased at
|
|
22047
|
-
* manifest build time, so this only needs to lowercase the runtime
|
|
22048
|
-
* values for comparison.
|
|
22049
|
-
*/
|
|
22050
|
-
function releaseExemptFromMinimumAge(exclude, publisherDid, slug) {
|
|
22051
|
-
if (!exclude || exclude.length === 0) return false;
|
|
22052
|
-
const didLower = publisherDid.toLowerCase();
|
|
22053
|
-
const fullDid = `${didLower}/${slug.toLowerCase()}`;
|
|
22054
|
-
for (const entry of exclude) {
|
|
22055
|
-
if (entry === didLower) return true;
|
|
22056
|
-
if (entry === fullDid) return true;
|
|
22057
|
-
}
|
|
22058
|
-
return false;
|
|
22059
|
-
}
|
|
22060
|
-
async function searchRegistryPackages(config, opts) {
|
|
22061
|
-
return (await getDiscoveryClient(config)).searchPackages(opts);
|
|
22062
|
-
}
|
|
22063
|
-
async function resolveRegistryPackage(config, handle, slug) {
|
|
22064
|
-
return (await getDiscoveryClient(config)).resolvePackage(handle, slug);
|
|
22065
|
-
}
|
|
22066
|
-
async function getRegistryPackage(config, did, slug) {
|
|
22067
|
-
return (await getDiscoveryClient(config)).getPackage(did, slug);
|
|
22068
|
-
}
|
|
22069
|
-
async function getLatestRegistryRelease(config, did, slug) {
|
|
22070
|
-
return (await getDiscoveryClient(config)).getLatestRelease(did, slug);
|
|
22071
|
-
}
|
|
22072
|
-
/**
|
|
22073
|
-
* Resolve a publisher DID to its claimed handle using the same
|
|
22074
|
-
* `LocalActorResolver` pattern as `@emdash-cms/plugin-cli` and
|
|
22075
|
-
* `@emdash-cms/auth-atproto`. Bidirectional verification (handle's
|
|
22076
|
-
* domain points back to the same DID) is part of the resolver --
|
|
22077
|
-
* `LocalActorResolver` returns the sentinel `"handle.invalid"` when
|
|
22078
|
-
* the `alsoKnownAs` handle is present but doesn't round-trip.
|
|
22079
|
-
*
|
|
22080
|
-
* Three distinct outcomes the UI can render:
|
|
22081
|
-
*
|
|
22082
|
-
* - `{ status: "ok", handle }` — verified handle, round-trip OK.
|
|
22083
|
-
* - `{ status: "invalid" }` — DID claims a handle but it doesn't
|
|
22084
|
-
* resolve back. The publisher's handle setup is broken; the admin
|
|
22085
|
-
* should see a clear "Invalid handle" indicator rather than the
|
|
22086
|
-
* raw DID.
|
|
22087
|
-
* - `{ status: "missing" }` — no handle claimed at all (no
|
|
22088
|
-
* `alsoKnownAs`), or the DID document couldn't be fetched (network
|
|
22089
|
-
* error, unsupported DID method).
|
|
22090
|
-
*/
|
|
22091
|
-
let actorResolver = null;
|
|
22092
|
-
async function getActorResolver() {
|
|
22093
|
-
if (actorResolver) return actorResolver;
|
|
22094
|
-
const { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } = await import("@atcute/identity-resolver");
|
|
22095
|
-
actorResolver = new LocalActorResolver({
|
|
22096
|
-
handleResolver: new CompositeHandleResolver({ methods: {
|
|
22097
|
-
dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
|
|
22098
|
-
http: new WellKnownHandleResolver()
|
|
22099
|
-
} }),
|
|
22100
|
-
didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
|
|
22101
|
-
plc: new PlcDidDocumentResolver(),
|
|
22102
|
-
web: new WebDidDocumentResolver()
|
|
22103
|
-
} })
|
|
22104
|
-
});
|
|
22105
|
-
return actorResolver;
|
|
22106
|
-
}
|
|
22107
|
-
/**
|
|
22108
|
-
* localStorage-backed cache for DID→handle resolutions. Handles are
|
|
22109
|
-
* stable for hours-to-days in practice, but bound the cache so a
|
|
22110
|
-
* compromised handle eventually flips back to "invalid" without a
|
|
22111
|
-
* forced refresh. 24h matches the typical atproto handle TTL.
|
|
22112
|
-
*
|
|
22113
|
-
* Failures (network errors, unsupported DID method) are *not* cached --
|
|
22114
|
-
* those should retry on the next render.
|
|
22115
|
-
*/
|
|
22116
|
-
const HANDLE_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
22117
|
-
const HANDLE_CACHE_KEY_PREFIX = "emdash:did-handle:";
|
|
22118
|
-
function readHandleCache(did) {
|
|
22119
|
-
if (typeof localStorage === "undefined") return null;
|
|
22120
|
-
try {
|
|
22121
|
-
const raw = localStorage.getItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`);
|
|
22122
|
-
if (!raw) return null;
|
|
22123
|
-
const parsed = JSON.parse(raw);
|
|
22124
|
-
if (!parsed || typeof parsed.expiresAt !== "number" || parsed.expiresAt < Date.now()) return null;
|
|
22125
|
-
return parsed.resolution;
|
|
22126
|
-
} catch {
|
|
22127
|
-
return null;
|
|
22128
|
-
}
|
|
22129
|
-
}
|
|
22130
|
-
function writeHandleCache(did, resolution) {
|
|
22131
|
-
if (typeof localStorage === "undefined") return;
|
|
22132
|
-
try {
|
|
22133
|
-
const entry = {
|
|
22134
|
-
resolution,
|
|
22135
|
-
expiresAt: Date.now() + HANDLE_CACHE_TTL_MS
|
|
22136
|
-
};
|
|
22137
|
-
localStorage.setItem(`${HANDLE_CACHE_KEY_PREFIX}${did}`, JSON.stringify(entry));
|
|
22138
|
-
} catch {}
|
|
22139
|
-
}
|
|
22140
|
-
async function resolveDidToHandle(did) {
|
|
22141
|
-
const cached = readHandleCache(did);
|
|
22142
|
-
if (cached) return cached;
|
|
22143
|
-
let result;
|
|
22144
|
-
try {
|
|
22145
|
-
const resolved = await (await getActorResolver()).resolve(did);
|
|
22146
|
-
if (resolved.handle === "handle.invalid") result = { status: "invalid" };
|
|
22147
|
-
else if (resolved.handle) result = {
|
|
22148
|
-
status: "ok",
|
|
22149
|
-
handle: resolved.handle
|
|
22150
|
-
};
|
|
22151
|
-
else result = { status: "missing" };
|
|
22152
|
-
} catch (err) {
|
|
22153
|
-
console.warn(`[registry] DID->handle resolution failed for ${did}:`, err);
|
|
22154
|
-
return { status: "missing" };
|
|
22155
|
-
}
|
|
22156
|
-
writeHandleCache(did, result);
|
|
22157
|
-
return result;
|
|
22158
|
-
}
|
|
22159
|
-
const INSTALL_ENDPOINT = `${API_BASE}/admin/plugins/registry/install`;
|
|
22160
|
-
/**
|
|
22161
|
-
* Install a plugin from the registry.
|
|
22162
|
-
*
|
|
22163
|
-
* Posts to the EmDash server, which re-resolves the same `(handle,
|
|
22164
|
-
* slug)` against the aggregator, re-verifies the bundle's checksum
|
|
22165
|
-
* against the signed release record, and writes the install. Surfaces
|
|
22166
|
-
* structured error codes (`RELEASE_YANKED`, `CHECKSUM_MISMATCH`,
|
|
22167
|
-
* `DECLARED_ACCESS_DRIFT`, etc.) that callers map to localized
|
|
22168
|
-
* messages.
|
|
22169
|
-
*/
|
|
22170
|
-
async function installRegistryPlugin(body) {
|
|
22171
|
-
const response = await apiFetch(INSTALL_ENDPOINT, {
|
|
22172
|
-
method: "POST",
|
|
22173
|
-
headers: { "Content-Type": "application/json" },
|
|
22174
|
-
body: JSON.stringify(body)
|
|
22175
|
-
});
|
|
22176
|
-
if (!response.ok) await throwResponseError(response, i18n._({
|
|
22177
|
-
id: "/LWKyx",
|
|
22178
|
-
message: "Failed to install plugin"
|
|
22179
|
-
}));
|
|
22180
|
-
return (await response.json()).data;
|
|
22181
|
-
}
|
|
22182
|
-
|
|
22183
22934
|
//#endregion
|
|
22184
22935
|
//#region src/components/PublisherHandle.tsx
|
|
22185
22936
|
/**
|
|
@@ -22473,10 +23224,15 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22473
23224
|
const { data: installedPlugins } = useQuery({
|
|
22474
23225
|
queryKey: ["plugins"],
|
|
22475
23226
|
queryFn: async () => {
|
|
22476
|
-
const { fetchPlugins } = await import("./plugins-
|
|
23227
|
+
const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
|
|
22477
23228
|
return fetchPlugins();
|
|
22478
23229
|
}
|
|
22479
23230
|
});
|
|
23231
|
+
const { data: manifest } = useQuery({
|
|
23232
|
+
queryKey: ["manifest"],
|
|
23233
|
+
queryFn: fetchManifest
|
|
23234
|
+
});
|
|
23235
|
+
const hostEnv = React$1.useMemo(() => hostEnvFromManifest(manifest), [manifest]);
|
|
22480
23236
|
const slashIdx = pluginId.lastIndexOf("/");
|
|
22481
23237
|
const publisher = slashIdx > 0 ? pluginId.slice(0, slashIdx) : "";
|
|
22482
23238
|
const slug = slashIdx > 0 ? pluginId.slice(slashIdx + 1) : "";
|
|
@@ -22494,17 +23250,42 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22494
23250
|
enabled: Boolean(publisher && slug)
|
|
22495
23251
|
});
|
|
22496
23252
|
const handleResult = usePublisherHandle(pkg?.did ?? "", pkg?.handle);
|
|
22497
|
-
const { data:
|
|
23253
|
+
const { data: releasesData } = useQuery({
|
|
22498
23254
|
queryKey: [
|
|
22499
23255
|
"registry",
|
|
22500
|
-
"
|
|
23256
|
+
"releases",
|
|
22501
23257
|
config.aggregatorUrl,
|
|
23258
|
+
config.acceptLabelers,
|
|
22502
23259
|
pkg?.did,
|
|
22503
23260
|
slug
|
|
22504
23261
|
],
|
|
22505
|
-
queryFn: () =>
|
|
23262
|
+
queryFn: () => listRegistryReleases(config, pkg.did, slug, { limit: 100 }),
|
|
22506
23263
|
enabled: Boolean(pkg?.did && slug)
|
|
22507
23264
|
});
|
|
23265
|
+
const releases = React$1.useMemo(() => (releasesData?.releases ?? []).filter((r) => r.release !== null && !isYanked(r)), [releasesData]);
|
|
23266
|
+
const hasFilteredAllReleases = (releasesData?.releases.length ?? 0) > 0 && releases.length === 0;
|
|
23267
|
+
const defaultVersion = React$1.useMemo(() => {
|
|
23268
|
+
if (!pkg || releases.length === 0) return void 0;
|
|
23269
|
+
return (releases.find((r) => releasePassesPolicy(r, {
|
|
23270
|
+
did: pkg.did,
|
|
23271
|
+
slug
|
|
23272
|
+
}, config.policy)) ?? releases[0])?.version;
|
|
23273
|
+
}, [
|
|
23274
|
+
pkg,
|
|
23275
|
+
releases,
|
|
23276
|
+
slug,
|
|
23277
|
+
config.policy
|
|
23278
|
+
]);
|
|
23279
|
+
const [selectedVersion, setSelectedVersion] = React$1.useState(void 0);
|
|
23280
|
+
const [prevPluginId, setPrevPluginId] = React$1.useState(pluginId);
|
|
23281
|
+
if (prevPluginId !== pluginId) {
|
|
23282
|
+
setPrevPluginId(pluginId);
|
|
23283
|
+
setSelectedVersion(void 0);
|
|
23284
|
+
}
|
|
23285
|
+
if (selectedVersion !== void 0 && releases.length > 0 && !releases.some((r) => r.version === selectedVersion)) setSelectedVersion(void 0);
|
|
23286
|
+
const effectiveVersion = selectedVersion ?? defaultVersion;
|
|
23287
|
+
const release = React$1.useMemo(() => releases.find((r) => r.version === effectiveVersion), [releases, effectiveVersion]);
|
|
23288
|
+
const isPreRelease = release ? isPreReleaseVersion(release.version) : false;
|
|
22508
23289
|
const ext = (release?.release?.extensions)?.["com.emdashcms.experimental.package.releaseExtension"];
|
|
22509
23290
|
const capabilities = Array.isArray(ext?.capabilities) ? canonicalCapabilitiesForDriftCheck(ext?.capabilities) : canonicalCapabilitiesForDriftCheck(declaredAccessToCapabilityList(ext?.declaredAccess));
|
|
22510
23291
|
const pkgProfile = pkg?.profile ?? null;
|
|
@@ -22526,11 +23307,51 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22526
23307
|
}] : [];
|
|
22527
23308
|
});
|
|
22528
23309
|
const repoHref = safeExternalHref(release?.release?.repo);
|
|
22529
|
-
const
|
|
23310
|
+
const verifiedLabel = (pkg?.labels ?? []).find((l) => l.val === "verified");
|
|
23311
|
+
const verified = Boolean(verifiedLabel);
|
|
23312
|
+
const verifiedLabeller = typeof verifiedLabel?.src === "string" ? verifiedLabel.src : null;
|
|
23313
|
+
const sections = presentSections(pkgProfile);
|
|
23314
|
+
const [selectedSection, setSelectedSection] = React$1.useState(void 0);
|
|
23315
|
+
const defaultSection = sections[0]?.key;
|
|
23316
|
+
const activeSection = selectedSection && sections.some((s) => s.key === selectedSection) ? selectedSection : defaultSection;
|
|
23317
|
+
const activePane = sections.find((s) => s.key === activeSection);
|
|
23318
|
+
const sbom = extractSbom(release?.release?.sbom);
|
|
23319
|
+
const sbomHref = sbom ? sbomDownloadHref(sbom.url) : null;
|
|
23320
|
+
const lastUpdated = typeof pkgProfile?.lastUpdated === "string" ? pkgProfile.lastUpdated : null;
|
|
23321
|
+
const mediaArtifacts = extractMediaArtifacts(release?.release?.artifacts);
|
|
23322
|
+
const artifactDid = pkg?.did;
|
|
23323
|
+
const artifactVersion = release?.version;
|
|
23324
|
+
const iconSrc = mediaArtifacts.icon && artifactDid ? artifactProxyUrl({
|
|
23325
|
+
did: artifactDid,
|
|
23326
|
+
slug,
|
|
23327
|
+
version: artifactVersion,
|
|
23328
|
+
kind: "icon"
|
|
23329
|
+
}) : null;
|
|
23330
|
+
const bannerSrc = mediaArtifacts.banner && artifactDid ? artifactProxyUrl({
|
|
23331
|
+
did: artifactDid,
|
|
23332
|
+
slug,
|
|
23333
|
+
version: artifactVersion,
|
|
23334
|
+
kind: "banner"
|
|
23335
|
+
}) : null;
|
|
23336
|
+
const screenshots = artifactDid ? mediaArtifacts.screenshots.map((shot) => ({
|
|
23337
|
+
...shot,
|
|
23338
|
+
src: artifactProxyUrl({
|
|
23339
|
+
did: artifactDid,
|
|
23340
|
+
slug,
|
|
23341
|
+
version: artifactVersion,
|
|
23342
|
+
kind: "screenshot",
|
|
23343
|
+
index: shot.index
|
|
23344
|
+
})
|
|
23345
|
+
})) : [];
|
|
22530
23346
|
const policyOk = release && pkg ? releasePassesPolicy(release, {
|
|
22531
23347
|
did: pkg.did,
|
|
22532
23348
|
slug
|
|
22533
23349
|
}, config.policy) : true;
|
|
23350
|
+
const envMismatches = React$1.useMemo(() => {
|
|
23351
|
+
if (!release) return [];
|
|
23352
|
+
return checkEnvCompatibility(release.release?.requires, hostEnv);
|
|
23353
|
+
}, [release, hostEnv]);
|
|
23354
|
+
const envOk = envMismatches.length === 0;
|
|
22534
23355
|
const installedEntry = React$1.useMemo(() => {
|
|
22535
23356
|
if (!pkg || !installedPlugins) return void 0;
|
|
22536
23357
|
return installedPlugins.find((p) => p.source === "registry" && p.registryPublisherDid === pkg.did && p.registrySlug === slug);
|
|
@@ -22589,14 +23410,33 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22589
23410
|
className: "space-y-6",
|
|
22590
23411
|
children: [
|
|
22591
23412
|
/* @__PURE__ */ jsx(BackLink, {}),
|
|
23413
|
+
bannerSrc ? /* @__PURE__ */ jsx("img", {
|
|
23414
|
+
src: bannerSrc,
|
|
23415
|
+
alt: _t({
|
|
23416
|
+
id: "aOmjNp",
|
|
23417
|
+
message: "{0} banner",
|
|
23418
|
+
values: { 0: displayName ?? slug }
|
|
23419
|
+
}),
|
|
23420
|
+
className: "h-40 w-full rounded-xl object-cover",
|
|
23421
|
+
loading: "lazy"
|
|
23422
|
+
}) : null,
|
|
22592
23423
|
/* @__PURE__ */ jsxs("div", {
|
|
22593
|
-
className: "flex items-start gap-4",
|
|
23424
|
+
className: "flex flex-wrap items-start gap-4",
|
|
22594
23425
|
children: [
|
|
22595
23426
|
/* @__PURE__ */ jsx("div", {
|
|
22596
|
-
className: "rounded-xl bg-kumo-subtle
|
|
22597
|
-
children: /* @__PURE__ */ jsx("
|
|
23427
|
+
className: "overflow-hidden rounded-xl bg-kumo-subtle text-kumo-subtle",
|
|
23428
|
+
children: iconSrc ? /* @__PURE__ */ jsx("img", {
|
|
23429
|
+
src: iconSrc,
|
|
23430
|
+
alt: _t({
|
|
23431
|
+
id: "dVaAiX",
|
|
23432
|
+
message: "{0} icon",
|
|
23433
|
+
values: { 0: displayName ?? slug }
|
|
23434
|
+
}),
|
|
23435
|
+
className: "block h-16 w-16 object-cover",
|
|
23436
|
+
loading: "lazy"
|
|
23437
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
22598
23438
|
"aria-hidden": true,
|
|
22599
|
-
className: "block h-
|
|
23439
|
+
className: "block h-16 w-16"
|
|
22600
23440
|
})
|
|
22601
23441
|
}),
|
|
22602
23442
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -22607,11 +23447,30 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22607
23447
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
22608
23448
|
className: "truncate text-3xl font-bold",
|
|
22609
23449
|
children: displayName ?? slug
|
|
22610
|
-
}), verified ? /* @__PURE__ */ jsx(
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
|
|
22614
|
-
|
|
23450
|
+
}), verified ? /* @__PURE__ */ jsx(Tooltip, {
|
|
23451
|
+
content: verifiedLabeller ? _t({
|
|
23452
|
+
id: "/rITsG",
|
|
23453
|
+
message: "Verified publisher. A labeller ({verifiedLabeller}) has confirmed this publisher's identity.",
|
|
23454
|
+
values: { verifiedLabeller }
|
|
23455
|
+
}) : _t({
|
|
23456
|
+
id: "PCUvJ4",
|
|
23457
|
+
message: "Verified publisher. A labeller has confirmed this publisher's identity."
|
|
23458
|
+
}),
|
|
23459
|
+
render: /* @__PURE__ */ jsx("button", {
|
|
23460
|
+
type: "button",
|
|
23461
|
+
className: "inline-flex shrink-0 cursor-help rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-kumo-brand",
|
|
23462
|
+
"aria-label": verifiedLabeller ? _t({
|
|
23463
|
+
id: "s7Woxs",
|
|
23464
|
+
message: "Verified publisher, confirmed by labeller {verifiedLabeller}",
|
|
23465
|
+
values: { verifiedLabeller }
|
|
23466
|
+
}) : _t({
|
|
23467
|
+
id: "70QfpA",
|
|
23468
|
+
message: "Verified publisher"
|
|
23469
|
+
}),
|
|
23470
|
+
children: /* @__PURE__ */ jsx(ShieldCheck, {
|
|
23471
|
+
className: "h-5 w-5 text-kumo-brand",
|
|
23472
|
+
"aria-hidden": true
|
|
23473
|
+
})
|
|
22615
23474
|
})
|
|
22616
23475
|
}) : null]
|
|
22617
23476
|
}),
|
|
@@ -22630,40 +23489,124 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22630
23489
|
})
|
|
22631
23490
|
]
|
|
22632
23491
|
}),
|
|
22633
|
-
release ? /* @__PURE__ */ jsxs("
|
|
22634
|
-
className: "
|
|
23492
|
+
release ? /* @__PURE__ */ jsxs("div", {
|
|
23493
|
+
className: "mt-1 flex flex-wrap items-center gap-2",
|
|
22635
23494
|
children: [
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
23495
|
+
/* @__PURE__ */ jsx("p", {
|
|
23496
|
+
className: "text-xs text-kumo-subtle",
|
|
23497
|
+
children: _t({
|
|
23498
|
+
id: "YYdC3A",
|
|
23499
|
+
message: "Version {0}",
|
|
23500
|
+
values: { 0: release.version }
|
|
23501
|
+
})
|
|
22640
23502
|
}),
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
23503
|
+
isPreRelease ? /* @__PURE__ */ jsx(Badge, { children: _t({
|
|
23504
|
+
id: "omrja/",
|
|
23505
|
+
message: "Pre-release"
|
|
23506
|
+
}) }) : null,
|
|
23507
|
+
sbom?.format ? /* @__PURE__ */ jsx(Badge, { children: _t({
|
|
23508
|
+
id: "mQmHhZ",
|
|
23509
|
+
message: "SBOM · {0}",
|
|
23510
|
+
values: { 0: sbom.format }
|
|
23511
|
+
}) }) : sbom ? /* @__PURE__ */ jsx(Badge, { children: _t({
|
|
23512
|
+
id: "SBWpbn",
|
|
23513
|
+
message: "SBOM"
|
|
23514
|
+
}) }) : null,
|
|
23515
|
+
sbomHref ? /* @__PURE__ */ jsx("a", {
|
|
23516
|
+
href: sbomHref,
|
|
23517
|
+
target: "_blank",
|
|
23518
|
+
rel: "noopener noreferrer",
|
|
23519
|
+
download: true,
|
|
23520
|
+
className: "text-xs text-kumo-brand hover:underline",
|
|
23521
|
+
children: _t({
|
|
23522
|
+
id: "gm9IDy",
|
|
23523
|
+
message: "Download SBOM"
|
|
23524
|
+
})
|
|
23525
|
+
}) : null
|
|
22647
23526
|
]
|
|
23527
|
+
}) : null,
|
|
23528
|
+
release ? /* @__PURE__ */ jsxs("dl", {
|
|
23529
|
+
className: "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-kumo-subtle",
|
|
23530
|
+
children: [lastUpdated ? /* @__PURE__ */ jsxs("div", {
|
|
23531
|
+
className: "flex items-center gap-1",
|
|
23532
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
23533
|
+
className: "font-medium",
|
|
23534
|
+
children: _t({
|
|
23535
|
+
id: "+b7T3G",
|
|
23536
|
+
message: "Updated"
|
|
23537
|
+
})
|
|
23538
|
+
}), /* @__PURE__ */ jsx("dd", { children: formatDate(lastUpdated) })]
|
|
23539
|
+
}) : null, /* @__PURE__ */ jsxs("div", {
|
|
23540
|
+
className: "flex items-center gap-1",
|
|
23541
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
23542
|
+
className: "font-medium",
|
|
23543
|
+
children: _t({
|
|
23544
|
+
id: "I3/r1f",
|
|
23545
|
+
message: "Indexed"
|
|
23546
|
+
})
|
|
23547
|
+
}), /* @__PURE__ */ jsx("dd", { children: formatDate(release.indexedAt) })]
|
|
23548
|
+
})]
|
|
22648
23549
|
}) : null
|
|
22649
23550
|
]
|
|
22650
23551
|
}),
|
|
22651
|
-
/* @__PURE__ */
|
|
22652
|
-
|
|
22653
|
-
|
|
22654
|
-
|
|
22655
|
-
|
|
22656
|
-
|
|
22657
|
-
|
|
22658
|
-
|
|
22659
|
-
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22666
|
-
|
|
23552
|
+
/* @__PURE__ */ jsxs("div", {
|
|
23553
|
+
className: "flex min-w-0 flex-wrap items-center gap-2",
|
|
23554
|
+
children: [releases.length > 1 ? /* @__PURE__ */ jsx(Select, {
|
|
23555
|
+
"aria-label": _t({
|
|
23556
|
+
id: "eE0JZ4",
|
|
23557
|
+
message: "Version"
|
|
23558
|
+
}),
|
|
23559
|
+
className: "w-full max-w-[220px]",
|
|
23560
|
+
value: effectiveVersion ?? "",
|
|
23561
|
+
onValueChange: (v) => setSelectedVersion(v ?? void 0),
|
|
23562
|
+
renderValue: (v) => typeof v === "string" ? v : "",
|
|
23563
|
+
children: releases.map((r) => {
|
|
23564
|
+
const preRelease = isPreReleaseVersion(r.version);
|
|
23565
|
+
const policyBlocked = !releasePassesPolicy(r, {
|
|
23566
|
+
did: pkg.did,
|
|
23567
|
+
slug
|
|
23568
|
+
}, config.policy);
|
|
23569
|
+
return /* @__PURE__ */ jsx(Select.Option, {
|
|
23570
|
+
value: r.version,
|
|
23571
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
23572
|
+
className: "flex items-center gap-2",
|
|
23573
|
+
children: [
|
|
23574
|
+
/* @__PURE__ */ jsx("span", { children: r.version }),
|
|
23575
|
+
preRelease ? /* @__PURE__ */ jsx("span", {
|
|
23576
|
+
className: "text-xs text-kumo-subtle",
|
|
23577
|
+
children: _t({
|
|
23578
|
+
id: "D2C1D+",
|
|
23579
|
+
message: "(pre-release)"
|
|
23580
|
+
})
|
|
23581
|
+
}) : null,
|
|
23582
|
+
policyBlocked ? /* @__PURE__ */ jsx("span", {
|
|
23583
|
+
className: "text-xs text-kumo-subtle",
|
|
23584
|
+
children: _t({
|
|
23585
|
+
id: "W3SUIF",
|
|
23586
|
+
message: "(too new)"
|
|
23587
|
+
})
|
|
23588
|
+
}) : null
|
|
23589
|
+
]
|
|
23590
|
+
})
|
|
23591
|
+
}, r.version);
|
|
23592
|
+
})
|
|
23593
|
+
}) : null, isInstalled ? /* @__PURE__ */ jsx(Button, {
|
|
23594
|
+
variant: "secondary",
|
|
23595
|
+
disabled: true,
|
|
23596
|
+
children: _t({
|
|
23597
|
+
id: "eQkgKV",
|
|
23598
|
+
message: "Installed"
|
|
23599
|
+
})
|
|
23600
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
23601
|
+
variant: "primary",
|
|
23602
|
+
disabled: !release || !policyOk || !envOk || handleResult.status === "invalid",
|
|
23603
|
+
onClick: () => setShowConsent(true),
|
|
23604
|
+
children: _t({
|
|
23605
|
+
id: "TKQ7K+",
|
|
23606
|
+
message: "Install"
|
|
23607
|
+
})
|
|
23608
|
+
})]
|
|
23609
|
+
})
|
|
22667
23610
|
]
|
|
22668
23611
|
}),
|
|
22669
23612
|
handleResult.status === "invalid" ? /* @__PURE__ */ jsxs("div", {
|
|
@@ -22683,6 +23626,23 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22683
23626
|
})
|
|
22684
23627
|
})] })]
|
|
22685
23628
|
}) : null,
|
|
23629
|
+
hasFilteredAllReleases ? /* @__PURE__ */ jsxs("div", {
|
|
23630
|
+
className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
|
|
23631
|
+
role: "status",
|
|
23632
|
+
children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
23633
|
+
className: "font-medium",
|
|
23634
|
+
children: _t({
|
|
23635
|
+
id: "rqsq95",
|
|
23636
|
+
message: "No installable releases"
|
|
23637
|
+
})
|
|
23638
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
23639
|
+
className: "mt-1 text-sm text-kumo-default",
|
|
23640
|
+
children: _t({
|
|
23641
|
+
id: "o+La9d",
|
|
23642
|
+
message: "Every published release of this plugin has been withdrawn or could not be verified. Check back later, or contact the publisher."
|
|
23643
|
+
})
|
|
23644
|
+
})] })]
|
|
23645
|
+
}) : null,
|
|
22686
23646
|
release && !policyOk ? /* @__PURE__ */ jsxs("div", {
|
|
22687
23647
|
className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
|
|
22688
23648
|
role: "status",
|
|
@@ -22701,10 +23661,66 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22701
23661
|
})
|
|
22702
23662
|
})] })]
|
|
22703
23663
|
}) : null,
|
|
23664
|
+
release && !envOk ? /* @__PURE__ */ jsxs("div", {
|
|
23665
|
+
className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
|
|
23666
|
+
role: "status",
|
|
23667
|
+
children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [
|
|
23668
|
+
/* @__PURE__ */ jsx("p", {
|
|
23669
|
+
className: "font-medium",
|
|
23670
|
+
children: _t({
|
|
23671
|
+
id: "25SzC8",
|
|
23672
|
+
message: "Not compatible with this environment"
|
|
23673
|
+
})
|
|
23674
|
+
}),
|
|
23675
|
+
/* @__PURE__ */ jsx("p", {
|
|
23676
|
+
className: "mt-1 text-sm text-kumo-default",
|
|
23677
|
+
children: _t({
|
|
23678
|
+
id: "RwFsM0",
|
|
23679
|
+
message: "This release requires a newer environment than your site currently runs. Upgrade before installing."
|
|
23680
|
+
})
|
|
23681
|
+
}),
|
|
23682
|
+
/* @__PURE__ */ jsx("ul", {
|
|
23683
|
+
className: "mt-2 space-y-1 text-sm text-kumo-default",
|
|
23684
|
+
children: envMismatches.map((m) => /* @__PURE__ */ jsx("li", { children: _t({
|
|
23685
|
+
id: "XMcEUU",
|
|
23686
|
+
message: "{0} {1} required — you have {2}.",
|
|
23687
|
+
values: {
|
|
23688
|
+
0: envLabel(m.key),
|
|
23689
|
+
1: m.required,
|
|
23690
|
+
2: m.host
|
|
23691
|
+
}
|
|
23692
|
+
}) }, m.key))
|
|
23693
|
+
})
|
|
23694
|
+
] })]
|
|
23695
|
+
}) : null,
|
|
22704
23696
|
description ? /* @__PURE__ */ jsx("p", {
|
|
22705
23697
|
className: "text-base text-kumo-default",
|
|
22706
23698
|
children: description
|
|
22707
23699
|
}) : null,
|
|
23700
|
+
screenshots.length > 0 ? /* @__PURE__ */ jsx("section", {
|
|
23701
|
+
"aria-label": _t({
|
|
23702
|
+
id: "lqROaa",
|
|
23703
|
+
message: "Screenshots"
|
|
23704
|
+
}),
|
|
23705
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
23706
|
+
className: "flex snap-x gap-3 overflow-x-auto pb-2",
|
|
23707
|
+
children: screenshots.map((shot, i) => /* @__PURE__ */ jsx("li", {
|
|
23708
|
+
className: "shrink-0 snap-start",
|
|
23709
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
23710
|
+
src: shot.src,
|
|
23711
|
+
alt: _t({
|
|
23712
|
+
id: "3rcds3",
|
|
23713
|
+
message: "Screenshot {0}",
|
|
23714
|
+
values: { 0: i + 1 }
|
|
23715
|
+
}),
|
|
23716
|
+
width: shot.width,
|
|
23717
|
+
height: shot.height,
|
|
23718
|
+
className: "h-48 w-auto rounded-lg border border-kumo-default object-cover",
|
|
23719
|
+
loading: "lazy"
|
|
23720
|
+
})
|
|
23721
|
+
}, shot.index))
|
|
23722
|
+
})
|
|
23723
|
+
}) : null,
|
|
22708
23724
|
licenseText || repoHref || keywordList.length > 0 ? /* @__PURE__ */ jsxs("section", {
|
|
22709
23725
|
className: "flex flex-wrap items-center gap-2",
|
|
22710
23726
|
children: [
|
|
@@ -22787,6 +23803,27 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22787
23803
|
className: "mt-2 flex flex-wrap gap-2",
|
|
22788
23804
|
children: capabilities.map((c) => /* @__PURE__ */ jsx(Badge, { children: c }, c))
|
|
22789
23805
|
})] }) : null,
|
|
23806
|
+
sections.length > 0 && activePane ? /* @__PURE__ */ jsxs("section", {
|
|
23807
|
+
"aria-label": _t({
|
|
23808
|
+
id: "TYxt+3",
|
|
23809
|
+
message: "Plugin details"
|
|
23810
|
+
}),
|
|
23811
|
+
children: [sections.length > 1 ? /* @__PURE__ */ jsx(Tabs, {
|
|
23812
|
+
variant: "underline",
|
|
23813
|
+
value: activeSection,
|
|
23814
|
+
onValueChange: (v) => setSelectedSection(v),
|
|
23815
|
+
tabs: sections.map((s) => ({
|
|
23816
|
+
value: s.key,
|
|
23817
|
+
label: _t(SECTION_LABELS[s.key])
|
|
23818
|
+
}))
|
|
23819
|
+
}) : /* @__PURE__ */ jsx("h2", {
|
|
23820
|
+
className: "text-sm font-semibold text-kumo-subtle",
|
|
23821
|
+
children: _t(SECTION_LABELS[activePane.key])
|
|
23822
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
23823
|
+
className: "prose prose-sm mt-4 max-w-none rounded-lg border bg-kumo-base p-6",
|
|
23824
|
+
children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(activePane.markdown) } })
|
|
23825
|
+
})]
|
|
23826
|
+
}) : null,
|
|
22790
23827
|
showConsent && release ? /* @__PURE__ */ jsx(CapabilityConsentDialog, {
|
|
22791
23828
|
mode: "install",
|
|
22792
23829
|
pluginName: displayName ?? slug,
|
|
@@ -22802,6 +23839,28 @@ function RegistryPluginDetail({ pluginId, config }) {
|
|
|
22802
23839
|
]
|
|
22803
23840
|
});
|
|
22804
23841
|
}
|
|
23842
|
+
const SECTION_LABELS = {
|
|
23843
|
+
description: {
|
|
23844
|
+
id: "Nu4oKW",
|
|
23845
|
+
message: "Description"
|
|
23846
|
+
},
|
|
23847
|
+
installation: {
|
|
23848
|
+
id: "Xry+oU",
|
|
23849
|
+
message: "Installation"
|
|
23850
|
+
},
|
|
23851
|
+
faq: {
|
|
23852
|
+
id: "/lDBHm",
|
|
23853
|
+
message: "FAQ"
|
|
23854
|
+
},
|
|
23855
|
+
changelog: {
|
|
23856
|
+
id: "CWe7wB",
|
|
23857
|
+
message: "Changelog"
|
|
23858
|
+
},
|
|
23859
|
+
security: {
|
|
23860
|
+
id: "a3LDKx",
|
|
23861
|
+
message: "Security"
|
|
23862
|
+
}
|
|
23863
|
+
};
|
|
22805
23864
|
function BackLink() {
|
|
22806
23865
|
const { _: _t2 } = useLingui();
|
|
22807
23866
|
return /* @__PURE__ */ jsxs(Link$1, {
|
|
@@ -22833,6 +23892,43 @@ function declaredAccessToCapabilityList(declaredAccess) {
|
|
|
22833
23892
|
}
|
|
22834
23893
|
return out;
|
|
22835
23894
|
}
|
|
23895
|
+
const PRE_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+-/;
|
|
23896
|
+
/**
|
|
23897
|
+
* Detects semver pre-release identifiers (`1.0.0-alpha.1`, `2.0.0-rc.2`). The
|
|
23898
|
+
* release lexicon does not enforce semver shape, so a permissive `includes("-")`
|
|
23899
|
+
* check would light up the badge for malformed values like `-1.0.0` or
|
|
23900
|
+
* `abc-def`. Require a `MAJOR.MINOR.PATCH-` prefix instead.
|
|
23901
|
+
*/
|
|
23902
|
+
function isPreReleaseVersion(version) {
|
|
23903
|
+
return PRE_RELEASE_VERSION_RE.test(version);
|
|
23904
|
+
}
|
|
23905
|
+
/**
|
|
23906
|
+
* Human-readable name for a `requires` env key. The known EmDash environments
|
|
23907
|
+
* get their proper product names; anything else falls back to the key with the
|
|
23908
|
+
* `env:` prefix stripped (product names, not localised strings).
|
|
23909
|
+
*/
|
|
23910
|
+
function envLabel(key) {
|
|
23911
|
+
if (key === "env:emdash") return "EmDash";
|
|
23912
|
+
if (key === "env:astro") return "Astro";
|
|
23913
|
+
return key.startsWith("env:") ? key.slice(4) : key;
|
|
23914
|
+
}
|
|
23915
|
+
const YANKED_LABEL_VALUE = "security:yanked";
|
|
23916
|
+
/**
|
|
23917
|
+
* Aggregators forward labels applied by their configured labellers. `security:yanked`
|
|
23918
|
+
* is a hard-enforcement label that publishers can self-apply (or that a labeller
|
|
23919
|
+
* applies on their behalf) to retract a release after publication. Sites whose
|
|
23920
|
+
* `acceptLabelers` config includes the labeller never see yanked releases at all
|
|
23921
|
+
* (server filtering), but sites without it receive yanked releases interleaved
|
|
23922
|
+
* with installable ones — filter them out so they never reach the picker.
|
|
23923
|
+
*
|
|
23924
|
+
* `neg` (negated labels) is intentionally ignored to match the server install
|
|
23925
|
+
* handler, which only checks `l.val === "security:yanked"`. Diverging here would
|
|
23926
|
+
* let the UI surface an install affordance the server will reject with
|
|
23927
|
+
* `RELEASE_YANKED`. Honoring `neg` on both sides is a separate follow-up.
|
|
23928
|
+
*/
|
|
23929
|
+
function isYanked(release) {
|
|
23930
|
+
return (release.labels ?? []).some((l) => l.val === YANKED_LABEL_VALUE);
|
|
23931
|
+
}
|
|
22836
23932
|
function formatDate(iso) {
|
|
22837
23933
|
try {
|
|
22838
23934
|
return new Date(iso).toLocaleDateString();
|
|
@@ -28147,15 +29243,6 @@ function SidebarNav({ manifest }) {
|
|
|
28147
29243
|
border-top: 1px solid rgba(255,255,255,0.08);
|
|
28148
29244
|
}
|
|
28149
29245
|
|
|
28150
|
-
/* Keep all nav icons visible when sidebar collapses to icon mode */
|
|
28151
|
-
.emdash-sidebar[data-state="collapsed"] [data-sidebar="group-content"] {
|
|
28152
|
-
grid-template-rows: 1fr !important;
|
|
28153
|
-
}
|
|
28154
|
-
/* Mobile drawer: kumo's Sheet has no data-state attribute, so group-content
|
|
28155
|
-
stays at grid-rows-[0fr] (hidden). Force it open in the mobile sidebar. */
|
|
28156
|
-
.emdash-sidebar[data-mobile="true"] [data-sidebar="group-content"] {
|
|
28157
|
-
grid-template-rows: 1fr !important;
|
|
28158
|
-
}
|
|
28159
29246
|
/* Collapsed separators — thin centered line */
|
|
28160
29247
|
.emdash-sidebar[data-state="collapsed"] [data-sidebar="separator"] {
|
|
28161
29248
|
margin: 0.375rem 0.625rem;
|
|
@@ -28239,52 +29326,36 @@ function SidebarNav({ manifest }) {
|
|
|
28239
29326
|
isActive: isItemActive("/", currentPath)
|
|
28240
29327
|
}) }) }),
|
|
28241
29328
|
/* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
|
|
28242
|
-
visibleContent.length > 1 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
|
|
28243
|
-
|
|
28244
|
-
|
|
28245
|
-
|
|
28246
|
-
|
|
28247
|
-
|
|
28248
|
-
|
|
28249
|
-
message: "Content"
|
|
28250
|
-
})
|
|
28251
|
-
}), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleContent.filter((i) => i.to !== "/")) }) })]
|
|
28252
|
-
}),
|
|
29329
|
+
visibleContent.length > 1 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
|
|
29330
|
+
className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
|
|
29331
|
+
children: _t({
|
|
29332
|
+
id: "4b3oEV",
|
|
29333
|
+
message: "Content"
|
|
29334
|
+
})
|
|
29335
|
+
}), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleContent.filter((i) => i.to !== "/")) })] }),
|
|
28253
29336
|
/* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
|
|
28254
|
-
visibleManage.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
|
|
28255
|
-
|
|
28256
|
-
|
|
28257
|
-
|
|
28258
|
-
|
|
28259
|
-
|
|
28260
|
-
|
|
28261
|
-
message: "Manage"
|
|
28262
|
-
})
|
|
28263
|
-
}), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleManage) }) })]
|
|
28264
|
-
}),
|
|
29337
|
+
visibleManage.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
|
|
29338
|
+
className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
|
|
29339
|
+
children: _t({
|
|
29340
|
+
id: "wckWOP",
|
|
29341
|
+
message: "Manage"
|
|
29342
|
+
})
|
|
29343
|
+
}), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleManage) })] }),
|
|
28265
29344
|
/* @__PURE__ */ jsx(KumoSidebar.Separator, {}),
|
|
28266
|
-
visibleAdmin.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, {
|
|
28267
|
-
|
|
28268
|
-
|
|
28269
|
-
|
|
28270
|
-
|
|
28271
|
-
|
|
28272
|
-
|
|
28273
|
-
|
|
28274
|
-
|
|
28275
|
-
|
|
28276
|
-
|
|
28277
|
-
|
|
28278
|
-
|
|
28279
|
-
|
|
28280
|
-
children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
|
|
28281
|
-
className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
|
|
28282
|
-
children: _t({
|
|
28283
|
-
id: "ohUJJM",
|
|
28284
|
-
message: "Plugins"
|
|
28285
|
-
})
|
|
28286
|
-
}), /* @__PURE__ */ jsx(KumoSidebar.GroupContent, { children: /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visiblePlugins) }) })]
|
|
28287
|
-
})] })
|
|
29345
|
+
visibleAdmin.length > 0 && /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
|
|
29346
|
+
className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
|
|
29347
|
+
children: _t({
|
|
29348
|
+
id: "U3pytU",
|
|
29349
|
+
message: "Admin"
|
|
29350
|
+
})
|
|
29351
|
+
}), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visibleAdmin) })] }),
|
|
29352
|
+
visiblePlugins.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(KumoSidebar.Separator, {}), /* @__PURE__ */ jsxs(KumoSidebar.Group, { children: [/* @__PURE__ */ jsx(KumoSidebar.GroupLabel, {
|
|
29353
|
+
className: "[&>span]:text-start [&_svg]:rtl:-scale-x-100 [&_svg]:rtl:-scale-y-100",
|
|
29354
|
+
children: _t({
|
|
29355
|
+
id: "ohUJJM",
|
|
29356
|
+
message: "Plugins"
|
|
29357
|
+
})
|
|
29358
|
+
}), /* @__PURE__ */ jsx(KumoSidebar.Menu, { children: renderNavItems(visiblePlugins) })] })] })
|
|
28288
29359
|
] }),
|
|
28289
29360
|
/* @__PURE__ */ jsx(KumoSidebar.Footer, { children: /* @__PURE__ */ jsxs("p", {
|
|
28290
29361
|
className: "emdash-nav-label px-3 py-2 text-[11px] text-white/30",
|
|
@@ -29522,22 +30593,23 @@ function TermFormDialog({ open, onClose, taxonomyName, taxonomyDef, term, allTer
|
|
|
29522
30593
|
className: "flex items-start justify-between gap-4 mb-4",
|
|
29523
30594
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
29524
30595
|
className: "flex flex-col space-y-1.5",
|
|
29525
|
-
children: [/* @__PURE__ */
|
|
30596
|
+
children: [/* @__PURE__ */ jsx(Dialog.Title, {
|
|
29526
30597
|
className: "text-lg font-semibold leading-none tracking-tight",
|
|
29527
|
-
children:
|
|
29528
|
-
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
}) : _t3({
|
|
29532
|
-
id: "m16xKo",
|
|
29533
|
-
message: "Add"
|
|
29534
|
-
}),
|
|
29535
|
-
" ",
|
|
29536
|
-
taxonomyDef.labelSingular || _t3({
|
|
30598
|
+
children: term ? _t3({
|
|
30599
|
+
id: "N6j2JH",
|
|
30600
|
+
message: "Edit {0}",
|
|
30601
|
+
values: { 0: taxonomyDef.labelSingular || _t3({
|
|
29537
30602
|
id: "k3xOSk",
|
|
29538
30603
|
message: "Term"
|
|
29539
|
-
})
|
|
29540
|
-
|
|
30604
|
+
}) }
|
|
30605
|
+
}) : _t3({
|
|
30606
|
+
id: "3nLdaX",
|
|
30607
|
+
message: "Add {0}",
|
|
30608
|
+
values: { 0: taxonomyDef.labelSingular || _t3({
|
|
30609
|
+
id: "k3xOSk",
|
|
30610
|
+
message: "Term"
|
|
30611
|
+
}) }
|
|
30612
|
+
})
|
|
29541
30613
|
}), /* @__PURE__ */ jsx(Dialog.Description, {
|
|
29542
30614
|
className: "text-sm text-kumo-subtle",
|
|
29543
30615
|
children: term ? _t3({
|
|
@@ -30062,20 +31134,17 @@ function TaxonomyManager({ taxonomyName }) {
|
|
|
30062
31134
|
message: "New Taxonomy"
|
|
30063
31135
|
})
|
|
30064
31136
|
}),
|
|
30065
|
-
/* @__PURE__ */
|
|
31137
|
+
/* @__PURE__ */ jsx(Button, {
|
|
30066
31138
|
icon: /* @__PURE__ */ jsx(Plus, {}),
|
|
30067
31139
|
onClick: () => setFormOpen(true),
|
|
30068
|
-
children:
|
|
30069
|
-
|
|
30070
|
-
|
|
30071
|
-
|
|
30072
|
-
}),
|
|
30073
|
-
" ",
|
|
30074
|
-
taxonomyDef.labelSingular || _t5({
|
|
31140
|
+
children: _t5({
|
|
31141
|
+
id: "3nLdaX",
|
|
31142
|
+
message: "Add {0}",
|
|
31143
|
+
values: { 0: taxonomyDef.labelSingular || _t5({
|
|
30075
31144
|
id: "k3xOSk",
|
|
30076
31145
|
message: "Term"
|
|
30077
|
-
})
|
|
30078
|
-
|
|
31146
|
+
}) }
|
|
31147
|
+
})
|
|
30079
31148
|
})
|
|
30080
31149
|
]
|
|
30081
31150
|
})]
|
|
@@ -34218,6 +35287,13 @@ function AuthorMappingStep({ authorMappings, emdashUsers, onMappingChange, onCon
|
|
|
34218
35287
|
|
|
34219
35288
|
//#endregion
|
|
34220
35289
|
//#region src/routes/bylines.tsx
|
|
35290
|
+
/**
|
|
35291
|
+
* True when the load-more snapshot still matches the current filter state.
|
|
35292
|
+
* Used to discard appends from requests whose filters have changed mid-flight.
|
|
35293
|
+
*/
|
|
35294
|
+
function loadMoreSnapshotMatches(snapshot, current) {
|
|
35295
|
+
return snapshot.search === current.search && snapshot.guestFilter === current.guestFilter && snapshot.locale === current.locale;
|
|
35296
|
+
}
|
|
34221
35297
|
function toFormState(byline) {
|
|
34222
35298
|
if (!byline) return {
|
|
34223
35299
|
slug: "",
|
|
@@ -34243,21 +35319,39 @@ function getUserLabel(user) {
|
|
|
34243
35319
|
function BylinesPage() {
|
|
34244
35320
|
const { _: _t } = useLingui();
|
|
34245
35321
|
const queryClient = useQueryClient();
|
|
35322
|
+
const navigate = useNavigate$1();
|
|
35323
|
+
const { locale: routeLocale } = useSearch({ from: "/_admin/bylines" });
|
|
34246
35324
|
const [search, setSearch] = React$1.useState("");
|
|
34247
35325
|
const [guestFilter, setGuestFilter] = React$1.useState("all");
|
|
34248
35326
|
const [selectedId, setSelectedId] = React$1.useState(null);
|
|
34249
35327
|
const [showDeleteConfirm, setShowDeleteConfirm] = React$1.useState(false);
|
|
34250
35328
|
const [allItems, setAllItems] = React$1.useState([]);
|
|
34251
35329
|
const [nextCursor, setNextCursor] = React$1.useState(void 0);
|
|
35330
|
+
const { data: manifest } = useQuery({
|
|
35331
|
+
queryKey: ["manifest"],
|
|
35332
|
+
queryFn: fetchManifest
|
|
35333
|
+
});
|
|
35334
|
+
const i18n = useI18nConfig(manifest);
|
|
35335
|
+
const isMultiLocale = !!i18n && i18n.locales.length > 1;
|
|
35336
|
+
const activeLocale = routeLocale ?? i18n?.defaultLocale ?? void 0;
|
|
35337
|
+
const handleLocaleChange = (locale) => {
|
|
35338
|
+
navigate({
|
|
35339
|
+
to: "/bylines",
|
|
35340
|
+
search: { locale: locale || void 0 }
|
|
35341
|
+
});
|
|
35342
|
+
setSelectedId(null);
|
|
35343
|
+
};
|
|
34252
35344
|
const { data, isLoading, error } = useQuery({
|
|
34253
35345
|
queryKey: [
|
|
34254
35346
|
"bylines",
|
|
34255
35347
|
search,
|
|
34256
|
-
guestFilter
|
|
35348
|
+
guestFilter,
|
|
35349
|
+
activeLocale ?? null
|
|
34257
35350
|
],
|
|
34258
35351
|
queryFn: () => fetchBylines({
|
|
34259
35352
|
search: search || void 0,
|
|
34260
35353
|
isGuest: guestFilter === "all" ? void 0 : guestFilter === "guest",
|
|
35354
|
+
locale: activeLocale,
|
|
34261
35355
|
limit: 50
|
|
34262
35356
|
})
|
|
34263
35357
|
});
|
|
@@ -34273,28 +35367,44 @@ function BylinesPage() {
|
|
|
34273
35367
|
});
|
|
34274
35368
|
const users = usersData?.items ?? [];
|
|
34275
35369
|
const loadMoreMutation = useMutation({
|
|
34276
|
-
mutationFn: async () => {
|
|
34277
|
-
|
|
34278
|
-
|
|
34279
|
-
|
|
34280
|
-
|
|
34281
|
-
|
|
34282
|
-
|
|
34283
|
-
|
|
35370
|
+
mutationFn: async (snapshot) => {
|
|
35371
|
+
return {
|
|
35372
|
+
result: await fetchBylines({
|
|
35373
|
+
search: snapshot.search || void 0,
|
|
35374
|
+
isGuest: snapshot.guestFilter === "all" ? void 0 : snapshot.guestFilter === "guest",
|
|
35375
|
+
locale: snapshot.locale,
|
|
35376
|
+
limit: 50,
|
|
35377
|
+
cursor: snapshot.cursor
|
|
35378
|
+
}),
|
|
35379
|
+
snapshot
|
|
35380
|
+
};
|
|
34284
35381
|
},
|
|
34285
|
-
onSuccess: (result) => {
|
|
34286
|
-
if (
|
|
34287
|
-
|
|
34288
|
-
|
|
34289
|
-
|
|
35382
|
+
onSuccess: ({ result, snapshot }) => {
|
|
35383
|
+
if (!loadMoreSnapshotMatches(snapshot, {
|
|
35384
|
+
search,
|
|
35385
|
+
guestFilter,
|
|
35386
|
+
locale: activeLocale
|
|
35387
|
+
})) return;
|
|
35388
|
+
setAllItems((prev) => [...prev, ...result.items]);
|
|
35389
|
+
setNextCursor(result.nextCursor);
|
|
34290
35390
|
}
|
|
34291
35391
|
});
|
|
34292
35392
|
const items = allItems;
|
|
34293
|
-
const
|
|
35393
|
+
const { data: selectedRemote } = useQuery({
|
|
35394
|
+
queryKey: ["byline", selectedId],
|
|
35395
|
+
queryFn: () => selectedId ? fetchByline(selectedId) : Promise.resolve(null),
|
|
35396
|
+
enabled: !!selectedId
|
|
35397
|
+
});
|
|
35398
|
+
const selected = selectedRemote ?? items.find((item) => item.id === selectedId) ?? null;
|
|
34294
35399
|
const [form, setForm] = React$1.useState(() => toFormState(null));
|
|
34295
35400
|
React$1.useEffect(() => {
|
|
34296
35401
|
setForm(toFormState(selected));
|
|
34297
|
-
}, [
|
|
35402
|
+
}, [selected]);
|
|
35403
|
+
const { data: translationsData } = useQuery({
|
|
35404
|
+
queryKey: ["byline-translations", selectedId],
|
|
35405
|
+
queryFn: () => selectedId ? fetchBylineTranslations(selectedId) : Promise.resolve({ items: [] }),
|
|
35406
|
+
enabled: !!selectedId && isMultiLocale
|
|
35407
|
+
});
|
|
34298
35408
|
const createMutation = useMutation({
|
|
34299
35409
|
mutationFn: () => createByline({
|
|
34300
35410
|
slug: form.slug,
|
|
@@ -34302,7 +35412,8 @@ function BylinesPage() {
|
|
|
34302
35412
|
bio: form.bio || null,
|
|
34303
35413
|
websiteUrl: form.websiteUrl || null,
|
|
34304
35414
|
userId: form.userId,
|
|
34305
|
-
isGuest: form.isGuest
|
|
35415
|
+
isGuest: form.isGuest,
|
|
35416
|
+
locale: activeLocale
|
|
34306
35417
|
}),
|
|
34307
35418
|
onSuccess: (created) => {
|
|
34308
35419
|
queryClient.invalidateQueries({ queryKey: ["bylines"] });
|
|
@@ -34323,6 +35434,7 @@ function BylinesPage() {
|
|
|
34323
35434
|
},
|
|
34324
35435
|
onSuccess: () => {
|
|
34325
35436
|
queryClient.invalidateQueries({ queryKey: ["bylines"] });
|
|
35437
|
+
if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline", selectedId] });
|
|
34326
35438
|
}
|
|
34327
35439
|
});
|
|
34328
35440
|
const deleteMutation = useMutation({
|
|
@@ -34336,6 +35448,26 @@ function BylinesPage() {
|
|
|
34336
35448
|
setShowDeleteConfirm(false);
|
|
34337
35449
|
}
|
|
34338
35450
|
});
|
|
35451
|
+
const [pendingTranslationLocale, setPendingTranslationLocale] = React$1.useState(null);
|
|
35452
|
+
const translateMutation = useMutation({
|
|
35453
|
+
mutationFn: (targetLocale) => {
|
|
35454
|
+
if (!selectedId) throw new Error("No byline selected");
|
|
35455
|
+
setPendingTranslationLocale(targetLocale);
|
|
35456
|
+
return createBylineTranslation(selectedId, { locale: targetLocale });
|
|
35457
|
+
},
|
|
35458
|
+
onSettled: () => {
|
|
35459
|
+
setPendingTranslationLocale(null);
|
|
35460
|
+
},
|
|
35461
|
+
onSuccess: (created) => {
|
|
35462
|
+
queryClient.invalidateQueries({ queryKey: ["bylines"] });
|
|
35463
|
+
if (selectedId) queryClient.invalidateQueries({ queryKey: ["byline-translations", selectedId] });
|
|
35464
|
+
navigate({
|
|
35465
|
+
to: "/bylines",
|
|
35466
|
+
search: { locale: created.locale }
|
|
35467
|
+
});
|
|
35468
|
+
setSelectedId(created.id);
|
|
35469
|
+
}
|
|
35470
|
+
});
|
|
34339
35471
|
if (isLoading) return /* @__PURE__ */ jsx("div", {
|
|
34340
35472
|
className: "flex items-center justify-center min-h-[30vh]",
|
|
34341
35473
|
children: /* @__PURE__ */ jsx(Loader, {})
|
|
@@ -34349,230 +35481,273 @@ function BylinesPage() {
|
|
|
34349
35481
|
})
|
|
34350
35482
|
});
|
|
34351
35483
|
const isSaving = createMutation.isPending || updateMutation.isPending;
|
|
34352
|
-
const mutationError = createMutation.error || updateMutation.error || deleteMutation.error;
|
|
35484
|
+
const mutationError = createMutation.error || updateMutation.error || deleteMutation.error || translateMutation.error;
|
|
34353
35485
|
return /* @__PURE__ */ jsxs("div", {
|
|
34354
|
-
className: "
|
|
35486
|
+
className: "space-y-4",
|
|
34355
35487
|
children: [
|
|
35488
|
+
isMultiLocale && i18n && activeLocale ? /* @__PURE__ */ jsxs("div", {
|
|
35489
|
+
className: "flex items-center justify-between",
|
|
35490
|
+
children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", {
|
|
35491
|
+
className: "text-2xl font-semibold",
|
|
35492
|
+
children: _t({
|
|
35493
|
+
id: "xLGa7J",
|
|
35494
|
+
message: "Bylines"
|
|
35495
|
+
})
|
|
35496
|
+
}) }), /* @__PURE__ */ jsx(LocaleSwitcher, {
|
|
35497
|
+
locales: i18n.locales,
|
|
35498
|
+
defaultLocale: i18n.defaultLocale,
|
|
35499
|
+
value: activeLocale,
|
|
35500
|
+
onChange: handleLocaleChange
|
|
35501
|
+
})]
|
|
35502
|
+
}) : null,
|
|
34356
35503
|
/* @__PURE__ */ jsxs("div", {
|
|
34357
|
-
className: "
|
|
35504
|
+
className: "grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]",
|
|
34358
35505
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
34359
|
-
className: "
|
|
34360
|
-
children: [/* @__PURE__ */
|
|
34361
|
-
|
|
34362
|
-
|
|
34363
|
-
|
|
34364
|
-
|
|
34365
|
-
|
|
34366
|
-
|
|
34367
|
-
|
|
34368
|
-
|
|
34369
|
-
|
|
34370
|
-
className: "flex-
|
|
34371
|
-
children: /* @__PURE__ */ jsx(
|
|
34372
|
-
"
|
|
34373
|
-
|
|
34374
|
-
|
|
34375
|
-
|
|
34376
|
-
|
|
34377
|
-
onValueChange: (v) => setGuestFilter(v ?? "all"),
|
|
34378
|
-
items: {
|
|
34379
|
-
all: _t({
|
|
34380
|
-
id: "VK3dVJ",
|
|
34381
|
-
message: "All bylines"
|
|
34382
|
-
}),
|
|
34383
|
-
guest: _t({
|
|
34384
|
-
id: "+0jFzA",
|
|
34385
|
-
message: "Guest only"
|
|
35506
|
+
className: "rounded-lg border p-4",
|
|
35507
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
35508
|
+
className: "mb-4 space-y-2",
|
|
35509
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
35510
|
+
placeholder: _t({
|
|
35511
|
+
id: "4L2LjM",
|
|
35512
|
+
message: "Search bylines"
|
|
35513
|
+
}),
|
|
35514
|
+
value: search,
|
|
35515
|
+
onChange: (e) => setSearch(e.target.value)
|
|
35516
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
35517
|
+
className: "flex items-center gap-2",
|
|
35518
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
35519
|
+
className: "flex-1",
|
|
35520
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
35521
|
+
"aria-label": _t({
|
|
35522
|
+
id: "kYxs0a",
|
|
35523
|
+
message: "Filter byline type"
|
|
34386
35524
|
}),
|
|
34387
|
-
|
|
34388
|
-
|
|
34389
|
-
|
|
34390
|
-
|
|
35525
|
+
value: guestFilter,
|
|
35526
|
+
onValueChange: (v) => setGuestFilter(v ?? "all"),
|
|
35527
|
+
items: {
|
|
35528
|
+
all: _t({
|
|
35529
|
+
id: "VK3dVJ",
|
|
35530
|
+
message: "All bylines"
|
|
35531
|
+
}),
|
|
35532
|
+
guest: _t({
|
|
35533
|
+
id: "+0jFzA",
|
|
35534
|
+
message: "Guest only"
|
|
35535
|
+
}),
|
|
35536
|
+
linked: _t({
|
|
35537
|
+
id: "RZ5T1J",
|
|
35538
|
+
message: "Linked only"
|
|
35539
|
+
})
|
|
35540
|
+
},
|
|
35541
|
+
className: "w-full"
|
|
35542
|
+
})
|
|
35543
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
35544
|
+
variant: "secondary",
|
|
35545
|
+
onClick: () => {
|
|
35546
|
+
setSelectedId(null);
|
|
35547
|
+
setForm(toFormState(null));
|
|
34391
35548
|
},
|
|
34392
|
-
|
|
34393
|
-
|
|
34394
|
-
|
|
34395
|
-
|
|
34396
|
-
|
|
34397
|
-
setSelectedId(null);
|
|
34398
|
-
setForm(toFormState(null));
|
|
34399
|
-
},
|
|
34400
|
-
children: _t({
|
|
34401
|
-
id: "isRobC",
|
|
34402
|
-
message: "New"
|
|
34403
|
-
})
|
|
35549
|
+
children: _t({
|
|
35550
|
+
id: "isRobC",
|
|
35551
|
+
message: "New"
|
|
35552
|
+
})
|
|
35553
|
+
})]
|
|
34404
35554
|
})]
|
|
35555
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
35556
|
+
className: "space-y-2 max-h-[70vh] overflow-auto",
|
|
35557
|
+
children: [
|
|
35558
|
+
items.map((item) => {
|
|
35559
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
35560
|
+
type: "button",
|
|
35561
|
+
onClick: () => setSelectedId(item.id),
|
|
35562
|
+
className: `w-full rounded border p-3 text-start ${item.id === selectedId ? "border-kumo-brand bg-kumo-brand/10" : "border-kumo-line"}`,
|
|
35563
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
35564
|
+
className: "font-medium",
|
|
35565
|
+
children: item.displayName
|
|
35566
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
35567
|
+
className: "text-xs text-kumo-subtle",
|
|
35568
|
+
children: [item.slug, item.isGuest ? _t({
|
|
35569
|
+
id: "89z9Fh",
|
|
35570
|
+
message: " - Guest"
|
|
35571
|
+
}) : item.userId ? _t({
|
|
35572
|
+
id: "X4uMa1",
|
|
35573
|
+
message: " - Linked"
|
|
35574
|
+
}) : ""]
|
|
35575
|
+
})]
|
|
35576
|
+
}, item.id);
|
|
35577
|
+
}),
|
|
35578
|
+
items.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
35579
|
+
className: "text-sm text-kumo-subtle",
|
|
35580
|
+
children: _t({
|
|
35581
|
+
id: "Qc/a/A",
|
|
35582
|
+
message: "No bylines found"
|
|
35583
|
+
})
|
|
35584
|
+
}),
|
|
35585
|
+
nextCursor && /* @__PURE__ */ jsx(Button, {
|
|
35586
|
+
variant: "secondary",
|
|
35587
|
+
className: "w-full mt-2",
|
|
35588
|
+
onClick: () => loadMoreMutation.mutate({
|
|
35589
|
+
search,
|
|
35590
|
+
guestFilter,
|
|
35591
|
+
locale: activeLocale,
|
|
35592
|
+
cursor: nextCursor
|
|
35593
|
+
}),
|
|
35594
|
+
disabled: loadMoreMutation.isPending,
|
|
35595
|
+
children: loadMoreMutation.isPending ? _t({
|
|
35596
|
+
id: "Z3FXyt",
|
|
35597
|
+
message: "Loading..."
|
|
35598
|
+
}) : _t({
|
|
35599
|
+
id: "yQ2kGp",
|
|
35600
|
+
message: "Load more"
|
|
35601
|
+
})
|
|
35602
|
+
})
|
|
35603
|
+
]
|
|
34405
35604
|
})]
|
|
34406
35605
|
}), /* @__PURE__ */ jsxs("div", {
|
|
34407
|
-
className: "
|
|
35606
|
+
className: "rounded-lg border p-6 space-y-6",
|
|
34408
35607
|
children: [
|
|
34409
|
-
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
34414
|
-
|
|
34415
|
-
className: "font-medium",
|
|
34416
|
-
children: item.displayName
|
|
34417
|
-
}), /* @__PURE__ */ jsxs("p", {
|
|
34418
|
-
className: "text-xs text-kumo-subtle",
|
|
34419
|
-
children: [item.slug, item.isGuest ? _t({
|
|
34420
|
-
id: "89z9Fh",
|
|
34421
|
-
message: " - Guest"
|
|
34422
|
-
}) : item.userId ? _t({
|
|
34423
|
-
id: "X4uMa1",
|
|
34424
|
-
message: " - Linked"
|
|
34425
|
-
}) : ""]
|
|
34426
|
-
})]
|
|
34427
|
-
}, item.id);
|
|
34428
|
-
}),
|
|
34429
|
-
items.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
34430
|
-
className: "text-sm text-kumo-subtle",
|
|
34431
|
-
children: _t({
|
|
34432
|
-
id: "Qc/a/A",
|
|
34433
|
-
message: "No bylines found"
|
|
34434
|
-
})
|
|
34435
|
-
}),
|
|
34436
|
-
nextCursor && /* @__PURE__ */ jsx(Button, {
|
|
34437
|
-
variant: "secondary",
|
|
34438
|
-
className: "w-full mt-2",
|
|
34439
|
-
onClick: () => loadMoreMutation.mutate(),
|
|
34440
|
-
disabled: loadMoreMutation.isPending,
|
|
34441
|
-
children: loadMoreMutation.isPending ? _t({
|
|
34442
|
-
id: "Z3FXyt",
|
|
34443
|
-
message: "Loading..."
|
|
35608
|
+
/* @__PURE__ */ jsx("h2", {
|
|
35609
|
+
className: "text-lg font-semibold",
|
|
35610
|
+
children: selected ? _t({
|
|
35611
|
+
id: "N6j2JH",
|
|
35612
|
+
message: "Edit {0}",
|
|
35613
|
+
values: { 0: selected.displayName }
|
|
34444
35614
|
}) : _t({
|
|
34445
|
-
id: "
|
|
34446
|
-
message: "
|
|
35615
|
+
id: "d6HWlw",
|
|
35616
|
+
message: "Create byline"
|
|
34447
35617
|
})
|
|
34448
|
-
})
|
|
34449
|
-
]
|
|
34450
|
-
})]
|
|
34451
|
-
}),
|
|
34452
|
-
/* @__PURE__ */ jsxs("div", {
|
|
34453
|
-
className: "rounded-lg border p-6",
|
|
34454
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
34455
|
-
className: "text-lg font-semibold mb-4",
|
|
34456
|
-
children: selected ? _t({
|
|
34457
|
-
id: "N6j2JH",
|
|
34458
|
-
message: "Edit {0}",
|
|
34459
|
-
values: { 0: selected.displayName }
|
|
34460
|
-
}) : _t({
|
|
34461
|
-
id: "d6HWlw",
|
|
34462
|
-
message: "Create byline"
|
|
34463
|
-
})
|
|
34464
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
34465
|
-
className: "space-y-4",
|
|
34466
|
-
children: [
|
|
34467
|
-
/* @__PURE__ */ jsx(Input, {
|
|
34468
|
-
label: _t({
|
|
34469
|
-
id: "pfa8F0",
|
|
34470
|
-
message: "Display name"
|
|
34471
|
-
}),
|
|
34472
|
-
value: form.displayName,
|
|
34473
|
-
onChange: (e) => setForm((prev) => ({
|
|
34474
|
-
...prev,
|
|
34475
|
-
displayName: e.target.value
|
|
34476
|
-
}))
|
|
34477
35618
|
}),
|
|
34478
|
-
/* @__PURE__ */
|
|
34479
|
-
|
|
34480
|
-
|
|
34481
|
-
|
|
34482
|
-
|
|
34483
|
-
|
|
34484
|
-
|
|
34485
|
-
|
|
34486
|
-
|
|
34487
|
-
|
|
34488
|
-
|
|
34489
|
-
|
|
34490
|
-
|
|
34491
|
-
id: "XYcw7G",
|
|
34492
|
-
message: "Website URL"
|
|
34493
|
-
}),
|
|
34494
|
-
value: form.websiteUrl,
|
|
34495
|
-
onChange: (e) => setForm((prev) => ({
|
|
34496
|
-
...prev,
|
|
34497
|
-
websiteUrl: e.target.value
|
|
34498
|
-
}))
|
|
34499
|
-
}),
|
|
34500
|
-
/* @__PURE__ */ jsx(InputArea, {
|
|
34501
|
-
label: _t({
|
|
34502
|
-
id: "E+oKCO",
|
|
34503
|
-
message: "Bio"
|
|
34504
|
-
}),
|
|
34505
|
-
value: form.bio,
|
|
34506
|
-
onChange: (e) => setForm((prev) => ({
|
|
34507
|
-
...prev,
|
|
34508
|
-
bio: e.target.value
|
|
34509
|
-
})),
|
|
34510
|
-
rows: 5
|
|
34511
|
-
}),
|
|
34512
|
-
/* @__PURE__ */ jsx(Select, {
|
|
34513
|
-
label: _t({
|
|
34514
|
-
id: "nti0N3",
|
|
34515
|
-
message: "Linked user"
|
|
34516
|
-
}),
|
|
34517
|
-
value: form.userId ?? "",
|
|
34518
|
-
onValueChange: (v) => {
|
|
34519
|
-
const val = v || null;
|
|
34520
|
-
setForm((prev) => ({
|
|
34521
|
-
...prev,
|
|
34522
|
-
userId: val,
|
|
34523
|
-
isGuest: val ? false : prev.isGuest
|
|
34524
|
-
}));
|
|
34525
|
-
},
|
|
34526
|
-
items: {
|
|
34527
|
-
"": _t({
|
|
34528
|
-
id: "n9o2+L",
|
|
34529
|
-
message: "No linked user"
|
|
35619
|
+
/* @__PURE__ */ jsxs("div", {
|
|
35620
|
+
className: "space-y-4",
|
|
35621
|
+
children: [
|
|
35622
|
+
/* @__PURE__ */ jsx(Input, {
|
|
35623
|
+
label: _t({
|
|
35624
|
+
id: "pfa8F0",
|
|
35625
|
+
message: "Display name"
|
|
35626
|
+
}),
|
|
35627
|
+
value: form.displayName,
|
|
35628
|
+
onChange: (e) => setForm((prev) => ({
|
|
35629
|
+
...prev,
|
|
35630
|
+
displayName: e.target.value
|
|
35631
|
+
}))
|
|
34530
35632
|
}),
|
|
34531
|
-
|
|
34532
|
-
|
|
34533
|
-
|
|
34534
|
-
|
|
34535
|
-
|
|
34536
|
-
|
|
34537
|
-
|
|
34538
|
-
|
|
34539
|
-
|
|
34540
|
-
|
|
34541
|
-
|
|
34542
|
-
|
|
34543
|
-
|
|
34544
|
-
|
|
34545
|
-
|
|
35633
|
+
/* @__PURE__ */ jsx(Input, {
|
|
35634
|
+
label: _t({
|
|
35635
|
+
id: "L85WcV",
|
|
35636
|
+
message: "Slug"
|
|
35637
|
+
}),
|
|
35638
|
+
value: form.slug,
|
|
35639
|
+
onChange: (e) => setForm((prev) => ({
|
|
35640
|
+
...prev,
|
|
35641
|
+
slug: e.target.value
|
|
35642
|
+
}))
|
|
35643
|
+
}),
|
|
35644
|
+
/* @__PURE__ */ jsx(Input, {
|
|
35645
|
+
label: _t({
|
|
35646
|
+
id: "XYcw7G",
|
|
35647
|
+
message: "Website URL"
|
|
35648
|
+
}),
|
|
35649
|
+
value: form.websiteUrl,
|
|
35650
|
+
onChange: (e) => setForm((prev) => ({
|
|
35651
|
+
...prev,
|
|
35652
|
+
websiteUrl: e.target.value
|
|
35653
|
+
}))
|
|
35654
|
+
}),
|
|
35655
|
+
/* @__PURE__ */ jsx(InputArea, {
|
|
35656
|
+
label: _t({
|
|
35657
|
+
id: "E+oKCO",
|
|
35658
|
+
message: "Bio"
|
|
35659
|
+
}),
|
|
35660
|
+
value: form.bio,
|
|
35661
|
+
onChange: (e) => setForm((prev) => ({
|
|
35662
|
+
...prev,
|
|
35663
|
+
bio: e.target.value
|
|
35664
|
+
})),
|
|
35665
|
+
rows: 5
|
|
35666
|
+
}),
|
|
35667
|
+
/* @__PURE__ */ jsx(Select, {
|
|
35668
|
+
label: _t({
|
|
35669
|
+
id: "nti0N3",
|
|
35670
|
+
message: "Linked user"
|
|
35671
|
+
}),
|
|
35672
|
+
value: form.userId ?? "",
|
|
35673
|
+
onValueChange: (v) => {
|
|
35674
|
+
const val = v || null;
|
|
35675
|
+
setForm((prev) => ({
|
|
35676
|
+
...prev,
|
|
35677
|
+
userId: val,
|
|
35678
|
+
isGuest: val ? false : prev.isGuest
|
|
35679
|
+
}));
|
|
35680
|
+
},
|
|
35681
|
+
items: {
|
|
35682
|
+
"": _t({
|
|
35683
|
+
id: "n9o2+L",
|
|
35684
|
+
message: "No linked user"
|
|
35685
|
+
}),
|
|
35686
|
+
...Object.fromEntries(users.map((u) => [u.id, getUserLabel(u)]))
|
|
35687
|
+
},
|
|
35688
|
+
className: "w-full"
|
|
35689
|
+
}),
|
|
35690
|
+
/* @__PURE__ */ jsx(Switch, {
|
|
35691
|
+
label: _t({
|
|
35692
|
+
id: "6CRaxp",
|
|
35693
|
+
message: "Guest byline"
|
|
35694
|
+
}),
|
|
35695
|
+
checked: form.isGuest,
|
|
35696
|
+
onCheckedChange: (checked) => setForm((prev) => ({
|
|
35697
|
+
...prev,
|
|
35698
|
+
isGuest: checked,
|
|
35699
|
+
userId: checked ? null : prev.userId
|
|
35700
|
+
}))
|
|
35701
|
+
}),
|
|
35702
|
+
/* @__PURE__ */ jsx(DialogError, { message: getMutationError(mutationError) }),
|
|
35703
|
+
/* @__PURE__ */ jsxs("div", {
|
|
35704
|
+
className: "flex gap-2 pt-2",
|
|
35705
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
35706
|
+
onClick: () => {
|
|
35707
|
+
if (selected) updateMutation.mutate();
|
|
35708
|
+
else createMutation.mutate();
|
|
35709
|
+
},
|
|
35710
|
+
disabled: !form.displayName || !form.slug || isSaving,
|
|
35711
|
+
children: isSaving ? _t({
|
|
35712
|
+
id: "XvjC4F",
|
|
35713
|
+
message: "Saving..."
|
|
35714
|
+
}) : selected ? _t({
|
|
35715
|
+
id: "tfDRzk",
|
|
35716
|
+
message: "Save"
|
|
35717
|
+
}) : _t({
|
|
35718
|
+
id: "hYgDIe",
|
|
35719
|
+
message: "Create"
|
|
35720
|
+
})
|
|
35721
|
+
}), selected && /* @__PURE__ */ jsx(Button, {
|
|
35722
|
+
variant: "destructive",
|
|
35723
|
+
onClick: () => setShowDeleteConfirm(true),
|
|
35724
|
+
disabled: deleteMutation.isPending,
|
|
35725
|
+
children: _t({
|
|
35726
|
+
id: "cnGeoo",
|
|
35727
|
+
message: "Delete"
|
|
35728
|
+
})
|
|
35729
|
+
})]
|
|
35730
|
+
})
|
|
35731
|
+
]
|
|
34546
35732
|
}),
|
|
34547
|
-
/* @__PURE__ */ jsx(
|
|
34548
|
-
|
|
34549
|
-
|
|
34550
|
-
|
|
34551
|
-
|
|
34552
|
-
|
|
34553
|
-
|
|
35733
|
+
selected && isMultiLocale && i18n ? /* @__PURE__ */ jsx("div", {
|
|
35734
|
+
className: "border-t pt-6",
|
|
35735
|
+
children: /* @__PURE__ */ jsx(TranslationsPanel, {
|
|
35736
|
+
locales: i18n.locales,
|
|
35737
|
+
defaultLocale: i18n.defaultLocale,
|
|
35738
|
+
currentLocale: selected.locale,
|
|
35739
|
+
translations: translationsData?.items ?? [],
|
|
35740
|
+
onOpen: (summary) => {
|
|
35741
|
+
navigate({
|
|
35742
|
+
to: "/bylines",
|
|
35743
|
+
search: { locale: summary.locale }
|
|
35744
|
+
});
|
|
35745
|
+
setSelectedId(summary.id);
|
|
34554
35746
|
},
|
|
34555
|
-
|
|
34556
|
-
|
|
34557
|
-
|
|
34558
|
-
|
|
34559
|
-
}) : selected ? _t({
|
|
34560
|
-
id: "tfDRzk",
|
|
34561
|
-
message: "Save"
|
|
34562
|
-
}) : _t({
|
|
34563
|
-
id: "hYgDIe",
|
|
34564
|
-
message: "Create"
|
|
34565
|
-
})
|
|
34566
|
-
}), selected && /* @__PURE__ */ jsx(Button, {
|
|
34567
|
-
variant: "destructive",
|
|
34568
|
-
onClick: () => setShowDeleteConfirm(true),
|
|
34569
|
-
disabled: deleteMutation.isPending,
|
|
34570
|
-
children: _t({
|
|
34571
|
-
id: "cnGeoo",
|
|
34572
|
-
message: "Delete"
|
|
34573
|
-
})
|
|
34574
|
-
})]
|
|
34575
|
-
})
|
|
35747
|
+
onCreate: (locale) => translateMutation.mutate(locale),
|
|
35748
|
+
pendingLocale: pendingTranslationLocale
|
|
35749
|
+
})
|
|
35750
|
+
}) : null
|
|
34576
35751
|
]
|
|
34577
35752
|
})]
|
|
34578
35753
|
}),
|
|
@@ -36140,10 +37315,11 @@ function ContentNewPage() {
|
|
|
36140
37315
|
queryKey: ["manifest"],
|
|
36141
37316
|
queryFn: fetchManifest
|
|
36142
37317
|
});
|
|
37318
|
+
const pickerLocale = locale ?? manifest?.i18n?.defaultLocale;
|
|
36143
37319
|
const createMutation = useMutation({
|
|
36144
37320
|
mutationFn: (data) => createContent(collection, {
|
|
36145
37321
|
...data,
|
|
36146
|
-
locale
|
|
37322
|
+
locale: pickerLocale
|
|
36147
37323
|
}),
|
|
36148
37324
|
onSuccess: (result) => {
|
|
36149
37325
|
queryClient.invalidateQueries({ queryKey: ["content", collection] });
|
|
@@ -36157,14 +37333,23 @@ function ContentNewPage() {
|
|
|
36157
37333
|
}
|
|
36158
37334
|
});
|
|
36159
37335
|
const pluginBlocks = React$1.useMemo(() => manifest ? getPluginBlocks(manifest) : [], [manifest]);
|
|
36160
|
-
const { data: bylinesData } = useQuery({
|
|
36161
|
-
queryKey: [
|
|
36162
|
-
|
|
37336
|
+
const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
|
|
37337
|
+
queryKey: [
|
|
37338
|
+
"bylines",
|
|
37339
|
+
"picker",
|
|
37340
|
+
pickerLocale ?? null
|
|
37341
|
+
],
|
|
37342
|
+
queryFn: () => fetchBylines({
|
|
37343
|
+
locale: pickerLocale,
|
|
37344
|
+
limit: 100
|
|
37345
|
+
}),
|
|
37346
|
+
enabled: !!manifest
|
|
36163
37347
|
});
|
|
36164
37348
|
const createBylineMutation = useMutation({
|
|
36165
37349
|
mutationFn: (input) => createByline({
|
|
36166
37350
|
...input,
|
|
36167
|
-
isGuest: true
|
|
37351
|
+
isGuest: true,
|
|
37352
|
+
locale: pickerLocale
|
|
36168
37353
|
}),
|
|
36169
37354
|
onSuccess: () => {
|
|
36170
37355
|
queryClient.invalidateQueries({ queryKey: ["bylines"] });
|
|
@@ -36190,10 +37375,13 @@ function ContentNewPage() {
|
|
|
36190
37375
|
collectionLabel: collectionConfig.labelSingular || collectionConfig.label,
|
|
36191
37376
|
fields: collectionConfig.fields,
|
|
36192
37377
|
isNew: true,
|
|
37378
|
+
entryLocale: pickerLocale,
|
|
37379
|
+
i18n: manifest?.i18n,
|
|
36193
37380
|
isSaving: createMutation.isPending,
|
|
36194
37381
|
onSave: handleSave,
|
|
36195
37382
|
pluginBlocks,
|
|
36196
37383
|
availableBylines: bylinesData?.items,
|
|
37384
|
+
availableBylinesLoaded: bylinesLoaded,
|
|
36197
37385
|
selectedBylines,
|
|
36198
37386
|
onBylinesChange: setSelectedBylines,
|
|
36199
37387
|
onQuickCreateByline: async (input) => {
|
|
@@ -36303,14 +37491,24 @@ function ContentEditPage() {
|
|
|
36303
37491
|
enabled: !!currentUser && currentUser.role >= ROLE_EDITOR,
|
|
36304
37492
|
staleTime: 300 * 1e3
|
|
36305
37493
|
});
|
|
36306
|
-
const
|
|
36307
|
-
|
|
36308
|
-
|
|
37494
|
+
const itemLocale = rawItem?.locale ?? void 0;
|
|
37495
|
+
const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
|
|
37496
|
+
queryKey: [
|
|
37497
|
+
"bylines",
|
|
37498
|
+
"picker",
|
|
37499
|
+
itemLocale ?? null
|
|
37500
|
+
],
|
|
37501
|
+
queryFn: () => fetchBylines({
|
|
37502
|
+
locale: itemLocale,
|
|
37503
|
+
limit: 100
|
|
37504
|
+
}),
|
|
37505
|
+
enabled: !!itemLocale
|
|
36309
37506
|
});
|
|
36310
37507
|
const createBylineMutation = useMutation({
|
|
36311
37508
|
mutationFn: (input) => createByline({
|
|
36312
37509
|
...input,
|
|
36313
|
-
isGuest: true
|
|
37510
|
+
isGuest: true,
|
|
37511
|
+
locale: itemLocale
|
|
36314
37512
|
}),
|
|
36315
37513
|
onSuccess: () => {
|
|
36316
37514
|
queryClient.invalidateQueries({ queryKey: ["bylines"] });
|
|
@@ -36693,6 +37891,7 @@ function ContentEditPage() {
|
|
|
36693
37891
|
hasSeo: collectionConfig.hasSeo,
|
|
36694
37892
|
onSeoChange: handleSeoChange,
|
|
36695
37893
|
availableBylines: bylinesData?.items,
|
|
37894
|
+
availableBylinesLoaded: bylinesLoaded,
|
|
36696
37895
|
onQuickCreateByline: async (input) => {
|
|
36697
37896
|
return await createBylineMutation.mutateAsync(input);
|
|
36698
37897
|
},
|
|
@@ -36985,7 +38184,7 @@ function MarketplaceBrowsePage() {
|
|
|
36985
38184
|
const { data: plugins } = useQuery({
|
|
36986
38185
|
queryKey: ["plugins"],
|
|
36987
38186
|
queryFn: async () => {
|
|
36988
|
-
const { fetchPlugins } = await import("./plugins-
|
|
38187
|
+
const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
|
|
36989
38188
|
return fetchPlugins();
|
|
36990
38189
|
}
|
|
36991
38190
|
});
|
|
@@ -37016,7 +38215,7 @@ function MarketplaceDetailPage() {
|
|
|
37016
38215
|
const { data: plugins } = useQuery({
|
|
37017
38216
|
queryKey: ["plugins"],
|
|
37018
38217
|
queryFn: async () => {
|
|
37019
|
-
const { fetchPlugins } = await import("./plugins-
|
|
38218
|
+
const { fetchPlugins } = await import("./plugins-BZzztFdK.js").then((n) => n.a);
|
|
37020
38219
|
return fetchPlugins();
|
|
37021
38220
|
}
|
|
37022
38221
|
});
|
|
@@ -37100,10 +38299,15 @@ const usersRoute = createRoute({
|
|
|
37100
38299
|
path: "/users",
|
|
37101
38300
|
component: UsersPage
|
|
37102
38301
|
});
|
|
38302
|
+
function parseBylinesLocaleSearch(search) {
|
|
38303
|
+
if (typeof search.locale === "string" && search.locale.length > 0) return { locale: search.locale };
|
|
38304
|
+
return { locale: void 0 };
|
|
38305
|
+
}
|
|
37103
38306
|
const bylinesRoute = createRoute({
|
|
37104
38307
|
getParentRoute: () => adminLayoutRoute,
|
|
37105
38308
|
path: "/bylines",
|
|
37106
|
-
component: BylinesPage
|
|
38309
|
+
component: BylinesPage,
|
|
38310
|
+
validateSearch: parseBylinesLocaleSearch
|
|
37107
38311
|
});
|
|
37108
38312
|
const contentTypesListRoute = createRoute({
|
|
37109
38313
|
getParentRoute: () => adminLayoutRoute,
|
|
@@ -37488,5 +38692,5 @@ function AdminApp({ pluginAdmins = EMPTY_PLUGINS, authProviders = EMPTY_AUTH_PRO
|
|
|
37488
38692
|
}
|
|
37489
38693
|
|
|
37490
38694
|
//#endregion
|
|
37491
|
-
export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
|
|
38695
|
+
export { API_BASE, API_TOKEN_SCOPES, AdminApp, AdminApp as App, AuthProviderProvider, CAPABILITY_LABELS, ContentEditor, ContentList, DEFAULT_LOCALE, Dashboard, EditorHeader, Header, Link, LoginPage, MediaLibrary, MediaPickerModal, PasskeyLogin, PasskeyRegistration, PluginAdminProvider, PortableTextEditor, SUPPORTED_LOCALES, SUPPORTED_LOCALE_CODES, SaveButton, Settings, SetupWizard, Shell, KumoSidebar as Sidebar, SidebarNav, analyzeWpPluginSite, analyzeWxr, apiFetch, bulkCommentAction, checkPluginUpdates, cn, compareRevisions, completeSignup, createAdminRouter, createAllowedDomain, createApiToken, createByline, createBylineTranslation, createCollection, createContent, createField, createMenu, createMenuItem, createMenuTranslation, createRedirect, createSection, createTaxonomy, createTerm, createTermTranslation, createWidget, createWidgetArea, deleteAllowedDomain, deleteByline, deleteCollection, deleteComment, deleteContent, deleteField, deleteFromProvider, deleteMedia, deleteMenu, deleteMenuItem, deletePasskey, deleteRedirect, deleteSection, deleteTerm, deleteWidget, deleteWidgetArea, describeCapability, disablePlugin, disableUser, discardDraft, duplicateContent, enablePlugin, enableUser, executeWpPluginImport, executeWxrImport, fetch404Summary, fetchAllowedDomains, fetchApiTokens, fetchAuthMode, fetchByline, fetchBylineTranslations, fetchBylines, fetchCollection, fetchCollections, fetchComment, fetchCommentCounts, fetchComments, fetchContent, fetchContentList, fetchDashboardStats, fetchEmailSettings, fetchFields, fetchManifest, fetchMarketplacePlugin, fetchMediaList, fetchMediaProviders, fetchMenu, fetchMenuTranslations, fetchMenus, fetchOrphanedTables, fetchPasskeys, fetchPlugin, fetchPlugins, fetchProviderMedia, fetchRedirects, fetchRevision, fetchRevisions, fetchSection, fetchSections, fetchSettings, fetchTaxonomyDef, fetchTaxonomyDefs, fetchTermTranslations, fetchTerms, fetchTheme, fetchTranslations, fetchTrashedContent, fetchUser, fetchUsers, fetchWidgetArea, fetchWidgetAreas, fetchWidgetComponents, generatePreviewUrl, getDraftStatus, getPreviewUrl, hasAllowedDomains, importWxrMedia, installMarketplacePlugin, inviteUser, parseApiResponse, permanentDeleteContent, prepareWxrImport, probeImportUrl, publishContent, registerOrphanedTable, renamePasskey, reorderFields, reorderMenuItems, reorderWidgets, requestSignup, resolveLocale, restoreContent, restoreRevision, revokeApiToken, rewriteContentUrls, scheduleContent, searchMarketplace, searchThemes, sendRecoveryLink, sendTestEmail, setSearchEnabled, throwResponseError, uninstallMarketplacePlugin, unpublishContent, unscheduleContent, updateAllowedDomain, updateByline, updateCollection, updateCommentStatus, updateContent, updateField, updateMarketplacePlugin, updateMedia, updateMenu, updateMenuItem, updateRedirect, updateSection, updateSettings, updateTerm, updateUser, updateWidget, uploadMedia, uploadToProvider, useAuthProviderList, useAuthProviders, useCurrentUser, useLocale, useNavigate, useParams, usePluginAdmins, usePluginField, usePluginHasPages, usePluginHasWidgets, usePluginPage, usePluginWidget, validateInviteToken, verifySignupToken };
|
|
37492
38696
|
//# sourceMappingURL=index.js.map
|