@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.
Files changed (202) hide show
  1. package/README.md +20 -20
  2. package/app/.server/auth/README.md +8 -8
  3. package/app/.server/auth/authMiddleware.ts +9 -25
  4. package/app/.server/auth/exchangeAuthCode.ts +2 -6
  5. package/app/.server/auth/getS3Client.ts +3 -13
  6. package/app/.server/auth/getSessionCredentials.ts +6 -20
  7. package/app/.server/auth/getUserInfo.ts +2 -6
  8. package/app/.server/auth/keycloakAdmin/client.ts +2 -9
  9. package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
  10. package/app/.server/auth/keycloakAdmin/users.ts +7 -23
  11. package/app/.server/auth/oauthState.ts +4 -13
  12. package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
  13. package/app/.server/auth/refreshAuthTokens.ts +5 -19
  14. package/app/.server/auth/sessionMiddleware.ts +1 -4
  15. package/app/.server/auth/sessionStorage.ts +1 -4
  16. package/app/.server/auth/verifyIdToken.ts +1 -3
  17. package/app/.server/auth/wellKnownEndpoints.ts +1 -4
  18. package/app/.server/db/redis.ts +5 -1
  19. package/app/.server/logging.ts +1 -4
  20. package/app/.server/requestDurationMiddleware.ts +1 -4
  21. package/app/components/.client/ImageViewer/README.md +5 -5
  22. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
  23. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
  24. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
  25. package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
  26. package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
  27. package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
  28. package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
  29. package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
  30. package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
  31. package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
  32. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
  33. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
  34. package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
  35. package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
  36. package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
  37. package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
  38. package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
  39. package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
  40. package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
  41. package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
  42. package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
  43. package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
  44. package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
  45. package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
  46. package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
  47. package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
  48. package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
  49. package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
  50. package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
  51. package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
  52. package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
  53. package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
  54. package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
  55. package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
  56. package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
  57. package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
  58. package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
  59. package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
  60. package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
  61. package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
  62. package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
  63. package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
  64. package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
  65. package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
  66. package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
  67. package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
  68. package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
  69. package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
  70. package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
  71. package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
  72. package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
  73. package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
  74. package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
  75. package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
  76. package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
  77. package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
  78. package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
  79. package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
  80. package/app/components/AppHeader.tsx +1 -4
  81. package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
  82. package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
  83. package/app/components/ClientOnly.tsx +1 -1
  84. package/app/components/Container.tsx +3 -15
  85. package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
  86. package/app/components/DataGrid/DataGrid.tsx +7 -27
  87. package/app/components/DataGrid/WktSvg.tsx +2 -4
  88. package/app/components/DataGrid/getParquetSchema.ts +1 -4
  89. package/app/components/DescriptionList.tsx +1 -3
  90. package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
  91. package/app/components/DirectoryView/DirectoryView.tsx +10 -46
  92. package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
  93. package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
  94. package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
  95. package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
  96. package/app/components/DirectoryView/FilterBar.tsx +9 -48
  97. package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
  98. package/app/components/DirectoryView/filterNodes.ts +4 -11
  99. package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
  100. package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
  101. package/app/components/DirectoryView/useLayoutStore.ts +5 -25
  102. package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
  103. package/app/components/GlobalSearch/SearchBar.tsx +1 -7
  104. package/app/components/GlobalSearch/Suggestions.tsx +0 -1
  105. package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
  106. package/app/components/LavaLoader.tsx +4 -11
  107. package/app/components/Layout/Footer.tsx +1 -4
  108. package/app/components/Pills/ScopePill.tsx +2 -8
  109. package/app/components/Table/ColumnFilterInput.tsx +5 -20
  110. package/app/components/Table/ColumnResizeHandle.tsx +1 -5
  111. package/app/components/Table/ColumnSortButton.tsx +1 -5
  112. package/app/components/Table/SelectionFooter.tsx +3 -5
  113. package/app/components/Table/Table.tsx +5 -21
  114. package/app/components/Table/TableBodyRow.tsx +19 -31
  115. package/app/components/Table/TableHeaderRow.tsx +7 -28
  116. package/app/components/Table/TableMenu.tsx +4 -20
  117. package/app/components/Table/state/createTableStore.ts +3 -9
  118. package/app/components/Table/state/useTableStore.ts +1 -3
  119. package/app/components/Table/types.ts +4 -10
  120. package/app/components/Table/useColumnFilters.ts +1 -4
  121. package/app/components/Table/useColumnVisibility.ts +4 -11
  122. package/app/components/Table/useColumnWidths.ts +1 -3
  123. package/app/components/Table/useTableSorting.ts +4 -12
  124. package/app/components/Tooltip/Tooltip.tsx +4 -17
  125. package/app/components/Tooltip/TooltipSpan.tsx +5 -28
  126. package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
  127. package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
  128. package/app/components/Tooltip/useOverflowDetection.ts +2 -5
  129. package/app/components/UserMenu.tsx +2 -9
  130. package/app/entry.server.tsx +9 -19
  131. package/app/hooks/useSearchParam.ts +2 -4
  132. package/app/lib/bootstrapPluginsCore.ts +4 -9
  133. package/app/root.tsx +4 -15
  134. package/app/routes/admin/assertAdminScope.ts +1 -3
  135. package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
  136. package/app/routes/admin/assertGroupsInScope.ts +3 -11
  137. package/app/routes/admin/assertUsersInScope.ts +2 -8
  138. package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
  139. package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
  140. package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
  141. package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
  142. package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
  143. package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
  144. package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
  145. package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
  146. package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
  147. package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
  148. package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
  149. package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
  150. package/app/routes/admin/users/BulkActions.tsx +15 -38
  151. package/app/routes/admin/users/bulkUsers.action.ts +2 -9
  152. package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
  153. package/app/routes/admin/users/users.route.tsx +14 -63
  154. package/app/routes/api/cyberduck-profile.$name.ts +6 -2
  155. package/app/routes/auth/callback.route.tsx +8 -33
  156. package/app/routes/auth/login.route.tsx +8 -11
  157. package/app/routes/auth/logout.route.tsx +4 -14
  158. package/app/routes/config.route.tsx +1 -5
  159. package/app/routes/connections/connection.form.tsx +5 -14
  160. package/app/routes/connections/connection.schema.ts +4 -16
  161. package/app/routes/connections/connections.loader.ts +11 -23
  162. package/app/routes/connections/connections.route.tsx +1 -5
  163. package/app/routes/connections/connections.server.ts +1 -3
  164. package/app/routes/connections/createConnection.action.ts +2 -8
  165. package/app/routes/connections/createConnection.modal.tsx +3 -13
  166. package/app/routes/connections/deleteConnection.action.ts +1 -4
  167. package/app/routes/connections/updateConnection.action.ts +2 -8
  168. package/app/routes/connections/updateConnection.modal.tsx +4 -14
  169. package/app/routes/home/home.route.tsx +8 -33
  170. package/app/routes/layouts/ModalOutlet.tsx +6 -18
  171. package/app/routes/objects/objects.loader.ts +5 -19
  172. package/app/routes/objects/objects.route.tsx +11 -30
  173. package/app/routes/presign.route.tsx +5 -18
  174. package/app/routes/recent.route.tsx +1 -4
  175. package/app/routes/search.route.tsx +4 -17
  176. package/app/routes.ts +1 -4
  177. package/app/tailwind.css +17 -12
  178. package/app/types/cornerstone-codecs.d.ts +25 -29
  179. package/app/utils/connectionsStore/selectors.ts +2 -6
  180. package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
  181. package/app/utils/db/convertCsvToParquet.ts +1 -3
  182. package/app/utils/db/createDatabase.ts +1 -3
  183. package/app/utils/db/createSingleton.ts +1 -1
  184. package/app/utils/db/getBlobFromObjectNode.ts +3 -9
  185. package/app/utils/db/getGeomQuery.ts +1 -1
  186. package/app/utils/db/getMarkerInfoWasm.ts +1 -3
  187. package/app/utils/db/getTileBoundingBox.ts +1 -4
  188. package/app/utils/db/sqlQueries.ts +1 -4
  189. package/app/utils/fileType.ts +80 -10
  190. package/app/utils/filterObjects.ts +2 -5
  191. package/app/utils/localFilesStore/useFileStore.ts +7 -7
  192. package/app/utils/recentlyViewed.server.ts +1 -4
  193. package/app/utils/resourceId.ts +3 -11
  194. package/app/utils/s3Provider.ts +3 -7
  195. package/app/utils/signedFetch.ts +4 -13
  196. package/bin-src/codegen.ts +4 -1
  197. package/package.json +5 -1
  198. package/prisma/seed.ts +1 -2
  199. package/public/favicon/site.webmanifest +1 -1
  200. package/server.js +1 -4
  201. package/server.js.map +1 -1
  202. package/vite-plugins/cytario-plugins.ts +2 -8
@@ -33,13 +33,7 @@ const CopiedOverlay = () => (
33
33
 
34
34
  // ─── Right (default): pure CSS truncation ───────────────────────────
35
35
 
36
- const RightEllipsis = ({
37
- children,
38
- copyValue,
39
- }: {
40
- children: ReactNode;
41
- copyValue?: string;
42
- }) => {
36
+ const RightEllipsis = ({ children, copyValue }: { children: ReactNode; copyValue?: string }) => {
43
37
  const ref = useRef<HTMLSpanElement>(null);
44
38
  const isTruncated = useOverflowDetection(ref);
45
39
  const { handleClick, isCopied } = useCopyToClipboard(copyValue);
@@ -67,13 +61,7 @@ const RightEllipsis = ({
67
61
 
68
62
  // ─── Left: CSS direction trick ──────────────────────────────────────
69
63
 
70
- const LeftEllipsis = ({
71
- children,
72
- copyValue,
73
- }: {
74
- children: ReactNode;
75
- copyValue?: string;
76
- }) => {
64
+ const LeftEllipsis = ({ children, copyValue }: { children: ReactNode; copyValue?: string }) => {
77
65
  const ref = useRef<HTMLSpanElement>(null);
78
66
  const isTruncated = useOverflowDetection(ref);
79
67
  const { handleClick, isCopied } = useCopyToClipboard(copyValue);
@@ -104,13 +92,7 @@ const LeftEllipsis = ({
104
92
 
105
93
  const middleCx = "overflow-hidden whitespace-nowrap block min-w-0 w-full";
106
94
 
107
- const MiddleEllipsisString = ({
108
- text,
109
- copyValue,
110
- }: {
111
- text: string;
112
- copyValue?: string;
113
- }) => {
95
+ const MiddleEllipsisString = ({ text, copyValue }: { text: string; copyValue?: string }) => {
114
96
  const ref = useRef<HTMLSpanElement>(null);
115
97
  const displayed = useMiddleEllipsis(ref, text);
116
98
  const isTruncated = displayed !== text;
@@ -139,13 +121,8 @@ const MiddleEllipsisString = ({
139
121
 
140
122
  // ─── Public component ───────────────────────────────────────────────
141
123
 
142
- export const TooltipSpan = ({
143
- children,
144
- ellipsis = "right",
145
- copyValue,
146
- }: TooltipSpanProps) => {
147
- if (ellipsis === "left")
148
- return <LeftEllipsis copyValue={copyValue}>{children}</LeftEllipsis>;
124
+ export const TooltipSpan = ({ children, ellipsis = "right", copyValue }: TooltipSpanProps) => {
125
+ if (ellipsis === "left") return <LeftEllipsis copyValue={copyValue}>{children}</LeftEllipsis>;
149
126
  if (ellipsis === "middle" && typeof children === "string")
150
127
  return <MiddleEllipsisString text={children} copyValue={copyValue} />;
151
128
  return <RightEllipsis copyValue={copyValue}>{children}</RightEllipsis>;
@@ -7,9 +7,7 @@ interface UseCopyToClipboardResult {
7
7
  isCopied: boolean;
8
8
  }
9
9
 
10
- export function useCopyToClipboard(
11
- copyValue: string | undefined,
12
- ): UseCopyToClipboardResult {
10
+ export function useCopyToClipboard(copyValue: string | undefined): UseCopyToClipboardResult {
13
11
  const [isCopied, setIsCopied] = useState(false);
14
12
  const timerRef = useRef<number | null>(null);
15
13
 
@@ -19,9 +19,7 @@ function computeTruncated(el: HTMLElement, text: string): string {
19
19
  const startLen = Math.ceil(mid / 2);
20
20
  const endLen = Math.floor(mid / 2);
21
21
  el.textContent =
22
- text.slice(0, startLen) +
23
- ELLIPSIS +
24
- (endLen > 0 ? text.slice(text.length - endLen) : "");
22
+ text.slice(0, startLen) + ELLIPSIS + (endLen > 0 ? text.slice(text.length - endLen) : "");
25
23
 
26
24
  if (el.scrollWidth <= el.clientWidth) {
27
25
  lo = mid;
@@ -49,10 +47,7 @@ function computeTruncated(el: HTMLElement, text: string): string {
49
47
  * Observes the container width and returns a middle-truncated string.
50
48
  * Returns the original text if it fits within the container.
51
49
  */
52
- export function useMiddleEllipsis(
53
- ref: RefObject<HTMLSpanElement | null>,
54
- text: string,
55
- ): string {
50
+ export function useMiddleEllipsis(ref: RefObject<HTMLSpanElement | null>, text: string): string {
56
51
  const [displayed, setDisplayed] = useState(text);
57
52
 
58
53
  // useLayoutEffect runs before paint, so the user never sees the full text.
@@ -5,17 +5,14 @@ import { RefObject, useLayoutEffect, useState } from "react";
5
5
  * Uses ResizeObserver to recheck on any container size change
6
6
  * (column resize, flex layout shifts, not just window resize).
7
7
  */
8
- export function useOverflowDetection(
9
- ref: RefObject<HTMLElement | null>,
10
- ): boolean {
8
+ export function useOverflowDetection(ref: RefObject<HTMLElement | null>): boolean {
11
9
  const [isTruncated, setIsTruncated] = useState(false);
12
10
 
13
11
  useLayoutEffect(() => {
14
12
  const el = ref.current;
15
13
  if (!el) return;
16
14
 
17
- const check = () =>
18
- setIsTruncated(el.scrollWidth > el.offsetWidth);
15
+ const check = () => setIsTruncated(el.scrollWidth > el.offsetWidth);
19
16
 
20
17
  check();
21
18
 
@@ -26,9 +26,7 @@ export function UserMenu({ user, accountSettingsUrl }: UserMenuProps) {
26
26
  <div className="font-semibold">
27
27
  {user.given_name} {user.family_name}
28
28
  </div>
29
- <div className="text-[var(--color-text-secondary)]">
30
- {user.email}
31
- </div>
29
+ <div className="text-[var(--color-text-secondary)]">{user.email}</div>
32
30
  </div>
33
31
  </MenuHeader>
34
32
 
@@ -68,12 +66,7 @@ export function UserMenu({ user, accountSettingsUrl }: UserMenuProps) {
68
66
  </>
69
67
  )}
70
68
 
71
- <MenuItem
72
- id="account-settings"
73
- icon={Settings}
74
- href={accountSettingsUrl}
75
- target="_blank"
76
- >
69
+ <MenuItem id="account-settings" icon={Settings} href={accountSettingsUrl} target="_blank">
77
70
  Account Settings
78
71
  </MenuItem>
79
72
  <MenuItem id="logout" icon={LogOut} href="/logout">
@@ -30,30 +30,20 @@ export default async function handleRequest(
30
30
  // This is ignored so we can keep it in the template for visibility. Feel
31
31
  // free to delete this parameter in your app if you're not using it!
32
32
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
33
- loadContext: AppLoadContext
33
+ loadContext: AppLoadContext,
34
34
  ) {
35
35
  await bootstrapPromise;
36
36
 
37
37
  return isbot(request.headers.get("user-agent") || "")
38
- ? handleBotRequest(
39
- request,
40
- responseStatusCode,
41
- responseHeaders,
42
- reactRouterContext
43
- )
44
- : handleBrowserRequest(
45
- request,
46
- responseStatusCode,
47
- responseHeaders,
48
- reactRouterContext
49
- );
38
+ ? handleBotRequest(request, responseStatusCode, responseHeaders, reactRouterContext)
39
+ : handleBrowserRequest(request, responseStatusCode, responseHeaders, reactRouterContext);
50
40
  }
51
41
 
52
42
  function handleBotRequest(
53
43
  request: Request,
54
44
  responseStatusCode: number,
55
45
  responseHeaders: Headers,
56
- reactRouterContext: EntryContext
46
+ reactRouterContext: EntryContext,
57
47
  ) {
58
48
  return new Promise((resolve, reject) => {
59
49
  let shellRendered = false;
@@ -71,7 +61,7 @@ function handleBotRequest(
71
61
  new Response(stream, {
72
62
  headers: responseHeaders,
73
63
  status: responseStatusCode,
74
- })
64
+ }),
75
65
  );
76
66
 
77
67
  pipe(body);
@@ -88,7 +78,7 @@ function handleBotRequest(
88
78
  console.error(error);
89
79
  }
90
80
  },
91
- }
81
+ },
92
82
  );
93
83
 
94
84
  setTimeout(abort, ABORT_DELAY);
@@ -99,7 +89,7 @@ function handleBrowserRequest(
99
89
  request: Request,
100
90
  responseStatusCode: number,
101
91
  responseHeaders: Headers,
102
- reactRouterContext: EntryContext
92
+ reactRouterContext: EntryContext,
103
93
  ) {
104
94
  return new Promise((resolve, reject) => {
105
95
  let shellRendered = false;
@@ -117,7 +107,7 @@ function handleBrowserRequest(
117
107
  new Response(stream, {
118
108
  headers: responseHeaders,
119
109
  status: responseStatusCode,
120
- })
110
+ }),
121
111
  );
122
112
 
123
113
  pipe(body);
@@ -134,7 +124,7 @@ function handleBrowserRequest(
134
124
  console.error(error);
135
125
  }
136
126
  },
137
- }
127
+ },
138
128
  );
139
129
 
140
130
  setTimeout(abort, ABORT_DELAY);
@@ -1,8 +1,6 @@
1
1
  import { useSearchParams } from "react-router";
2
2
 
3
- export const useSearchParam = (
4
- paramName: string
5
- ): [string, (value: string) => void] => {
3
+ export const useSearchParam = (paramName: string): [string, (value: string) => void] => {
6
4
  const [searchParams, setSearchParams] = useSearchParams();
7
5
 
8
6
  const paramValue = searchParams.get(paramName) ?? "";
@@ -18,7 +16,7 @@ export const useSearchParam = (
18
16
  }
19
17
  return newParams;
20
18
  },
21
- { replace: true }
19
+ { replace: true },
22
20
  );
23
21
  };
24
22
 
@@ -1,9 +1,5 @@
1
1
  import type { CytarioPlugin, Logger, PluginContext } from "@cytario/plugin-api";
2
- import {
3
- IncompatiblePluginError,
4
- assertApiCompatible,
5
- hostApiVersion,
6
- } from "@cytario/plugin-api";
2
+ import { IncompatiblePluginError, assertApiCompatible, hostApiVersion } from "@cytario/plugin-api";
7
3
  import { formatRegistry } from "~/components/ImageViewer/state/formatRegistry";
8
4
 
9
5
  /**
@@ -37,10 +33,9 @@ export async function bootstrapPluginsCore(
37
33
  : err instanceof Error
38
34
  ? err.message
39
35
  : String(err);
40
- logger.error(
41
- `Skipping incompatible plugin "${plugin?.name ?? "<unknown>"}"`,
42
- { error: message },
43
- );
36
+ logger.error(`Skipping incompatible plugin "${plugin?.name ?? "<unknown>"}"`, {
37
+ error: message,
38
+ });
44
39
  continue;
45
40
  }
46
41
 
package/app/root.tsx CHANGED
@@ -20,10 +20,7 @@ import {
20
20
  } from "react-router";
21
21
 
22
22
  import { UserProfile } from "./.server/auth/getUserInfo";
23
- import {
24
- sessionContext,
25
- sessionMiddleware,
26
- } from "./.server/auth/sessionMiddleware";
23
+ import { sessionContext, sessionMiddleware } from "./.server/auth/sessionMiddleware";
27
24
  import { sessionStorage } from "./.server/auth/sessionStorage";
28
25
  import { AppHeader } from "./components/AppHeader";
29
26
  import { Section } from "./components/Container";
@@ -75,9 +72,7 @@ interface RootLoaderResponse {
75
72
  accountSettingsUrl?: string;
76
73
  }
77
74
 
78
- export const loader = async ({
79
- context,
80
- }: LoaderFunctionArgs): Promise<RootLoaderResponse> => {
75
+ export const loader = async ({ context }: LoaderFunctionArgs): Promise<RootLoaderResponse> => {
81
76
  const session = context.get(sessionContext);
82
77
  const user = session.get("user");
83
78
  const notification = session.get("notification");
@@ -158,10 +153,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
158
153
  <RouterProvider navigate={navigate} useHref={useHref}>
159
154
  {data?.user && <AppHeader />}
160
155
 
161
- <main
162
- id="main-content"
163
- className="relative flex-1 min-h-0 outline-none"
164
- >
156
+ <main id="main-content" className="relative flex-1 min-h-0 outline-none">
165
157
  {children}
166
158
  </main>
167
159
  </RouterProvider>
@@ -202,10 +194,7 @@ export function ErrorBoundary() {
202
194
  <div role="alert">
203
195
  <H1>{title}</H1>
204
196
  <p>{message}</p>
205
- <a
206
- href="/"
207
- className="text-cytario-purple-500 underline mt-4 inline-block"
208
- >
197
+ <a href="/" className="text-cytario-purple-500 underline mt-4 inline-block">
209
198
  Go home
210
199
  </a>
211
200
  </div>
@@ -9,9 +9,7 @@ export function assertAdminScope(
9
9
  const scope = new URL(url).searchParams.get("scope");
10
10
  if (!scope) throw new Response("Missing scope", { status: 400 });
11
11
 
12
- const isAdmin = adminScopes.some(
13
- (s) => scope === s || scope.startsWith(s + "/"),
14
- );
12
+ const isAdmin = adminScopes.some((s) => scope === s || scope.startsWith(s + "/"));
15
13
  if (!isAdmin) throw new Response("Not authorized", { status: 403 });
16
14
 
17
15
  return {
@@ -1,7 +1,4 @@
1
- import {
2
- flattenGroupsWithIds,
3
- getGroupWithMembers,
4
- } from "~/.server/auth/keycloakAdmin";
1
+ import { flattenGroupsWithIds, getGroupWithMembers } from "~/.server/auth/keycloakAdmin";
5
2
 
6
3
  /**
7
4
  * Validates that every groupPath belongs to the group tree of the given scope.
@@ -11,18 +8,13 @@ import {
11
8
  *
12
9
  * Throws 404 if the scope does not exist, 403 if any groupPath is out of scope.
13
10
  */
14
- export async function assertGroupPathsInScope(
15
- groupPaths: string[],
16
- scope: string,
17
- ): Promise<void> {
11
+ export async function assertGroupPathsInScope(groupPaths: string[], scope: string): Promise<void> {
18
12
  if (groupPaths.length === 0) return;
19
13
 
20
14
  const group = await getGroupWithMembers(scope);
21
15
  if (!group) throw new Response("Scope not found", { status: 404 });
22
16
 
23
- const scopeGroupPaths = new Set(
24
- flattenGroupsWithIds(group).map((g) => g.path),
25
- );
17
+ const scopeGroupPaths = new Set(flattenGroupsWithIds(group).map((g) => g.path));
26
18
 
27
19
  for (const groupPath of groupPaths) {
28
20
  if (!scopeGroupPaths.has(groupPath)) {
@@ -1,7 +1,4 @@
1
- import {
2
- flattenGroupsWithIds,
3
- getGroupWithMembers,
4
- } from "~/.server/auth/keycloakAdmin";
1
+ import { flattenGroupsWithIds, getGroupWithMembers } from "~/.server/auth/keycloakAdmin";
5
2
 
6
3
  /**
7
4
  * Validates that every groupId belongs to the group tree of the given scope.
@@ -11,18 +8,13 @@ import {
11
8
  *
12
9
  * Throws 404 if the scope does not exist, 403 if any groupId is out of scope.
13
10
  */
14
- export async function assertGroupsInScope(
15
- groupIds: string[],
16
- scope: string,
17
- ): Promise<void> {
11
+ export async function assertGroupsInScope(groupIds: string[], scope: string): Promise<void> {
18
12
  if (groupIds.length === 0) return;
19
13
 
20
14
  const group = await getGroupWithMembers(scope);
21
15
  if (!group) throw new Response("Scope not found", { status: 404 });
22
16
 
23
- const scopeGroupIds = new Set(
24
- flattenGroupsWithIds(group).map((g) => g.id),
25
- );
17
+ const scopeGroupIds = new Set(flattenGroupsWithIds(group).map((g) => g.id));
26
18
 
27
19
  for (const groupId of groupIds) {
28
20
  if (!scopeGroupIds.has(groupId)) {
@@ -1,12 +1,6 @@
1
- import {
2
- collectAllUsers,
3
- getGroupWithMembers,
4
- } from "~/.server/auth/keycloakAdmin";
1
+ import { collectAllUsers, getGroupWithMembers } from "~/.server/auth/keycloakAdmin";
5
2
 
6
- export async function assertUsersInScope(
7
- userIds: string[],
8
- scope: string,
9
- ): Promise<void> {
3
+ export async function assertUsersInScope(userIds: string[], scope: string): Promise<void> {
10
4
  const group = await getGroupWithMembers(scope);
11
5
  if (!group) throw new Response("Scope not found", { status: 404 });
12
6
 
@@ -8,10 +8,7 @@ import { getSession } from "~/.server/auth/getSession";
8
8
  import { inviteUser } from "~/.server/auth/keycloakAdmin/users";
9
9
  import { sessionStorage } from "~/.server/auth/sessionStorage";
10
10
 
11
- export const bulkInviteAction: ActionFunction = async ({
12
- request,
13
- context,
14
- }) => {
11
+ export const bulkInviteAction: ActionFunction = async ({ request, context }) => {
15
12
  const { user } = context.get(authContext);
16
13
  const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
17
14
 
@@ -27,15 +24,7 @@ export const bulkInviteAction: ActionFunction = async ({
27
24
  await assertGroupPathsInScope([groupPath], scope);
28
25
 
29
26
  const results = await Promise.allSettled(
30
- rows.map((row) =>
31
- inviteUser(
32
- row.email,
33
- row.firstName,
34
- row.lastName,
35
- groupPath,
36
- enabled,
37
- ),
38
- ),
27
+ rows.map((row) => inviteUser(row.email, row.firstName, row.lastName, groupPath, enabled)),
39
28
  );
40
29
 
41
30
  const succeeded = results.filter((r) => r.status === "fulfilled").length;
@@ -3,11 +3,7 @@ import { Plus, X } from "lucide-react";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { useSubmit } from "react-router";
5
5
 
6
- import {
7
- type BulkInviteRow,
8
- bulkInviteRowSchema,
9
- bulkInviteSchema,
10
- } from "./bulkInvite.schema";
6
+ import { type BulkInviteRow, bulkInviteRowSchema, bulkInviteSchema } from "./bulkInvite.schema";
11
7
  import { ScopePill } from "~/components/Pills/ScopePill";
12
8
 
13
9
  interface BulkInviteFormProps {
@@ -44,37 +40,28 @@ export function BulkInviteForm({
44
40
  const [groupPath, setGroupPath] = useState(scope);
45
41
  const [enabled, setEnabled] = useState(true);
46
42
  const [formError, setFormError] = useState<string | null>(null);
47
- const [rows, setRows] = useState<RowState[]>([
48
- emptyRow(),
49
- emptyRow(),
50
- emptyRow(),
51
- ]);
43
+ const [rows, setRows] = useState<RowState[]>([emptyRow(), emptyRow(), emptyRow()]);
52
44
 
53
45
  const tableRef = useRef<HTMLTableElement>(null);
54
46
 
55
- const updateRow = useCallback(
56
- (index: number, field: keyof BulkInviteRow, value: string) => {
57
- setRows((prev) =>
58
- prev.map((row, i) =>
59
- i === index
60
- ? {
61
- ...row,
62
- [field]: value,
63
- errors: { ...row.errors, [field]: undefined },
64
- }
65
- : row,
66
- ),
67
- );
68
- },
69
- [],
70
- );
71
-
72
- const removeRow = useCallback((index: number) => {
47
+ const updateRow = useCallback((index: number, field: keyof BulkInviteRow, value: string) => {
73
48
  setRows((prev) =>
74
- prev.length <= 1 ? prev : prev.filter((_, i) => i !== index),
49
+ prev.map((row, i) =>
50
+ i === index
51
+ ? {
52
+ ...row,
53
+ [field]: value,
54
+ errors: { ...row.errors, [field]: undefined },
55
+ }
56
+ : row,
57
+ ),
75
58
  );
76
59
  }, []);
77
60
 
61
+ const removeRow = useCallback((index: number) => {
62
+ setRows((prev) => (prev.length <= 1 ? prev : prev.filter((_, i) => i !== index)));
63
+ }, []);
64
+
78
65
  const addRow = useCallback(() => {
79
66
  setRows((prev) => [...prev, emptyRow()]);
80
67
  // Focus the email input of the new row after render
@@ -164,9 +151,7 @@ export function BulkInviteForm({
164
151
  />
165
152
  ) : (
166
153
  <Field label="Group Membership">
167
- <p className="text-sm text-slate-400">
168
- No groups available in this scope.
169
- </p>
154
+ <p className="text-sm text-slate-400">No groups available in this scope.</p>
170
155
  </Field>
171
156
  )}
172
157
  <div className="flex items-center gap-2">
@@ -179,9 +164,7 @@ export function BulkInviteForm({
179
164
  </div>
180
165
  </div>
181
166
 
182
- {formError && (
183
- <p className="text-sm text-rose-600 mb-4">{formError}</p>
184
- )}
167
+ {formError && <p className="text-sm text-rose-600 mb-4">{formError}</p>}
185
168
 
186
169
  <table ref={tableRef} className="w-full border-collapse">
187
170
  <thead>
@@ -8,10 +8,7 @@ import { addUserToGroup, createGroup } from "~/.server/auth/keycloakAdmin";
8
8
  import { KeycloakAdminError } from "~/.server/auth/keycloakAdmin/client";
9
9
  import { sessionStorage } from "~/.server/auth/sessionStorage";
10
10
 
11
- export const createGroupAction: ActionFunction = async ({
12
- request,
13
- context,
14
- }) => {
11
+ export const createGroupAction: ActionFunction = async ({ request, context }) => {
15
12
  const { user } = context.get(authContext);
16
13
  const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
17
14
 
@@ -25,10 +22,7 @@ export const createGroupAction: ActionFunction = async ({
25
22
  const session = await getSession(request);
26
23
 
27
24
  try {
28
- const { path, adminsGroupId } = await createGroup(
29
- scope,
30
- result.data.name,
31
- );
25
+ const { path, adminsGroupId } = await createGroup(scope, result.data.name);
32
26
 
33
27
  await addUserToGroup(user.sub, adminsGroupId);
34
28
 
@@ -45,8 +39,7 @@ export const createGroupAction: ActionFunction = async ({
45
39
  } catch (e) {
46
40
  console.error("Create group failed:", e);
47
41
 
48
- const status =
49
- e instanceof KeycloakAdminError ? e.status : undefined;
42
+ const status = e instanceof KeycloakAdminError ? e.status : undefined;
50
43
  const message =
51
44
  status === 409
52
45
  ? `A group named "${result.data.name}" already exists in this group.`
@@ -3,10 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
3
3
  import { Controller, useForm, useWatch } from "react-hook-form";
4
4
  import { useSubmit } from "react-router";
5
5
 
6
- import {
7
- type CreateGroupFormData,
8
- createGroupSchema,
9
- } from "./createGroup.schema";
6
+ import { type CreateGroupFormData, createGroupSchema } from "./createGroup.schema";
10
7
  import { ScopePill } from "~/components/Pills/ScopePill";
11
8
 
12
9
  interface CreateGroupFormProps {
@@ -35,11 +32,7 @@ export function CreateGroupForm({ scope }: CreateGroupFormProps) {
35
32
  };
36
33
 
37
34
  return (
38
- <form
39
- id="create-group-form"
40
- onSubmit={handleSubmit(onSubmit)}
41
- className="space-y-4"
42
- >
35
+ <form id="create-group-form" onSubmit={handleSubmit(onSubmit)} className="space-y-4">
43
36
  <Fieldset>
44
37
  <Field label="Group name" error={errors.name}>
45
38
  <Controller
@@ -20,11 +20,7 @@ export default function CreateGroupModal() {
20
20
  <Button onPress={() => navigate(-1)} variant="secondary">
21
21
  Cancel
22
22
  </Button>
23
- <Button
24
- type="submit"
25
- form="create-group-form"
26
- isDisabled={isSubmitting}
27
- >
23
+ <Button type="submit" form="create-group-form" isDisabled={isSubmitting}>
28
24
  {isSubmitting ? "Creating..." : "Create Group"}
29
25
  </Button>
30
26
  </footer>
@@ -8,10 +8,7 @@ import { getSession } from "~/.server/auth/getSession";
8
8
  import { inviteUser } from "~/.server/auth/keycloakAdmin/users";
9
9
  import { sessionStorage } from "~/.server/auth/sessionStorage";
10
10
 
11
- export const inviteUserAction: ActionFunction = async ({
12
- request,
13
- context,
14
- }) => {
11
+ export const inviteUserAction: ActionFunction = async ({ request, context }) => {
15
12
  const { user } = context.get(authContext);
16
13
  const { adminUrl, scope } = assertAdminScope(request.url, user.adminScopes);
17
14
 
@@ -63,11 +63,7 @@ export function InviteUserForm({
63
63
  };
64
64
 
65
65
  return (
66
- <form
67
- id="invite-form"
68
- onSubmit={handleSubmit(onSubmit)}
69
- className="space-y-4"
70
- >
66
+ <form id="invite-form" onSubmit={handleSubmit(onSubmit)} className="space-y-4">
71
67
  <Fieldset>
72
68
  <Field label="Email" error={errors.email}>
73
69
  <Controller
@@ -128,9 +124,7 @@ export function InviteUserForm({
128
124
  />
129
125
  ) : (
130
126
  <Field label="Group Membership">
131
- <p className="text-sm text-slate-400">
132
- No groups available in this scope.
133
- </p>
127
+ <p className="text-sm text-slate-400">No groups available in this scope.</p>
134
128
  </Field>
135
129
  )}
136
130
  <div className="flex items-center gap-2">
@@ -2,10 +2,7 @@ import { type LoaderFunction } from "react-router";
2
2
 
3
3
  import { assertAdminScope } from "../assertAdminScope";
4
4
  import { authContext } from "~/.server/auth/authMiddleware";
5
- import {
6
- getGroupWithMembers,
7
- GroupWithMembers,
8
- } from "~/.server/auth/keycloakAdmin/groups";
5
+ import { getGroupWithMembers, GroupWithMembers } from "~/.server/auth/keycloakAdmin/groups";
9
6
 
10
7
  function flattenGroupPaths(group: GroupWithMembers): string[] {
11
8
  return [group.path, ...group.subGroups.flatMap(flattenGroupPaths)];