@stigmer/react 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/AgentPicker.d.ts +10 -1
- package/agent/AgentPicker.d.ts.map +1 -1
- package/agent/AgentPicker.js +2 -2
- package/agent/AgentPicker.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +22 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts +53 -0
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
- package/channel/ChannelTemplatesDialog.js +150 -0
- package/channel/ChannelTemplatesDialog.js.map +1 -0
- package/channel/index.d.ts +8 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +4 -0
- package/channel/index.js.map +1 -1
- package/channel/providers.d.ts +12 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +14 -2
- package/channel/providers.js.map +1 -1
- package/channel/templatePresentation.d.ts +37 -0
- package/channel/templatePresentation.d.ts.map +1 -0
- package/channel/templatePresentation.js +69 -0
- package/channel/templatePresentation.js.map +1 -0
- package/channel/useChannelTemplateList.d.ts +64 -0
- package/channel/useChannelTemplateList.d.ts.map +1 -0
- package/channel/useChannelTemplateList.js +69 -0
- package/channel/useChannelTemplateList.js.map +1 -0
- package/channel/useChannelTemplateReadiness.d.ts +53 -0
- package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
- package/channel/useChannelTemplateReadiness.js +42 -0
- package/channel/useChannelTemplateReadiness.js.map +1 -0
- package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/useDashboardFailedRuns.js +2 -0
- package/dashboard/useDashboardFailedRuns.js.map +1 -1
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +9 -1
- package/index.js.map +1 -1
- package/library/useExportResource.d.ts +4 -3
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +1 -1
- package/library/useExportResource.js.map +1 -1
- package/organization/OrgProvider.d.ts +11 -4
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +28 -4
- package/organization/OrgProvider.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/useDeleteResource.d.ts +1 -1
- package/resource-detail/useDeleteResource.d.ts.map +1 -1
- package/resource-detail/useDeleteResource.js +5 -0
- package/resource-detail/useDeleteResource.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +6 -2
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/schedule/CadenceField.d.ts +31 -0
- package/schedule/CadenceField.d.ts.map +1 -0
- package/schedule/CadenceField.js +116 -0
- package/schedule/CadenceField.js.map +1 -0
- package/schedule/ScheduleDetailView.d.ts +58 -0
- package/schedule/ScheduleDetailView.d.ts.map +1 -0
- package/schedule/ScheduleDetailView.js +236 -0
- package/schedule/ScheduleDetailView.js.map +1 -0
- package/schedule/ScheduleForm.d.ts +43 -0
- package/schedule/ScheduleForm.d.ts.map +1 -0
- package/schedule/ScheduleForm.js +129 -0
- package/schedule/ScheduleForm.js.map +1 -0
- package/schedule/ScheduleRowActions.d.ts +20 -0
- package/schedule/ScheduleRowActions.d.ts.map +1 -0
- package/schedule/ScheduleRowActions.js +54 -0
- package/schedule/ScheduleRowActions.js.map +1 -0
- package/schedule/TimeZoneField.d.ts +32 -0
- package/schedule/TimeZoneField.d.ts.map +1 -0
- package/schedule/TimeZoneField.js +104 -0
- package/schedule/TimeZoneField.js.map +1 -0
- package/schedule/cadence.d.ts +95 -0
- package/schedule/cadence.d.ts.map +1 -0
- package/schedule/cadence.js +242 -0
- package/schedule/cadence.js.map +1 -0
- package/schedule/index.d.ts +31 -0
- package/schedule/index.d.ts.map +1 -0
- package/schedule/index.js +16 -0
- package/schedule/index.js.map +1 -0
- package/schedule/scheduleColumns.d.ts +26 -0
- package/schedule/scheduleColumns.d.ts.map +1 -0
- package/schedule/scheduleColumns.js +77 -0
- package/schedule/scheduleColumns.js.map +1 -0
- package/schedule/scheduleListFn.d.ts +50 -0
- package/schedule/scheduleListFn.d.ts.map +1 -0
- package/schedule/scheduleListFn.js +65 -0
- package/schedule/scheduleListFn.js.map +1 -0
- package/schedule/scheduleState.d.ts +43 -0
- package/schedule/scheduleState.d.ts.map +1 -0
- package/schedule/scheduleState.js +67 -0
- package/schedule/scheduleState.js.map +1 -0
- package/schedule/useCreateSchedule.d.ts +57 -0
- package/schedule/useCreateSchedule.d.ts.map +1 -0
- package/schedule/useCreateSchedule.js +61 -0
- package/schedule/useCreateSchedule.js.map +1 -0
- package/schedule/useResumeSchedule.d.ts +30 -0
- package/schedule/useResumeSchedule.d.ts.map +1 -0
- package/schedule/useResumeSchedule.js +45 -0
- package/schedule/useResumeSchedule.js.map +1 -0
- package/schedule/useSchedule.d.ts +39 -0
- package/schedule/useSchedule.d.ts.map +1 -0
- package/schedule/useSchedule.js +44 -0
- package/schedule/useSchedule.js.map +1 -0
- package/schedule/useScheduleCount.d.ts +38 -0
- package/schedule/useScheduleCount.d.ts.map +1 -0
- package/schedule/useScheduleCount.js +30 -0
- package/schedule/useScheduleCount.js.map +1 -0
- package/schedule/useScheduleList.d.ts +48 -0
- package/schedule/useScheduleList.d.ts.map +1 -0
- package/schedule/useScheduleList.js +43 -0
- package/schedule/useScheduleList.js.map +1 -0
- package/schedule/useSetScheduleEnabled.d.ts +31 -0
- package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
- package/schedule/useSetScheduleEnabled.js +53 -0
- package/schedule/useSetScheduleEnabled.js.map +1 -0
- package/schedule/useTriggerSchedule.d.ts +28 -0
- package/schedule/useTriggerSchedule.d.ts.map +1 -0
- package/schedule/useTriggerSchedule.js +43 -0
- package/schedule/useTriggerSchedule.js.map +1 -0
- package/search/useResourceCount.d.ts +2 -1
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +6 -1
- package/search/useResourceCount.js.map +1 -1
- package/src/agent/AgentPicker.tsx +13 -1
- package/src/channel/AgentChannelsPanel.tsx +43 -1
- package/src/channel/ChannelTemplatesDialog.tsx +433 -0
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
- package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
- package/src/channel/__tests__/providers.test.ts +9 -0
- package/src/channel/__tests__/templatePresentation.test.ts +96 -0
- package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
- package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
- package/src/channel/index.ts +15 -0
- package/src/channel/providers.ts +26 -2
- package/src/channel/templatePresentation.ts +82 -0
- package/src/channel/useChannelTemplateList.ts +123 -0
- package/src/channel/useChannelTemplateReadiness.ts +69 -0
- package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
- package/src/dashboard/useDashboardFailedRuns.ts +2 -0
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +23 -2
- package/src/index.ts +65 -0
- package/src/library/useExportResource.ts +4 -3
- package/src/organization/OrgProvider.tsx +29 -4
- package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
- package/src/resource-detail/useDeleteResource.ts +11 -1
- package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
- package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/schedule/CadenceField.tsx +365 -0
- package/src/schedule/ScheduleDetailView.tsx +583 -0
- package/src/schedule/ScheduleForm.tsx +405 -0
- package/src/schedule/ScheduleRowActions.tsx +124 -0
- package/src/schedule/TimeZoneField.tsx +199 -0
- package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
- package/src/schedule/__tests__/cadence.test.ts +278 -0
- package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
- package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
- package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
- package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
- package/src/schedule/__tests__/scheduleState.test.ts +133 -0
- package/src/schedule/cadence.ts +298 -0
- package/src/schedule/index.ts +57 -0
- package/src/schedule/scheduleColumns.tsx +118 -0
- package/src/schedule/scheduleListFn.ts +108 -0
- package/src/schedule/scheduleState.ts +96 -0
- package/src/schedule/useCreateSchedule.ts +88 -0
- package/src/schedule/useResumeSchedule.ts +67 -0
- package/src/schedule/useSchedule.ts +67 -0
- package/src/schedule/useScheduleCount.ts +57 -0
- package/src/schedule/useScheduleList.ts +83 -0
- package/src/schedule/useSetScheduleEnabled.ts +84 -0
- package/src/schedule/useTriggerSchedule.ts +65 -0
- package/src/search/__tests__/useResourceCount.test.tsx +89 -0
- package/src/search/useResourceCount.ts +8 -2
- package/src/workflow/WorkflowDashboard.tsx +1 -0
- package/src/workflow/useWorkflowExecutionList.ts +13 -2
- package/styles.css +1 -1
- package/workflow/CanvasTransitionEdge.d.ts +1 -1
- package/workflow/WorkflowDashboard.d.ts.map +1 -1
- package/workflow/WorkflowDashboard.js +1 -0
- package/workflow/WorkflowDashboard.js.map +1 -1
- package/workflow/useWorkflowExecutionList.d.ts +10 -0
- package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
- package/workflow/useWorkflowExecutionList.js +3 -2
- package/workflow/useWorkflowExecutionList.js.map +1 -1
package/src/index.ts
CHANGED
|
@@ -770,6 +770,61 @@ export type {
|
|
|
770
770
|
DatastoreDetailViewProps,
|
|
771
771
|
} from "./datastore/index.js";
|
|
772
772
|
|
|
773
|
+
// Schedule — the disabled-vs-paused state derivation, the direct-query
|
|
774
|
+
// list core + workbench adapter (schedules are not search-backed), data
|
|
775
|
+
// hooks (get by reference, paginated list, count), behavior hooks
|
|
776
|
+
// (create via apply, resume, trigger once, owner enable/disable via
|
|
777
|
+
// lossless manifest apply), the cadence model (preset ⇄ cron, no cron
|
|
778
|
+
// parsing — recognition of builder-emitted shapes only), the cadence
|
|
779
|
+
// builder, the creation form, and the detail view
|
|
780
|
+
export {
|
|
781
|
+
deriveScheduleState,
|
|
782
|
+
formatNextFire,
|
|
783
|
+
createScheduleListFn,
|
|
784
|
+
listSchedulesPage,
|
|
785
|
+
useSchedule,
|
|
786
|
+
useScheduleList,
|
|
787
|
+
useScheduleCount,
|
|
788
|
+
useCreateSchedule,
|
|
789
|
+
useResumeSchedule,
|
|
790
|
+
useTriggerSchedule,
|
|
791
|
+
useSetScheduleEnabled,
|
|
792
|
+
createScheduleColumns,
|
|
793
|
+
cadenceToCron,
|
|
794
|
+
cronToCadence,
|
|
795
|
+
describeCadence,
|
|
796
|
+
validateCron,
|
|
797
|
+
validateTimeZone,
|
|
798
|
+
WEEKDAY_LABELS,
|
|
799
|
+
CadenceField,
|
|
800
|
+
ScheduleForm,
|
|
801
|
+
ScheduleRowActions,
|
|
802
|
+
ScheduleDetailView,
|
|
803
|
+
ScheduleIcon,
|
|
804
|
+
} from "./schedule/index.js";
|
|
805
|
+
export type {
|
|
806
|
+
ScheduleState,
|
|
807
|
+
ScheduleStateInfo,
|
|
808
|
+
ScheduleListClient,
|
|
809
|
+
SchedulePage,
|
|
810
|
+
UseScheduleReturn,
|
|
811
|
+
UseScheduleListOptions,
|
|
812
|
+
UseScheduleListReturn,
|
|
813
|
+
UseScheduleCountOptions,
|
|
814
|
+
UseScheduleCountReturn,
|
|
815
|
+
UseCreateScheduleReturn,
|
|
816
|
+
UseResumeScheduleReturn,
|
|
817
|
+
UseTriggerScheduleReturn,
|
|
818
|
+
UseSetScheduleEnabledReturn,
|
|
819
|
+
CadencePreset,
|
|
820
|
+
CadenceKind,
|
|
821
|
+
CadenceFieldProps,
|
|
822
|
+
ScheduleFormProps,
|
|
823
|
+
ScheduleColumnsOptions,
|
|
824
|
+
ScheduleRowActionsProps,
|
|
825
|
+
ScheduleDetailViewProps,
|
|
826
|
+
} from "./schedule/index.js";
|
|
827
|
+
|
|
773
828
|
// Environment — data hooks, list hook, personal convenience hook, secret reveal, variable management, env var form, system env vars, and styled components
|
|
774
829
|
export {
|
|
775
830
|
useEnvironment,
|
|
@@ -1259,13 +1314,18 @@ export {
|
|
|
1259
1314
|
useConnectSlackChannel,
|
|
1260
1315
|
useInstallChannel,
|
|
1261
1316
|
useChannelToolReadiness,
|
|
1317
|
+
useChannelTemplateReadiness,
|
|
1262
1318
|
useChannelSessions,
|
|
1319
|
+
useChannelTemplateList,
|
|
1320
|
+
templateStatusPhase,
|
|
1321
|
+
splitTemplateBody,
|
|
1263
1322
|
AgentChannelsPanel,
|
|
1264
1323
|
ConnectSlackDialog,
|
|
1265
1324
|
ConnectWhatsAppDialog,
|
|
1266
1325
|
ChannelCredentialsDialog,
|
|
1267
1326
|
ChannelToolCredentials,
|
|
1268
1327
|
ChannelConversationsDialog,
|
|
1328
|
+
ChannelTemplatesDialog,
|
|
1269
1329
|
} from "./channel/index.js";
|
|
1270
1330
|
export type {
|
|
1271
1331
|
UseAgentChannelListReturn,
|
|
@@ -1277,13 +1337,18 @@ export type {
|
|
|
1277
1337
|
SlackConnectPhase,
|
|
1278
1338
|
UseInstallChannelReturn,
|
|
1279
1339
|
InstallChannelPhase,
|
|
1340
|
+
ChannelTemplateReadiness,
|
|
1280
1341
|
UseChannelSessionsReturn,
|
|
1342
|
+
UseChannelTemplateListOptions,
|
|
1343
|
+
UseChannelTemplateListReturn,
|
|
1344
|
+
TemplateBodySegment,
|
|
1281
1345
|
AgentChannelsPanelProps,
|
|
1282
1346
|
ConnectSlackDialogProps,
|
|
1283
1347
|
ConnectWhatsAppDialogProps,
|
|
1284
1348
|
ChannelCredentialsDialogProps,
|
|
1285
1349
|
ChannelToolCredentialsProps,
|
|
1286
1350
|
ChannelConversationsDialogProps,
|
|
1351
|
+
ChannelTemplatesDialogProps,
|
|
1287
1352
|
} from "./channel/index.js";
|
|
1288
1353
|
|
|
1289
1354
|
// Error — structured error display with classification, retry, and contextual guidance
|
|
@@ -5,6 +5,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
5
5
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
6
6
|
import type { Datastore } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
7
7
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
8
|
+
import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
8
9
|
import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
9
10
|
import { serializeManifest } from "@stigmer/sdk";
|
|
10
11
|
import { toast } from "../feedback/toast.js";
|
|
@@ -17,9 +18,9 @@ import { serializeWorkflowYaml } from "../workflow/serialize-workflow-yaml.js";
|
|
|
17
18
|
/** Options for {@link useExportResource}. */
|
|
18
19
|
export interface UseExportResourceOptions {
|
|
19
20
|
/** The resource kind — determines which serializer is used. */
|
|
20
|
-
readonly kind: "Agent" | "McpServer" | "Workflow" | "Datastore";
|
|
21
|
+
readonly kind: "Agent" | "McpServer" | "Workflow" | "Datastore" | "Schedule";
|
|
21
22
|
/** The proto resource to export, or `null` when not yet loaded. */
|
|
22
|
-
readonly resource: Agent | McpServer | Workflow | Datastore | null;
|
|
23
|
+
readonly resource: Agent | McpServer | Workflow | Datastore | Schedule | null;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** Return value of {@link useExportResource}. */
|
|
@@ -44,7 +45,7 @@ export interface UseExportResourceReturn {
|
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Headless export hook for Stigmer resources (Agent, McpServer,
|
|
47
|
-
* Workflow, Datastore).
|
|
48
|
+
* Workflow, Datastore, Schedule).
|
|
48
49
|
*
|
|
49
50
|
* Serializes the resource into YAML and JSON formats and provides
|
|
50
51
|
* stable callbacks for copying to clipboard or triggering a file
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "react";
|
|
13
13
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
14
14
|
import { useStigmer } from "../hooks.js";
|
|
15
|
+
import { useFetchCache } from "../internal/FetchCacheProvider.js";
|
|
15
16
|
|
|
16
17
|
/** Value exposed by {@link OrgProvider} via {@link useOrg}. */
|
|
17
18
|
export interface OrgContextValue {
|
|
@@ -63,19 +64,27 @@ function persistSlug(slug: string): void {
|
|
|
63
64
|
* organization selection, and persists the choice to `localStorage`
|
|
64
65
|
* under the key `stigmer:activeOrgSlug`.
|
|
65
66
|
*
|
|
66
|
-
* Must be rendered inside a {@link StigmerProvider}.
|
|
67
|
+
* Must be rendered inside a {@link StigmerProvider}. Mount it BELOW
|
|
68
|
+
* `FetchCacheProvider` (when one is used): switching the active org clears
|
|
69
|
+
* the nearest fetch cache, so view state cached under the previous org
|
|
70
|
+
* context — session and execution entries are keyed by id, not org — can
|
|
71
|
+
* never bleed into the new one. Navigation on switch stays the host's
|
|
72
|
+
* responsibility via `OrgSwitcher`'s `onOrgChanged` callback.
|
|
67
73
|
*
|
|
68
74
|
* @example
|
|
69
75
|
* ```tsx
|
|
70
76
|
* <StigmerProvider client={client}>
|
|
71
|
-
* <
|
|
72
|
-
* <
|
|
73
|
-
*
|
|
77
|
+
* <FetchCacheProvider>
|
|
78
|
+
* <OrgProvider>
|
|
79
|
+
* <App />
|
|
80
|
+
* </OrgProvider>
|
|
81
|
+
* </FetchCacheProvider>
|
|
74
82
|
* </StigmerProvider>
|
|
75
83
|
* ```
|
|
76
84
|
*/
|
|
77
85
|
export function OrgProvider({ children }: { children: ReactNode }) {
|
|
78
86
|
const stigmer = useStigmer();
|
|
87
|
+
const fetchCache = useFetchCache();
|
|
79
88
|
const [orgs, setOrgs] = useState<Organization[]>([]);
|
|
80
89
|
const [activeOrg, setActiveOrgState] = useState<Organization | null>(null);
|
|
81
90
|
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -140,6 +149,22 @@ export function OrgProvider({ children }: { children: ReactNode }) {
|
|
|
140
149
|
}
|
|
141
150
|
}, []);
|
|
142
151
|
|
|
152
|
+
// Org-switch invariant: view state cached under one org context must not
|
|
153
|
+
// survive into another. Covers every path that changes the active org
|
|
154
|
+
// (explicit switch, post-create refresh) while skipping the initial
|
|
155
|
+
// restore (previous slug is null). Cache keys for sessions/executions are
|
|
156
|
+
// id-scoped, not org-scoped, so a TTL'd entry would otherwise outlive the
|
|
157
|
+
// switch. No-op when no FetchCacheProvider is mounted.
|
|
158
|
+
const previousSlugRef = useRef<string | null>(null);
|
|
159
|
+
const activeSlug = activeOrg?.metadata?.slug ?? null;
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const previous = previousSlugRef.current;
|
|
162
|
+
previousSlugRef.current = activeSlug;
|
|
163
|
+
if (previous !== null && activeSlug !== null && previous !== activeSlug) {
|
|
164
|
+
fetchCache?.clear();
|
|
165
|
+
}
|
|
166
|
+
}, [activeSlug, fetchCache]);
|
|
167
|
+
|
|
143
168
|
const value = useMemo<OrgContextValue>(
|
|
144
169
|
() => ({
|
|
145
170
|
orgs,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { FetchCache } from "../../internal/fetch-cache";
|
|
7
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
8
|
+
import { OrgProvider, useOrg } from "../OrgProvider";
|
|
9
|
+
|
|
10
|
+
const acme = { metadata: { id: "acme", slug: "acme", name: "Acme" } } as Organization;
|
|
11
|
+
const globex = { metadata: { id: "globex", slug: "globex", name: "Globex" } } as Organization;
|
|
12
|
+
|
|
13
|
+
function createMockStigmer(orgs: Organization[]) {
|
|
14
|
+
return {
|
|
15
|
+
organization: {
|
|
16
|
+
findMyOrganizations: vi.fn().mockResolvedValue({ entries: orgs }),
|
|
17
|
+
},
|
|
18
|
+
} as never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wrapper(client: unknown, cache: FetchCache | null) {
|
|
22
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<StigmerContext.Provider value={client as never}>
|
|
25
|
+
<FetchCacheContext.Provider value={cache}>
|
|
26
|
+
<OrgProvider>{children}</OrgProvider>
|
|
27
|
+
</FetchCacheContext.Provider>
|
|
28
|
+
</StigmerContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("OrgProvider — org-switch cache invalidation", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
localStorage.clear();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("clears the fetch cache when the active org changes", async () => {
|
|
39
|
+
const cache = new FetchCache();
|
|
40
|
+
cache.set("session:ses_1", { stale: "from previous org context" });
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useOrg(), {
|
|
43
|
+
wrapper: wrapper(createMockStigmer([acme, globex]), cache),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
|
|
47
|
+
|
|
48
|
+
// Initial restore must NOT clear — only a genuine change of context does.
|
|
49
|
+
// Session/execution cache keys are id-scoped (not org-scoped), so without
|
|
50
|
+
// the clear a switched-away org's view state would survive its 5-min TTL.
|
|
51
|
+
expect(cache.has("session:ses_1")).toBe(true);
|
|
52
|
+
|
|
53
|
+
act(() => result.current.setActiveOrg(globex));
|
|
54
|
+
|
|
55
|
+
await waitFor(() => expect(cache.has("session:ses_1")).toBe(false));
|
|
56
|
+
expect(result.current.activeOrg?.metadata?.slug).toBe("globex");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not clear when re-selecting the already-active org", async () => {
|
|
60
|
+
const cache = new FetchCache();
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useOrg(), {
|
|
63
|
+
wrapper: wrapper(createMockStigmer([acme, globex]), cache),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
|
|
67
|
+
const active = result.current.activeOrg!;
|
|
68
|
+
|
|
69
|
+
cache.set("session:ses_1", { fresh: true });
|
|
70
|
+
act(() => result.current.setActiveOrg(active));
|
|
71
|
+
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
73
|
+
expect(cache.has("session:ses_1")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("works without a FetchCacheProvider (no-op, no crash)", async () => {
|
|
77
|
+
const { result } = renderHook(() => useOrg(), {
|
|
78
|
+
wrapper: wrapper(createMockStigmer([acme, globex]), null),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
|
|
82
|
+
|
|
83
|
+
act(() => result.current.setActiveOrg(globex));
|
|
84
|
+
|
|
85
|
+
await waitFor(() =>
|
|
86
|
+
expect(result.current.activeOrg?.metadata?.slug).toBe("globex"),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -6,7 +6,12 @@ import { toError } from "../internal/toError.js";
|
|
|
6
6
|
import { toast } from "../feedback/toast.js";
|
|
7
7
|
|
|
8
8
|
/** Resource types that support deletion from detail pages. */
|
|
9
|
-
export type DeletableResourceKind =
|
|
9
|
+
export type DeletableResourceKind =
|
|
10
|
+
| "agent"
|
|
11
|
+
| "skill"
|
|
12
|
+
| "mcpServer"
|
|
13
|
+
| "workflow"
|
|
14
|
+
| "schedule";
|
|
10
15
|
|
|
11
16
|
export interface UseDeleteResourceReturn {
|
|
12
17
|
/**
|
|
@@ -74,6 +79,9 @@ export function useDeleteResource(
|
|
|
74
79
|
case "workflow":
|
|
75
80
|
await stigmer.workflow.delete(resourceId);
|
|
76
81
|
break;
|
|
82
|
+
case "schedule":
|
|
83
|
+
await stigmer.schedule.delete(resourceId);
|
|
84
|
+
break;
|
|
77
85
|
}
|
|
78
86
|
toast.success(
|
|
79
87
|
resourceName
|
|
@@ -106,5 +114,7 @@ function kindLabel(kind: DeletableResourceKind): string {
|
|
|
106
114
|
return "MCP server";
|
|
107
115
|
case "workflow":
|
|
108
116
|
return "workflow";
|
|
117
|
+
case "schedule":
|
|
118
|
+
return "schedule";
|
|
109
119
|
}
|
|
110
120
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, renderHook, screen, waitFor, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { ListResult } from "@stigmer/sdk";
|
|
5
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
6
|
+
import { ResourceWorkbench } from "../components/ResourceWorkbench";
|
|
7
|
+
import { useResourceCollection } from "../hooks/useResourceCollection";
|
|
8
|
+
|
|
9
|
+
afterEach(cleanup);
|
|
10
|
+
|
|
11
|
+
function Wrapper({ children }: { children: ReactNode }) {
|
|
12
|
+
return (
|
|
13
|
+
<FetchCacheContext.Provider value={null}>
|
|
14
|
+
{children}
|
|
15
|
+
</FetchCacheContext.Provider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A row shaped like a resource proto: the id lives under metadata. */
|
|
20
|
+
interface ProtoLikeRow {
|
|
21
|
+
readonly metadata: { readonly id: string; readonly slug: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listFnReturning(entries: unknown[], totalCount = entries.length) {
|
|
25
|
+
return vi.fn(
|
|
26
|
+
async (): Promise<ListResult> => ({
|
|
27
|
+
entries: entries as ListResult["entries"],
|
|
28
|
+
totalCount,
|
|
29
|
+
totalPages: 1,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("ResourceWorkbench searchable prop", () => {
|
|
35
|
+
it("renders the search input by default", async () => {
|
|
36
|
+
render(
|
|
37
|
+
<ResourceWorkbench listFn={listFnReturning([])} org="acme" />,
|
|
38
|
+
{ wrapper: Wrapper },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await waitFor(() =>
|
|
42
|
+
expect(screen.getByRole("textbox", { name: "Search…" })).toBeTruthy(),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("hides the search input when searchable is false", async () => {
|
|
47
|
+
render(
|
|
48
|
+
<ResourceWorkbench
|
|
49
|
+
listFn={listFnReturning([])}
|
|
50
|
+
org="acme"
|
|
51
|
+
searchable={false}
|
|
52
|
+
/>,
|
|
53
|
+
{ wrapper: Wrapper },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await waitFor(() => expect(screen.getByText("No resources found")).toBeTruthy());
|
|
57
|
+
expect(screen.queryByRole("textbox")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("useResourceCollection row identity", () => {
|
|
62
|
+
const protoRows: ProtoLikeRow[] = [
|
|
63
|
+
{ metadata: { id: "sch_a", slug: "a" } },
|
|
64
|
+
{ metadata: { id: "sch_b", slug: "b" } },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const columns = [
|
|
68
|
+
{
|
|
69
|
+
id: "slug",
|
|
70
|
+
header: "Slug",
|
|
71
|
+
cell: (row: ProtoLikeRow) => row.metadata.slug,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
it("uses getItemId for rows without a top-level id", async () => {
|
|
76
|
+
// Hoisted: a listFn re-created per render would retrigger useFetch.
|
|
77
|
+
const listFn = listFnReturning(protoRows);
|
|
78
|
+
const { result } = renderHook(
|
|
79
|
+
() =>
|
|
80
|
+
useResourceCollection<ProtoLikeRow>({
|
|
81
|
+
listFn,
|
|
82
|
+
org: "acme",
|
|
83
|
+
columns,
|
|
84
|
+
getItemId: (row) => row.metadata.id,
|
|
85
|
+
}),
|
|
86
|
+
{ wrapper: Wrapper },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await waitFor(() => expect(result.current.items).toHaveLength(2));
|
|
90
|
+
const rowIds = result.current.table!.getRowModel().rows.map((r) => r.id);
|
|
91
|
+
expect(rowIds).toEqual(["sch_a", "sch_b"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("falls back to SearchResult.id, then the row index", async () => {
|
|
95
|
+
const mixed = [{ id: "res_1" }, { name: "no-id" }];
|
|
96
|
+
const listFn = listFnReturning(mixed);
|
|
97
|
+
const fallbackColumns = [{ id: "c", header: "C", cell: () => null }];
|
|
98
|
+
const { result } = renderHook(
|
|
99
|
+
() =>
|
|
100
|
+
useResourceCollection({
|
|
101
|
+
listFn,
|
|
102
|
+
org: "acme",
|
|
103
|
+
columns: fallbackColumns,
|
|
104
|
+
}),
|
|
105
|
+
{ wrapper: Wrapper },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await waitFor(() => expect(result.current.items).toHaveLength(2));
|
|
109
|
+
const rowIds = result.current.table!.getRowModel().rows.map((r) => r.id);
|
|
110
|
+
expect(rowIds).toEqual(["res_1", "1"]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -155,6 +155,15 @@ export interface ResourceWorkbenchProps<TData = SearchResult> {
|
|
|
155
155
|
|
|
156
156
|
// --- Layout ------------------------------------------------------------
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether the toolbar renders the text-search input. Set `false` for
|
|
160
|
+
* kinds whose list backend has no text search (e.g. schedules, which
|
|
161
|
+
* use the direct query instead of the search service) — a search box
|
|
162
|
+
* that silently matches nothing is worse than none (DD-006). Opt-in
|
|
163
|
+
* per DD-011: existing consumers keep the search box.
|
|
164
|
+
* @default true
|
|
165
|
+
*/
|
|
166
|
+
readonly searchable?: boolean;
|
|
158
167
|
/** Search input placeholder text. @default "Search\u2026" */
|
|
159
168
|
readonly searchPlaceholder?: string;
|
|
160
169
|
/** Accessible label for the workbench region. @default "Resource workbench" */
|
|
@@ -228,6 +237,7 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
228
237
|
emptyAction,
|
|
229
238
|
onRetry,
|
|
230
239
|
headerAction,
|
|
240
|
+
searchable = true,
|
|
231
241
|
searchPlaceholder = "Search\u2026",
|
|
232
242
|
"aria-label": ariaLabel = "Resource workbench",
|
|
233
243
|
className,
|
|
@@ -267,6 +277,7 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
267
277
|
onSortChange: filtersHook.setSort,
|
|
268
278
|
columns: columns as WorkbenchColumnDef<TData>[],
|
|
269
279
|
enableSelection,
|
|
280
|
+
getItemId,
|
|
270
281
|
refetchToken,
|
|
271
282
|
});
|
|
272
283
|
|
|
@@ -305,21 +316,27 @@ export function ResourceWorkbench<TData = SearchResult>({
|
|
|
305
316
|
>
|
|
306
317
|
{/* --- Toolbar: search + filters + view switcher --- */}
|
|
307
318
|
<div className="flex flex-wrap items-center gap-2">
|
|
308
|
-
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
319
|
+
{searchable ? (
|
|
320
|
+
<div className="relative flex-1">
|
|
321
|
+
<SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground-subtle" />
|
|
322
|
+
<input
|
|
323
|
+
type="text"
|
|
324
|
+
value={filtersHook.query}
|
|
325
|
+
onChange={(e) => filtersHook.setQuery(e.target.value)}
|
|
326
|
+
placeholder={searchPlaceholder}
|
|
327
|
+
aria-label={searchPlaceholder}
|
|
328
|
+
className={cn(
|
|
329
|
+
"w-full rounded-md border border-input bg-background py-1.5 pl-8 pr-3 text-sm",
|
|
330
|
+
"placeholder:text-muted-foreground-subtle",
|
|
331
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
332
|
+
)}
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
) : (
|
|
336
|
+
// Spacer keeps the scope toggle / view switcher / header action
|
|
337
|
+
// right-aligned, matching the searchable layout.
|
|
338
|
+
<div className="flex-1" aria-hidden="true" />
|
|
339
|
+
)}
|
|
323
340
|
{onScopeChange && (
|
|
324
341
|
<ScopeToggle value={controlledScope} onChange={onScopeChange} />
|
|
325
342
|
)}
|
|
@@ -57,6 +57,14 @@ export interface UseResourceCollectionOptions<TData = SearchResult> {
|
|
|
57
57
|
readonly columns?: readonly WorkbenchColumnDef<TData>[];
|
|
58
58
|
/** Enable row selection state tracking. @default false */
|
|
59
59
|
readonly enableSelection?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Extracts a stable unique ID from an item, used as the TanStack row
|
|
62
|
+
* id. Provide it whenever the row type does not carry a top-level
|
|
63
|
+
* `id` (e.g. full resource protos, where the id lives at
|
|
64
|
+
* `metadata.id`). When omitted, falls back to `SearchResult.id`, then
|
|
65
|
+
* the row index.
|
|
66
|
+
*/
|
|
67
|
+
readonly getItemId?: (item: TData) => string;
|
|
60
68
|
/**
|
|
61
69
|
* Opaque token that forces a background refetch whenever its value
|
|
62
70
|
* changes. Use it to re-read the list after an out-of-band mutation
|
|
@@ -157,6 +165,7 @@ export function useResourceCollection<TData = SearchResult>(
|
|
|
157
165
|
onSortChange,
|
|
158
166
|
columns = [],
|
|
159
167
|
enableSelection = false,
|
|
168
|
+
getItemId,
|
|
160
169
|
refetchToken,
|
|
161
170
|
} = options;
|
|
162
171
|
|
|
@@ -247,7 +256,8 @@ export function useResourceCollection<TData = SearchResult>(
|
|
|
247
256
|
onRowSelectionChange: enableSelection ? setRowSelection : undefined,
|
|
248
257
|
getCoreRowModel: getCoreRowModel(),
|
|
249
258
|
getSortedRowModel: getSortedRowModel(),
|
|
250
|
-
getRowId: (row) =>
|
|
259
|
+
getRowId: (row, index) =>
|
|
260
|
+
getItemId?.(row) || (row as SearchResult).id || String(index),
|
|
251
261
|
manualPagination: true,
|
|
252
262
|
manualSorting: true,
|
|
253
263
|
enableRowSelection: enableSelection,
|