@emdash-cms/admin 0.11.1 → 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-PhIYobDG.js → LocaleDirectionProvider-rC0KuZjM.js} +17 -17
- package/dist/{LocaleDirectionProvider-PhIYobDG.js.map → LocaleDirectionProvider-rC0KuZjM.js.map} +1 -1
- package/dist/index.d.ts +70 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1231 -98
- 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-0WS1kU2r.js +0 -6
- package/dist/messages-0WS1kU2r.js.map +0 -1
- package/dist/messages-B3nZXT9a.js +0 -6
- package/dist/messages-B3nZXT9a.js.map +0 -1
- package/dist/messages-BHe7DjK4.js +0 -6
- package/dist/messages-BHe7DjK4.js.map +0 -1
- package/dist/messages-BNm9xWl9.js +0 -6
- package/dist/messages-BNm9xWl9.js.map +0 -1
- package/dist/messages-Bf6-l0VR.js +0 -6
- package/dist/messages-Bf6-l0VR.js.map +0 -1
- package/dist/messages-C2QjBo2p.js +0 -6
- package/dist/messages-C2QjBo2p.js.map +0 -1
- package/dist/messages-CAYw_5r5.js +0 -6
- package/dist/messages-CAYw_5r5.js.map +0 -1
- package/dist/messages-CBAO8mkk.js +0 -6
- package/dist/messages-CBAO8mkk.js.map +0 -1
- package/dist/messages-CNsSgfXs.js +0 -6
- package/dist/messages-CNsSgfXs.js.map +0 -1
- package/dist/messages-CgrjHEi_.js +0 -6
- package/dist/messages-CgrjHEi_.js.map +0 -1
- package/dist/messages-DCoCdFuY.js +0 -6
- package/dist/messages-DCoCdFuY.js.map +0 -1
- package/dist/messages-DdvAysPo.js +0 -6
- package/dist/messages-DdvAysPo.js.map +0 -1
- package/dist/messages-Di1aYzxi.js +0 -6
- package/dist/messages-Di1aYzxi.js.map +0 -1
- package/dist/messages-NtPfr5Dr.js +0 -6
- package/dist/messages-NtPfr5Dr.js.map +0 -1
- package/dist/messages-Y1VN2Mk3.js +0 -6
- package/dist/messages-Y1VN2Mk3.js.map +0 -1
- package/dist/messages-cjsZTyVv.js +0 -6
- package/dist/messages-cjsZTyVv.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"
|
|
@@ -3122,7 +3122,7 @@ function probeImageDimensions(url, errorMessage) {
|
|
|
3122
3122
|
img.src = url;
|
|
3123
3123
|
});
|
|
3124
3124
|
}
|
|
3125
|
-
function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", mimeTypeFilters, fieldId, title: providedTitle, hideUrlInput = false, mediaKind = "image" }) {
|
|
3125
|
+
function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "image/", mimeTypeFilters, fieldId, title: providedTitle, hideUrlInput = false, mediaKind = "image", localOnly = false }) {
|
|
3126
3126
|
const { _: _t } = useLingui();
|
|
3127
3127
|
const isFileKind = mediaKind === "file";
|
|
3128
3128
|
const filters = React$1.useMemo(() => {
|
|
@@ -3170,11 +3170,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3170
3170
|
setUploadError(null);
|
|
3171
3171
|
setProviderDimensions({});
|
|
3172
3172
|
}
|
|
3173
|
-
}, [open]);
|
|
3173
|
+
}, [open, localOnly]);
|
|
3174
3174
|
const { data: providers } = useQuery({
|
|
3175
3175
|
queryKey: ["media-providers"],
|
|
3176
3176
|
queryFn: fetchMediaProviders,
|
|
3177
|
-
enabled: open,
|
|
3177
|
+
enabled: open && !localOnly,
|
|
3178
3178
|
placeholderData: []
|
|
3179
3179
|
});
|
|
3180
3180
|
const activeProviderInfo = React$1.useMemo(() => {
|
|
@@ -3191,12 +3191,15 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3191
3191
|
};
|
|
3192
3192
|
return providers?.find((p) => p.id === activeProvider);
|
|
3193
3193
|
}, [activeProvider, providers]);
|
|
3194
|
-
const { data: localData, isLoading: localLoading } =
|
|
3194
|
+
const { data: localData, isLoading: localLoading, fetchNextPage: fetchNextLocalPage, hasNextPage: hasNextLocalPage, isFetchingNextPage: isFetchingNextLocalPage } = useInfiniteQuery({
|
|
3195
3195
|
queryKey: ["media", filters?.join(",") ?? ""],
|
|
3196
|
-
queryFn: () => fetchMediaList({
|
|
3196
|
+
queryFn: ({ pageParam }) => fetchMediaList({
|
|
3197
3197
|
mimeType: filters,
|
|
3198
|
-
|
|
3198
|
+
cursor: pageParam,
|
|
3199
|
+
limit: 100
|
|
3199
3200
|
}),
|
|
3201
|
+
initialPageParam: void 0,
|
|
3202
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
3200
3203
|
enabled: open && activeProvider === "local"
|
|
3201
3204
|
});
|
|
3202
3205
|
const { data: providerData, isLoading: providerLoading } = useQuery({
|
|
@@ -3211,9 +3214,9 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3211
3214
|
limit: 50,
|
|
3212
3215
|
query: searchQuery || void 0
|
|
3213
3216
|
}),
|
|
3214
|
-
enabled: open && activeProvider !== "local"
|
|
3217
|
+
enabled: open && !localOnly && activeProvider !== "local"
|
|
3215
3218
|
});
|
|
3216
|
-
const isLoading = activeProvider === "local" ? localLoading : providerLoading;
|
|
3219
|
+
const isLoading = activeProvider === "local" ? localLoading || isFetchingNextLocalPage : providerLoading;
|
|
3217
3220
|
const [uploadError, setUploadError] = React$1.useState(null);
|
|
3218
3221
|
const uploadLocalMutation = useMutation({
|
|
3219
3222
|
mutationFn: (file) => uploadMedia(file, { fieldId }),
|
|
@@ -3255,11 +3258,14 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3255
3258
|
if (!old) return old;
|
|
3256
3259
|
return {
|
|
3257
3260
|
...old,
|
|
3258
|
-
|
|
3259
|
-
...
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3261
|
+
pages: old.pages.map((page) => ({
|
|
3262
|
+
...page,
|
|
3263
|
+
items: page.items.map((item) => item.id === id ? {
|
|
3264
|
+
...item,
|
|
3265
|
+
width,
|
|
3266
|
+
height
|
|
3267
|
+
} : item)
|
|
3268
|
+
}))
|
|
3263
3269
|
};
|
|
3264
3270
|
});
|
|
3265
3271
|
if (selectedItem?.providerId === "local" && selectedItem.item.id === id) setSelectedItem({
|
|
@@ -3285,11 +3291,11 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3285
3291
|
});
|
|
3286
3292
|
}, [dimensionsMutation]);
|
|
3287
3293
|
const items = React$1.useMemo(() => {
|
|
3288
|
-
if (activeProvider === "local") return (localData?.items || []).filter((item) => matchesAnyFilter(item.mimeType, filters));
|
|
3294
|
+
if (activeProvider === "local") return (localData?.pages.flatMap((page) => page.items) || []).filter((item) => matchesAnyFilter(item.mimeType, filters));
|
|
3289
3295
|
return providerData?.items || [];
|
|
3290
3296
|
}, [
|
|
3291
3297
|
activeProvider,
|
|
3292
|
-
localData
|
|
3298
|
+
localData,
|
|
3293
3299
|
providerData?.items,
|
|
3294
3300
|
filters
|
|
3295
3301
|
]);
|
|
@@ -3399,7 +3405,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3399
3405
|
name: "Library",
|
|
3400
3406
|
icon: void 0
|
|
3401
3407
|
}];
|
|
3402
|
-
if (providers) {
|
|
3408
|
+
if (providers && !localOnly) {
|
|
3403
3409
|
for (const p of providers) if (p.id !== "local") tabs.push({
|
|
3404
3410
|
id: p.id,
|
|
3405
3411
|
name: p.name,
|
|
@@ -3407,7 +3413,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3407
3413
|
});
|
|
3408
3414
|
}
|
|
3409
3415
|
return tabs;
|
|
3410
|
-
}, [providers]);
|
|
3416
|
+
}, [providers, localOnly]);
|
|
3411
3417
|
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
3412
3418
|
open,
|
|
3413
3419
|
onOpenChange: handleClose,
|
|
@@ -3444,7 +3450,7 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3444
3450
|
})
|
|
3445
3451
|
})]
|
|
3446
3452
|
}),
|
|
3447
|
-
!hideUrlInput && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
3453
|
+
!hideUrlInput && !localOnly && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
3448
3454
|
className: "border-b pb-4",
|
|
3449
3455
|
children: [
|
|
3450
3456
|
/* @__PURE__ */ jsx(Label, { children: _t({
|
|
@@ -3581,9 +3587,9 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3581
3587
|
}) : null,
|
|
3582
3588
|
className: "mb-3"
|
|
3583
3589
|
}),
|
|
3584
|
-
/* @__PURE__ */
|
|
3590
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3585
3591
|
className: "flex-1 overflow-y-auto min-h-0",
|
|
3586
|
-
children: isLoading ? /* @__PURE__ */ jsx("div", {
|
|
3592
|
+
children: [isLoading && items.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
3587
3593
|
className: "flex items-center justify-center h-full",
|
|
3588
3594
|
children: /* @__PURE__ */ jsx(Loader, {})
|
|
3589
3595
|
}) : items.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
@@ -3662,7 +3668,22 @@ function MediaPickerModal({ open, onOpenChange, onSelect, mimeTypeFilter = "imag
|
|
|
3662
3668
|
}));
|
|
3663
3669
|
}
|
|
3664
3670
|
}, item.id))
|
|
3665
|
-
})
|
|
3671
|
+
}), activeProvider === "local" && hasNextLocalPage && /* @__PURE__ */ jsx("div", {
|
|
3672
|
+
className: "flex justify-center py-3",
|
|
3673
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
3674
|
+
variant: "outline",
|
|
3675
|
+
size: "sm",
|
|
3676
|
+
onClick: () => void fetchNextLocalPage(),
|
|
3677
|
+
disabled: isFetchingNextLocalPage,
|
|
3678
|
+
children: isFetchingNextLocalPage ? _t({
|
|
3679
|
+
id: "Z3FXyt",
|
|
3680
|
+
message: "Loading..."
|
|
3681
|
+
}) : _t({
|
|
3682
|
+
id: "a4N/Bg",
|
|
3683
|
+
message: "Load More"
|
|
3684
|
+
})
|
|
3685
|
+
})
|
|
3686
|
+
})]
|
|
3666
3687
|
}),
|
|
3667
3688
|
/* @__PURE__ */ jsxs("div", {
|
|
3668
3689
|
className: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-t pt-4",
|
|
@@ -3853,7 +3874,11 @@ function ProviderMediaItem({ item, selected, onClick, onDoubleClick, onDimension
|
|
|
3853
3874
|
function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter, value, onChange }) {
|
|
3854
3875
|
const { _: _t } = useLingui();
|
|
3855
3876
|
const [pickerOpen, setPickerOpen] = React$1.useState(false);
|
|
3877
|
+
const [imageBroken, setImageBroken] = React$1.useState(false);
|
|
3856
3878
|
const url = typeof value === "string" && value.length > 0 ? value : "";
|
|
3879
|
+
React$1.useEffect(() => {
|
|
3880
|
+
setImageBroken(false);
|
|
3881
|
+
}, [url]);
|
|
3857
3882
|
const filter = mimeTypeFilter ?? "image/";
|
|
3858
3883
|
const canPreview = isSafePreviewUrl(url);
|
|
3859
3884
|
const handleSelect = (item) => {
|
|
@@ -3868,14 +3893,50 @@ function BlockKitMediaPickerField({ actionId, label, placeholder, mimeTypeFilter
|
|
|
3868
3893
|
className: "text-sm font-medium mb-1.5 block",
|
|
3869
3894
|
children: label
|
|
3870
3895
|
}),
|
|
3871
|
-
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", {
|
|
3872
3932
|
className: "relative group",
|
|
3873
3933
|
children: [/* @__PURE__ */ jsx("img", {
|
|
3874
3934
|
src: url,
|
|
3875
3935
|
alt: "",
|
|
3876
|
-
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",
|
|
3877
3937
|
referrerPolicy: "no-referrer",
|
|
3878
|
-
loading: "lazy"
|
|
3938
|
+
loading: "lazy",
|
|
3939
|
+
onError: () => setImageBroken(true)
|
|
3879
3940
|
}), /* @__PURE__ */ jsxs("div", {
|
|
3880
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",
|
|
3881
3942
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
@@ -7381,6 +7442,10 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
|
|
|
7381
7442
|
const selected = container.querySelector(`[data-index="${state.selectedIndex}"]`);
|
|
7382
7443
|
if (selected) selected.scrollIntoView({ block: "nearest" });
|
|
7383
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]);
|
|
7384
7449
|
if (!state.isOpen) return null;
|
|
7385
7450
|
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
7386
7451
|
ref: (node) => {
|
|
@@ -7389,6 +7454,9 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
|
|
|
7389
7454
|
},
|
|
7390
7455
|
style: floatingStyles,
|
|
7391
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
|
+
},
|
|
7392
7460
|
children: state.items.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
7393
7461
|
className: "text-sm text-kumo-subtle px-3 py-2",
|
|
7394
7462
|
children: _t({
|
|
@@ -7400,7 +7468,9 @@ function SlashCommandMenu({ state, onCommand, onClose: _onClose, setSelectedInde
|
|
|
7400
7468
|
"data-index": index,
|
|
7401
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"),
|
|
7402
7470
|
onClick: () => onCommand(item),
|
|
7403
|
-
onMouseEnter: () =>
|
|
7471
|
+
onMouseEnter: () => {
|
|
7472
|
+
if (hasMouseMovedRef.current) setSelectedIndex(index);
|
|
7473
|
+
},
|
|
7404
7474
|
children: [/* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 text-kumo-subtle flex-shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
7405
7475
|
className: "flex flex-col",
|
|
7406
7476
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -7969,7 +8039,7 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
|
|
|
7969
8039
|
/** When editing an existing block, store the node position for updateAttributes */
|
|
7970
8040
|
const editingBlockPosRef = React$1.useRef(null);
|
|
7971
8041
|
const [sectionPickerOpen, setSectionPickerOpen] = React$1.useState(false);
|
|
7972
|
-
const [slashMenuState,
|
|
8042
|
+
const [slashMenuState, setSlashMenuStateRaw] = React$1.useState({
|
|
7973
8043
|
isOpen: false,
|
|
7974
8044
|
items: [],
|
|
7975
8045
|
selectedIndex: 0,
|
|
@@ -7977,9 +8047,11 @@ function PortableTextEditor({ value, onChange, placeholder = "Start writing...",
|
|
|
7977
8047
|
range: null
|
|
7978
8048
|
});
|
|
7979
8049
|
const slashMenuStateRef = React$1.useRef(slashMenuState);
|
|
7980
|
-
React$1.
|
|
7981
|
-
slashMenuStateRef.current
|
|
7982
|
-
|
|
8050
|
+
const setSlashMenuState = React$1.useCallback((action) => {
|
|
8051
|
+
const next = typeof action === "function" ? action(slashMenuStateRef.current) : action;
|
|
8052
|
+
slashMenuStateRef.current = next;
|
|
8053
|
+
setSlashMenuStateRaw(next);
|
|
8054
|
+
}, []);
|
|
7983
8055
|
const slashCommands = React$1.useMemo(() => {
|
|
7984
8056
|
const cmds = [...defaultSlashCommands];
|
|
7985
8057
|
cmds.push({
|
|
@@ -11374,7 +11446,11 @@ function JsonFieldEditor({ label, id, value, onChange, required }) {
|
|
|
11374
11446
|
function ImageFieldRenderer({ id, label, description, value, onChange, required, allowedMimeTypes, fieldId }) {
|
|
11375
11447
|
const { _: _t5 } = useLingui();
|
|
11376
11448
|
const [pickerOpen, setPickerOpen] = React$1.useState(false);
|
|
11449
|
+
const [imageBroken, setImageBroken] = React$1.useState(false);
|
|
11377
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]);
|
|
11378
11454
|
const handleSelect = (item) => {
|
|
11379
11455
|
const isLocalProvider = !item.provider || item.provider === "local";
|
|
11380
11456
|
onChange({
|
|
@@ -11397,12 +11473,48 @@ function ImageFieldRenderer({ id, label, description, value, onChange, required,
|
|
|
11397
11473
|
id,
|
|
11398
11474
|
children: [
|
|
11399
11475
|
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
11400
|
-
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", {
|
|
11401
11512
|
className: "mt-2 relative group",
|
|
11402
11513
|
children: [/* @__PURE__ */ jsx("img", {
|
|
11403
11514
|
src: displayUrl,
|
|
11404
11515
|
alt: "",
|
|
11405
|
-
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)
|
|
11406
11518
|
}), /* @__PURE__ */ jsxs("div", {
|
|
11407
11519
|
className: "absolute top-2 end-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1",
|
|
11408
11520
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
@@ -12068,7 +12180,7 @@ function getItemTitle$1(item) {
|
|
|
12068
12180
|
/**
|
|
12069
12181
|
* Content list view with table display and trash tab
|
|
12070
12182
|
*/
|
|
12071
|
-
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 }) {
|
|
12072
12184
|
const { _: _t } = useLingui();
|
|
12073
12185
|
const [activeTab, setActiveTab] = React$1.useState("all");
|
|
12074
12186
|
const [searchQuery, setSearchQuery] = React$1.useState("");
|
|
@@ -12082,13 +12194,16 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12082
12194
|
const query = searchQuery.toLowerCase();
|
|
12083
12195
|
return items.filter((item) => getItemTitle$1(item).toLowerCase().includes(query));
|
|
12084
12196
|
}, [items, searchQuery]);
|
|
12085
|
-
const
|
|
12086
|
-
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);
|
|
12087
12201
|
React$1.useEffect(() => {
|
|
12088
|
-
if (
|
|
12202
|
+
if (!hasMore || !onLoadMore || searchQuery) return;
|
|
12203
|
+
if (clampedPage >= Math.ceil(filteredItems.length / PAGE_SIZE) - 1) onLoadMore();
|
|
12089
12204
|
}, [
|
|
12090
|
-
|
|
12091
|
-
|
|
12205
|
+
clampedPage,
|
|
12206
|
+
filteredItems.length,
|
|
12092
12207
|
hasMore,
|
|
12093
12208
|
onLoadMore,
|
|
12094
12209
|
searchQuery
|
|
@@ -12103,9 +12218,9 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12103
12218
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
12104
12219
|
className: "text-2xl font-bold",
|
|
12105
12220
|
children: collectionLabel
|
|
12106
|
-
}), i18n
|
|
12107
|
-
locales: i18n
|
|
12108
|
-
defaultLocale: i18n
|
|
12221
|
+
}), i18n && activeLocale && onLocaleChange && /* @__PURE__ */ jsx(LocaleSwitcher, {
|
|
12222
|
+
locales: i18n.locales,
|
|
12223
|
+
defaultLocale: i18n.defaultLocale,
|
|
12109
12224
|
value: activeLocale,
|
|
12110
12225
|
onChange: onLocaleChange,
|
|
12111
12226
|
size: "sm"
|
|
@@ -12199,7 +12314,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12199
12314
|
message: "Status"
|
|
12200
12315
|
})
|
|
12201
12316
|
}),
|
|
12202
|
-
i18n
|
|
12317
|
+
i18n && /* @__PURE__ */ jsx(SortableTh, {
|
|
12203
12318
|
field: "locale",
|
|
12204
12319
|
sort,
|
|
12205
12320
|
onSortChange,
|
|
@@ -12229,7 +12344,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12229
12344
|
}) }), /* @__PURE__ */ jsx("tbody", {
|
|
12230
12345
|
className: "divide-y divide-kumo-line",
|
|
12231
12346
|
children: isLoading && items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
|
|
12232
|
-
colSpan: i18n
|
|
12347
|
+
colSpan: i18n ? 5 : 4,
|
|
12233
12348
|
className: "px-4 py-8 text-center text-kumo-subtle",
|
|
12234
12349
|
children: /* @__PURE__ */ jsxs("span", {
|
|
12235
12350
|
className: "inline-flex items-center gap-2",
|
|
@@ -12239,7 +12354,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12239
12354
|
})]
|
|
12240
12355
|
})
|
|
12241
12356
|
}) }) : items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
|
|
12242
|
-
colSpan: i18n
|
|
12357
|
+
colSpan: i18n ? 5 : 4,
|
|
12243
12358
|
className: "px-4 py-8 text-center text-kumo-subtle",
|
|
12244
12359
|
children: [
|
|
12245
12360
|
_t({
|
|
@@ -12260,7 +12375,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12260
12375
|
})
|
|
12261
12376
|
]
|
|
12262
12377
|
}) }) : paginatedItems.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
|
|
12263
|
-
colSpan: i18n
|
|
12378
|
+
colSpan: i18n ? 5 : 4,
|
|
12264
12379
|
className: "px-4 py-8 text-center text-kumo-subtle",
|
|
12265
12380
|
children: _t({
|
|
12266
12381
|
id: "huTRDC",
|
|
@@ -12272,7 +12387,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12272
12387
|
collection,
|
|
12273
12388
|
onDelete,
|
|
12274
12389
|
onDuplicate,
|
|
12275
|
-
showLocale: !!i18n
|
|
12390
|
+
showLocale: !!i18n,
|
|
12276
12391
|
urlPattern
|
|
12277
12392
|
}, item.id))
|
|
12278
12393
|
})]
|
|
@@ -12282,21 +12397,11 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12282
12397
|
className: "flex items-center justify-between",
|
|
12283
12398
|
children: [/* @__PURE__ */ jsx("span", {
|
|
12284
12399
|
className: "text-sm text-kumo-subtle",
|
|
12285
|
-
children:
|
|
12286
|
-
|
|
12287
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
searchQuery
|
|
12291
|
-
}
|
|
12292
|
-
}) : i18n._({
|
|
12293
|
-
id: "Ufffi8",
|
|
12294
|
-
message: "{0, plural, one {#{1} item} other {#{2} items}}",
|
|
12295
|
-
values: {
|
|
12296
|
-
0: filteredItems.length,
|
|
12297
|
-
1: hasMore ? "+" : "",
|
|
12298
|
-
2: hasMore ? "+" : ""
|
|
12299
|
-
}
|
|
12400
|
+
children: renderItemCount({
|
|
12401
|
+
searchQuery,
|
|
12402
|
+
filteredCount: filteredItems.length,
|
|
12403
|
+
total,
|
|
12404
|
+
hasMore
|
|
12300
12405
|
})
|
|
12301
12406
|
}), /* @__PURE__ */ jsxs("div", {
|
|
12302
12407
|
className: "flex items-center gap-2",
|
|
@@ -12304,8 +12409,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12304
12409
|
/* @__PURE__ */ jsx(Button, {
|
|
12305
12410
|
variant: "outline",
|
|
12306
12411
|
shape: "square",
|
|
12307
|
-
disabled:
|
|
12308
|
-
onClick: () => setPage(
|
|
12412
|
+
disabled: clampedPage === 0,
|
|
12413
|
+
onClick: () => setPage(clampedPage - 1),
|
|
12309
12414
|
"aria-label": _t({
|
|
12310
12415
|
id: "aHKcKc",
|
|
12311
12416
|
message: "Previous page"
|
|
@@ -12318,7 +12423,7 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12318
12423
|
/* @__PURE__ */ jsxs("span", {
|
|
12319
12424
|
className: "text-sm",
|
|
12320
12425
|
children: [
|
|
12321
|
-
|
|
12426
|
+
clampedPage + 1,
|
|
12322
12427
|
" / ",
|
|
12323
12428
|
totalPages
|
|
12324
12429
|
]
|
|
@@ -12326,8 +12431,8 @@ function ContentList({ collection, collectionLabel, items, trashedItems = [], is
|
|
|
12326
12431
|
/* @__PURE__ */ jsx(Button, {
|
|
12327
12432
|
variant: "outline",
|
|
12328
12433
|
shape: "square",
|
|
12329
|
-
disabled:
|
|
12330
|
-
onClick: () => setPage(
|
|
12434
|
+
disabled: clampedPage >= totalPages - 1,
|
|
12435
|
+
onClick: () => setPage(clampedPage + 1),
|
|
12331
12436
|
"aria-label": _t({
|
|
12332
12437
|
id: "/dqduX",
|
|
12333
12438
|
message: "Next page"
|
|
@@ -12477,6 +12582,38 @@ function SortableTh({ field, sort, onSortChange, label }) {
|
|
|
12477
12582
|
})
|
|
12478
12583
|
});
|
|
12479
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
|
+
}
|
|
12480
12617
|
function ContentListItem({ item, collection, onDelete, onDuplicate, showLocale, urlPattern }) {
|
|
12481
12618
|
const { _: _t2 } = useLingui();
|
|
12482
12619
|
const title = getItemTitle$1(item);
|
|
@@ -17805,6 +17942,7 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
17805
17942
|
const queryClient = useQueryClient();
|
|
17806
17943
|
const toastManager = Toast.useToastManager();
|
|
17807
17944
|
const isMarketplace = plugin.source === "marketplace";
|
|
17945
|
+
const isRegistry = plugin.source === "registry";
|
|
17808
17946
|
const hasUpdate = !!updateInfo && updateInfo.installed !== updateInfo.latest;
|
|
17809
17947
|
const updateMutation = useMutation({
|
|
17810
17948
|
mutationFn: () => updateMarketplacePlugin(plugin.id, { confirmCapabilities: true }),
|
|
@@ -18136,6 +18274,13 @@ function PluginCard({ plugin, updateInfo, onEnable, onDisable, isToggling, hasMa
|
|
|
18136
18274
|
message: "Uninstall"
|
|
18137
18275
|
})
|
|
18138
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
|
+
})
|
|
18139
18284
|
})
|
|
18140
18285
|
]
|
|
18141
18286
|
})]
|
|
@@ -18284,7 +18429,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18284
18429
|
const isInstalled = installedPluginIds.has(pluginId);
|
|
18285
18430
|
if (isLoading) return /* @__PURE__ */ jsxs("div", {
|
|
18286
18431
|
className: "space-y-6",
|
|
18287
|
-
children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
|
|
18432
|
+
children: [/* @__PURE__ */ jsx(BackLink$1, {}), /* @__PURE__ */ jsxs("div", {
|
|
18288
18433
|
className: "animate-pulse space-y-4",
|
|
18289
18434
|
children: [
|
|
18290
18435
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -18302,7 +18447,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18302
18447
|
});
|
|
18303
18448
|
if (error || !plugin) return /* @__PURE__ */ jsxs("div", {
|
|
18304
18449
|
className: "space-y-6",
|
|
18305
|
-
children: [/* @__PURE__ */ jsx(BackLink, {}), /* @__PURE__ */ jsxs("div", {
|
|
18450
|
+
children: [/* @__PURE__ */ jsx(BackLink$1, {}), /* @__PURE__ */ jsxs("div", {
|
|
18306
18451
|
className: "rounded-lg border border-kumo-danger/50 bg-kumo-danger/10 p-6 text-center",
|
|
18307
18452
|
children: [
|
|
18308
18453
|
/* @__PURE__ */ jsx(Warning, { className: "mx-auto h-8 w-8 text-kumo-danger" }),
|
|
@@ -18340,7 +18485,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18340
18485
|
return /* @__PURE__ */ jsxs("div", {
|
|
18341
18486
|
className: "space-y-6",
|
|
18342
18487
|
children: [
|
|
18343
|
-
/* @__PURE__ */ jsx(BackLink, {}),
|
|
18488
|
+
/* @__PURE__ */ jsx(BackLink$1, {}),
|
|
18344
18489
|
/* @__PURE__ */ jsxs("div", {
|
|
18345
18490
|
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
18346
18491
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -18619,7 +18764,7 @@ function MarketplacePluginDetail({ pluginId, installedPluginIds = /* @__PURE__ *
|
|
|
18619
18764
|
]
|
|
18620
18765
|
});
|
|
18621
18766
|
}
|
|
18622
|
-
function BackLink() {
|
|
18767
|
+
function BackLink$1() {
|
|
18623
18768
|
const { _: _t2 } = useLingui();
|
|
18624
18769
|
return /* @__PURE__ */ jsxs(Link$1, {
|
|
18625
18770
|
to: "/plugins/marketplace",
|
|
@@ -18966,7 +19111,7 @@ function MediaDetailPanel({ item, onClose, onDeleted }) {
|
|
|
18966
19111
|
message: "Uploaded:"
|
|
18967
19112
|
})
|
|
18968
19113
|
}),
|
|
18969
|
-
/* @__PURE__ */ jsx("span", { children: formatDate(item.createdAt) })
|
|
19114
|
+
/* @__PURE__ */ jsx("span", { children: formatDate$1(item.createdAt) })
|
|
18970
19115
|
]
|
|
18971
19116
|
})
|
|
18972
19117
|
]
|
|
@@ -19084,7 +19229,7 @@ function MediaDetailPanel({ item, onClose, onDeleted }) {
|
|
|
19084
19229
|
onConfirm: () => deleteMutation.mutate()
|
|
19085
19230
|
})] });
|
|
19086
19231
|
}
|
|
19087
|
-
function formatDate(isoString) {
|
|
19232
|
+
function formatDate$1(isoString) {
|
|
19088
19233
|
return new Date(isoString).toLocaleDateString(void 0, {
|
|
19089
19234
|
year: "numeric",
|
|
19090
19235
|
month: "short",
|
|
@@ -19099,7 +19244,7 @@ function formatDate(isoString) {
|
|
|
19099
19244
|
/**
|
|
19100
19245
|
* Media library component with upload, provider tabs, and grid view
|
|
19101
19246
|
*/
|
|
19102
|
-
function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated }) {
|
|
19247
|
+
function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated, hasMore, onLoadMore }) {
|
|
19103
19248
|
const { _: _t } = useLingui();
|
|
19104
19249
|
const [viewMode, setViewMode] = React$1.useState("grid");
|
|
19105
19250
|
const [selectedItem, setSelectedItem] = React$1.useState(null);
|
|
@@ -19437,7 +19582,7 @@ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated
|
|
|
19437
19582
|
className: "ps-9"
|
|
19438
19583
|
})]
|
|
19439
19584
|
}),
|
|
19440
|
-
currentLoading ? /* @__PURE__ */ jsx("div", {
|
|
19585
|
+
currentLoading && currentItems.length === 0 && currentProviderItems.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
19441
19586
|
className: "flex items-center justify-center py-12",
|
|
19442
19587
|
children: /* @__PURE__ */ jsx(Loader, {})
|
|
19443
19588
|
}) : activeProvider === "local" && currentItems.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
@@ -19601,6 +19746,21 @@ function MediaLibrary({ items = [], isLoading, onUpload, onDelete, onItemUpdated
|
|
|
19601
19746
|
})]
|
|
19602
19747
|
})
|
|
19603
19748
|
}),
|
|
19749
|
+
activeProvider === "local" && hasMore && onLoadMore && /* @__PURE__ */ jsx("div", {
|
|
19750
|
+
className: "flex justify-center",
|
|
19751
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
19752
|
+
variant: "outline",
|
|
19753
|
+
onClick: onLoadMore,
|
|
19754
|
+
disabled: isLoading,
|
|
19755
|
+
children: isLoading ? _t({
|
|
19756
|
+
id: "Z3FXyt",
|
|
19757
|
+
message: "Loading..."
|
|
19758
|
+
}) : _t({
|
|
19759
|
+
id: "a4N/Bg",
|
|
19760
|
+
message: "Load More"
|
|
19761
|
+
})
|
|
19762
|
+
})
|
|
19763
|
+
}),
|
|
19604
19764
|
selectedItem && /* @__PURE__ */ jsx(MediaDetailPanel, {
|
|
19605
19765
|
item: selectedItem,
|
|
19606
19766
|
onClose: () => setSelectedItem(null),
|
|
@@ -20266,7 +20426,7 @@ function MenuEditor() {
|
|
|
20266
20426
|
newItems[targetIndex] = currentItem;
|
|
20267
20427
|
const reorderedItems = newItems.map((item, i) => ({
|
|
20268
20428
|
id: item.id,
|
|
20269
|
-
parentId: item.
|
|
20429
|
+
parentId: item.parentId,
|
|
20270
20430
|
sortOrder: i
|
|
20271
20431
|
}));
|
|
20272
20432
|
setLocalItems(newItems);
|
|
@@ -20545,9 +20705,9 @@ function MenuEditor() {
|
|
|
20545
20705
|
children: item.label
|
|
20546
20706
|
}), /* @__PURE__ */ jsxs("div", {
|
|
20547
20707
|
className: "text-sm text-kumo-subtle",
|
|
20548
|
-
children: [item.type === "custom" ? item.
|
|
20708
|
+
children: [item.type === "custom" ? item.customUrl : /* @__PURE__ */ jsx("span", {
|
|
20549
20709
|
className: "inline-flex items-center rounded-full bg-kumo-brand/10 px-2 py-0.5 text-xs font-medium text-kumo-brand",
|
|
20550
|
-
children: item.
|
|
20710
|
+
children: item.referenceCollection ?? item.type
|
|
20551
20711
|
}), item.target === "_blank" && _t({
|
|
20552
20712
|
id: "+Lmv8u",
|
|
20553
20713
|
message: " (opens in new window)"
|
|
@@ -20669,7 +20829,7 @@ function MenuEditor() {
|
|
|
20669
20829
|
id: "TQJouj",
|
|
20670
20830
|
message: "Enter a URL (https://…) or a relative path (/…)"
|
|
20671
20831
|
}),
|
|
20672
|
-
defaultValue: editingItem.
|
|
20832
|
+
defaultValue: editingItem.customUrl || ""
|
|
20673
20833
|
}),
|
|
20674
20834
|
/* @__PURE__ */ jsxs(Select, {
|
|
20675
20835
|
label: _t({
|
|
@@ -21791,6 +21951,806 @@ function Redirects() {
|
|
|
21791
21951
|
});
|
|
21792
21952
|
}
|
|
21793
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
|
+
|
|
21794
22754
|
//#endregion
|
|
21795
22755
|
//#region src/components/SandboxedPluginPage.tsx
|
|
21796
22756
|
/**
|
|
@@ -24556,15 +25516,25 @@ function GeneralSettings() {
|
|
|
24556
25516
|
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Label, { children: _t({
|
|
24557
25517
|
id: "iG7KNr",
|
|
24558
25518
|
message: "Logo"
|
|
24559
|
-
}) }), formData.logo?.
|
|
25519
|
+
}) }), formData.logo?.mediaId ? /* @__PURE__ */ jsxs("div", {
|
|
24560
25520
|
className: "mt-2 space-y-2",
|
|
24561
|
-
children: [/* @__PURE__ */ jsx("img", {
|
|
25521
|
+
children: [formData.logo.url ? /* @__PURE__ */ jsx("img", {
|
|
24562
25522
|
src: formData.logo.url,
|
|
24563
25523
|
alt: formData.logo.alt || _t({
|
|
24564
25524
|
id: "iG7KNr",
|
|
24565
25525
|
message: "Logo"
|
|
24566
25526
|
}),
|
|
24567
25527
|
className: "h-16 rounded border bg-kumo-tint object-contain p-2"
|
|
25528
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
25529
|
+
className: "flex min-h-16 items-center gap-2 rounded border border-dashed bg-kumo-tint px-3 py-2 text-sm text-kumo-subtle",
|
|
25530
|
+
role: "status",
|
|
25531
|
+
children: [/* @__PURE__ */ jsx(WarningCircle, {
|
|
25532
|
+
className: "h-4 w-4 flex-shrink-0",
|
|
25533
|
+
"aria-hidden": "true"
|
|
25534
|
+
}), /* @__PURE__ */ jsx("span", { children: _t({
|
|
25535
|
+
id: "Ja5IhY",
|
|
25536
|
+
message: "The referenced logo is no longer available. Pick a new one or remove the reference."
|
|
25537
|
+
}) })]
|
|
24568
25538
|
}), /* @__PURE__ */ jsxs("div", {
|
|
24569
25539
|
className: "flex gap-2",
|
|
24570
25540
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
@@ -24603,15 +25573,25 @@ function GeneralSettings() {
|
|
|
24603
25573
|
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Label, { children: _t({
|
|
24604
25574
|
id: "+Qaboy",
|
|
24605
25575
|
message: "Favicon"
|
|
24606
|
-
}) }), formData.favicon?.
|
|
25576
|
+
}) }), formData.favicon?.mediaId ? /* @__PURE__ */ jsxs("div", {
|
|
24607
25577
|
className: "mt-2 space-y-2",
|
|
24608
|
-
children: [/* @__PURE__ */ jsx("img", {
|
|
25578
|
+
children: [formData.favicon.url ? /* @__PURE__ */ jsx("img", {
|
|
24609
25579
|
src: formData.favicon.url,
|
|
24610
25580
|
alt: _t({
|
|
24611
25581
|
id: "+Qaboy",
|
|
24612
25582
|
message: "Favicon"
|
|
24613
25583
|
}),
|
|
24614
25584
|
className: "h-8 w-8 rounded border bg-kumo-tint object-contain p-1"
|
|
25585
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
25586
|
+
className: "flex min-h-8 items-center gap-2 rounded border border-dashed bg-kumo-tint px-2 py-1 text-xs text-kumo-subtle",
|
|
25587
|
+
role: "status",
|
|
25588
|
+
children: [/* @__PURE__ */ jsx(WarningCircle, {
|
|
25589
|
+
className: "h-3 w-3 flex-shrink-0",
|
|
25590
|
+
"aria-hidden": "true"
|
|
25591
|
+
}), /* @__PURE__ */ jsx("span", { children: _t({
|
|
25592
|
+
id: "z58F7a",
|
|
25593
|
+
message: "Referenced favicon unavailable."
|
|
25594
|
+
}) })]
|
|
24615
25595
|
}), /* @__PURE__ */ jsxs("div", {
|
|
24616
25596
|
className: "flex gap-2",
|
|
24617
25597
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
@@ -24722,6 +25702,7 @@ function GeneralSettings() {
|
|
|
24722
25702
|
onOpenChange: setLogoPickerOpen,
|
|
24723
25703
|
onSelect: handleLogoSelect,
|
|
24724
25704
|
mimeTypeFilter: "image/",
|
|
25705
|
+
localOnly: true,
|
|
24725
25706
|
title: _t({
|
|
24726
25707
|
id: "JUZ3aC",
|
|
24727
25708
|
message: "Select Logo"
|
|
@@ -24732,6 +25713,7 @@ function GeneralSettings() {
|
|
|
24732
25713
|
onOpenChange: setFaviconPickerOpen,
|
|
24733
25714
|
onSelect: handleFaviconSelect,
|
|
24734
25715
|
mimeTypeFilter: "image/",
|
|
25716
|
+
localOnly: true,
|
|
24735
25717
|
title: _t({
|
|
24736
25718
|
id: "LmzxcB",
|
|
24737
25719
|
message: "Select Favicon"
|
|
@@ -25237,6 +26219,7 @@ function SeoSettings() {
|
|
|
25237
26219
|
});
|
|
25238
26220
|
const [formData, setFormData] = React$1.useState({});
|
|
25239
26221
|
const [saveStatus, setSaveStatus] = React$1.useState(null);
|
|
26222
|
+
const [ogImagePickerOpen, setOgImagePickerOpen] = React$1.useState(false);
|
|
25240
26223
|
React$1.useEffect(() => {
|
|
25241
26224
|
if (settings) setFormData(settings);
|
|
25242
26225
|
}, [settings]);
|
|
@@ -25281,6 +26264,29 @@ function SeoSettings() {
|
|
|
25281
26264
|
}
|
|
25282
26265
|
}));
|
|
25283
26266
|
};
|
|
26267
|
+
const handleDefaultOgImageSelect = (media) => {
|
|
26268
|
+
setFormData((prev) => ({
|
|
26269
|
+
...prev,
|
|
26270
|
+
seo: {
|
|
26271
|
+
...prev.seo,
|
|
26272
|
+
defaultOgImage: {
|
|
26273
|
+
mediaId: media.id,
|
|
26274
|
+
alt: media.alt || "",
|
|
26275
|
+
url: media.url
|
|
26276
|
+
}
|
|
26277
|
+
}
|
|
26278
|
+
}));
|
|
26279
|
+
setOgImagePickerOpen(false);
|
|
26280
|
+
};
|
|
26281
|
+
const handleDefaultOgImageRemove = () => {
|
|
26282
|
+
setFormData((prev) => ({
|
|
26283
|
+
...prev,
|
|
26284
|
+
seo: {
|
|
26285
|
+
...prev.seo,
|
|
26286
|
+
defaultOgImage: void 0
|
|
26287
|
+
}
|
|
26288
|
+
}));
|
|
26289
|
+
};
|
|
25284
26290
|
if (isLoading) return /* @__PURE__ */ jsxs("div", {
|
|
25285
26291
|
className: "space-y-6",
|
|
25286
26292
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -25363,6 +26369,73 @@ function SeoSettings() {
|
|
|
25363
26369
|
message: "Character between page title and site name (e.g., \"My Post | My Site\")"
|
|
25364
26370
|
})
|
|
25365
26371
|
}),
|
|
26372
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
26373
|
+
/* @__PURE__ */ jsx(Label, { children: _t({
|
|
26374
|
+
id: "rqCo6p",
|
|
26375
|
+
message: "Default Social Image"
|
|
26376
|
+
}) }),
|
|
26377
|
+
/* @__PURE__ */ jsx("p", {
|
|
26378
|
+
className: "mt-1 text-sm text-kumo-subtle",
|
|
26379
|
+
children: _t({
|
|
26380
|
+
id: "YkLTCK",
|
|
26381
|
+
message: "Used as the fallback Open Graph image when a page has none. Recommended size: 1200×630."
|
|
26382
|
+
})
|
|
26383
|
+
}),
|
|
26384
|
+
formData.seo?.defaultOgImage?.mediaId ? /* @__PURE__ */ jsxs("div", {
|
|
26385
|
+
className: "mt-2 space-y-2",
|
|
26386
|
+
children: [formData.seo.defaultOgImage.url ? /* @__PURE__ */ jsx("img", {
|
|
26387
|
+
src: formData.seo.defaultOgImage.url,
|
|
26388
|
+
alt: formData.seo.defaultOgImage.alt || _t({
|
|
26389
|
+
id: "GOoV+w",
|
|
26390
|
+
message: "Default social image"
|
|
26391
|
+
}),
|
|
26392
|
+
className: "h-32 rounded border bg-kumo-tint object-contain p-2"
|
|
26393
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
26394
|
+
className: "flex min-h-32 items-center gap-2 rounded border border-dashed bg-kumo-tint px-3 py-2 text-sm text-kumo-subtle",
|
|
26395
|
+
role: "status",
|
|
26396
|
+
children: [/* @__PURE__ */ jsx(WarningCircle, {
|
|
26397
|
+
className: "h-4 w-4 flex-shrink-0",
|
|
26398
|
+
"aria-hidden": "true"
|
|
26399
|
+
}), /* @__PURE__ */ jsx("span", { children: _t({
|
|
26400
|
+
id: "xPSZCT",
|
|
26401
|
+
message: "The referenced image is no longer available. Pick a new one or remove the reference."
|
|
26402
|
+
}) })]
|
|
26403
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
26404
|
+
className: "flex gap-2",
|
|
26405
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
26406
|
+
type: "button",
|
|
26407
|
+
variant: "outline",
|
|
26408
|
+
size: "sm",
|
|
26409
|
+
icon: /* @__PURE__ */ jsx(Upload, {}),
|
|
26410
|
+
onClick: () => setOgImagePickerOpen(true),
|
|
26411
|
+
children: _t({
|
|
26412
|
+
id: "mwX/w0",
|
|
26413
|
+
message: "Change Image"
|
|
26414
|
+
})
|
|
26415
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
26416
|
+
type: "button",
|
|
26417
|
+
variant: "outline",
|
|
26418
|
+
size: "sm",
|
|
26419
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
26420
|
+
onClick: handleDefaultOgImageRemove,
|
|
26421
|
+
children: _t({
|
|
26422
|
+
id: "t/YqKh",
|
|
26423
|
+
message: "Remove"
|
|
26424
|
+
})
|
|
26425
|
+
})]
|
|
26426
|
+
})]
|
|
26427
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
26428
|
+
type: "button",
|
|
26429
|
+
variant: "outline",
|
|
26430
|
+
icon: /* @__PURE__ */ jsx(Upload, {}),
|
|
26431
|
+
onClick: () => setOgImagePickerOpen(true),
|
|
26432
|
+
className: "mt-2",
|
|
26433
|
+
children: _t({
|
|
26434
|
+
id: "bAiEJP",
|
|
26435
|
+
message: "Select Image"
|
|
26436
|
+
})
|
|
26437
|
+
})
|
|
26438
|
+
] }),
|
|
25366
26439
|
/* @__PURE__ */ jsx(Input, {
|
|
25367
26440
|
label: _t({
|
|
25368
26441
|
id: "uP6I1q",
|
|
@@ -25414,6 +26487,22 @@ function SeoSettings() {
|
|
|
25414
26487
|
})
|
|
25415
26488
|
})
|
|
25416
26489
|
})]
|
|
26490
|
+
}),
|
|
26491
|
+
/* @__PURE__ */ jsx(MediaPickerModal, {
|
|
26492
|
+
open: ogImagePickerOpen,
|
|
26493
|
+
onOpenChange: setOgImagePickerOpen,
|
|
26494
|
+
onSelect: handleDefaultOgImageSelect,
|
|
26495
|
+
mimeTypeFilters: [
|
|
26496
|
+
"image/jpeg",
|
|
26497
|
+
"image/png",
|
|
26498
|
+
"image/webp",
|
|
26499
|
+
"image/gif"
|
|
26500
|
+
],
|
|
26501
|
+
localOnly: true,
|
|
26502
|
+
title: _t({
|
|
26503
|
+
id: "lxsmBp",
|
|
26504
|
+
message: "Select Default Social Image"
|
|
26505
|
+
})
|
|
25417
26506
|
})
|
|
25418
26507
|
]
|
|
25419
26508
|
});
|
|
@@ -26790,7 +27879,16 @@ function SidebarNav({ manifest }) {
|
|
|
26790
27879
|
minRole: ROLE_ADMIN$1
|
|
26791
27880
|
}
|
|
26792
27881
|
];
|
|
26793
|
-
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({
|
|
26794
27892
|
to: "/plugins/marketplace",
|
|
26795
27893
|
label: _t({
|
|
26796
27894
|
id: "Zt5PUS",
|
|
@@ -26798,7 +27896,8 @@ function SidebarNav({ manifest }) {
|
|
|
26798
27896
|
}),
|
|
26799
27897
|
icon: Storefront,
|
|
26800
27898
|
minRole: ROLE_ADMIN$1
|
|
26801
|
-
}
|
|
27899
|
+
});
|
|
27900
|
+
if (manifest.marketplace) adminItems.push({
|
|
26802
27901
|
to: "/themes/marketplace",
|
|
26803
27902
|
label: _t({
|
|
26804
27903
|
id: "fJ3EbT",
|
|
@@ -34839,6 +35938,7 @@ function ContentListPage() {
|
|
|
34839
35938
|
const items = React$1.useMemo(() => {
|
|
34840
35939
|
return data?.pages.flatMap((page) => page.items) || [];
|
|
34841
35940
|
}, [data]);
|
|
35941
|
+
const total = data?.pages[0]?.total ?? items.length;
|
|
34842
35942
|
if (!manifest) return /* @__PURE__ */ jsx(LoadingScreen, {});
|
|
34843
35943
|
const collectionConfig = manifest.collections[collection];
|
|
34844
35944
|
if (!collectionConfig) return /* @__PURE__ */ jsx(NotFoundPage, { message: `Collection "${collection}" not found` });
|
|
@@ -34869,7 +35969,8 @@ function ContentListPage() {
|
|
|
34869
35969
|
onLocaleChange: handleLocaleChange,
|
|
34870
35970
|
urlPattern: collectionConfig.urlPattern,
|
|
34871
35971
|
sort,
|
|
34872
|
-
onSortChange: setSort
|
|
35972
|
+
onSortChange: setSort,
|
|
35973
|
+
total
|
|
34873
35974
|
});
|
|
34874
35975
|
}
|
|
34875
35976
|
const contentNewRoute = createRoute({
|
|
@@ -35460,9 +36561,14 @@ const mediaRoute = createRoute({
|
|
|
35460
36561
|
});
|
|
35461
36562
|
function MediaPage() {
|
|
35462
36563
|
const queryClient = useQueryClient();
|
|
35463
|
-
const { data, isLoading, error } =
|
|
36564
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } = useInfiniteQuery({
|
|
35464
36565
|
queryKey: ["media"],
|
|
35465
|
-
queryFn: () => fetchMediaList(
|
|
36566
|
+
queryFn: ({ pageParam }) => fetchMediaList({
|
|
36567
|
+
cursor: pageParam,
|
|
36568
|
+
limit: 100
|
|
36569
|
+
}),
|
|
36570
|
+
initialPageParam: void 0,
|
|
36571
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor
|
|
35466
36572
|
});
|
|
35467
36573
|
const uploadMutation = useMutation({
|
|
35468
36574
|
mutationFn: (file) => uploadMedia(file),
|
|
@@ -35476,10 +36582,15 @@ function MediaPage() {
|
|
|
35476
36582
|
queryClient.invalidateQueries({ queryKey: ["media"] });
|
|
35477
36583
|
}
|
|
35478
36584
|
});
|
|
36585
|
+
const items = React$1.useMemo(() => {
|
|
36586
|
+
return data?.pages.flatMap((page) => page.items) || [];
|
|
36587
|
+
}, [data]);
|
|
35479
36588
|
if (error) return /* @__PURE__ */ jsx(ErrorScreen, { error: error.message });
|
|
35480
36589
|
return /* @__PURE__ */ jsx(MediaLibrary, {
|
|
35481
|
-
items
|
|
35482
|
-
isLoading,
|
|
36590
|
+
items,
|
|
36591
|
+
isLoading: isLoading || isFetchingNextPage,
|
|
36592
|
+
hasMore: !!hasNextPage,
|
|
36593
|
+
onLoadMore: () => void fetchNextPage(),
|
|
35483
36594
|
onUpload: (file) => uploadMutation.mutate(file),
|
|
35484
36595
|
onDelete: (id) => deleteMutation.mutate(id)
|
|
35485
36596
|
});
|
|
@@ -35716,6 +36827,10 @@ const marketplaceBrowseRoute = createRoute({
|
|
|
35716
36827
|
component: MarketplaceBrowsePage
|
|
35717
36828
|
});
|
|
35718
36829
|
function MarketplaceBrowsePage() {
|
|
36830
|
+
const { data: manifest } = useQuery({
|
|
36831
|
+
queryKey: ["manifest"],
|
|
36832
|
+
queryFn: fetchManifest
|
|
36833
|
+
});
|
|
35719
36834
|
const { data: plugins } = useQuery({
|
|
35720
36835
|
queryKey: ["plugins"],
|
|
35721
36836
|
queryFn: async () => {
|
|
@@ -35723,10 +36838,18 @@ function MarketplaceBrowsePage() {
|
|
|
35723
36838
|
return fetchPlugins();
|
|
35724
36839
|
}
|
|
35725
36840
|
});
|
|
35726
|
-
|
|
36841
|
+
const installedIds = React$1.useMemo(() => {
|
|
35727
36842
|
if (!plugins) return /* @__PURE__ */ new Set();
|
|
35728
36843
|
return new Set(plugins.map((p) => p.id));
|
|
35729
|
-
}, [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 });
|
|
35730
36853
|
}
|
|
35731
36854
|
const marketplaceDetailRoute = createRoute({
|
|
35732
36855
|
getParentRoute: () => adminLayoutRoute,
|
|
@@ -35735,6 +36858,10 @@ const marketplaceDetailRoute = createRoute({
|
|
|
35735
36858
|
});
|
|
35736
36859
|
function MarketplaceDetailPage() {
|
|
35737
36860
|
const { pluginId } = useParams({ from: "/_admin/plugins/marketplace/$pluginId" });
|
|
36861
|
+
const { data: manifest } = useQuery({
|
|
36862
|
+
queryKey: ["manifest"],
|
|
36863
|
+
queryFn: fetchManifest
|
|
36864
|
+
});
|
|
35738
36865
|
const { data: plugins } = useQuery({
|
|
35739
36866
|
queryKey: ["plugins"],
|
|
35740
36867
|
queryFn: async () => {
|
|
@@ -35742,12 +36869,18 @@ function MarketplaceDetailPage() {
|
|
|
35742
36869
|
return fetchPlugins();
|
|
35743
36870
|
}
|
|
35744
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
|
+
});
|
|
35745
36881
|
return /* @__PURE__ */ jsx(MarketplacePluginDetail, {
|
|
35746
36882
|
pluginId,
|
|
35747
|
-
installedPluginIds:
|
|
35748
|
-
if (!plugins) return /* @__PURE__ */ new Set();
|
|
35749
|
-
return new Set(plugins.map((p) => p.id));
|
|
35750
|
-
}, [plugins])
|
|
36883
|
+
installedPluginIds: installedIds
|
|
35751
36884
|
});
|
|
35752
36885
|
}
|
|
35753
36886
|
const themeMarketplaceBrowseRoute = createRoute({
|