@cytario/web 2.1.4 → 2.1.5
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/README.md +20 -20
- package/app/.server/auth/README.md +8 -8
- package/app/.server/auth/authMiddleware.ts +9 -25
- package/app/.server/auth/exchangeAuthCode.ts +2 -6
- package/app/.server/auth/getS3Client.ts +3 -13
- package/app/.server/auth/getSessionCredentials.ts +6 -20
- package/app/.server/auth/getUserInfo.ts +2 -6
- package/app/.server/auth/keycloakAdmin/client.ts +2 -9
- package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
- package/app/.server/auth/keycloakAdmin/users.ts +7 -23
- package/app/.server/auth/oauthState.ts +4 -13
- package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
- package/app/.server/auth/refreshAuthTokens.ts +5 -19
- package/app/.server/auth/sessionMiddleware.ts +1 -4
- package/app/.server/auth/sessionStorage.ts +1 -4
- package/app/.server/auth/verifyIdToken.ts +1 -3
- package/app/.server/auth/wellKnownEndpoints.ts +1 -4
- package/app/.server/db/redis.ts +5 -1
- package/app/.server/logging.ts +1 -4
- package/app/.server/requestDurationMiddleware.ts +1 -4
- package/app/components/.client/ImageViewer/README.md +5 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
- package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
- package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
- package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
- package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
- package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
- package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
- package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
- package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
- package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
- package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
- package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
- package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
- package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
- package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
- package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
- package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
- package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
- package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
- package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
- package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
- package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
- package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
- package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
- package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
- package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
- package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
- package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
- package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
- package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
- package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
- package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
- package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
- package/app/components/AppHeader.tsx +1 -4
- package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
- package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
- package/app/components/ClientOnly.tsx +1 -1
- package/app/components/Container.tsx +3 -15
- package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
- package/app/components/DataGrid/DataGrid.tsx +7 -27
- package/app/components/DataGrid/WktSvg.tsx +2 -4
- package/app/components/DataGrid/getParquetSchema.ts +1 -4
- package/app/components/DescriptionList.tsx +1 -3
- package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
- package/app/components/DirectoryView/DirectoryView.tsx +10 -46
- package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
- package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
- package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
- package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
- package/app/components/DirectoryView/FilterBar.tsx +9 -48
- package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
- package/app/components/DirectoryView/filterNodes.ts +4 -11
- package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
- package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
- package/app/components/DirectoryView/useLayoutStore.ts +5 -25
- package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
- package/app/components/GlobalSearch/SearchBar.tsx +1 -7
- package/app/components/GlobalSearch/Suggestions.tsx +0 -1
- package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
- package/app/components/LavaLoader.tsx +4 -11
- package/app/components/Layout/Footer.tsx +1 -4
- package/app/components/Pills/ScopePill.tsx +2 -8
- package/app/components/Table/ColumnFilterInput.tsx +5 -20
- package/app/components/Table/ColumnResizeHandle.tsx +1 -5
- package/app/components/Table/ColumnSortButton.tsx +1 -5
- package/app/components/Table/SelectionFooter.tsx +3 -5
- package/app/components/Table/Table.tsx +5 -21
- package/app/components/Table/TableBodyRow.tsx +19 -31
- package/app/components/Table/TableHeaderRow.tsx +7 -28
- package/app/components/Table/TableMenu.tsx +4 -20
- package/app/components/Table/state/createTableStore.ts +3 -9
- package/app/components/Table/state/useTableStore.ts +1 -3
- package/app/components/Table/types.ts +4 -10
- package/app/components/Table/useColumnFilters.ts +1 -4
- package/app/components/Table/useColumnVisibility.ts +4 -11
- package/app/components/Table/useColumnWidths.ts +1 -3
- package/app/components/Table/useTableSorting.ts +4 -12
- package/app/components/Tooltip/Tooltip.tsx +4 -17
- package/app/components/Tooltip/TooltipSpan.tsx +5 -28
- package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
- package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
- package/app/components/Tooltip/useOverflowDetection.ts +2 -5
- package/app/components/UserMenu.tsx +2 -9
- package/app/entry.server.tsx +9 -19
- package/app/hooks/useSearchParam.ts +2 -4
- package/app/lib/bootstrapPluginsCore.ts +4 -9
- package/app/root.tsx +4 -15
- package/app/routes/admin/assertAdminScope.ts +1 -3
- package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
- package/app/routes/admin/assertGroupsInScope.ts +3 -11
- package/app/routes/admin/assertUsersInScope.ts +2 -8
- package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
- package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
- package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
- package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
- package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
- package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
- package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
- package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
- package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
- package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
- package/app/routes/admin/users/BulkActions.tsx +15 -38
- package/app/routes/admin/users/bulkUsers.action.ts +2 -9
- package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
- package/app/routes/admin/users/users.route.tsx +14 -63
- package/app/routes/api/cyberduck-profile.$name.ts +6 -2
- package/app/routes/auth/callback.route.tsx +8 -33
- package/app/routes/auth/login.route.tsx +8 -11
- package/app/routes/auth/logout.route.tsx +4 -14
- package/app/routes/config.route.tsx +1 -5
- package/app/routes/connections/connection.form.tsx +5 -14
- package/app/routes/connections/connection.schema.ts +4 -16
- package/app/routes/connections/connections.loader.ts +11 -23
- package/app/routes/connections/connections.route.tsx +1 -5
- package/app/routes/connections/connections.server.ts +1 -3
- package/app/routes/connections/createConnection.action.ts +2 -8
- package/app/routes/connections/createConnection.modal.tsx +3 -13
- package/app/routes/connections/deleteConnection.action.ts +1 -4
- package/app/routes/connections/updateConnection.action.ts +2 -8
- package/app/routes/connections/updateConnection.modal.tsx +4 -14
- package/app/routes/home/home.route.tsx +8 -33
- package/app/routes/layouts/ModalOutlet.tsx +6 -18
- package/app/routes/objects/objects.loader.ts +5 -19
- package/app/routes/objects/objects.route.tsx +11 -30
- package/app/routes/presign.route.tsx +5 -18
- package/app/routes/recent.route.tsx +1 -4
- package/app/routes/search.route.tsx +4 -17
- package/app/routes.ts +1 -4
- package/app/tailwind.css +17 -12
- package/app/types/cornerstone-codecs.d.ts +25 -29
- package/app/utils/connectionsStore/selectors.ts +2 -6
- package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
- package/app/utils/db/convertCsvToParquet.ts +1 -3
- package/app/utils/db/createDatabase.ts +1 -3
- package/app/utils/db/createSingleton.ts +1 -1
- package/app/utils/db/getBlobFromObjectNode.ts +3 -9
- package/app/utils/db/getGeomQuery.ts +1 -1
- package/app/utils/db/getMarkerInfoWasm.ts +1 -3
- package/app/utils/db/getTileBoundingBox.ts +1 -4
- package/app/utils/db/sqlQueries.ts +1 -4
- package/app/utils/fileType.ts +80 -10
- package/app/utils/filterObjects.ts +2 -5
- package/app/utils/localFilesStore/useFileStore.ts +7 -7
- package/app/utils/recentlyViewed.server.ts +1 -4
- package/app/utils/resourceId.ts +3 -11
- package/app/utils/s3Provider.ts +3 -7
- package/app/utils/signedFetch.ts +4 -13
- package/bin-src/codegen.ts +4 -1
- package/package.json +5 -1
- package/prisma/seed.ts +1 -2
- package/public/favicon/site.webmanifest +1 -1
- package/server.js +1 -4
- package/server.js.map +1 -1
- package/vite-plugins/cytario-plugins.ts +2 -8
|
@@ -12,24 +12,21 @@ import { ClientOnly } from "~/components/ClientOnly";
|
|
|
12
12
|
import { ProviderPill } from "~/components/Pills/ProviderPill";
|
|
13
13
|
import { ScopePill } from "~/components/Pills/ScopePill";
|
|
14
14
|
import { useNodeInfoModal } from "~/hooks/useNodeInfoModal";
|
|
15
|
-
import {
|
|
16
|
-
select,
|
|
17
|
-
selectHttpsUrl,
|
|
18
|
-
} from "~/utils/connectionsStore/selectors";
|
|
15
|
+
import { select, selectHttpsUrl } from "~/utils/connectionsStore/selectors";
|
|
19
16
|
import { useConnectionsStore } from "~/utils/connectionsStore/useConnectionsStore";
|
|
20
17
|
import { getNodeIcon, isImageFile } from "~/utils/fileType";
|
|
21
18
|
import { buildConnectionPath, constructS3Url } from "~/utils/resourceId";
|
|
22
19
|
import { createSignedFetch } from "~/utils/signedFetch";
|
|
23
20
|
|
|
24
21
|
const ViewerStoreProvider = lazy(() =>
|
|
25
|
-
import("~/components/.client/ImageViewer/state/store/ViewerStoreContext").then(
|
|
26
|
-
|
|
27
|
-
),
|
|
22
|
+
import("~/components/.client/ImageViewer/state/store/ViewerStoreContext").then((mod) => ({
|
|
23
|
+
default: mod.ViewerStoreProvider,
|
|
24
|
+
})),
|
|
28
25
|
);
|
|
29
26
|
const ImagePreview = lazy(() =>
|
|
30
|
-
import("~/components/.client/ImageViewer/components/Image/ImagePreview").then(
|
|
31
|
-
|
|
32
|
-
),
|
|
27
|
+
import("~/components/.client/ImageViewer/components/Image/ImagePreview").then((mod) => ({
|
|
28
|
+
default: mod.ImagePreview,
|
|
29
|
+
})),
|
|
33
30
|
);
|
|
34
31
|
|
|
35
32
|
const gridClasses: Partial<Record<ViewMode, string>> = {
|
|
@@ -40,15 +37,12 @@ const gridClasses: Partial<Record<ViewMode, string>> = {
|
|
|
40
37
|
|
|
41
38
|
/** Create a signedFetch that lazily resolves credentials from the connections store. */
|
|
42
39
|
function useSignedFetch(connectionName: string) {
|
|
43
|
-
const connectionConfig = useConnectionsStore(
|
|
44
|
-
select.connectionConfig(connectionName),
|
|
45
|
-
);
|
|
40
|
+
const connectionConfig = useConnectionsStore(select.connectionConfig(connectionName));
|
|
46
41
|
|
|
47
42
|
const signedFetch = useMemo(() => {
|
|
48
43
|
if (!connectionConfig) return null;
|
|
49
44
|
return createSignedFetch(
|
|
50
|
-
() =>
|
|
51
|
-
useConnectionsStore.getState().connections[connectionName]?.credentials,
|
|
45
|
+
() => useConnectionsStore.getState().connections[connectionName]?.credentials,
|
|
52
46
|
connectionConfig,
|
|
53
47
|
);
|
|
54
48
|
}, [connectionName, connectionConfig]);
|
|
@@ -56,13 +50,7 @@ function useSignedFetch(connectionName: string) {
|
|
|
56
50
|
return { connectionConfig, signedFetch };
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
function BucketCardGridItem({
|
|
60
|
-
node,
|
|
61
|
-
connectionName,
|
|
62
|
-
}: {
|
|
63
|
-
node: TreeNode;
|
|
64
|
-
connectionName: string;
|
|
65
|
-
}) {
|
|
53
|
+
function BucketCardGridItem({ node, connectionName }: { node: TreeNode; connectionName: string }) {
|
|
66
54
|
const navigate = useNavigate();
|
|
67
55
|
const { connectionConfig, signedFetch } = useSignedFetch(connectionName);
|
|
68
56
|
|
|
@@ -73,10 +61,7 @@ function BucketCardGridItem({
|
|
|
73
61
|
// `_Object.Key` (already absolute — includes any configured prefix).
|
|
74
62
|
const previewKey = node._Object?.Key ?? null;
|
|
75
63
|
const hasPreview = !!previewKey && isImageFile(previewKey) && !!signedFetch;
|
|
76
|
-
const s3Url =
|
|
77
|
-
hasPreview && connectionConfig
|
|
78
|
-
? constructS3Url(connectionConfig, previewKey)
|
|
79
|
-
: "";
|
|
64
|
+
const s3Url = hasPreview && connectionConfig ? constructS3Url(connectionConfig, previewKey) : "";
|
|
80
65
|
|
|
81
66
|
return (
|
|
82
67
|
<StorageConnectionCard
|
|
@@ -97,11 +82,7 @@ function BucketCardGridItem({
|
|
|
97
82
|
>
|
|
98
83
|
{hasPreview && signedFetch && (
|
|
99
84
|
<ClientOnly>
|
|
100
|
-
<Suspense
|
|
101
|
-
fallback={
|
|
102
|
-
<div className="animate-pulse w-full h-full bg-slate-600" />
|
|
103
|
-
}
|
|
104
|
-
>
|
|
85
|
+
<Suspense fallback={<div className="animate-pulse w-full h-full bg-slate-600" />}>
|
|
105
86
|
<ViewerStoreProvider url={s3Url} signedFetch={signedFetch}>
|
|
106
87
|
<ImagePreview />
|
|
107
88
|
</ViewerStoreProvider>
|
|
@@ -124,8 +105,7 @@ function FileCardGridItem({
|
|
|
124
105
|
const navigate = useNavigate();
|
|
125
106
|
const handleInfo = useNodeInfoModal(node);
|
|
126
107
|
|
|
127
|
-
const { connectionConfig: config, signedFetch } =
|
|
128
|
-
useSignedFetch(connectionName);
|
|
108
|
+
const { connectionConfig: config, signedFetch } = useSignedFetch(connectionName);
|
|
129
109
|
// `_Object.Key` is absolute (prefix already applied) and set for both file
|
|
130
110
|
// nodes (from listing) and directory nodes (first image inside, via
|
|
131
111
|
// buildDirectoryTree). File nodes without `_Object` — e.g. recently-viewed
|
|
@@ -146,10 +126,7 @@ function FileCardGridItem({
|
|
|
146
126
|
const hasPreview = !!s3Url && !!signedFetch;
|
|
147
127
|
|
|
148
128
|
const nodeIcon = getNodeIcon(node);
|
|
149
|
-
const size =
|
|
150
|
-
node.type === "file" && node._Object?.Size
|
|
151
|
-
? filesize(node._Object.Size)
|
|
152
|
-
: undefined;
|
|
129
|
+
const size = node.type === "file" && node._Object?.Size ? filesize(node._Object.Size) : undefined;
|
|
153
130
|
|
|
154
131
|
return (
|
|
155
132
|
<FileCard
|
|
@@ -162,11 +139,7 @@ function FileCardGridItem({
|
|
|
162
139
|
>
|
|
163
140
|
{hasPreview && signedFetch && (
|
|
164
141
|
<ClientOnly>
|
|
165
|
-
<Suspense
|
|
166
|
-
fallback={
|
|
167
|
-
<div className="animate-pulse w-full h-full bg-slate-600" />
|
|
168
|
-
}
|
|
169
|
-
>
|
|
142
|
+
<Suspense fallback={<div className="animate-pulse w-full h-full bg-slate-600" />}>
|
|
170
143
|
<ViewerStoreProvider url={s3Url} signedFetch={signedFetch}>
|
|
171
144
|
<ImagePreview />
|
|
172
145
|
</ViewerStoreProvider>
|
|
@@ -196,13 +169,7 @@ export function DirectoryViewGrid({
|
|
|
196
169
|
{nodes.map((node) => {
|
|
197
170
|
const key = node.id;
|
|
198
171
|
if (kind === "connections") {
|
|
199
|
-
return
|
|
200
|
-
<BucketCardGridItem
|
|
201
|
-
key={key}
|
|
202
|
-
node={node}
|
|
203
|
-
connectionName={node.connectionName}
|
|
204
|
-
/>
|
|
205
|
-
);
|
|
172
|
+
return <BucketCardGridItem key={key} node={node} connectionName={node.connectionName} />;
|
|
206
173
|
}
|
|
207
174
|
return (
|
|
208
175
|
<FileCardGridItem
|
|
@@ -94,10 +94,7 @@ export function DirectoryViewTableConnection({
|
|
|
94
94
|
const connections = useConnectionsStore(select.connections);
|
|
95
95
|
|
|
96
96
|
const data = useMemo(
|
|
97
|
-
() =>
|
|
98
|
-
nodes
|
|
99
|
-
.map((n) => connections[n.connectionName]?.connectionConfig)
|
|
100
|
-
.filter(Boolean),
|
|
97
|
+
() => nodes.map((n) => connections[n.connectionName]?.connectionConfig).filter(Boolean),
|
|
101
98
|
[nodes, connections],
|
|
102
99
|
);
|
|
103
100
|
|
|
@@ -3,11 +3,7 @@ import { filesize } from "filesize";
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { Link } from "react-router";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
TreeNode,
|
|
8
|
-
computeDirectorySize,
|
|
9
|
-
computeDirectoryLastModified,
|
|
10
|
-
} from "./buildDirectoryTree";
|
|
6
|
+
import { TreeNode, computeDirectorySize, computeDirectoryLastModified } from "./buildDirectoryTree";
|
|
11
7
|
import { CellRenderers, ColumnConfig, Table } from "~/components/Table/Table";
|
|
12
8
|
import { getFileType } from "~/utils/fileType";
|
|
13
9
|
import { formatHumanReadableDate } from "~/utils/formatHumanReadableDate";
|
|
@@ -74,8 +70,7 @@ const fileCellRenderers: CellRenderers<FileRow> = {
|
|
|
74
70
|
// for directories, file-type icon for files) to match the grid's FileCard.
|
|
75
71
|
name: (row) => <Link to={`/connections/${row.id}`}>{row.name}</Link>,
|
|
76
72
|
file_type: (row) => <Pill>{row.file_type}</Pill>,
|
|
77
|
-
last_modified: (row) =>
|
|
78
|
-
row.last_modified ? formatHumanReadableDate(row.last_modified) : null,
|
|
73
|
+
last_modified: (row) => (row.last_modified ? formatHumanReadableDate(row.last_modified) : null),
|
|
79
74
|
size: (row) => (row.size ? filesize(row.size).toString() : null),
|
|
80
75
|
};
|
|
81
76
|
|
|
@@ -44,11 +44,7 @@ export function NodeLinkIcon({ node }: { node: TreeNode }) {
|
|
|
44
44
|
* this wrapper and {@link DirectoryTree} should collapse into a single
|
|
45
45
|
* component used across the app.
|
|
46
46
|
*/
|
|
47
|
-
export function DirectoryViewTree({
|
|
48
|
-
nodes,
|
|
49
|
-
searchTerm,
|
|
50
|
-
kind,
|
|
51
|
-
}: DirectoryViewTreeProps) {
|
|
47
|
+
export function DirectoryViewTree({ nodes, searchTerm, kind }: DirectoryViewTreeProps) {
|
|
52
48
|
const navigate = useNavigate();
|
|
53
49
|
|
|
54
50
|
if (nodes.length === 0) return <DirectoryViewEmptyState kind={kind} />;
|
|
@@ -63,12 +59,8 @@ export function DirectoryViewTree({
|
|
|
63
59
|
size="comfortable"
|
|
64
60
|
height={600}
|
|
65
61
|
searchTerm={searchTerm}
|
|
66
|
-
searchMatch={(node, term) =>
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
onActivate={(node) =>
|
|
70
|
-
navigate(buildConnectionPath(node.connectionName, node.pathName))
|
|
71
|
-
}
|
|
62
|
+
searchMatch={(node, term) => node.name.toLowerCase().includes(term.toLowerCase())}
|
|
63
|
+
onActivate={(node) => navigate(buildConnectionPath(node.connectionName, node.pathName))}
|
|
72
64
|
/>
|
|
73
65
|
</div>
|
|
74
66
|
);
|
|
@@ -84,11 +76,7 @@ interface DirectoryTreeProps {
|
|
|
84
76
|
className?: string;
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
function DirectoryTreeRecursive({
|
|
88
|
-
nodes,
|
|
89
|
-
action,
|
|
90
|
-
className,
|
|
91
|
-
}: DirectoryTreeProps) {
|
|
79
|
+
function DirectoryTreeRecursive({ nodes, action, className }: DirectoryTreeProps) {
|
|
92
80
|
return (
|
|
93
81
|
<ul className="pl-6">
|
|
94
82
|
{nodes.map((node) => {
|
|
@@ -121,11 +109,7 @@ function DirectoryTreeRecursive({
|
|
|
121
109
|
</Link>
|
|
122
110
|
</div>
|
|
123
111
|
{node.children && node.children.length > 0 && (
|
|
124
|
-
<DirectoryTreeRecursive
|
|
125
|
-
nodes={node.children}
|
|
126
|
-
action={action}
|
|
127
|
-
className={className}
|
|
128
|
-
/>
|
|
112
|
+
<DirectoryTreeRecursive nodes={node.children} action={action} className={className} />
|
|
129
113
|
)}
|
|
130
114
|
</li>
|
|
131
115
|
);
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Button,
|
|
3
|
-
IconButton,
|
|
4
|
-
Input,
|
|
5
|
-
Pill,
|
|
6
|
-
Select,
|
|
7
|
-
type SelectItem,
|
|
8
|
-
} from "@cytario/design";
|
|
1
|
+
import { Button, IconButton, Input, Pill, Select, type SelectItem } from "@cytario/design";
|
|
9
2
|
import { FilterX, X } from "lucide-react";
|
|
10
3
|
import { type ReactNode, useMemo } from "react";
|
|
11
4
|
|
|
@@ -40,19 +33,12 @@ interface FilterBarProps {
|
|
|
40
33
|
* Introduced in C-82 to give grid and tree views the same filter capabilities
|
|
41
34
|
* as the table view.
|
|
42
35
|
*/
|
|
43
|
-
export function FilterBar({
|
|
44
|
-
columns,
|
|
45
|
-
tableId,
|
|
46
|
-
dynamicOptions,
|
|
47
|
-
}: FilterBarProps) {
|
|
36
|
+
export function FilterBar({ columns, tableId, dynamicOptions }: FilterBarProps) {
|
|
48
37
|
const { columnFilters, setColumnFilters, resetFilters } = useColumnFilters({
|
|
49
38
|
tableId,
|
|
50
39
|
});
|
|
51
40
|
|
|
52
|
-
const filterable = useMemo(
|
|
53
|
-
() => columns.filter((c) => c.enableColumnFilter),
|
|
54
|
-
[columns],
|
|
55
|
-
);
|
|
41
|
+
const filterable = useMemo(() => columns.filter((c) => c.enableColumnFilter), [columns]);
|
|
56
42
|
|
|
57
43
|
const hasActive = columnFilters.length > 0;
|
|
58
44
|
|
|
@@ -65,9 +51,7 @@ export function FilterBar({
|
|
|
65
51
|
key={col.id}
|
|
66
52
|
column={col}
|
|
67
53
|
options={col.filterOptions ?? dynamicOptions?.[col.id]}
|
|
68
|
-
value={
|
|
69
|
-
(columnFilters.find((f) => f.id === col.id)?.value as string) ?? ""
|
|
70
|
-
}
|
|
54
|
+
value={(columnFilters.find((f) => f.id === col.id)?.value as string) ?? ""}
|
|
71
55
|
onChange={(next) => {
|
|
72
56
|
setColumnFilters((prev) => {
|
|
73
57
|
const rest = prev.filter((f) => f.id !== col.id);
|
|
@@ -77,12 +61,7 @@ export function FilterBar({
|
|
|
77
61
|
/>
|
|
78
62
|
))}
|
|
79
63
|
{hasActive && (
|
|
80
|
-
<Button
|
|
81
|
-
variant="ghost"
|
|
82
|
-
size="sm"
|
|
83
|
-
iconLeft={FilterX}
|
|
84
|
-
onPress={resetFilters}
|
|
85
|
-
>
|
|
64
|
+
<Button variant="ghost" size="sm" iconLeft={FilterX} onPress={resetFilters}>
|
|
86
65
|
Clear all
|
|
87
66
|
</Button>
|
|
88
67
|
)}
|
|
@@ -97,23 +76,11 @@ interface FilterControlProps {
|
|
|
97
76
|
onChange: (value: string) => void;
|
|
98
77
|
}
|
|
99
78
|
|
|
100
|
-
function FilterControl({
|
|
101
|
-
column,
|
|
102
|
-
options,
|
|
103
|
-
value,
|
|
104
|
-
onChange,
|
|
105
|
-
}: FilterControlProps) {
|
|
79
|
+
function FilterControl({ column, options, value, onChange }: FilterControlProps) {
|
|
106
80
|
const clear = () => onChange("");
|
|
107
81
|
|
|
108
82
|
if (column.filterType === "select") {
|
|
109
|
-
return
|
|
110
|
-
<SelectFilter
|
|
111
|
-
column={column}
|
|
112
|
-
options={options}
|
|
113
|
-
value={value}
|
|
114
|
-
onChange={onChange}
|
|
115
|
-
/>
|
|
116
|
-
);
|
|
83
|
+
return <SelectFilter column={column} options={options} value={value} onChange={onChange} />;
|
|
117
84
|
}
|
|
118
85
|
|
|
119
86
|
return (
|
|
@@ -154,9 +121,7 @@ function SelectFilter({
|
|
|
154
121
|
if (!options) return [ALL_OPTION];
|
|
155
122
|
return [
|
|
156
123
|
ALL_OPTION,
|
|
157
|
-
...options
|
|
158
|
-
.filter((o) => o.value !== "")
|
|
159
|
-
.map((o) => ({ id: o.value, name: o.label })),
|
|
124
|
+
...options.filter((o) => o.value !== "").map((o) => ({ id: o.value, name: o.label })),
|
|
160
125
|
];
|
|
161
126
|
}, [options]);
|
|
162
127
|
|
|
@@ -166,11 +131,7 @@ function SelectFilter({
|
|
|
166
131
|
if (!column.filterRender) return undefined;
|
|
167
132
|
const render = column.filterRender;
|
|
168
133
|
function FilterOption(item: SelectItem): ReactNode {
|
|
169
|
-
return item.id === ALL_KEY ? (
|
|
170
|
-
<AllPill />
|
|
171
|
-
) : (
|
|
172
|
-
render({ label: item.name, value: item.id })
|
|
173
|
-
);
|
|
134
|
+
return item.id === ALL_KEY ? <AllPill /> : render({ label: item.name, value: item.id });
|
|
174
135
|
}
|
|
175
136
|
return FilterOption;
|
|
176
137
|
}, [column.filterRender]);
|
|
@@ -100,10 +100,7 @@ function buildDirectoryTreeRecursive(
|
|
|
100
100
|
_Object: obj,
|
|
101
101
|
};
|
|
102
102
|
currentDir.push(existingDir);
|
|
103
|
-
} else if (
|
|
104
|
-
isImageFile(obj.Key ?? "") &&
|
|
105
|
-
!isImageFile(existingDir._Object?.Key ?? "")
|
|
106
|
-
) {
|
|
103
|
+
} else if (isImageFile(obj.Key ?? "") && !isImageFile(existingDir._Object?.Key ?? "")) {
|
|
107
104
|
existingDir._Object = obj;
|
|
108
105
|
}
|
|
109
106
|
|
|
@@ -118,10 +115,7 @@ function buildDirectoryTreeRecursive(
|
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
/** Depth-first search for a node by its `id`. */
|
|
121
|
-
export function findNodeById(
|
|
122
|
-
nodes: TreeNode[],
|
|
123
|
-
id: string,
|
|
124
|
-
): TreeNode | undefined {
|
|
118
|
+
export function findNodeById(nodes: TreeNode[], id: string): TreeNode | undefined {
|
|
125
119
|
for (const node of nodes) {
|
|
126
120
|
if (node.id === id) return node;
|
|
127
121
|
if (node.children.length > 0) {
|
|
@@ -136,23 +130,16 @@ export function findNodeById(
|
|
|
136
130
|
export function computeDirectorySize(node: TreeNode): number {
|
|
137
131
|
if (node.type === "file") return node._Object?.Size ?? 0;
|
|
138
132
|
if (node.children.length === 0) return node._Object?.Size ?? 0;
|
|
139
|
-
return node.children.reduce(
|
|
140
|
-
(sum, child) => sum + computeDirectorySize(child),
|
|
141
|
-
0,
|
|
142
|
-
);
|
|
133
|
+
return node.children.reduce((sum, child) => sum + computeDirectorySize(child), 0);
|
|
143
134
|
}
|
|
144
135
|
|
|
145
136
|
/** Latest LastModified timestamp under a directory node. */
|
|
146
137
|
export function computeDirectoryLastModified(node: TreeNode): number {
|
|
147
138
|
if (node.type === "file") {
|
|
148
|
-
return node._Object?.LastModified
|
|
149
|
-
? new Date(node._Object.LastModified).getTime()
|
|
150
|
-
: 0;
|
|
139
|
+
return node._Object?.LastModified ? new Date(node._Object.LastModified).getTime() : 0;
|
|
151
140
|
}
|
|
152
141
|
if (node.children.length === 0) {
|
|
153
|
-
return node._Object?.LastModified
|
|
154
|
-
? new Date(node._Object.LastModified).getTime()
|
|
155
|
-
: 0;
|
|
142
|
+
return node._Object?.LastModified ? new Date(node._Object.LastModified).getTime() : 0;
|
|
156
143
|
}
|
|
157
144
|
return node.children.reduce(
|
|
158
145
|
(max, child) => Math.max(max, computeDirectoryLastModified(child)),
|
|
@@ -182,13 +169,7 @@ export function buildDirectoryTree(
|
|
|
182
169
|
const pathName = obj.Key.replace(prefix || "", "");
|
|
183
170
|
const pathSegments = pathName.split("/");
|
|
184
171
|
|
|
185
|
-
buildDirectoryTreeRecursive(
|
|
186
|
-
root,
|
|
187
|
-
pathSegments,
|
|
188
|
-
obj,
|
|
189
|
-
connectionName,
|
|
190
|
-
basePath,
|
|
191
|
-
);
|
|
172
|
+
buildDirectoryTreeRecursive(root, pathSegments, obj, connectionName, basePath);
|
|
192
173
|
});
|
|
193
174
|
|
|
194
175
|
return root;
|
|
@@ -20,15 +20,13 @@ type NodeAccessor = (node: TreeNode) => string;
|
|
|
20
20
|
|
|
21
21
|
const fileAccessors: Record<string, NodeAccessor> = {
|
|
22
22
|
name: (node) => node.name,
|
|
23
|
-
file_type: (node) =>
|
|
24
|
-
node.type === "file" ? getFileType(node.name) : "Directory",
|
|
23
|
+
file_type: (node) => (node.type === "file" ? getFileType(node.name) : "Directory"),
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
function makeConnectionAccessors(
|
|
28
27
|
connections: Record<string, Connection>,
|
|
29
28
|
): Record<string, NodeAccessor> {
|
|
30
|
-
const config = (node: TreeNode) =>
|
|
31
|
-
connections[node.connectionName]?.connectionConfig;
|
|
29
|
+
const config = (node: TreeNode) => connections[node.connectionName]?.connectionConfig;
|
|
32
30
|
return {
|
|
33
31
|
name: (node) => node.name,
|
|
34
32
|
provider: (node) => config(node)?.provider ?? "",
|
|
@@ -41,9 +39,7 @@ export function getNodeAccessors(
|
|
|
41
39
|
kind: DirectoryKind,
|
|
42
40
|
connections: Record<string, Connection> = {},
|
|
43
41
|
): Record<string, NodeAccessor> {
|
|
44
|
-
return kind === "connections"
|
|
45
|
-
? makeConnectionAccessors(connections)
|
|
46
|
-
: fileAccessors;
|
|
42
|
+
return kind === "connections" ? makeConnectionAccessors(connections) : fileAccessors;
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
/**
|
|
@@ -51,10 +47,7 @@ export function getNodeAccessors(
|
|
|
51
47
|
* Filtering is applied recursively so that hidden children inside visible
|
|
52
48
|
* directories are also removed (required for the tree view).
|
|
53
49
|
*/
|
|
54
|
-
export function filterHiddenNodes(
|
|
55
|
-
nodes: TreeNode[],
|
|
56
|
-
showHidden: boolean,
|
|
57
|
-
): TreeNode[] {
|
|
50
|
+
export function filterHiddenNodes(nodes: TreeNode[], showHidden: boolean): TreeNode[] {
|
|
58
51
|
if (showHidden) return nodes;
|
|
59
52
|
|
|
60
53
|
return nodes
|
|
@@ -9,11 +9,7 @@ import { RouteModal } from "~/components/RouteModal";
|
|
|
9
9
|
* Provides information and a download link for a Cyberduck connection profile.
|
|
10
10
|
* Opens via `?modal=cyberduck&connectionName=my-connection`.
|
|
11
11
|
*/
|
|
12
|
-
export default function CyberduckModal({
|
|
13
|
-
onClose,
|
|
14
|
-
}: {
|
|
15
|
-
onClose: () => void;
|
|
16
|
-
}) {
|
|
12
|
+
export default function CyberduckModal({ onClose }: { onClose: () => void }) {
|
|
17
13
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
18
14
|
const connectionName = searchParams.get("connectionName");
|
|
19
15
|
|
|
@@ -34,14 +30,13 @@ export default function CyberduckModal({
|
|
|
34
30
|
return (
|
|
35
31
|
<RouteModal title="Access with Cyberduck" onClose={handleClose}>
|
|
36
32
|
<p className="text-(--color-text-secondary)">
|
|
37
|
-
Cyberduck is a free desktop application that allows you to browse and
|
|
38
|
-
|
|
33
|
+
Cyberduck is a free desktop application that allows you to browse and manage your cloud
|
|
34
|
+
storage files with a familiar file manager interface.
|
|
39
35
|
</p>
|
|
40
36
|
|
|
41
37
|
<p className="text-(--color-text-secondary)">
|
|
42
|
-
Download a pre-configured connection profile for{
|
|
43
|
-
|
|
44
|
-
authentication and bucket access in Cyberduck.
|
|
38
|
+
Download a pre-configured connection profile for <strong>{connectionName}</strong> that will
|
|
39
|
+
automatically set up your authentication and bucket access in Cyberduck.
|
|
45
40
|
</p>
|
|
46
41
|
|
|
47
42
|
<div className="bg-(--color-surface-subtle) border border-(--color-border-default) rounded-sm px-4 py-2 flex flex-col gap-2">
|
|
@@ -49,11 +44,7 @@ export default function CyberduckModal({
|
|
|
49
44
|
<ol className="list-decimal list-inside space-y-1 text-sm text-(--color-text-secondary)">
|
|
50
45
|
<li>
|
|
51
46
|
Download and install{" "}
|
|
52
|
-
<Link
|
|
53
|
-
href="https://cyberduck.io/download/"
|
|
54
|
-
target="_blank"
|
|
55
|
-
rel="noopener noreferrer"
|
|
56
|
-
>
|
|
47
|
+
<Link href="https://cyberduck.io/download/" target="_blank" rel="noopener noreferrer">
|
|
57
48
|
Cyberduck
|
|
58
49
|
</Link>{" "}
|
|
59
50
|
(version 8.7.0 or later)
|
|
@@ -4,11 +4,7 @@ import { useParams, useSearchParams } from "react-router";
|
|
|
4
4
|
|
|
5
5
|
import { RouteModal } from "~/components/RouteModal";
|
|
6
6
|
|
|
7
|
-
export default function FileInfoModal({
|
|
8
|
-
onClose,
|
|
9
|
-
}: {
|
|
10
|
-
onClose: (extraKeys?: string[]) => void;
|
|
11
|
-
}) {
|
|
7
|
+
export default function FileInfoModal({ onClose }: { onClose: (extraKeys?: string[]) => void }) {
|
|
12
8
|
const [searchParams] = useSearchParams();
|
|
13
9
|
const { name: connectionName } = useParams();
|
|
14
10
|
|
|
@@ -44,11 +44,7 @@ export const useLayoutStore = create<LayoutStore>()(
|
|
|
44
44
|
),
|
|
45
45
|
showFilters: false,
|
|
46
46
|
toggleShowFilters: () =>
|
|
47
|
-
set(
|
|
48
|
-
(state) => ({ showFilters: !state.showFilters }),
|
|
49
|
-
false,
|
|
50
|
-
"toggleShowFilters",
|
|
51
|
-
),
|
|
47
|
+
set((state) => ({ showFilters: !state.showFilters }), false, "toggleShowFilters"),
|
|
52
48
|
headerSlot: null,
|
|
53
49
|
setHeaderSlot: (headerSlot) => set({ headerSlot }),
|
|
54
50
|
}),
|
|
@@ -61,34 +57,18 @@ export const useLayoutStore = create<LayoutStore>()(
|
|
|
61
57
|
{
|
|
62
58
|
0: (state) => {
|
|
63
59
|
const s = state as { viewMode?: string };
|
|
64
|
-
const OLD_VALID = [
|
|
65
|
-
"list",
|
|
66
|
-
"list-wide",
|
|
67
|
-
"grid-sm",
|
|
68
|
-
"grid-md",
|
|
69
|
-
"grid-lg",
|
|
70
|
-
];
|
|
60
|
+
const OLD_VALID = ["list", "list-wide", "grid-sm", "grid-md", "grid-lg"];
|
|
71
61
|
return {
|
|
72
|
-
viewMode: OLD_VALID.includes(s?.viewMode ?? "")
|
|
73
|
-
? (s.viewMode as string)
|
|
74
|
-
: "grid",
|
|
62
|
+
viewMode: OLD_VALID.includes(s?.viewMode ?? "") ? (s.viewMode as string) : "grid",
|
|
75
63
|
showHiddenFiles: false,
|
|
76
64
|
showFilters: false,
|
|
77
65
|
};
|
|
78
66
|
},
|
|
79
67
|
1: (state) => {
|
|
80
68
|
const s = state as { viewMode?: string };
|
|
81
|
-
const OLD_VALID = [
|
|
82
|
-
"list",
|
|
83
|
-
"list-wide",
|
|
84
|
-
"grid-sm",
|
|
85
|
-
"grid-md",
|
|
86
|
-
"grid-lg",
|
|
87
|
-
];
|
|
69
|
+
const OLD_VALID = ["list", "list-wide", "grid-sm", "grid-md", "grid-lg"];
|
|
88
70
|
return {
|
|
89
|
-
viewMode: OLD_VALID.includes(s?.viewMode ?? "")
|
|
90
|
-
? (s.viewMode as string)
|
|
91
|
-
: "grid",
|
|
71
|
+
viewMode: OLD_VALID.includes(s?.viewMode ?? "") ? (s.viewMode as string) : "grid",
|
|
92
72
|
showHiddenFiles: false,
|
|
93
73
|
showFilters: false,
|
|
94
74
|
};
|
|
@@ -40,10 +40,7 @@ export const GlobalSearch = () => {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
const handleClickOutside = (event: Event) => {
|
|
43
|
-
if (
|
|
44
|
-
parentRef.current &&
|
|
45
|
-
!parentRef.current.contains(event.target as Node)
|
|
46
|
-
) {
|
|
43
|
+
if (parentRef.current && !parentRef.current.contains(event.target as Node)) {
|
|
47
44
|
setShowResults(false);
|
|
48
45
|
}
|
|
49
46
|
};
|
|
@@ -17,13 +17,7 @@ export function SearchBar({ value, onChange, onClear }: SearchBarProps) {
|
|
|
17
17
|
placeholder="Search..."
|
|
18
18
|
/>
|
|
19
19
|
|
|
20
|
-
{value ?
|
|
21
|
-
<IconButton
|
|
22
|
-
icon={X}
|
|
23
|
-
onPress={onClear}
|
|
24
|
-
aria-label="Clear search"
|
|
25
|
-
/>
|
|
26
|
-
) : null}
|
|
20
|
+
{value ? <IconButton icon={X} onPress={onClear} aria-label="Clear search" /> : null}
|
|
27
21
|
</div>
|
|
28
22
|
);
|
|
29
23
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
FormatExtension,
|
|
3
|
-
FormatHandler,
|
|
4
|
-
FormatRegistry,
|
|
5
|
-
} from "@cytario/plugin-api";
|
|
1
|
+
import type { FormatExtension, FormatHandler, FormatRegistry } from "@cytario/plugin-api";
|
|
6
2
|
import { getExtension } from "~/utils/fileType";
|
|
7
3
|
|
|
8
4
|
/**
|
|
@@ -87,8 +83,7 @@ class FormatRegistryImpl {
|
|
|
87
83
|
*/
|
|
88
84
|
scopedFor(pluginName: string): FormatRegistry {
|
|
89
85
|
return {
|
|
90
|
-
register: (extension, handler) =>
|
|
91
|
-
this.add(pluginName, extension, handler),
|
|
86
|
+
register: (extension, handler) => this.add(pluginName, extension, handler),
|
|
92
87
|
};
|
|
93
88
|
}
|
|
94
89
|
|
|
@@ -97,15 +92,9 @@ class FormatRegistryImpl {
|
|
|
97
92
|
* same plugin name) is a no-op so HMR re-runs do not throw.
|
|
98
93
|
* Cross-plugin overlap on any key throws `DuplicateRegistrationError`.
|
|
99
94
|
*/
|
|
100
|
-
add(
|
|
101
|
-
pluginName: string,
|
|
102
|
-
extension: FormatExtension,
|
|
103
|
-
handler: FormatHandler,
|
|
104
|
-
): void {
|
|
95
|
+
add(pluginName: string, extension: FormatExtension, handler: FormatHandler): void {
|
|
105
96
|
const keys = normalizeKeys(extension);
|
|
106
|
-
const existing = this.registrations.find((r) =>
|
|
107
|
-
keysCollide(r.keys, keys),
|
|
108
|
-
);
|
|
97
|
+
const existing = this.registrations.find((r) => keysCollide(r.keys, keys));
|
|
109
98
|
if (existing) {
|
|
110
99
|
if (existing.pluginName === pluginName) return;
|
|
111
100
|
throw new DuplicateRegistrationError(
|
|
@@ -136,9 +125,7 @@ class FormatRegistryImpl {
|
|
|
136
125
|
}
|
|
137
126
|
}
|
|
138
127
|
}
|
|
139
|
-
throw new UnknownFormatError(
|
|
140
|
-
`No format handler registered for URL: ${url}`,
|
|
141
|
-
);
|
|
128
|
+
throw new UnknownFormatError(`No format handler registered for URL: ${url}`);
|
|
142
129
|
}
|
|
143
130
|
|
|
144
131
|
list(): readonly Registration[] {
|