@manta-eu/react 0.1.10 → 0.2.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.
Files changed (100) hide show
  1. package/dist/components/chat-view/ChatTurnErrorNotice.d.ts.map +1 -1
  2. package/dist/components/chat-view/ChatTurnErrorNotice.js +3 -1
  3. package/dist/components/chat-view/ChatTurnErrorNotice.js.map +1 -1
  4. package/dist/components/layouts/ConnectedCustomApp.d.ts +13 -0
  5. package/dist/components/layouts/ConnectedCustomApp.d.ts.map +1 -0
  6. package/dist/components/layouts/ConnectedCustomApp.js +37 -0
  7. package/dist/components/layouts/ConnectedCustomApp.js.map +1 -0
  8. package/dist/components/layouts/Sidebar.d.ts.map +1 -1
  9. package/dist/components/layouts/Sidebar.js +6 -3
  10. package/dist/components/layouts/Sidebar.js.map +1 -1
  11. package/dist/components/table-skeleton/index.d.ts +2 -0
  12. package/dist/components/table-skeleton/index.d.ts.map +1 -1
  13. package/dist/components/table-skeleton/index.js +2 -1
  14. package/dist/components/table-skeleton/index.js.map +1 -1
  15. package/dist/graph/createNodeRouter.js +3 -3
  16. package/dist/graph/createNodeRouter.js.map +1 -1
  17. package/dist/graph/nodes/BrandFontSuggestionView.d.ts +3 -0
  18. package/dist/graph/nodes/BrandFontSuggestionView.d.ts.map +1 -0
  19. package/dist/graph/nodes/{BrandFontFamilyView.js → BrandFontSuggestionView.js} +21 -13
  20. package/dist/graph/nodes/BrandFontSuggestionView.js.map +1 -0
  21. package/dist/graph/nodes/brand-font.d.ts +8 -0
  22. package/dist/graph/nodes/brand-font.d.ts.map +1 -1
  23. package/dist/graph/nodes/brand-font.js +6 -0
  24. package/dist/graph/nodes/brand-font.js.map +1 -1
  25. package/dist/graph/nodes/brand-shared.d.ts +0 -3
  26. package/dist/graph/nodes/brand-shared.d.ts.map +1 -1
  27. package/dist/graph/nodes/brand-shared.js +1 -4
  28. package/dist/graph/nodes/brand-shared.js.map +1 -1
  29. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  30. package/dist/hooks/useAuth.d.ts +2 -2
  31. package/dist/hooks/useAuth.d.ts.map +1 -1
  32. package/dist/hooks/useAuth.js +2 -2
  33. package/dist/hooks/useAuth.js.map +1 -1
  34. package/dist/hooks/useOnboardingState.js +1 -1
  35. package/dist/hooks/useOnboardingState.js.map +1 -1
  36. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  37. package/dist/lib/datastore-route-table.d.ts +2 -1
  38. package/dist/lib/datastore-route-table.d.ts.map +1 -1
  39. package/dist/lib/datastore-route-table.js +2 -1
  40. package/dist/lib/datastore-route-table.js.map +1 -1
  41. package/dist/providers/AuthContext.d.ts +2 -1
  42. package/dist/providers/AuthContext.d.ts.map +1 -1
  43. package/dist/providers/AuthContext.js.map +1 -1
  44. package/dist/providers/ChatManagerProvider.d.ts.map +1 -1
  45. package/dist/providers/ChatManagerProvider.js +19 -8
  46. package/dist/providers/ChatManagerProvider.js.map +1 -1
  47. package/dist/routes/data-explorer/declared-namespaces.d.ts +3 -0
  48. package/dist/routes/data-explorer/declared-namespaces.d.ts.map +1 -1
  49. package/dist/routes/data-explorer/declared-namespaces.js +21 -13
  50. package/dist/routes/data-explorer/declared-namespaces.js.map +1 -1
  51. package/dist/routes/data-explorer/namespaces.d.ts +1 -0
  52. package/dist/routes/data-explorer/namespaces.d.ts.map +1 -1
  53. package/dist/routes/data-explorer/namespaces.js +7 -6
  54. package/dist/routes/data-explorer/namespaces.js.map +1 -1
  55. package/dist/routes/datastores/components/DeleteDatastoreDialog.d.ts +10 -0
  56. package/dist/routes/datastores/components/DeleteDatastoreDialog.d.ts.map +1 -0
  57. package/dist/routes/datastores/components/DeleteDatastoreDialog.js +20 -0
  58. package/dist/routes/datastores/components/DeleteDatastoreDialog.js.map +1 -0
  59. package/dist/routes/datastores/flatten.d.ts +1 -0
  60. package/dist/routes/datastores/flatten.d.ts.map +1 -1
  61. package/dist/routes/datastores/flatten.js +17 -0
  62. package/dist/routes/datastores/flatten.js.map +1 -1
  63. package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts +3 -1
  64. package/dist/routes/datastores/hooks/useDatastoreColumns.d.ts.map +1 -1
  65. package/dist/routes/datastores/hooks/useDatastoreColumns.js +14 -6
  66. package/dist/routes/datastores/hooks/useDatastoreColumns.js.map +1 -1
  67. package/dist/routes/datastores/index.d.ts.map +1 -1
  68. package/dist/routes/datastores/index.js +31 -5
  69. package/dist/routes/datastores/index.js.map +1 -1
  70. package/dist/routes/integrations/components/ProviderGroup.js +2 -2
  71. package/dist/routes/integrations/components/ProviderGroup.js.map +1 -1
  72. package/package.json +7 -7
  73. package/src/components/chat-view/ChatTurnErrorNotice.tsx +3 -1
  74. package/src/components/layouts/ConnectedCustomApp.tsx +63 -0
  75. package/src/components/layouts/Sidebar.tsx +9 -3
  76. package/src/components/table-skeleton/index.tsx +7 -1
  77. package/src/graph/createNodeRouter.tsx +3 -3
  78. package/src/graph/nodes/{BrandFontFamilyView.tsx → BrandFontSuggestionView.tsx} +39 -31
  79. package/src/graph/nodes/brand-font.ts +17 -0
  80. package/src/graph/nodes/brand-shared.tsx +1 -4
  81. package/src/hooks/useAuth.ts +3 -3
  82. package/src/hooks/useOnboardingState.ts +1 -1
  83. package/src/lib/datastore-route-table.ts +4 -2
  84. package/src/providers/AuthContext.ts +3 -1
  85. package/src/providers/ChatManagerProvider.tsx +16 -8
  86. package/src/routes/data-explorer/declared-namespaces.ts +21 -13
  87. package/src/routes/data-explorer/namespaces.ts +7 -6
  88. package/src/routes/datastores/components/DeleteDatastoreDialog.tsx +78 -0
  89. package/src/routes/datastores/flatten.ts +15 -0
  90. package/src/routes/datastores/hooks/useDatastoreColumns.tsx +88 -49
  91. package/src/routes/datastores/index.tsx +41 -2
  92. package/src/routes/integrations/components/ProviderGroup.tsx +2 -2
  93. package/dist/graph/nodes/BrandFontFamilyView.d.ts +0 -3
  94. package/dist/graph/nodes/BrandFontFamilyView.d.ts.map +0 -1
  95. package/dist/graph/nodes/BrandFontFamilyView.js.map +0 -1
  96. package/dist/graph/nodes/BrandFontUploadButton.d.ts +0 -6
  97. package/dist/graph/nodes/BrandFontUploadButton.d.ts.map +0 -1
  98. package/dist/graph/nodes/BrandFontUploadButton.js +0 -69
  99. package/dist/graph/nodes/BrandFontUploadButton.js.map +0 -1
  100. package/src/graph/nodes/BrandFontUploadButton.tsx +0 -95
@@ -9,14 +9,19 @@ export type TableSkeletonProps = {
9
9
  renderRow: (rowIndex: number) => ReactNode;
10
10
  withWrapper?: boolean;
11
11
  className?: string;
12
+ headerClassName?: string;
13
+ rowClassName?: string;
12
14
  };
13
15
 
14
16
  export function TableSkeleton(props: TableSkeletonProps) {
15
17
  const { gridColsClassName, headerColumnCount, rows = 8, renderRow, withWrapper = true, className } = props;
18
+ const { headerClassName, rowClassName } = props;
16
19
 
17
20
  const content = (
18
21
  <>
19
- <div className={cn("grid gap-3 border-b border-muted-border bg-faint px-4 py-3", gridColsClassName)}>
22
+ <div
23
+ className={cn("grid gap-3 border-b border-muted-border bg-faint px-4 py-3", gridColsClassName, headerClassName)}
24
+ >
20
25
  {Array.from({ length: headerColumnCount }, (_, index) => (
21
26
  <Skeleton key={`table-skeleton-header-${index}`} className="h-4 w-2/3 rounded-md" />
22
27
  ))}
@@ -27,6 +32,7 @@ export function TableSkeleton(props: TableSkeletonProps) {
27
32
  className={cn(
28
33
  "grid items-center gap-3 border-b border-muted-border px-4 py-4 last:border-b-0",
29
34
  gridColsClassName,
35
+ rowClassName,
30
36
  )}
31
37
  >
32
38
  {renderRow(rowIndex)}
@@ -12,7 +12,7 @@ import { AttioNoteView } from "./nodes/AttioNoteView";
12
12
  import { AttioTaskView } from "./nodes/AttioTaskView";
13
13
  import { BrandAssetAnalysisActionView } from "./nodes/BrandAssetAnalysisActionView";
14
14
  import { BrandAssetAnalysisView } from "./nodes/BrandAssetAnalysisView";
15
- import { BrandFontFamilyView } from "./nodes/BrandFontFamilyView";
15
+ import { BrandFontSuggestionView } from "./nodes/BrandFontSuggestionView";
16
16
  import { BrandGuidelinesView } from "./nodes/BrandGuidelinesView";
17
17
  import { CoresignalCompanyView } from "./nodes/CoresignalCompanyView";
18
18
  import { CoresignalEmployeeView } from "./nodes/CoresignalEmployeeView";
@@ -123,7 +123,7 @@ const typedNodeEntries: ReadonlyArray<readonly [IRI, EntityView]> = [
123
123
  // Asserted by a Datalog rule, not by a node shape, so codegen emits no class term for it.
124
124
  [expandPrefix("brand:AssetAnalysis"), BrandAssetAnalysisView],
125
125
  [brand.BrandGuidelines, BrandGuidelinesView],
126
- [brand.FontFamily, BrandFontFamilyView],
126
+ [brand.FontSuggestion, BrandFontSuggestionView],
127
127
  ];
128
128
 
129
129
  export const typedEntityTypeIris: ReadonlySet<IRI> = new Set(typedNodeEntries.map(([type]) => type as IRI));
@@ -135,7 +135,7 @@ export const diffSubsumingViewIris: ReadonlySet<IRI> = new Set([
135
135
  expandPrefix("brand:AssetAnalysisShape"),
136
136
  expandPrefix("brand:AssetAnalysis"),
137
137
  brand.BrandGuidelines,
138
- brand.FontFamily,
138
+ brand.FontSuggestion,
139
139
  ]);
140
140
 
141
141
  export const typedActionShapeIris: ReadonlySet<IRI> = new Set(actionShapeNodeEntries.map(([iri]) => iri));
@@ -1,4 +1,4 @@
1
- import { BrandFontFileSchema } from "@manta-eu/ldo";
1
+ import { BrandFontFileSchema, BrandSuggestedWeightSchema } from "@manta-eu/ldo";
2
2
  import { redirectPath } from "@manta-eu/ldo/redirect-queries";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
4
  import { namespaces } from "@manta-eu/rdf-lib/namespaces";
@@ -8,11 +8,13 @@ import type { EntityViewProps } from "../types";
8
8
  import {
9
9
  fontFaceDescriptors,
10
10
  preferredFontFile,
11
+ sortedWeights,
11
12
  specimenWeight,
13
+ weightLabel,
12
14
  type StoredFontFile,
15
+ type SuggestedWeight,
13
16
  } from "./brand-font";
14
17
  import { BrandNodeBody, BrandStatusBadge } from "./brand-shared";
15
- import { BrandFontUploadButton } from "./BrandFontUploadButton";
16
18
  import { EntityCardShell } from "./EntityCardShell";
17
19
  import { NodePropertyTable } from "./NodePropertyTable";
18
20
  import { readBrandString, toIriArray } from "./typed-node-helpers";
@@ -20,11 +22,26 @@ import { useEntitiesByIris } from "#hooks/use-entity";
20
22
  import { useFontFace } from "#hooks/use-font-face";
21
23
  import { useRedirectEndpoint } from "#hooks/use-redirect-endpoint";
22
24
 
23
- const statusPredicate = `${namespaces.brand}entryStatus`;
24
- const availabilityPredicate = `${namespaces.brand}fontAvailability`;
25
- const fontFilePredicate = `${namespaces.brand}fontFile`;
25
+ const weightPredicate = `${namespaces.brand}hasSuggestedWeight`;
26
26
  const specimenText = "Aa Bb Cc Qq Rr 0123456789 — The quick brown fox jumps over the lazy dog";
27
27
 
28
+ function useSuggestedWeights(iris: string[]): SuggestedWeight[] {
29
+ const { data } = useEntitiesByIris({ schema: BrandSuggestedWeightSchema, iris });
30
+
31
+ return (data ?? []).flatMap((weight) =>
32
+ weight
33
+ ? [
34
+ {
35
+ weight: weight.fontWeight,
36
+ style: weight.fontStyle,
37
+ availability: weight.fontAvailability,
38
+ coveringFiles: weight.coveringFile ?? [],
39
+ },
40
+ ]
41
+ : [],
42
+ );
43
+ }
44
+
28
45
  function useStoredFontFiles(iris: string[]): StoredFontFile[] {
29
46
  const { data } = useEntitiesByIris({ schema: BrandFontFileSchema, iris });
30
47
 
@@ -69,48 +86,39 @@ function FontSpecimen(props: FontSpecimenProps) {
69
86
  );
70
87
  }
71
88
 
72
- export const BrandFontFamilyView = memo(function BrandFontFamilyView(props: EntityViewProps) {
89
+ export const BrandFontSuggestionView = memo(function BrandFontSuggestionView(props: EntityViewProps) {
73
90
  const { data, selected = false, ringColor, onContextMenu } = props;
74
91
 
75
92
  const family = readBrandString(data.jsonLd, "family") ?? data.label;
76
- const status = readBrandString(data.jsonLd, "entryStatus");
77
- const availability = readBrandString(data.jsonLd, "fontAvailability");
78
- const fileIris = toIriArray(data.jsonLd[fontFilePredicate] ?? data.jsonLd["brand:fontFile"]);
79
- const file = preferredFontFile(useStoredFontFiles(fileIris));
93
+ const weights = sortedWeights(
94
+ useSuggestedWeights(toIriArray(data.jsonLd[weightPredicate] ?? data.jsonLd["brand:hasSuggestedWeight"])),
95
+ );
96
+ const file = preferredFontFile(useStoredFontFiles([...new Set(weights.flatMap((weight) => weight.coveringFiles))]));
80
97
 
81
98
  return (
82
99
  <EntityCardShell
83
100
  icon={Type}
84
- label={`Font Family: ${family}`}
101
+ label={`Font Suggestion: ${family}`}
85
102
  labelMaxWidth="max-w-140"
86
103
  selected={selected}
87
104
  ringColor={ringColor}
88
105
  onContextMenu={onContextMenu}
89
106
  >
90
- <BrandNodeBody
91
- right={
92
- <div className="flex items-center gap-1">
93
- <BrandStatusBadge status={availability} />
94
- <BrandStatusBadge status={status} />
95
- </div>
96
- }
97
- >
107
+ <BrandNodeBody>
98
108
  {isPresent(file) && <FontSpecimen file={file} />}
99
109
 
100
- {isPresent(availability) && availability !== "available" && (
101
- <div className="flex items-center justify-between gap-2">
102
- {availability === "needs-upload" && (
103
- <span className="typography-tiny text-muted-foreground">No font file found for this family.</span>
104
- )}
105
- <BrandFontUploadButton family={family} />
106
- </div>
110
+ {isPresent(weights) && (
111
+ <ul className="flex flex-col gap-1">
112
+ {weights.map((weight) => (
113
+ <li key={weightLabel(weight)} className="flex items-center justify-between gap-2">
114
+ <span className="typography-tiny text-default-foreground">{weightLabel(weight)}</span>
115
+ <BrandStatusBadge status={weight.availability} />
116
+ </li>
117
+ ))}
118
+ </ul>
107
119
  )}
108
120
 
109
- <NodePropertyTable
110
- properties={data.properties}
111
- jsonLd={data.jsonLd}
112
- omitPredicates={[statusPredicate, availabilityPredicate]}
113
- />
121
+ <NodePropertyTable properties={data.properties} jsonLd={data.jsonLd} omitPredicates={[weightPredicate]} />
114
122
  </BrandNodeBody>
115
123
  </EntityCardShell>
116
124
  );
@@ -27,3 +27,20 @@ export function fontFaceDescriptors(file: StoredFontFile): FontFaceDescriptors {
27
27
  export function specimenWeight(file: StoredFontFile): number {
28
28
  return Math.min(Math.max(400, file.weightMin), file.weightMax);
29
29
  }
30
+
31
+ export type SuggestedWeight = {
32
+ weight: number;
33
+ style: "normal" | "italic";
34
+ availability: "available" | "looking-up" | "needs-upload";
35
+ coveringFiles: readonly string[];
36
+ };
37
+
38
+ export function sortedWeights<W extends Pick<SuggestedWeight, "weight" | "style">>(weights: readonly W[]): W[] {
39
+ return [...weights].sort(
40
+ (a, b) => a.weight - b.weight || Number(a.style === "italic") - Number(b.style === "italic"),
41
+ );
42
+ }
43
+
44
+ export function weightLabel(weight: Pick<SuggestedWeight, "weight" | "style">): string {
45
+ return weight.style === "italic" ? `${weight.weight} italic` : `${weight.weight}`;
46
+ }
@@ -11,14 +11,11 @@ import { cn } from "#style";
11
11
 
12
12
  export const brandAmber = "#d97706";
13
13
 
14
- // Every `sh:in` value on the brand status paths in kgc/shacl/shapes/brand.ttl; an unlisted one means a shape moved on.
14
+ // Every `sh:in` value of brand:fontAvailability in kgc/shacl/shapes/brand-font-suggestion.ttl.
15
15
  const toneByStatus = {
16
- accepted: "positive",
17
16
  available: "positive",
18
- draft: "neutral",
19
17
  "looking-up": "neutral",
20
18
  "needs-upload": "caution",
21
- rejected: "negative",
22
19
  } as const satisfies Record<string, BadgeTone>;
23
20
 
24
21
  export type BrandStatus = keyof typeof toneByStatus;
@@ -26,7 +26,7 @@ export type UseAuthResult = {
26
26
  isAuthenticated: boolean;
27
27
  isOperator: boolean;
28
28
  canManageTeam: boolean;
29
- canManageExternalApps: boolean;
29
+ canManageCustomApps: boolean;
30
30
  orgRoles: OrgRoleDefinition[];
31
31
  canManageIntegrations: boolean;
32
32
  canManageStreams: boolean;
@@ -79,7 +79,7 @@ export const contextResponseSchema = z.object({
79
79
  canManageStreams: z.boolean().optional().default(false),
80
80
  canManageSharedMemories: z.boolean().optional().default(false),
81
81
  canManageTeam: z.boolean().optional().default(false),
82
- canManageExternalApps: z.boolean().optional().default(false),
82
+ canManageCustomApps: z.boolean().optional().default(false),
83
83
  datastore: datastoreSchema.nullish(),
84
84
  }),
85
85
  datastores: z.array(datastoreSchema),
@@ -297,7 +297,7 @@ export function useAuth(): UseAuthResult {
297
297
  datastoreAccessGranted: contextData?.datastoreAccessGranted ?? true,
298
298
  isOperator: contextData?.user?.isOperator ?? false,
299
299
  canManageTeam: contextData?.user?.canManageTeam ?? false,
300
- canManageExternalApps: contextData?.user?.canManageExternalApps ?? false,
300
+ canManageCustomApps: contextData?.user?.canManageCustomApps ?? false,
301
301
  orgRoles: contextData?.orgRoles ?? [],
302
302
  canManageIntegrations: contextData?.user?.canManageIntegrations ?? false,
303
303
  canManageStreams: contextData?.user?.canManageStreams ?? false,
@@ -1,7 +1,7 @@
1
+ import { onboardingCompletedCookieName as onboardingCookieName } from "@manta-eu/client/shared/constants";
1
2
  import Cookies from "js-cookie";
2
3
  import { useCallback, useState } from "react";
3
4
 
4
- const onboardingCookieName = "manta-onboarding-completed";
5
5
  const onboardingCookieExpiryDays = 365;
6
6
 
7
7
  type OnboardingState = {
@@ -17,7 +17,8 @@ export type RouteElements = {
17
17
  Integrations?: ReactNode;
18
18
  ShopifyConnect?: ReactNode;
19
19
  Team?: ReactNode;
20
- ExternalApps?: ReactNode;
20
+ CustomApps?: ReactNode;
21
+ CustomAppUsers?: ReactNode;
21
22
  DataExplorerLayout?: ReactNode;
22
23
  DataExplorerIndex?: ReactNode;
23
24
  DataExplorerRecent?: ReactNode;
@@ -45,7 +46,8 @@ export const globalRoutes: RouteDefinition[] = [
45
46
  { path: "/datastores", render: (e) => e.Datastores || notFoundElement(e) },
46
47
  // Global, not datastore-scoped: Shopify sends the merchant here before any datastore is known.
47
48
  { path: "/shopify/connect", render: (e) => e.ShopifyConnect || notFoundElement(e) },
48
- { path: "/custom-apps", render: (e) => e.ExternalApps || notFoundElement(e) },
49
+ { path: "/custom-apps", render: (e) => e.CustomApps || notFoundElement(e) },
50
+ { path: "/custom-apps/:clientId/users", render: (e) => e.CustomAppUsers || notFoundElement(e) },
49
51
  ];
50
52
 
51
53
  export const datastoreRoutes: RouteDefinition[] = [
@@ -9,7 +9,7 @@ export type AuthContextValue = Pick<
9
9
  | "logout"
10
10
  | "isOperator"
11
11
  | "canManageTeam"
12
- | "canManageExternalApps"
12
+ | "canManageCustomApps"
13
13
  | "orgRoles"
14
14
  | "canManageIntegrations"
15
15
  | "canManageStreams"
@@ -18,6 +18,8 @@ export type AuthContextValue = Pick<
18
18
  | "allowedModels"
19
19
  > & {
20
20
  user: User;
21
+ // Only Manta's own web app may list the apps connected to someone; any other host's token is refused.
22
+ canDisconnectCustomApps?: boolean;
21
23
  };
22
24
 
23
25
  export const AuthContext = createContext<AuthContextValue | undefined>(undefined);
@@ -80,6 +80,7 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
80
80
  const conversationSelectionRef = useRef(conversationSelection);
81
81
  const pathnameRef = useRef(location.pathname);
82
82
  const routedConversationIdRef = useRef(routedConversationId);
83
+ const pendingPlanNavigationRef = useRef<{ pathname: string } | undefined>(undefined);
83
84
 
84
85
  // Where navigate() was last sent, which react-router's deferred location has not reached yet.
85
86
  const requestedPathRef = useRef(location.pathname);
@@ -351,7 +352,11 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
351
352
  );
352
353
 
353
354
  const selectContext = useCallback(
354
- (context: ChatContext): Promise<void> => transitionContext({ kind: "select", context }).then(() => undefined),
355
+ (context: ChatContext): Promise<void> => {
356
+ // The current page may synchronize its old context while an explicit plan selection is waiting.
357
+ if (pendingPlanNavigationRef.current?.pathname === pathnameRef.current) return Promise.resolve();
358
+ return transitionContext({ kind: "select", context }).then(() => undefined);
359
+ },
355
360
  [transitionContext],
356
361
  );
357
362
 
@@ -390,17 +395,20 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
390
395
 
391
396
  const context = { kind: ChatContextKind.Plan, planGraphIri: planIri } as const;
392
397
  const source = activeConversationFromSelection(conversationSelectionRef.current);
393
- if (source) {
394
- try {
398
+ const navigation = { pathname: pathnameRef.current };
399
+ pendingPlanNavigationRef.current = navigation;
400
+ try {
401
+ if (source) {
395
402
  const transitioned = await reassignCurrentConversation(context);
396
403
  if (!transitioned) return;
397
- } catch {
398
- toast.error("Couldn't move this chat to the plan. Please try again.");
399
- return;
400
404
  }
401
- }
402
405
 
403
- void navigate(buildPlanHref(planIri), { state: { fromChat: true } });
406
+ void navigate(buildPlanHref(planIri), { state: { fromChat: true } });
407
+ } catch {
408
+ toast.error("Couldn't move this chat to the plan. Please try again.");
409
+ } finally {
410
+ if (pendingPlanNavigationRef.current === navigation) pendingPlanNavigationRef.current = undefined;
411
+ }
404
412
  },
405
413
  [navigate, onSelectPlan, reassignCurrentConversation],
406
414
  );
@@ -1,22 +1,29 @@
1
- import { ontologyGraphIri } from "@manta-eu/client/shared/rdf-namespaces";
1
+ import { expandPrefix, ontologyGraphIri } from "@manta-eu/client/shared/rdf-namespaces";
2
2
  import { isPresent } from "@manta-eu/lib/present";
3
3
  import { DataFactory, type RDF } from "ldkit/rdf";
4
4
  import { SELECT } from "ldkit/sparql";
5
5
  import { useMemo } from "react";
6
- import { prefixes, registerDeclaredNamespaces, type DeclaredNamespace } from "./namespaces";
6
+ import { prefixes, realmPath, registerDeclaredNamespaces, type DeclaredNamespace } from "./namespaces";
7
7
  import { bindingValue, useSparqlSelect } from "#ldo-ui";
8
8
 
9
9
  const df = new DataFactory();
10
10
 
11
- const declaredNamespacesQuery = `${prefixes}${SELECT`?prefix ?namespace ?source ?title ?sourceLabel`.WHERE`
12
- GRAPH ${df.namedNode(ontologyGraphIri)} {
13
- [] sh:declare ?declaration .
14
- ?declaration sh:prefix ?prefix ; sh:namespace ?namespace .
15
- OPTIONAL {
16
- ?declaration rdfs:isDefinedBy ?source .
17
- OPTIONAL { ?source dcterms:title ?title }
18
- OPTIONAL { ?source rdfs:label ?sourceLabel }
11
+ const declaredNamespacesQuery = `${prefixes}${SELECT`?prefix ?namespace ?source ?title ?sourceLabel ?connectionName`
12
+ .WHERE`
13
+ {
14
+ GRAPH ${df.namedNode(ontologyGraphIri)} {
15
+ [] sh:declare ?declaration .
16
+ ?declaration sh:prefix ?prefix ; sh:namespace ?namespace .
17
+ OPTIONAL {
18
+ ?declaration rdfs:isDefinedBy ?source .
19
+ OPTIONAL { ?source dcterms:title ?title }
20
+ OPTIONAL { ?source rdfs:label ?sourceLabel }
21
+ }
19
22
  }
23
+ } UNION {
24
+ ?declaration a sh:PrefixDeclaration ; sh:namespace ?namespace ; schema:identifier ?id .
25
+ ?connection a ${df.namedNode(expandPrefix(":DataConnection"))} ; schema:identifier ?id ; schema:name ?connectionName .
26
+ FILTER NOT EXISTS { ?connection ${df.namedNode(expandPrefix(":deletedAt"))} ?deleted }
20
27
  }
21
28
  `.build()}`;
22
29
 
@@ -28,15 +35,16 @@ function sourceFrom(binding: RDF.Bindings) {
28
35
  return { iri, label: bindingValue(binding, "title") ?? bindingValue(binding, "sourceLabel") ?? iri };
29
36
  }
30
37
 
31
- function declaredNamespacesFrom(bindings: RDF.Bindings[]): Record<string, DeclaredNamespace> {
38
+ export function declaredNamespacesFrom(bindings: RDF.Bindings[]): Record<string, DeclaredNamespace> {
32
39
  return Object.fromEntries(
33
40
  bindings.flatMap<[string, DeclaredNamespace]>((binding) => {
34
- const prefix = bindingValue(binding, "prefix");
35
41
  const iri = bindingValue(binding, "namespace");
42
+ const connectionName = bindingValue(binding, "connectionName");
43
+ const prefix = isPresent(connectionName) && isPresent(iri) ? realmPath(iri) : bindingValue(binding, "prefix");
36
44
 
37
45
  if (!isPresent(prefix) || !isPresent(iri)) return [];
38
46
 
39
- return [[prefix, { iri, label: prefix, source: sourceFrom(binding) }]];
47
+ return [[prefix, { iri, label: connectionName ?? prefix, source: sourceFrom(binding) }]];
40
48
  }),
41
49
  );
42
50
  }
@@ -78,12 +78,13 @@ export function prefixForIri(iri: string): string | undefined {
78
78
 
79
79
  if (registered) return registered;
80
80
 
81
- if (!iri.startsWith(realmVocabRoot)) return undefined;
81
+ return realmPath(iri);
82
+ }
82
83
 
83
- const rest = iri.slice(realmVocabRoot.length);
84
- const segment = rest.split(/[#/]/)[0];
84
+ export function realmPath(iri: string): string | undefined {
85
+ if (!iri.startsWith(realmVocabRoot) || !iri.includes("#")) return undefined;
85
86
 
86
- return rest.startsWith(`${segment}#`) ? segment || undefined : undefined;
87
+ return iri.slice(realmVocabRoot.length).split("#")[0] || undefined;
87
88
  }
88
89
 
89
90
  export function localName(iri: string, prefix: string): string {
@@ -118,11 +119,11 @@ export function groupCrumb(prefix: string): { label: string; to: string } {
118
119
  }
119
120
 
120
121
  export function classPath(prefix: string, name: string): string {
121
- return `${explorerPath}/${prefix}/${name}`;
122
+ return `${namespacePath(prefix)}/${name}`;
122
123
  }
123
124
 
124
125
  export function recordPath(prefix: string, type: string, id: string): string {
125
- return `${explorerPath}/${prefix}/${type}/${encodeURIComponent(id)}`;
126
+ return `${classPath(prefix, type)}/${encodeURIComponent(id)}`;
126
127
  }
127
128
 
128
129
  export type ClassRef = { iri: string; prefix: string; localName: string; path: string };
@@ -0,0 +1,78 @@
1
+ import { useState } from "react";
2
+ import type { DatastoreRow } from "../flatten";
3
+ import { Button } from "#components/button/index";
4
+ import { Input } from "#components/input/index";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "#components/modal/index";
13
+
14
+ type DeleteDatastoreDialogProps = {
15
+ target: DatastoreRow;
16
+ isDeleting: boolean;
17
+ onConfirm: (confirmName: string) => void;
18
+ onCancel: () => void;
19
+ };
20
+
21
+ export function DeleteDatastoreDialog(props: DeleteDatastoreDialogProps) {
22
+ const { target, isDeleting, onConfirm, onCancel } = props;
23
+ const [typed, setTyped] = useState("");
24
+
25
+ const canDelete = typed.trim() === target.name.trim() && !isDeleting;
26
+ const deletesOrganization = target.isOrganization && target.mirrored !== true;
27
+
28
+ function confirm() {
29
+ if (canDelete) onConfirm(target.name);
30
+ }
31
+
32
+ return (
33
+ <Dialog open onOpenChange={(nextOpen) => !nextOpen && !isDeleting && onCancel()}>
34
+ <DialogContent className="flex flex-col overflow-hidden" showCloseButton={!isDeleting}>
35
+ <DialogHeader>
36
+ <DialogTitle
37
+ text={deletesOrganization ? "Delete organization permanently" : "Delete datastore permanently"}
38
+ />
39
+ <DialogDescription>This can't be undone. Deleting &quot;{target.name}&quot; removes:</DialogDescription>
40
+ </DialogHeader>
41
+
42
+ <ul className="list-disc pl-5 typography-small text-muted-foreground">
43
+ <li>its knowledge graph and everything in it</li>
44
+ <li>its connected integrations and their secrets</li>
45
+ <li>its uploaded files and chats</li>
46
+ {deletesOrganization && <li>the organization, its groups and memberships</li>}
47
+ {!target.isOrganization && !target.isUnlinked && <li>the group and its memberships</li>}
48
+ </ul>
49
+ {target.isOrganization && !deletesOrganization && (
50
+ <p className="typography-small text-muted-foreground">
51
+ The organization itself stays, because the identity provider holds it. It is listed as having no datastore
52
+ afterwards.
53
+ </p>
54
+ )}
55
+
56
+ <Input
57
+ autoFocus
58
+ label={`Type "${target.name}" to confirm`}
59
+ value={typed}
60
+ disabled={isDeleting}
61
+ onChange={(event) => setTyped(event.target.value)}
62
+ onKeyDown={(event) => {
63
+ if (event.key === "Enter") confirm();
64
+ }}
65
+ />
66
+
67
+ <DialogFooter>
68
+ <Button variant="tertiary" onClick={onCancel} disabled={isDeleting}>
69
+ Cancel
70
+ </Button>
71
+ <Button variant="destructive" onClick={confirm} disabled={!canDelete} isLoading={isDeleting}>
72
+ Delete permanently
73
+ </Button>
74
+ </DialogFooter>
75
+ </DialogContent>
76
+ </Dialog>
77
+ );
78
+ }
@@ -79,6 +79,21 @@ export function visibleDatastoreRows(rows: DatastoreRow[], visibility: Datastore
79
79
  return rows.filter((_row, index) => shown[index] || hasVisibleDescendant(rows, shown, index));
80
80
  }
81
81
 
82
+ // The server refuses to delete a datastore while others live under it.
83
+ export function nestedDatastoreNamesByRowId(rows: DatastoreRow[]): Map<string, string[]> {
84
+ const nested = new Map<string, string[]>();
85
+ rows.forEach((row, index) => {
86
+ const names: string[] = [];
87
+ for (let i = index + 1; i < rows.length; i++) {
88
+ const descendant = rows[i];
89
+ if (!descendant || descendant.depth <= row.depth) break;
90
+ if (isPresent(descendant.datastoreId)) names.push(descendant.name);
91
+ }
92
+ if (isPresent(names)) nested.set(row.id, names);
93
+ });
94
+ return nested;
95
+ }
96
+
82
97
  export function hiddenRowCount(
83
98
  rows: DatastoreRow[],
84
99
  visibility: DatastoreRowVisibility,