@emdash-cms/admin 0.12.0 → 0.13.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-cjRhNz57.js → LocaleDirectionProvider-rC0KuZjM.js} +17 -17
- package/dist/{LocaleDirectionProvider-cjRhNz57.js.map → LocaleDirectionProvider-rC0KuZjM.js.map} +1 -1
- package/dist/index.d.ts +49 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1019 -63
- 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-BB7Ta5GU.js +6 -0
- package/dist/messages-BB7Ta5GU.js.map +1 -0
- package/dist/messages-BMyaLK4h.js +6 -0
- package/dist/messages-BMyaLK4h.js.map +1 -0
- package/dist/messages-BRIi_T5q.js +6 -0
- package/dist/messages-BRIi_T5q.js.map +1 -0
- package/dist/messages-Be0FUVz_.js +6 -0
- package/dist/messages-Be0FUVz_.js.map +1 -0
- package/dist/messages-BvE8RBIJ.js +6 -0
- package/dist/messages-BvE8RBIJ.js.map +1 -0
- package/dist/messages-CBHhMxhB.js +6 -0
- package/dist/messages-CBHhMxhB.js.map +1 -0
- package/dist/messages-CQJpnG4Q.js +6 -0
- package/dist/messages-CQJpnG4Q.js.map +1 -0
- package/dist/messages-CZpDTTb_.js +6 -0
- package/dist/messages-CZpDTTb_.js.map +1 -0
- package/dist/messages-CemjGhIt.js +6 -0
- package/dist/messages-CemjGhIt.js.map +1 -0
- package/dist/messages-Czya1QvU.js +6 -0
- package/dist/messages-Czya1QvU.js.map +1 -0
- package/dist/messages-DR7ouL8H.js +6 -0
- package/dist/messages-DR7ouL8H.js.map +1 -0
- package/dist/messages-DyiZEfUj.js +6 -0
- package/dist/messages-DyiZEfUj.js.map +1 -0
- package/dist/messages-JMuy3Tpu.js +6 -0
- package/dist/messages-JMuy3Tpu.js.map +1 -0
- package/dist/messages-h9IORlKh.js +6 -0
- package/dist/messages-h9IORlKh.js.map +1 -0
- package/dist/messages-iu8SG58F.js +6 -0
- package/dist/messages-iu8SG58F.js.map +1 -0
- package/dist/messages-uD16X789.js +6 -0
- package/dist/messages-uD16X789.js.map +1 -0
- package/dist/plugins-qrjvPK4g.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -3
- package/dist/messages-9_R3aCM9.js +0 -6
- package/dist/messages-9_R3aCM9.js.map +0 -1
- package/dist/messages-A-cROb0f.js +0 -6
- package/dist/messages-A-cROb0f.js.map +0 -1
- package/dist/messages-B3-g6YaO.js +0 -6
- package/dist/messages-B3-g6YaO.js.map +0 -1
- package/dist/messages-B5UEpxLj.js +0 -6
- package/dist/messages-B5UEpxLj.js.map +0 -1
- package/dist/messages-BAEa2eLw.js +0 -6
- package/dist/messages-BAEa2eLw.js.map +0 -1
- package/dist/messages-BHndsjBE.js +0 -6
- package/dist/messages-BHndsjBE.js.map +0 -1
- package/dist/messages-BNPbaoQs.js +0 -6
- package/dist/messages-BNPbaoQs.js.map +0 -1
- package/dist/messages-BieStAB8.js +0 -6
- package/dist/messages-BieStAB8.js.map +0 -1
- package/dist/messages-COU-tgjA.js +0 -6
- package/dist/messages-COU-tgjA.js.map +0 -1
- package/dist/messages-CXVvqhvr.js +0 -6
- package/dist/messages-CXVvqhvr.js.map +0 -1
- package/dist/messages-CgTwIPDv.js +0 -6
- package/dist/messages-CgTwIPDv.js.map +0 -1
- package/dist/messages-CtPGWm7g.js +0 -6
- package/dist/messages-CtPGWm7g.js.map +0 -1
- package/dist/messages-DES9CrLv.js +0 -6
- package/dist/messages-DES9CrLv.js.map +0 -1
- package/dist/messages-DQTZQuDt.js +0 -6
- package/dist/messages-DQTZQuDt.js.map +0 -1
- package/dist/messages-Dw1e-SNm.js +0 -6
- package/dist/messages-Dw1e-SNm.js.map +0 -1
- package/dist/messages-Dzvc57yv.js +0 -6
- package/dist/messages-Dzvc57yv.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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-qrjvPK4g.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-
|
|
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-rC0KuZjM.js";
|
|
3
3
|
import "./locales/index.js";
|
|
4
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";
|
|
5
5
|
import { i18n } from "@lingui/core";
|
|
@@ -10,7 +10,7 @@ import * as React$1 from "react";
|
|
|
10
10
|
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
11
11
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
12
|
import { Dialog as Dialog$1 } from "@cloudflare/kumo/primitives";
|
|
13
|
-
import { ArrowCounterClockwise, ArrowRight, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paperclip, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, Table, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
|
|
13
|
+
import { ArrowCounterClockwise, ArrowRight, ArrowRightIcon, ArrowSquareOut, ArrowUUpLeft, ArrowUUpRight, ArrowsClockwise, ArrowsInSimple, ArrowsLeftRight, ArrowsOutSimple, BracketsCurly, Calendar, CalendarBlank, CaretDown, CaretRightIcon, CaretUp, CaretUpDown, ChatCircle, Check, CheckCircle, CircleDashed, CircleNotch, ClockCounterClockwise, Cloud, Code, CodeBlock, Columns, Copy, Cube, Database, DeviceMobile, DotsSixVertical, DownloadSimple, Envelope, EnvelopeSimple, Eye, EyeSlash, File, FileArrowDown, FileText, FloppyDisk, FolderOpen, Gear, GithubLogo, Globe, GlobeSimple, GridFour, HardDrive, Hash, Image as Image$1, ImageBroken, ImageSquare, Info, Key, Link as Link$2, LinkBreak, LinkSimple, List, ListBullets, ListChecks, ListNumbers, MagnifyingGlass, Minus, Monitor, Moon, Palette, PaperPlaneTilt, Paperclip, Paragraph, Pencil, PencilSimple, PlugsConnected, Plus, Prohibit, PuzzlePiece, Quotes, Rows, Ruler, ShareNetwork, Shield, ShieldCheck, ShieldWarning, SignOut, SlidersHorizontal, Sparkle, SquaresFour, Stack, Storefront, Sun, Table, TextAlignCenter, TextAlignLeft, TextAlignRight, TextB, TextHOne, TextHThree, TextHTwo, TextItalic, TextStrikethrough, TextT, TextUnderline, ToggleLeft, Trash, Upload, User, UserCircle, UserPlus, Users, Warning, WarningCircle, WebhooksLogo, X, YoutubeLogo } from "@phosphor-icons/react";
|
|
14
14
|
import { clsx } from "clsx";
|
|
15
15
|
import { twMerge } from "tailwind-merge";
|
|
16
16
|
import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
|
|
@@ -2053,7 +2053,7 @@ async function createMenuItem(menuName, input, options = {}) {
|
|
|
2053
2053
|
* Update a menu item
|
|
2054
2054
|
*/
|
|
2055
2055
|
async function updateMenuItem(menuName, itemId, input, options = {}) {
|
|
2056
|
-
return parseApiResponse(await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items
|
|
2056
|
+
return parseApiResponse(await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items/${itemId}`, options.locale), {
|
|
2057
2057
|
method: "PUT",
|
|
2058
2058
|
headers: { "Content-Type": "application/json" },
|
|
2059
2059
|
body: JSON.stringify(input)
|
|
@@ -2063,7 +2063,7 @@ async function updateMenuItem(menuName, itemId, input, options = {}) {
|
|
|
2063
2063
|
* Delete a menu item
|
|
2064
2064
|
*/
|
|
2065
2065
|
async function deleteMenuItem(menuName, itemId, options = {}) {
|
|
2066
|
-
const response = await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items
|
|
2066
|
+
const response = await apiFetch(withLocale$1(`${API_BASE}/menus/${menuName}/items/${itemId}`, options.locale), { method: "DELETE" });
|
|
2067
2067
|
if (!response.ok) await throwResponseError(response, i18n._({
|
|
2068
2068
|
id: "HNaiZF",
|
|
2069
2069
|
message: "Failed to delete menu item"
|
|
@@ -3874,7 +3874,11 @@ function ProviderMediaItem({ item, selected, onClick, onDoubleClick, onDimension
|
|
|
3874
3874
|
function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter, value, onChange }) {
|
|
3875
3875
|
const { _: _t } = useLingui();
|
|
3876
3876
|
const [pickerOpen, setPickerOpen] = React$1.useState(false);
|
|
3877
|
+
const [imageBroken, setImageBroken] = React$1.useState(false);
|
|
3877
3878
|
const url = typeof value === "string" && value.length > 0 ? value : "";
|
|
3879
|
+
React$1.useEffect(() => {
|
|
3880
|
+
setImageBroken(false);
|
|
3881
|
+
}, [url]);
|
|
3878
3882
|
const filter = mimeTypeFilter ?? "image/";
|
|
3879
3883
|
const canPreview = isSafePreviewUrl(url);
|
|
3880
3884
|
const handleSelect = (item) => {
|
|
@@ -3889,14 +3893,50 @@ function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter
|
|
|
3889
3893
|
className: "text-sm font-medium mb-1.5 block",
|
|
3890
3894
|
children: label
|
|
3891
3895
|
}),
|
|
3892
|
-
canPreview ? /* @__PURE__ */ jsxs("div", {
|
|
3896
|
+
canPreview ? imageBroken ? /* @__PURE__ */ jsxs("div", {
|
|
3897
|
+
className: "relative group min-h-20",
|
|
3898
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
3899
|
+
className: "min-h-20 w-full rounded-md border border-kumo-line bg-kumo-muted flex items-center justify-center gap-2 text-kumo-subtle",
|
|
3900
|
+
children: [/* @__PURE__ */ jsx(ImageBroken, { className: "h-5 w-5" }), /* @__PURE__ */ jsx("span", {
|
|
3901
|
+
className: "text-sm",
|
|
3902
|
+
children: _t({
|
|
3903
|
+
id: "1pdLQw",
|
|
3904
|
+
message: "Image not found"
|
|
3905
|
+
})
|
|
3906
|
+
})]
|
|
3907
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
3908
|
+
className: "absolute top-2 end-2 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto transition-opacity flex gap-1",
|
|
3909
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
3910
|
+
type: "button",
|
|
3911
|
+
size: "sm",
|
|
3912
|
+
variant: "secondary",
|
|
3913
|
+
onClick: () => setPickerOpen(true),
|
|
3914
|
+
children: _t({
|
|
3915
|
+
id: "o+XJ9D",
|
|
3916
|
+
message: "Change"
|
|
3917
|
+
})
|
|
3918
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
3919
|
+
type: "button",
|
|
3920
|
+
shape: "square",
|
|
3921
|
+
variant: "destructive",
|
|
3922
|
+
className: "h-8 w-8",
|
|
3923
|
+
onClick: () => onChange(actionId, ""),
|
|
3924
|
+
"aria-label": _t({
|
|
3925
|
+
id: "t/YqKh",
|
|
3926
|
+
message: "Remove"
|
|
3927
|
+
}),
|
|
3928
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
3929
|
+
})]
|
|
3930
|
+
})]
|
|
3931
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
3893
3932
|
className: "relative group",
|
|
3894
3933
|
children: [/* @__PURE__ */ jsx("img", {
|
|
3895
3934
|
src: url,
|
|
3896
3935
|
alt: "",
|
|
3897
|
-
className: "max-h-40 w-full rounded-md border border-kumo-line object-contain bg-kumo-muted",
|
|
3936
|
+
className: "max-h-40 min-h-20 w-full rounded-md border border-kumo-line object-contain bg-kumo-muted",
|
|
3898
3937
|
referrerPolicy: "no-referrer",
|
|
3899
|
-
loading: "lazy"
|
|
3938
|
+
loading: "lazy",
|
|
3939
|
+
onError: () => setImageBroken(true)
|
|
3900
3940
|
}), /* @__PURE__ */ jsxs("div", {
|
|
3901
3941
|
className: "absolute top-2 end-2 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto transition-opacity flex gap-1",
|
|
3902
3942
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
@@ -7402,6 +7442,10 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
|
|
|
7402
7442
|
const selected = container.querySelector(`[data-index="${state.selectedIndex}"]`);
|
|
7403
7443
|
if (selected) selected.scrollIntoView({ block: "nearest" });
|
|
7404
7444
|
}, [state.selectedIndex, state.isOpen]);
|
|
7445
|
+
const hasMouseMovedRef = React$1.useRef(false);
|
|
7446
|
+
React$1.useEffect(() => {
|
|
7447
|
+
if (!state.isOpen) hasMouseMovedRef.current = false;
|
|
7448
|
+
}, [state.isOpen]);
|
|
7405
7449
|
if (!state.isOpen) return null;
|
|
7406
7450
|
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
7407
7451
|
ref: (node) => {
|
|
@@ -7410,6 +7454,9 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
|
|
|
7410
7454
|
},
|
|
7411
7455
|
style: floatingStyles,
|
|
7412
7456
|
className: "z-[100] rounded-lg border bg-kumo-overlay p-1 shadow-lg min-w-[220px] max-h-[300px] overflow-y-auto",
|
|
7457
|
+
onPointerMove: () => {
|
|
7458
|
+
hasMouseMovedRef.current = true;
|
|
7459
|
+
},
|
|
7413
7460
|
children: state.items.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
7414
7461
|
className: "text-sm text-kumo-subtle px-3 py-2",
|
|
7415
7462
|
children: _t({
|
|
@@ -7421,7 +7468,9 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
|
|
|
7421
7468
|
"data-index": index,
|
|
7422
7469
|
className: cn("flex items-center gap-3 w-full px-3 py-2 text-sm rounded text-start", index === state.selectedIndex ? "bg-kumo-tint text-kumo-default" : "hover:bg-kumo-tint/50"),
|
|
7423
7470
|
onClick: () => onCommand(item),
|
|
7424
|
-
onMouseEnter: () =>
|
|
7471
|
+
onMouseEnter: () => {
|
|
7472
|
+
if (hasMouseMovedRef.current) setSelectedIndex(index);
|
|
7473
|
+
},
|
|
7425
7474
|
children: [/* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 text-kumo-subtle flex-shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
7426
7475
|
className: "flex flex-col",
|
|
7427
7476
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -11397,7 +11446,11 @@ function JsonFieldEditor({ label, id, value, onChange, required }) {
|
|
|
11397
11446
|
function ImageFieldRenderer({ id, label, description, value, onChange, required, allowedMimeTypes, fieldId }) {
|
|
11398
11447
|
const { _: _t5 } = useLingui();
|
|
11399
11448
|
const [pickerOpen, setPickerOpen] = React$1.useState(false);
|
|
11449
|
+
const [imageBroken, setImageBroken] = React$1.useState(false);
|
|
11400
11450
|
const displayUrl = typeof value === "string" ? value : value?.previewUrl || value?.src || (value && (!value.provider || value.provider === "local") ? `/_emdash/api/media/file/${typeof value.meta?.storageKey === "string" ? value.meta.storageKey : value.id}` : void 0);
|
|
11451
|
+
React$1.useEffect(() => {
|
|
11452
|
+
setImageBroken(false);
|
|
11453
|
+
}, [displayUrl]);
|
|
11401
11454
|
const handleSelect = (item) => {
|
|
11402
11455
|
const isLocalProvider = !item.provider || item.provider === "local";
|
|
11403
11456
|
onChange({
|
|
@@ -11420,12 +11473,48 @@ function ImageFieldRenderer({ id, label, description, value, onChange, required,
|
|
|
11420
11473
|
id,
|
|
11421
11474
|
children: [
|
|
11422
11475
|
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
11423
|
-
displayUrl ? /* @__PURE__ */ jsxs("div", {
|
|
11476
|
+
displayUrl ? imageBroken ? /* @__PURE__ */ jsxs("div", {
|
|
11477
|
+
className: "mt-2 relative group",
|
|
11478
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
11479
|
+
className: "min-h-20 rounded-lg border bg-kumo-muted flex items-center justify-center gap-2 text-kumo-subtle",
|
|
11480
|
+
children: [/* @__PURE__ */ jsx(ImageBroken, { className: "h-5 w-5" }), /* @__PURE__ */ jsx("span", {
|
|
11481
|
+
className: "text-sm",
|
|
11482
|
+
children: _t5({
|
|
11483
|
+
id: "1pdLQw",
|
|
11484
|
+
message: "Image not found"
|
|
11485
|
+
})
|
|
11486
|
+
})]
|
|
11487
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
11488
|
+
className: "absolute top-2 end-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
|
|
11489
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
11490
|
+
type: "button",
|
|
11491
|
+
size: "sm",
|
|
11492
|
+
variant: "secondary",
|
|
11493
|
+
onClick: () => setPickerOpen(true),
|
|
11494
|
+
children: _t5({
|
|
11495
|
+
id: "o+XJ9D",
|
|
11496
|
+
message: "Change"
|
|
11497
|
+
})
|
|
11498
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
11499
|
+
type: "button",
|
|
11500
|
+
shape: "square",
|
|
11501
|
+
variant: "destructive",
|
|
11502
|
+
className: "h-8 w-8",
|
|
11503
|
+
onClick: handleRemove,
|
|
11504
|
+
"aria-label": _t5({
|
|
11505
|
+
id: "5ywtDz",
|
|
11506
|
+
message: "Remove image"
|
|
11507
|
+
}),
|
|
11508
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
11509
|
+
})]
|
|
11510
|
+
})]
|
|
11511
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
11424
11512
|
className: "mt-2 relative group",
|
|
11425
11513
|
children: [/* @__PURE__ */ jsx("img", {
|
|
11426
11514
|
src: displayUrl,
|
|
11427
11515
|
alt: "",
|
|
11428
|
-
className: "max-h-48 rounded-lg border object-cover"
|
|
11516
|
+
className: "max-h-48 min-h-20 rounded-lg border object-cover",
|
|
11517
|
+
onError: () => setImageBroken(true)
|
|
11429
11518
|
}), /* @__PURE__ */ jsxs("div", {
|
|
11430
11519
|
className: "absolute top-2 end-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
|
|
11431
11520
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
@@ -12091,7 +12180,7 @@ function getItemTitle$1(item) {
|
|
|
12091
12180
|
/**
|
|
12092
12181
|
* Content list view with table display and trash tab
|
|
12093
12182
|
*/
|
|
12094
|
-
function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n
|
|
12183
|
+
function ContentList({ collection, collectionLabel, items, trashedItems = [], isLoading, isTrashedLoading, onDelete, onDuplicate, onRestore, onPermanentDelete, onLoadMore, onLoadMoreTrashed, hasMore, hasMoreTrashed, trashedCount = 0, i18n, activeLocale, onLocaleChange, urlPattern, sort, onSortChange, total }) {
|
|
12095
12184
|
const { _: _t } = useLingui();
|
|
12096
12185
|
const [activeTab, setActiveTab] = React$1.useState("all");
|
|
12097
12186
|
const [searchQuery, setSearchQuery] = React$1.useState("");
|
|
@@ -12105,13 +12194,16 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12105
12194
|
const query = searchQuery.toLowerCase();
|
|
12106
12195
|
return items.filter((item) => getItemTitle$1(item).toLowerCase().includes(query));
|
|
12107
12196
|
}, [items, searchQuery]);
|
|
12108
|
-
const
|
|
12109
|
-
const
|
|
12197
|
+
const effectiveTotal = typeof total === "number" && !searchQuery ? total : filteredItems.length;
|
|
12198
|
+
const totalPages = Math.max(1, Math.ceil(effectiveTotal / PAGE_SIZE));
|
|
12199
|
+
const clampedPage = Math.min(page, totalPages - 1);
|
|
12200
|
+
const paginatedItems = filteredItems.slice(clampedPage * PAGE_SIZE, (clampedPage + 1) * PAGE_SIZE);
|
|
12110
12201
|
React$1.useEffect(() => {
|
|
12111
|
-
if (
|
|
12202
|
+
if (!hasMore || !onLoadMore || searchQuery) return;
|
|
12203
|
+
if (clampedPage >= Math.ceil(filteredItems.length / PAGE_SIZE) - 1) onLoadMore();
|
|
12112
12204
|
}, [
|
|
12113
|
-
|
|
12114
|
-
|
|
12205
|
+
clampedPage,
|
|
12206
|
+
filteredItems.length,
|
|
12115
12207
|
hasMore,
|
|
12116
12208
|
onLoadMore,
|
|
12117
12209
|
searchQuery
|
|
@@ -12126,9 +12218,9 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12126
12218
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
12127
12219
|
className: "text-2xl font-bold",
|
|
12128
12220
|
children: collectionLabel
|
|
12129
|
-
}), i18n
|
|
12130
|
-
locales: i18n
|
|
12131
|
-
defaultLocale: i18n
|
|
12221
|
+
}), i18n && activeLocale && onLocaleChange && /* @__PURE__ */ jsx(LocaleSwitcher, {
|
|
12222
|
+
locales: i18n.locales,
|
|
12223
|
+
defaultLocale: i18n.defaultLocale,
|
|
12132
12224
|
value: activeLocale,
|
|
12133
12225
|
onChange: onLocaleChange,
|
|
12134
12226
|
size: "sm"
|
|
@@ -12222,7 +12314,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12222
12314
|
message: "Status"
|
|
12223
12315
|
})
|
|
12224
12316
|
}),
|
|
12225
|
-
i18n
|
|
12317
|
+
i18n && /* @__PURE__ */ jsx(SortableTh, {
|
|
12226
12318
|
field: "locale",
|
|
12227
12319
|
sort,
|
|
12228
12320
|
onSortChange,
|
|
@@ -12252,7 +12344,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12252
12344
|
}) }), /* @__PURE__ */ jsx("tbody", {
|
|
12253
12345
|
className: "divide-y divide-kumo-line",
|
|
12254
12346
|
children: isLoading && items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
|
|
12255
|
-
colSpan: i18n
|
|
12347
|
+
colSpan: i18n ? 5 : 4,
|
|
12256
12348
|
className: "px-4 py-8 text-center text-kumo-subtle",
|
|
12257
12349
|
children: /* @__PURE__ */ jsxs("span", {
|
|
12258
12350
|
className: "inline-flex items-center gap-2",
|
|
@@ -12262,7 +12354,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12262
12354
|
})]
|
|
12263
12355
|
})
|
|
12264
12356
|
}) }) : items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
|
|
12265
|
-
colSpan: i18n
|
|
12357
|
+
colSpan: i18n ? 5 : 4,
|
|
12266
12358
|
className: "px-4 py-8 text-center text-kumo-subtle",
|
|
12267
12359
|
children: [
|
|
12268
12360
|
_t({
|
|
@@ -12283,7 +12375,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12283
12375
|
})
|
|
12284
12376
|
]
|
|
12285
12377
|
}) }) : paginatedItems.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
|
|
12286
|
-
colSpan: i18n
|
|
12378
|
+
colSpan: i18n ? 5 : 4,
|
|
12287
12379
|
className: "px-4 py-8 text-center text-kumo-subtle",
|
|
12288
12380
|
children: _t({
|
|
12289
12381
|
id: "huTRDC",
|
|
@@ -12295,7 +12387,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12295
12387
|
collection,
|
|
12296
12388
|
onDelete,
|
|
12297
12389
|
onDuplicate,
|
|
12298
|
-
showLocale: !!i18n
|
|
12390
|
+
showLocale: !!i18n,
|
|
12299
12391
|
urlPattern
|
|
12300
12392
|
}, item.id))
|
|
12301
12393
|
})]
|
|
@@ -12305,21 +12397,11 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12305
12397
|
className: "flex items-center justify-between",
|
|
12306
12398
|
children: [/* @__PURE__ */ jsx("span", {
|
|
12307
12399
|
className: "text-sm text-kumo-subtle",
|
|
12308
|
-
children:
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
searchQuery
|
|
12314
|
-
}
|
|
12315
|
-
}) : i18n._({
|
|
12316
|
-
id: "Ufffi8",
|
|
12317
|
-
message: "{0, plural, one {#{1} item} other {#{2} items}}",
|
|
12318
|
-
values: {
|
|
12319
|
-
0: filteredItems.length,
|
|
12320
|
-
1: hasMore ? "+" : "",
|
|
12321
|
-
2: hasMore ? "+" : ""
|
|
12322
|
-
}
|
|
12400
|
+
children: renderItemCount({
|
|
12401
|
+
searchQuery,
|
|
12402
|
+
filteredCount: filteredItems.length,
|
|
12403
|
+
total,
|
|
12404
|
+
hasMore
|
|
12323
12405
|
})
|
|
12324
12406
|
}), /* @__PURE__ */ jsxs("div", {
|
|
12325
12407
|
className: "flex items-center gap-2",
|
|
@@ -12327,8 +12409,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12327
12409
|
/* @__PURE__ */ jsx(Button, {
|
|
12328
12410
|
variant: "outline",
|
|
12329
12411
|
shape: "square",
|
|
12330
|
-
disabled:
|
|
12331
|
-
onClick: () => setPage(
|
|
12412
|
+
disabled: clampedPage === 0,
|
|
12413
|
+
onClick: () => setPage(clampedPage - 1),
|
|
12332
12414
|
"aria-label": _t({
|
|
12333
12415
|
id: "aHKcKc",
|
|
12334
12416
|
message: "Previous page"
|
|
@@ -12341,7 +12423,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12341
12423
|
/* @__PURE__ */ jsxs("span", {
|
|
12342
12424
|
className: "text-sm",
|
|
12343
12425
|
children: [
|
|
12344
|
-
|
|
12426
|
+
clampedPage + 1,
|
|
12345
12427
|
" / ",
|
|
12346
12428
|
totalPages
|
|
12347
12429
|
]
|
|
@@ -12349,8 +12431,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12349
12431
|
/* @__PURE__ */ jsx(Button, {
|
|
12350
12432
|
variant: "outline",
|
|
12351
12433
|
shape: "square",
|
|
12352
|
-
disabled:
|
|
12353
|
-
onClick: () => setPage(
|
|
12434
|
+
disabled: clampedPage >= totalPages - 1,
|
|
12435
|
+
onClick: () => setPage(clampedPage + 1),
|
|
12354
12436
|
"aria-label": _t({
|
|
12355
12437
|
id: "/dqduX",
|
|
12356
12438
|
message: "Next page"
|
|
@@ -12500,6 +12582,38 @@ function SortableTh({ field, sort, onSortChange, label }) {
|
|
|
12500
12582
|
})
|
|
12501
12583
|
});
|
|
12502
12584
|
}
|
|
12585
|
+
/**
|
|
12586
|
+
* Render the row-count line above pagination. The rules are:
|
|
12587
|
+
* - A search query always wins — say how many matches there are.
|
|
12588
|
+
* - When the server reported a total, use it (no `+` suffix needed —
|
|
12589
|
+
* we know the count).
|
|
12590
|
+
* - Otherwise fall back to the pre-refactor behavior: loaded count,
|
|
12591
|
+
* with `+` when there are more pages the user hasn't fetched yet.
|
|
12592
|
+
*/
|
|
12593
|
+
function renderItemCount({ searchQuery, filteredCount, total, hasMore }) {
|
|
12594
|
+
if (searchQuery) return i18n._({
|
|
12595
|
+
id: "WqhW35",
|
|
12596
|
+
message: "{filteredCount, plural, one {# item matching \"{searchQuery}\"} other {# items matching \"{searchQuery}\"}}",
|
|
12597
|
+
values: {
|
|
12598
|
+
filteredCount,
|
|
12599
|
+
searchQuery
|
|
12600
|
+
}
|
|
12601
|
+
});
|
|
12602
|
+
if (typeof total === "number") return i18n._({
|
|
12603
|
+
id: "Q/6//K",
|
|
12604
|
+
message: "{total, plural, one {# item} other {# items}}",
|
|
12605
|
+
values: { total }
|
|
12606
|
+
});
|
|
12607
|
+
return i18n._({
|
|
12608
|
+
id: "FWXiie",
|
|
12609
|
+
message: "{filteredCount, plural, one {#{0} item} other {#{1} items}}",
|
|
12610
|
+
values: {
|
|
12611
|
+
0: hasMore ? "+" : "",
|
|
12612
|
+
1: hasMore ? "+" : "",
|
|
12613
|
+
filteredCount
|
|
12614
|
+
}
|
|
12615
|
+
});
|
|
12616
|
+
}
|
|
12503
12617
|
function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale, urlPattern }) {
|
|
12504
12618
|
const { _: _t2 } = useLingui();
|
|
12505
12619
|
const title = getItemTitle$1(item);
|
|
@@ -17828,6 +17942,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
17828
17942
|
const queryClient = useQueryClient();
|
|
17829
17943
|
const toastManager = Toast.useToastManager();
|
|
17830
17944
|
const isMarketplace = plugin.source === "marketplace";
|
|
17945
|
+
const isRegistry = plugin.source === "registry";
|
|
17831
17946
|
const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
|
|
17832
17947
|
const updateMutation = useMutation({
|
|
17833
17948
|
mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
|
|
@@ -18159,6 +18274,13 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18159
18274
|
message: "Uninstall"
|
|
18160
18275
|
})
|
|
18161
18276
|
})
|
|
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
|
+
})
|
|
18162
18284
|
})
|
|
18163
18285
|
]
|
|
18164
18286
|
})]
|
|
@@ -18307,7 +18429,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18307
18429
|
const isInstalled = installedPluginIds.has(pluginId);
|
|
18308
18430
|
if (isLoading) return /* @__PURE__ */ jsxs("div", {
|
|
18309
18431
|
className: "space-y-6",
|
|
18310
|
-
children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
|
|
18432
|
+
children: [/* @__PURE__ */ jsx(BackLink$1, {}), /* @__PURE__ */ jsxs("div", {
|
|
18311
18433
|
className: "animate-pulse space-y-4",
|
|
18312
18434
|
children: [
|
|
18313
18435
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -18325,7 +18447,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18325
18447
|
});
|
|
18326
18448
|
if (error || !plugin) return /* @__PURE__ */ jsxs("div", {
|
|
18327
18449
|
className: "space-y-6",
|
|
18328
|
-
children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
|
|
18450
|
+
children: [/* @__PURE__ */ jsx(BackLink$1, {}), /* @__PURE__ */ jsxs("div", {
|
|
18329
18451
|
className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-6 text-center",
|
|
18330
18452
|
children: [
|
|
18331
18453
|
/* @__PURE__ */ jsx(Warning, { className: "mx-auto h-8 w-8 text-kumo-danger" }),
|
|
@@ -18363,7 +18485,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18363
18485
|
return /* @__PURE__ */ jsxs("div", {
|
|
18364
18486
|
className: "space-y-6",
|
|
18365
18487
|
children: [
|
|
18366
|
-
/* @__PURE__ */ jsx(BackLink, {}),
|
|
18488
|
+
/* @__PURE__ */ jsx(BackLink$1, {}),
|
|
18367
18489
|
/* @__PURE__ */ jsxs("div", {
|
|
18368
18490
|
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
18369
18491
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -18642,7 +18764,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18642
18764
|
]
|
|
18643
18765
|
});
|
|
18644
18766
|
}
|
|
18645
|
-
function BackLink() {
|
|
18767
|
+
function BackLink$1() {
|
|
18646
18768
|
const { _: _t2 } = useLingui();
|
|
18647
18769
|
return /* @__PURE__ */ jsxs(Link$1, {
|
|
18648
18770
|
to: "/plugins/marketplace",
|
|
@@ -18989,7 +19111,7 @@ function MediaDetailPanel({ item, onClose, onDeleted }) {
|
|
|
18989
19111
|
message: "Uploaded:"
|
|
18990
19112
|
})
|
|
18991
19113
|
}),
|
|
18992
|
-
/* @__PURE__ */ jsx("span", { children: formatDate(item.createdAt) })
|
|
19114
|
+
/* @__PURE__ */ jsx("span", { children: formatDate$1(item.createdAt) })
|
|
18993
19115
|
]
|
|
18994
19116
|
})
|
|
18995
19117
|
]
|
|
@@ -19107,7 +19229,7 @@ function MediaDetailPanel({ item, onClose, onDeleted }) {
|
|
|
19107
19229
|
onConfirm: () => deleteMutation.mutate()
|
|
19108
19230
|
})] });
|
|
19109
19231
|
}
|
|
19110
|
-
function formatDate(isoString) {
|
|
19232
|
+
function formatDate$1(isoString) {
|
|
19111
19233
|
return new Date(isoString).toLocaleDateString(void 0, {
|
|
19112
19234
|
year: "numeric",
|
|
19113
19235
|
month: "short",
|
|
@@ -20304,7 +20426,7 @@ function MenuEditor() {
|
|
|
20304
20426
|
newItems[targetIndex] = currentItem;
|
|
20305
20427
|
const reorderedItems = newItems.map((item, i) => ({
|
|
20306
20428
|
id: item.id,
|
|
20307
|
-
parentId: item.
|
|
20429
|
+
parentId: item.parentId,
|
|
20308
20430
|
sortOrder: i
|
|
20309
20431
|
}));
|
|
20310
20432
|
setLocalItems(newItems);
|
|
@@ -20583,9 +20705,9 @@ function MenuEditor() {
|
|
|
20583
20705
|
children: item.label
|
|
20584
20706
|
}), /* @__PURE__ */ jsxs("div", {
|
|
20585
20707
|
className: "text-sm text-kumo-subtle",
|
|
20586
|
-
children: [item.type === "custom" ? item.
|
|
20708
|
+
children: [item.type === "custom" ? item.customUrl : /* @__PURE__ */ jsx("span", {
|
|
20587
20709
|
className: "inline-flex items-center rounded-full bg-kumo-brand/10 px-2 py-0.5 text-xs font-medium text-kumo-brand",
|
|
20588
|
-
children: item.
|
|
20710
|
+
children: item.referenceCollection ?? item.type
|
|
20589
20711
|
}), item.target === "_blank" && _t({
|
|
20590
20712
|
id: "+Lmv8u",
|
|
20591
20713
|
message: " (opens in new window)"
|
|
@@ -20707,7 +20829,7 @@ function MenuEditor() {
|
|
|
20707
20829
|
id: "TQJouj",
|
|
20708
20830
|
message: "Enter a URL (https://…) or a relative path (/…)"
|
|
20709
20831
|
}),
|
|
20710
|
-
defaultValue: editingItem.
|
|
20832
|
+
defaultValue: editingItem.customUrl || ""
|
|
20711
20833
|
}),
|
|
20712
20834
|
/* @__PURE__ */ jsxs(Select, {
|
|
20713
20835
|
label: _t({
|
|
@@ -21829,6 +21951,806 @@ function Redirects() {
|
|
|
21829
21951
|
});
|
|
21830
21952
|
}
|
|
21831
21953
|
|
|
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 await discovery.searchPackages({
|
|
21967
|
+
q: opts.q,
|
|
21968
|
+
cursor: opts.cursor,
|
|
21969
|
+
limit: opts.limit
|
|
21970
|
+
});
|
|
21971
|
+
},
|
|
21972
|
+
async resolvePackage(handle, slug) {
|
|
21973
|
+
return await discovery.resolvePackage({
|
|
21974
|
+
handle,
|
|
21975
|
+
slug
|
|
21976
|
+
});
|
|
21977
|
+
},
|
|
21978
|
+
async getPackage(did, slug) {
|
|
21979
|
+
return await discovery.getPackage({
|
|
21980
|
+
did,
|
|
21981
|
+
slug
|
|
21982
|
+
});
|
|
21983
|
+
},
|
|
21984
|
+
async getLatestRelease(did, slug) {
|
|
21985
|
+
return await discovery.getLatestRelease({
|
|
21986
|
+
did,
|
|
21987
|
+
package: slug
|
|
21988
|
+
});
|
|
21989
|
+
},
|
|
21990
|
+
async listReleases(did, slug, cursor) {
|
|
21991
|
+
return await 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
|
+
//#endregion
|
|
22184
|
+
//#region src/components/PublisherHandle.tsx
|
|
22185
|
+
/**
|
|
22186
|
+
* Renders an atproto publisher's identity, with three branches:
|
|
22187
|
+
*
|
|
22188
|
+
* - **Verified handle**: shows `@handle`. Our local
|
|
22189
|
+
* `LocalActorResolver` round-tripped the DID document's
|
|
22190
|
+
* `alsoKnownAs` back to the same DID (verified by DNS TXT or
|
|
22191
|
+
* `.well-known`, not by the aggregator).
|
|
22192
|
+
* - **Unverified publisher**: DID document claims a handle but the
|
|
22193
|
+
* handle's domain doesn't point back to the same DID, OR the
|
|
22194
|
+
* aggregator's claimed handle doesn't match the bidirectionally
|
|
22195
|
+
* verified one. Treat as untrusted -- the publisher might be
|
|
22196
|
+
* impersonating someone else, or the aggregator might be lying
|
|
22197
|
+
* about a handle. Surface as `Unverified publisher` in error
|
|
22198
|
+
* styling. Callers should also disable destructive actions
|
|
22199
|
+
* (install, etc.).
|
|
22200
|
+
* - **Missing handle**: no handle claimed in the DID document (no
|
|
22201
|
+
* `alsoKnownAs`), or the DID document couldn't be fetched
|
|
22202
|
+
* (network error, unsupported DID method).
|
|
22203
|
+
*
|
|
22204
|
+
* `aggregatorHandle` is what the registry's `searchPackages` /
|
|
22205
|
+
* `resolvePackage` endpoint returned for this DID. It is NEVER trusted
|
|
22206
|
+
* on its own -- the aggregator is an untrusted indexer that could be
|
|
22207
|
+
* compromised or buggy. We always run our own DID->handle round-trip
|
|
22208
|
+
* via `LocalActorResolver` (cached in localStorage for 24h) and use
|
|
22209
|
+
* the aggregator's value only to *cross-check*: if the aggregator
|
|
22210
|
+
* claims a handle that differs from what the DID document
|
|
22211
|
+
* bidirectionally verifies, the publisher is marked invalid.
|
|
22212
|
+
*/
|
|
22213
|
+
/** Trailing dot(s) on an FQDN, stripped before handle comparison. */
|
|
22214
|
+
const TRAILING_DOT = /\.+$/;
|
|
22215
|
+
/**
|
|
22216
|
+
* Hook form: returns the same tri-state result without rendering. Use
|
|
22217
|
+
* when a parent needs to coordinate UI (e.g. disable install) based on
|
|
22218
|
+
* the resolution.
|
|
22219
|
+
*/
|
|
22220
|
+
function usePublisherHandle(did, aggregatorHandle) {
|
|
22221
|
+
const { data: didHandleResolution, isPending } = useQuery({
|
|
22222
|
+
queryKey: [
|
|
22223
|
+
"registry",
|
|
22224
|
+
"did-handle",
|
|
22225
|
+
did
|
|
22226
|
+
],
|
|
22227
|
+
queryFn: () => resolveDidToHandle(did),
|
|
22228
|
+
enabled: Boolean(did),
|
|
22229
|
+
staleTime: 300 * 1e3
|
|
22230
|
+
});
|
|
22231
|
+
if (isPending || !didHandleResolution) return { status: "missing" };
|
|
22232
|
+
if (didHandleResolution.status === "missing") return { status: "missing" };
|
|
22233
|
+
if (didHandleResolution.status === "invalid") return { status: "invalid" };
|
|
22234
|
+
const verifiedHandle = didHandleResolution.handle.toLowerCase();
|
|
22235
|
+
if (aggregatorHandle) {
|
|
22236
|
+
if (aggregatorHandle.toLowerCase().replace(TRAILING_DOT, "") !== verifiedHandle) return { status: "invalid" };
|
|
22237
|
+
}
|
|
22238
|
+
return {
|
|
22239
|
+
status: "ok",
|
|
22240
|
+
handle: didHandleResolution.handle
|
|
22241
|
+
};
|
|
22242
|
+
}
|
|
22243
|
+
function PublisherHandle({ did, aggregatorHandle, onResolved, variant = "card", className }) {
|
|
22244
|
+
const { _: _t } = useLingui();
|
|
22245
|
+
const result = usePublisherHandle(did, aggregatorHandle);
|
|
22246
|
+
const onResolvedRef = React$1.useRef(onResolved);
|
|
22247
|
+
onResolvedRef.current = onResolved;
|
|
22248
|
+
React$1.useEffect(() => {
|
|
22249
|
+
onResolvedRef.current?.(result);
|
|
22250
|
+
}, [result.status, result.handle]);
|
|
22251
|
+
const textClass = variant === "card" ? "text-xs" : "text-sm";
|
|
22252
|
+
if (result.status === "ok" && result.handle) return /* @__PURE__ */ jsxs("span", {
|
|
22253
|
+
className: `truncate ${textClass} text-kumo-subtle ${className ?? ""}`,
|
|
22254
|
+
children: ["@", result.handle]
|
|
22255
|
+
});
|
|
22256
|
+
if (result.status === "invalid") return /* @__PURE__ */ jsx("span", {
|
|
22257
|
+
className: `truncate ${textClass} font-medium text-kumo-error ${className ?? ""}`,
|
|
22258
|
+
children: _t({
|
|
22259
|
+
id: "N3r0DC",
|
|
22260
|
+
message: "Unverified publisher"
|
|
22261
|
+
})
|
|
22262
|
+
});
|
|
22263
|
+
return /* @__PURE__ */ jsx("span", {
|
|
22264
|
+
className: `truncate ${textClass} text-kumo-subtle ${className ?? ""}`,
|
|
22265
|
+
children: did
|
|
22266
|
+
});
|
|
22267
|
+
}
|
|
22268
|
+
|
|
22269
|
+
//#endregion
|
|
22270
|
+
//#region src/components/RegistryBrowse.tsx
|
|
22271
|
+
/**
|
|
22272
|
+
* Registry Browse
|
|
22273
|
+
*
|
|
22274
|
+
* Grid of plugin cards backed by the experimental decentralized plugin
|
|
22275
|
+
* registry's aggregator. Search box debounces directly into the
|
|
22276
|
+
* aggregator's `searchPackages` XRPC -- the aggregator is a public,
|
|
22277
|
+
* read-only service, so no server proxy is involved.
|
|
22278
|
+
*
|
|
22279
|
+
* Cards navigate to `/plugins/marketplace/$pluginId` (the same path the
|
|
22280
|
+
* marketplace browse uses); the router branches to the registry detail
|
|
22281
|
+
* component when `manifest.registry` is configured.
|
|
22282
|
+
*/
|
|
22283
|
+
function RegistryBrowse({ config, installedRegistryUris = /* @__PURE__ */ new Set() }) {
|
|
22284
|
+
const { _: _t } = useLingui();
|
|
22285
|
+
const [searchQuery, setSearchQuery] = React$1.useState("");
|
|
22286
|
+
const [debouncedQuery, setDebouncedQuery] = React$1.useState("");
|
|
22287
|
+
React$1.useEffect(() => {
|
|
22288
|
+
const timer = setTimeout(setDebouncedQuery, 300, searchQuery);
|
|
22289
|
+
return () => clearTimeout(timer);
|
|
22290
|
+
}, [searchQuery]);
|
|
22291
|
+
const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
|
22292
|
+
queryKey: [
|
|
22293
|
+
"registry",
|
|
22294
|
+
"search",
|
|
22295
|
+
config.aggregatorUrl,
|
|
22296
|
+
debouncedQuery
|
|
22297
|
+
],
|
|
22298
|
+
queryFn: ({ pageParam }) => searchRegistryPackages(config, {
|
|
22299
|
+
q: debouncedQuery || void 0,
|
|
22300
|
+
cursor: pageParam,
|
|
22301
|
+
limit: 20
|
|
22302
|
+
}),
|
|
22303
|
+
initialPageParam: void 0,
|
|
22304
|
+
getNextPageParam: (lastPage) => lastPage.cursor
|
|
22305
|
+
});
|
|
22306
|
+
const packages = data?.pages.flatMap((p) => p.packages);
|
|
22307
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
22308
|
+
className: "space-y-6",
|
|
22309
|
+
children: [
|
|
22310
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
|
|
22311
|
+
className: "text-3xl font-bold",
|
|
22312
|
+
children: _t({
|
|
22313
|
+
id: "jNlCXv",
|
|
22314
|
+
message: "Plugin Registry"
|
|
22315
|
+
})
|
|
22316
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
22317
|
+
className: "mt-1 text-kumo-subtle",
|
|
22318
|
+
children: _t({
|
|
22319
|
+
id: "ceMA7u",
|
|
22320
|
+
message: "Browse and install plugins published to the decentralized registry."
|
|
22321
|
+
})
|
|
22322
|
+
})] }),
|
|
22323
|
+
/* @__PURE__ */ jsx("div", {
|
|
22324
|
+
className: "flex flex-col gap-3 sm:flex-row sm:items-center",
|
|
22325
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
22326
|
+
className: "relative flex-1",
|
|
22327
|
+
children: [/* @__PURE__ */ jsx(MagnifyingGlass, { className: "absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-kumo-subtle" }), /* @__PURE__ */ jsx(Input, {
|
|
22328
|
+
type: "search",
|
|
22329
|
+
placeholder: _t({
|
|
22330
|
+
id: "slq1pb",
|
|
22331
|
+
message: "Search plugins..."
|
|
22332
|
+
}),
|
|
22333
|
+
value: searchQuery,
|
|
22334
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
22335
|
+
className: "ps-9",
|
|
22336
|
+
"aria-label": _t({
|
|
22337
|
+
id: "S4qZX0",
|
|
22338
|
+
message: "Search plugins"
|
|
22339
|
+
})
|
|
22340
|
+
})]
|
|
22341
|
+
})
|
|
22342
|
+
}),
|
|
22343
|
+
error ? /* @__PURE__ */ jsx("div", {
|
|
22344
|
+
className: "rounded-md border border-kumo-error bg-kumo-error/10 p-4 text-kumo-error",
|
|
22345
|
+
role: "alert",
|
|
22346
|
+
children: _t({
|
|
22347
|
+
id: "obuqKa",
|
|
22348
|
+
message: "Failed to load plugins. The registry aggregator may be unreachable."
|
|
22349
|
+
})
|
|
22350
|
+
}) : null,
|
|
22351
|
+
isLoading ? /* @__PURE__ */ jsx("div", {
|
|
22352
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
22353
|
+
children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-32 animate-pulse rounded-md border border-kumo-border bg-kumo-subtle" }, i))
|
|
22354
|
+
}) : null,
|
|
22355
|
+
packages && packages.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
22356
|
+
className: "rounded-md border border-kumo-border bg-kumo-subtle p-8 text-center text-kumo-subtle",
|
|
22357
|
+
children: debouncedQuery ? _t({
|
|
22358
|
+
id: "5ZLwXV",
|
|
22359
|
+
message: "No plugins match \"{debouncedQuery}\".",
|
|
22360
|
+
values: { debouncedQuery }
|
|
22361
|
+
}) : _t({
|
|
22362
|
+
id: "+ugpTI",
|
|
22363
|
+
message: "No plugins have been published to this registry yet."
|
|
22364
|
+
})
|
|
22365
|
+
}) : null,
|
|
22366
|
+
packages && packages.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
22367
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
22368
|
+
children: packages.map((pkg) => /* @__PURE__ */ jsx(RegistryPackageCard, {
|
|
22369
|
+
pkg,
|
|
22370
|
+
installed: installedRegistryUris.has(pkg.uri)
|
|
22371
|
+
}, pkg.uri))
|
|
22372
|
+
}) : null,
|
|
22373
|
+
hasNextPage ? /* @__PURE__ */ jsx("div", {
|
|
22374
|
+
className: "flex justify-center",
|
|
22375
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
22376
|
+
type: "button",
|
|
22377
|
+
className: "rounded-md border border-kumo-border bg-kumo-surface px-4 py-2 text-sm hover:bg-kumo-subtle disabled:opacity-50",
|
|
22378
|
+
onClick: () => fetchNextPage(),
|
|
22379
|
+
disabled: isFetchingNextPage,
|
|
22380
|
+
children: isFetchingNextPage ? _t({
|
|
22381
|
+
id: "Z3FXyt",
|
|
22382
|
+
message: "Loading..."
|
|
22383
|
+
}) : _t({
|
|
22384
|
+
id: "yQ2kGp",
|
|
22385
|
+
message: "Load more"
|
|
22386
|
+
})
|
|
22387
|
+
})
|
|
22388
|
+
}) : null
|
|
22389
|
+
]
|
|
22390
|
+
});
|
|
22391
|
+
}
|
|
22392
|
+
function RegistryPackageCard({ pkg, installed }) {
|
|
22393
|
+
const { _: _t2 } = useLingui();
|
|
22394
|
+
const linkSegment = usePublisherHandle(pkg.did, pkg.handle).handle ?? pkg.did;
|
|
22395
|
+
const profile = pkg.profile;
|
|
22396
|
+
const verified = (pkg.labels ?? []).some((l) => l.val === "verified");
|
|
22397
|
+
return /* @__PURE__ */ jsx(Link$1, {
|
|
22398
|
+
to: "/plugins/marketplace/$pluginId",
|
|
22399
|
+
params: { pluginId: `${linkSegment}/${pkg.slug}` },
|
|
22400
|
+
className: "block rounded-md border border-kumo-border bg-kumo-surface p-4 transition-colors hover:bg-kumo-subtle focus:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand",
|
|
22401
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
22402
|
+
className: "flex items-start gap-3",
|
|
22403
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
22404
|
+
className: "mt-1 rounded-md bg-kumo-subtle p-2 text-kumo-subtle",
|
|
22405
|
+
children: /* @__PURE__ */ jsx(PuzzlePiece, { className: "h-5 w-5" })
|
|
22406
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
22407
|
+
className: "min-w-0 flex-1",
|
|
22408
|
+
children: [
|
|
22409
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22410
|
+
className: "flex items-center gap-2",
|
|
22411
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
22412
|
+
className: "truncate font-semibold",
|
|
22413
|
+
children: profile.name ?? pkg.slug
|
|
22414
|
+
}), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
|
|
22415
|
+
className: "h-4 w-4 shrink-0 text-kumo-brand",
|
|
22416
|
+
"aria-label": _t2({
|
|
22417
|
+
id: "70QfpA",
|
|
22418
|
+
message: "Verified publisher"
|
|
22419
|
+
})
|
|
22420
|
+
}) : null]
|
|
22421
|
+
}),
|
|
22422
|
+
/* @__PURE__ */ jsx(PublisherHandle, {
|
|
22423
|
+
did: pkg.did,
|
|
22424
|
+
aggregatorHandle: pkg.handle,
|
|
22425
|
+
variant: "card"
|
|
22426
|
+
}),
|
|
22427
|
+
profile.description ? /* @__PURE__ */ jsx("p", {
|
|
22428
|
+
className: "mt-2 line-clamp-2 text-sm text-kumo-default",
|
|
22429
|
+
children: profile.description
|
|
22430
|
+
}) : null,
|
|
22431
|
+
installed ? /* @__PURE__ */ jsx("div", {
|
|
22432
|
+
className: "mt-3",
|
|
22433
|
+
children: /* @__PURE__ */ jsx(Badge, {
|
|
22434
|
+
variant: "success",
|
|
22435
|
+
children: _t2({
|
|
22436
|
+
id: "eQkgKV",
|
|
22437
|
+
message: "Installed"
|
|
22438
|
+
})
|
|
22439
|
+
})
|
|
22440
|
+
}) : null
|
|
22441
|
+
]
|
|
22442
|
+
})]
|
|
22443
|
+
})
|
|
22444
|
+
});
|
|
22445
|
+
}
|
|
22446
|
+
|
|
22447
|
+
//#endregion
|
|
22448
|
+
//#region src/components/RegistryPluginDetail.tsx
|
|
22449
|
+
/**
|
|
22450
|
+
* Registry Plugin Detail
|
|
22451
|
+
*
|
|
22452
|
+
* Detail view for a plugin from the experimental decentralized plugin
|
|
22453
|
+
* registry. Resolves `(handle, slug)` directly against the configured
|
|
22454
|
+
* aggregator; install routes through the EmDash server's
|
|
22455
|
+
* `/_emdash/api/admin/plugins/registry/install` endpoint, which
|
|
22456
|
+
* re-resolves and re-verifies before writing the install.
|
|
22457
|
+
*
|
|
22458
|
+
* Identified in the URL by a `pluginId` that is `${handle}/${slug}`.
|
|
22459
|
+
* The router wraps this component when `manifest.registry` is set on
|
|
22460
|
+
* the same route the marketplace detail uses, so existing bookmarks /
|
|
22461
|
+
* sidebar entries stay stable.
|
|
22462
|
+
*/
|
|
22463
|
+
function RegistryPluginDetail({ pluginId, config }) {
|
|
22464
|
+
const { _: _t } = useLingui();
|
|
22465
|
+
const queryClient = useQueryClient();
|
|
22466
|
+
const [showConsent, setShowConsent] = React$1.useState(false);
|
|
22467
|
+
const { data: installedPlugins } = useQuery({
|
|
22468
|
+
queryKey: ["plugins"],
|
|
22469
|
+
queryFn: async () => {
|
|
22470
|
+
const { fetchPlugins } = await import("./plugins-qrjvPK4g.js").then((n) => n.a);
|
|
22471
|
+
return fetchPlugins();
|
|
22472
|
+
}
|
|
22473
|
+
});
|
|
22474
|
+
const slashIdx = pluginId.lastIndexOf("/");
|
|
22475
|
+
const publisher = slashIdx > 0 ? pluginId.slice(0, slashIdx) : "";
|
|
22476
|
+
const slug = slashIdx > 0 ? pluginId.slice(slashIdx + 1) : "";
|
|
22477
|
+
const isDid = publisher.startsWith("did:");
|
|
22478
|
+
const { data: pkg, isLoading: isLoadingPkg } = useQuery({
|
|
22479
|
+
queryKey: [
|
|
22480
|
+
"registry",
|
|
22481
|
+
"package",
|
|
22482
|
+
config.aggregatorUrl,
|
|
22483
|
+
publisher,
|
|
22484
|
+
slug,
|
|
22485
|
+
isDid
|
|
22486
|
+
],
|
|
22487
|
+
queryFn: () => isDid ? getRegistryPackage(config, publisher, slug) : resolveRegistryPackage(config, publisher, slug),
|
|
22488
|
+
enabled: Boolean(publisher && slug)
|
|
22489
|
+
});
|
|
22490
|
+
const handleResult = usePublisherHandle(pkg?.did ?? "", pkg?.handle);
|
|
22491
|
+
const { data: release } = useQuery({
|
|
22492
|
+
queryKey: [
|
|
22493
|
+
"registry",
|
|
22494
|
+
"latest-release",
|
|
22495
|
+
config.aggregatorUrl,
|
|
22496
|
+
pkg?.did,
|
|
22497
|
+
slug
|
|
22498
|
+
],
|
|
22499
|
+
queryFn: () => getLatestRegistryRelease(config, pkg.did, slug),
|
|
22500
|
+
enabled: Boolean(pkg?.did && slug)
|
|
22501
|
+
});
|
|
22502
|
+
const ext = (release?.release)?.extensions?.["com.emdashcms.experimental.package.releaseExtension"];
|
|
22503
|
+
const capabilities = Array.isArray(ext?.capabilities) ? canonicalCapabilitiesForDriftCheck(ext?.capabilities) : canonicalCapabilitiesForDriftCheck(declaredAccessToCapabilityList(ext?.declaredAccess));
|
|
22504
|
+
const profile = pkg?.profile;
|
|
22505
|
+
const verified = (pkg?.labels ?? []).some((l) => l.val === "verified");
|
|
22506
|
+
const policyOk = release && pkg ? releasePassesPolicy(release, {
|
|
22507
|
+
did: pkg.did,
|
|
22508
|
+
slug
|
|
22509
|
+
}, config.policy) : true;
|
|
22510
|
+
const installedEntry = React$1.useMemo(() => {
|
|
22511
|
+
if (!pkg || !installedPlugins) return void 0;
|
|
22512
|
+
return installedPlugins.find((p) => p.source === "registry" && p.registryPublisherDid === pkg.did && p.registrySlug === slug);
|
|
22513
|
+
}, [
|
|
22514
|
+
pkg,
|
|
22515
|
+
installedPlugins,
|
|
22516
|
+
slug
|
|
22517
|
+
]);
|
|
22518
|
+
const isInstalled = Boolean(installedEntry);
|
|
22519
|
+
const installMutation = useMutation({
|
|
22520
|
+
mutationFn: () => {
|
|
22521
|
+
if (!pkg) throw new Error("Package not loaded");
|
|
22522
|
+
return installRegistryPlugin({
|
|
22523
|
+
did: pkg.did,
|
|
22524
|
+
slug,
|
|
22525
|
+
version: release?.version,
|
|
22526
|
+
acknowledgedDeclaredAccess: capabilities
|
|
22527
|
+
});
|
|
22528
|
+
},
|
|
22529
|
+
onSuccess: () => {
|
|
22530
|
+
setShowConsent(false);
|
|
22531
|
+
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
|
22532
|
+
queryClient.invalidateQueries({ queryKey: ["manifest"] });
|
|
22533
|
+
queryClient.invalidateQueries({ queryKey: ["registry"] });
|
|
22534
|
+
}
|
|
22535
|
+
});
|
|
22536
|
+
if (isLoadingPkg) return /* @__PURE__ */ jsxs("div", {
|
|
22537
|
+
className: "space-y-6",
|
|
22538
|
+
children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
|
|
22539
|
+
className: "animate-pulse space-y-4",
|
|
22540
|
+
children: [
|
|
22541
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22542
|
+
className: "flex items-center gap-4",
|
|
22543
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-xl bg-kumo-subtle" }), /* @__PURE__ */ jsxs("div", {
|
|
22544
|
+
className: "space-y-2",
|
|
22545
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-6 w-48 rounded bg-kumo-subtle" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-32 rounded bg-kumo-subtle" })]
|
|
22546
|
+
})]
|
|
22547
|
+
}),
|
|
22548
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-full rounded bg-kumo-subtle" }),
|
|
22549
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-3/4 rounded bg-kumo-subtle" })
|
|
22550
|
+
]
|
|
22551
|
+
})]
|
|
22552
|
+
});
|
|
22553
|
+
if (!pkg) return /* @__PURE__ */ jsxs("div", {
|
|
22554
|
+
className: "space-y-6",
|
|
22555
|
+
children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsx("div", {
|
|
22556
|
+
className: "rounded-md border border-kumo-error bg-kumo-error/10 p-4 text-kumo-error",
|
|
22557
|
+
role: "alert",
|
|
22558
|
+
children: _t({
|
|
22559
|
+
id: "wZ5IqF",
|
|
22560
|
+
message: "Plugin not found. The publisher handle or slug may be incorrect."
|
|
22561
|
+
})
|
|
22562
|
+
})]
|
|
22563
|
+
});
|
|
22564
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
22565
|
+
className: "space-y-6",
|
|
22566
|
+
children: [
|
|
22567
|
+
/* @__PURE__ */ jsx(BackLink, {}),
|
|
22568
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22569
|
+
className: "flex items-start gap-4",
|
|
22570
|
+
children: [
|
|
22571
|
+
/* @__PURE__ */ jsx("div", {
|
|
22572
|
+
className: "rounded-xl bg-kumo-subtle p-3 text-kumo-subtle",
|
|
22573
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
22574
|
+
"aria-hidden": true,
|
|
22575
|
+
className: "block h-10 w-10"
|
|
22576
|
+
})
|
|
22577
|
+
}),
|
|
22578
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22579
|
+
className: "min-w-0 flex-1",
|
|
22580
|
+
children: [
|
|
22581
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22582
|
+
className: "flex items-center gap-2",
|
|
22583
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
22584
|
+
className: "truncate text-3xl font-bold",
|
|
22585
|
+
children: profile?.name ?? slug
|
|
22586
|
+
}), verified ? /* @__PURE__ */ jsx(ShieldCheck, {
|
|
22587
|
+
className: "h-5 w-5 shrink-0 text-kumo-brand",
|
|
22588
|
+
"aria-label": _t({
|
|
22589
|
+
id: "70QfpA",
|
|
22590
|
+
message: "Verified publisher"
|
|
22591
|
+
})
|
|
22592
|
+
}) : null]
|
|
22593
|
+
}),
|
|
22594
|
+
/* @__PURE__ */ jsxs("p", {
|
|
22595
|
+
className: "text-sm text-kumo-subtle",
|
|
22596
|
+
children: [
|
|
22597
|
+
_t({
|
|
22598
|
+
id: "D3P+jw",
|
|
22599
|
+
message: "Published by"
|
|
22600
|
+
}),
|
|
22601
|
+
" ",
|
|
22602
|
+
/* @__PURE__ */ jsx(PublisherHandle, {
|
|
22603
|
+
did: pkg.did,
|
|
22604
|
+
aggregatorHandle: pkg.handle,
|
|
22605
|
+
variant: "detail"
|
|
22606
|
+
})
|
|
22607
|
+
]
|
|
22608
|
+
}),
|
|
22609
|
+
release ? /* @__PURE__ */ jsxs("p", {
|
|
22610
|
+
className: "text-xs text-kumo-subtle",
|
|
22611
|
+
children: [
|
|
22612
|
+
_t({
|
|
22613
|
+
id: "YYdC3A",
|
|
22614
|
+
message: "Version {0}",
|
|
22615
|
+
values: { 0: release.version }
|
|
22616
|
+
}),
|
|
22617
|
+
" · ",
|
|
22618
|
+
_t({
|
|
22619
|
+
id: "294VT5",
|
|
22620
|
+
message: "indexed {0}",
|
|
22621
|
+
values: { 0: formatDate(release.indexedAt) }
|
|
22622
|
+
})
|
|
22623
|
+
]
|
|
22624
|
+
}) : null
|
|
22625
|
+
]
|
|
22626
|
+
}),
|
|
22627
|
+
/* @__PURE__ */ jsx("div", { children: isInstalled ? /* @__PURE__ */ jsx(Button, {
|
|
22628
|
+
variant: "secondary",
|
|
22629
|
+
disabled: true,
|
|
22630
|
+
children: _t({
|
|
22631
|
+
id: "eQkgKV",
|
|
22632
|
+
message: "Installed"
|
|
22633
|
+
})
|
|
22634
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
22635
|
+
variant: "primary",
|
|
22636
|
+
disabled: !release || !policyOk || handleResult.status === "invalid",
|
|
22637
|
+
onClick: () => setShowConsent(true),
|
|
22638
|
+
children: _t({
|
|
22639
|
+
id: "TKQ7K+",
|
|
22640
|
+
message: "Install"
|
|
22641
|
+
})
|
|
22642
|
+
}) })
|
|
22643
|
+
]
|
|
22644
|
+
}),
|
|
22645
|
+
handleResult.status === "invalid" ? /* @__PURE__ */ jsxs("div", {
|
|
22646
|
+
className: "flex items-start gap-3 rounded-md border border-kumo-error bg-kumo-error/10 p-4 text-kumo-error",
|
|
22647
|
+
role: "alert",
|
|
22648
|
+
children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
22649
|
+
className: "font-medium",
|
|
22650
|
+
children: _t({
|
|
22651
|
+
id: "R9fBA+",
|
|
22652
|
+
message: "We couldn't verify this publisher's identity"
|
|
22653
|
+
})
|
|
22654
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
22655
|
+
className: "mt-1 text-sm text-kumo-default",
|
|
22656
|
+
children: _t({
|
|
22657
|
+
id: "Iu2xLX",
|
|
22658
|
+
message: "This publisher claims a name they couldn't prove they own — possibly impersonating someone else. Install is disabled. If you know the publisher and trust them, ask them to fix their identity setup before retrying."
|
|
22659
|
+
})
|
|
22660
|
+
})] })]
|
|
22661
|
+
}) : null,
|
|
22662
|
+
release && !policyOk ? /* @__PURE__ */ jsxs("div", {
|
|
22663
|
+
className: "flex items-start gap-3 rounded-md border border-kumo-warning bg-kumo-warning/10 p-4 text-kumo-warning",
|
|
22664
|
+
role: "status",
|
|
22665
|
+
children: [/* @__PURE__ */ jsx(Warning, { className: "mt-0.5 h-5 w-5 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
22666
|
+
className: "font-medium",
|
|
22667
|
+
children: _t({
|
|
22668
|
+
id: "+2GRS6",
|
|
22669
|
+
message: "Release is too new to install"
|
|
22670
|
+
})
|
|
22671
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
22672
|
+
className: "mt-1 text-sm text-kumo-default",
|
|
22673
|
+
children: _t({
|
|
22674
|
+
id: "77FdWO",
|
|
22675
|
+
message: "Your site requires releases to be at least {0} old before they can be installed. This release will become installable later.",
|
|
22676
|
+
values: { 0: formatHoldback(config.policy?.minimumReleaseAgeSeconds ?? 0) }
|
|
22677
|
+
})
|
|
22678
|
+
})] })]
|
|
22679
|
+
}) : null,
|
|
22680
|
+
profile?.description ? /* @__PURE__ */ jsx("p", {
|
|
22681
|
+
className: "text-base text-kumo-default",
|
|
22682
|
+
children: profile.description
|
|
22683
|
+
}) : null,
|
|
22684
|
+
capabilities.length > 0 ? /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", {
|
|
22685
|
+
className: "text-sm font-semibold text-kumo-subtle",
|
|
22686
|
+
children: _t({
|
|
22687
|
+
id: "FENDVB",
|
|
22688
|
+
message: "Declared permissions"
|
|
22689
|
+
})
|
|
22690
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
22691
|
+
className: "mt-2 flex flex-wrap gap-2",
|
|
22692
|
+
children: capabilities.map((c) => /* @__PURE__ */ jsx(Badge, { children: c }, c))
|
|
22693
|
+
})] }) : null,
|
|
22694
|
+
showConsent && release ? /* @__PURE__ */ jsx(CapabilityConsentDialog, {
|
|
22695
|
+
mode: "install",
|
|
22696
|
+
pluginName: profile?.name ?? slug,
|
|
22697
|
+
capabilities,
|
|
22698
|
+
isPending: installMutation.isPending,
|
|
22699
|
+
error: getMutationError(installMutation.error),
|
|
22700
|
+
onConfirm: () => installMutation.mutate(),
|
|
22701
|
+
onCancel: () => {
|
|
22702
|
+
setShowConsent(false);
|
|
22703
|
+
installMutation.reset();
|
|
22704
|
+
}
|
|
22705
|
+
}) : null
|
|
22706
|
+
]
|
|
22707
|
+
});
|
|
22708
|
+
}
|
|
22709
|
+
function BackLink() {
|
|
22710
|
+
const { _: _t2 } = useLingui();
|
|
22711
|
+
return /* @__PURE__ */ jsxs(Link$1, {
|
|
22712
|
+
to: "/plugins/marketplace",
|
|
22713
|
+
className: "inline-flex items-center gap-1 text-sm text-kumo-subtle hover:text-kumo-default",
|
|
22714
|
+
children: [/* @__PURE__ */ jsx(ArrowPrev, { className: "h-4 w-4" }), _t2({
|
|
22715
|
+
id: "fnGsOh",
|
|
22716
|
+
message: "Back to plugins"
|
|
22717
|
+
})]
|
|
22718
|
+
});
|
|
22719
|
+
}
|
|
22720
|
+
/**
|
|
22721
|
+
* Flatten an RFC-0001 `declaredAccess` block (`{ content: { read: true },
|
|
22722
|
+
* email: { send: { allowedHosts: [...] } }, ... }`) into the legacy
|
|
22723
|
+
* `capabilities: string[]` shape that the existing sandbox runtime
|
|
22724
|
+
* enforces today. One entry per declared operation under each
|
|
22725
|
+
* category. Unknown values are skipped silently -- the consent dialog
|
|
22726
|
+
* shows only what the current runtime recognises.
|
|
22727
|
+
*/
|
|
22728
|
+
function declaredAccessToCapabilityList(declaredAccess) {
|
|
22729
|
+
if (!declaredAccess || typeof declaredAccess !== "object") return [];
|
|
22730
|
+
const out = [];
|
|
22731
|
+
for (const [category, value] of Object.entries(declaredAccess)) {
|
|
22732
|
+
if (!value || typeof value !== "object") continue;
|
|
22733
|
+
for (const [operation, opValue] of Object.entries(value)) {
|
|
22734
|
+
if (opValue === false) continue;
|
|
22735
|
+
out.push(`${category}:${operation}`);
|
|
22736
|
+
}
|
|
22737
|
+
}
|
|
22738
|
+
return out;
|
|
22739
|
+
}
|
|
22740
|
+
function formatDate(iso) {
|
|
22741
|
+
try {
|
|
22742
|
+
return new Date(iso).toLocaleDateString();
|
|
22743
|
+
} catch {
|
|
22744
|
+
return iso;
|
|
22745
|
+
}
|
|
22746
|
+
}
|
|
22747
|
+
function formatHoldback(seconds) {
|
|
22748
|
+
if (seconds <= 0) return "0s";
|
|
22749
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)} min`;
|
|
22750
|
+
if (seconds < 1440 * 60) return `${Math.round(seconds / 60 / 60)} h`;
|
|
22751
|
+
return `${Math.round(seconds / 60 / 60 / 24)} d`;
|
|
22752
|
+
}
|
|
22753
|
+
|
|
21832
22754
|
//#endregion
|
|
21833
22755
|
//#region src/components/SandboxedPluginPage.tsx
|
|
21834
22756
|
/**
|
|
@@ -26957,7 +27879,16 @@ function SidebarNav({ manifest }) {
|
|
|
26957
27879
|
minRole: ROLE_ADMIN$1
|
|
26958
27880
|
}
|
|
26959
27881
|
];
|
|
26960
|
-
if (manifest.
|
|
27882
|
+
if (manifest.registry) adminItems.push({
|
|
27883
|
+
to: "/plugins/marketplace",
|
|
27884
|
+
label: _t({
|
|
27885
|
+
id: "kVxO/O",
|
|
27886
|
+
message: "Registry"
|
|
27887
|
+
}),
|
|
27888
|
+
icon: Storefront,
|
|
27889
|
+
minRole: ROLE_ADMIN$1
|
|
27890
|
+
});
|
|
27891
|
+
else if (manifest.marketplace) adminItems.push({
|
|
26961
27892
|
to: "/plugins/marketplace",
|
|
26962
27893
|
label: _t({
|
|
26963
27894
|
id: "Zt5PUS",
|
|
@@ -26965,7 +27896,8 @@ function SidebarNav({ manifest }) {
|
|
|
26965
27896
|
}),
|
|
26966
27897
|
icon: Storefront,
|
|
26967
27898
|
minRole: ROLE_ADMIN$1
|
|
26968
|
-
}
|
|
27899
|
+
});
|
|
27900
|
+
if (manifest.marketplace) adminItems.push({
|
|
26969
27901
|
to: "/themes/marketplace",
|
|
26970
27902
|
label: _t({
|
|
26971
27903
|
id: "fJ3EbT",
|
|
@@ -35006,6 +35938,7 @@ function ContentListPage() {
|
|
|
35006
35938
|
const items = React$1.useMemo(() => {
|
|
35007
35939
|
return data?.pages.flatMap((page) => page.items) || [];
|
|
35008
35940
|
}, [data]);
|
|
35941
|
+
const total = data?.pages[0]?.total ?? items.length;
|
|
35009
35942
|
if (!manifest) return /* @__PURE__ */ jsx(LoadingScreen, {});
|
|
35010
35943
|
const collectionConfig = manifest.collections[collection];
|
|
35011
35944
|
if (!collectionConfig) return /* @__PURE__ */ jsx(NotFoundPage, { message: `Collection "${collection}" not found` });
|
|
@@ -35036,7 +35969,8 @@ function ContentListPage() {
|
|
|
35036
35969
|
onLocaleChange: handleLocaleChange,
|
|
35037
35970
|
urlPattern: collectionConfig.urlPattern,
|
|
35038
35971
|
sort,
|
|
35039
|
-
onSortChange: setSort
|
|
35972
|
+
onSortChange: setSort,
|
|
35973
|
+
total
|
|
35040
35974
|
});
|
|
35041
35975
|
}
|
|
35042
35976
|
const contentNewRoute = createRoute({
|
|
@@ -35893,6 +36827,10 @@ const marketplaceBrowseRoute = createRoute({
|
|
|
35893
36827
|
component: MarketplaceBrowsePage
|
|
35894
36828
|
});
|
|
35895
36829
|
function MarketplaceBrowsePage() {
|
|
36830
|
+
const { data: manifest } = useQuery({
|
|
36831
|
+
queryKey: ["manifest"],
|
|
36832
|
+
queryFn: fetchManifest
|
|
36833
|
+
});
|
|
35896
36834
|
const { data: plugins } = useQuery({
|
|
35897
36835
|
queryKey: ["plugins"],
|
|
35898
36836
|
queryFn: async () => {
|
|
@@ -35900,10 +36838,18 @@ function MarketplaceBrowsePage() {
|
|
|
35900
36838
|
return fetchPlugins();
|
|
35901
36839
|
}
|
|
35902
36840
|
});
|
|
35903
|
-
|
|
36841
|
+
const installedIds = React$1.useMemo(() => {
|
|
35904
36842
|
if (!plugins) return /* @__PURE__ */ new Set();
|
|
35905
36843
|
return new Set(plugins.map((p) => p.id));
|
|
35906
|
-
}, [plugins])
|
|
36844
|
+
}, [plugins]);
|
|
36845
|
+
if (manifest?.registry) {
|
|
36846
|
+
const installedRegistryUris = new Set((plugins ?? []).filter((p) => p.source === "registry" && p.registryPublisherDid && p.registrySlug).map((p) => `at://${p.registryPublisherDid}/com.emdashcms.experimental.package.profile/${p.registrySlug}`));
|
|
36847
|
+
return /* @__PURE__ */ jsx(RegistryBrowse, {
|
|
36848
|
+
config: manifest.registry,
|
|
36849
|
+
installedRegistryUris
|
|
36850
|
+
});
|
|
36851
|
+
}
|
|
36852
|
+
return /* @__PURE__ */ jsx(MarketplaceBrowse, { installedPluginIds: installedIds });
|
|
35907
36853
|
}
|
|
35908
36854
|
const marketplaceDetailRoute = createRoute({
|
|
35909
36855
|
getParentRoute: () => adminLayoutRoute,
|
|
@@ -35912,6 +36858,10 @@ const marketplaceDetailRoute = createRoute({
|
|
|
35912
36858
|
});
|
|
35913
36859
|
function MarketplaceDetailPage() {
|
|
35914
36860
|
const { pluginId } = useParams({ from: "/_admin/plugins/marketplace/$pluginId" });
|
|
36861
|
+
const { data: manifest } = useQuery({
|
|
36862
|
+
queryKey: ["manifest"],
|
|
36863
|
+
queryFn: fetchManifest
|
|
36864
|
+
});
|
|
35915
36865
|
const { data: plugins } = useQuery({
|
|
35916
36866
|
queryKey: ["plugins"],
|
|
35917
36867
|
queryFn: async () => {
|
|
@@ -35919,12 +36869,18 @@ function MarketplaceDetailPage() {
|
|
|
35919
36869
|
return fetchPlugins();
|
|
35920
36870
|
}
|
|
35921
36871
|
});
|
|
36872
|
+
const installedIds = React$1.useMemo(() => {
|
|
36873
|
+
if (!plugins) return /* @__PURE__ */ new Set();
|
|
36874
|
+
return new Set(plugins.map((p) => p.id));
|
|
36875
|
+
}, [plugins]);
|
|
36876
|
+
const looksLikeRegistryId = pluginId.includes("/");
|
|
36877
|
+
if (manifest?.registry && looksLikeRegistryId) return /* @__PURE__ */ jsx(RegistryPluginDetail, {
|
|
36878
|
+
pluginId,
|
|
36879
|
+
config: manifest.registry
|
|
36880
|
+
});
|
|
35922
36881
|
return /* @__PURE__ */ jsx(MarketplacePluginDetail, {
|
|
35923
36882
|
pluginId,
|
|
35924
|
-
installedPluginIds:
|
|
35925
|
-
if (!plugins) return /* @__PURE__ */ new Set();
|
|
35926
|
-
return new Set(plugins.map((p) => p.id));
|
|
35927
|
-
}, [plugins])
|
|
36883
|
+
installedPluginIds: installedIds
|
|
35928
36884
|
});
|
|
35929
36885
|
}
|
|
35930
36886
|
const themeMarketplaceBrowseRoute = createRoute({
|