@stigmer/react 3.12.4 → 3.12.6
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/AgentDetailView.js +2 -2
- package/agent/AgentDetailView.js.map +1 -1
- package/agent/index.d.ts +0 -1
- package/agent/index.d.ts.map +1 -1
- package/agent/index.js +0 -1
- package/agent/index.js.map +1 -1
- package/agent/useUpdateAgent.d.ts +3 -3
- package/agent/useUpdateAgent.js +3 -3
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +5 -7
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/agent-instance/useUpdateAgentInstance.d.ts +1 -1
- package/agent-instance/useUpdateAgentInstance.js +1 -1
- package/attachment/AttachmentChipList.js +16 -6
- package/attachment/AttachmentChipList.js.map +1 -1
- package/attachment/index.d.ts +1 -1
- package/attachment/index.d.ts.map +1 -1
- package/attachment/index.js.map +1 -1
- package/attachment/useAttachments.d.ts +51 -8
- package/attachment/useAttachments.d.ts.map +1 -1
- package/attachment/useAttachments.js +74 -3
- package/attachment/useAttachments.js.map +1 -1
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +10 -5
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelRunConfigDialog.d.ts +51 -0
- package/channel/ChannelRunConfigDialog.d.ts.map +1 -0
- package/channel/ChannelRunConfigDialog.js +121 -0
- package/channel/ChannelRunConfigDialog.js.map +1 -0
- package/channel/index.d.ts +2 -0
- package/channel/index.d.ts.map +1 -1
- package/channel/index.js +1 -0
- package/channel/index.js.map +1 -1
- package/channel/useSaveAgentChannel.d.ts.map +1 -1
- package/channel/useSaveAgentChannel.js +6 -3
- package/channel/useSaveAgentChannel.js.map +1 -1
- package/channel-app/ChannelAppDetailPanel.js +9 -9
- package/channel-app/ChannelAppDetailPanel.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +6 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +9 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +39 -13
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationComposer.d.ts +19 -3
- package/conversation/ConversationComposer.d.ts.map +1 -1
- package/conversation/ConversationComposer.js +4 -4
- package/conversation/ConversationComposer.js.map +1 -1
- package/conversation/ConversationTemplatePickerDialog.d.ts +63 -0
- package/conversation/ConversationTemplatePickerDialog.d.ts.map +1 -0
- package/conversation/ConversationTemplatePickerDialog.js +151 -0
- package/conversation/ConversationTemplatePickerDialog.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -1
- package/conversation/ConversationsWorkbench.js +16 -5
- package/conversation/ConversationsWorkbench.js.map +1 -1
- package/conversation/index.d.ts +3 -1
- package/conversation/index.d.ts.map +1 -1
- package/conversation/index.js +1 -0
- package/conversation/index.js.map +1 -1
- package/conversation/useConversationParticipation.d.ts +41 -4
- package/conversation/useConversationParticipation.d.ts.map +1 -1
- package/conversation/useConversationParticipation.js +13 -2
- package/conversation/useConversationParticipation.js.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.js +12 -3
- package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
- package/cursor-accounts/cursor-account-coverage.d.ts +30 -1
- package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -1
- package/cursor-accounts/cursor-account-coverage.js +21 -0
- package/cursor-accounts/cursor-account-coverage.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +15 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +5 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/identity-account/AccountPreferencesPanel.d.ts +36 -0
- package/identity-account/AccountPreferencesPanel.d.ts.map +1 -0
- package/identity-account/AccountPreferencesPanel.js +202 -0
- package/identity-account/AccountPreferencesPanel.js.map +1 -0
- package/identity-account/index.d.ts +8 -0
- package/identity-account/index.d.ts.map +1 -1
- package/identity-account/index.js +4 -0
- package/identity-account/index.js.map +1 -1
- package/identity-account/useAccountExecutionDefaults.d.ts +36 -0
- package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -0
- package/identity-account/useAccountExecutionDefaults.js +46 -0
- package/identity-account/useAccountExecutionDefaults.js.map +1 -0
- package/identity-account/useMyIdentityAccount.d.ts +55 -0
- package/identity-account/useMyIdentityAccount.d.ts.map +1 -0
- package/identity-account/useMyIdentityAccount.js +37 -0
- package/identity-account/useMyIdentityAccount.js.map +1 -0
- package/identity-account/useUpdateIdentityAccount.d.ts +42 -0
- package/identity-account/useUpdateIdentityAccount.d.ts.map +1 -0
- package/identity-account/useUpdateIdentityAccount.js +53 -0
- package/identity-account/useUpdateIdentityAccount.js.map +1 -0
- package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.js +16 -15
- package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
- package/index.d.ts +15 -13
- package/index.d.ts.map +1 -1
- package/index.js +11 -8
- package/index.js.map +1 -1
- package/internal/StandingContextField.d.ts +37 -0
- package/internal/StandingContextField.d.ts.map +1 -0
- package/internal/StandingContextField.js +24 -0
- package/internal/StandingContextField.js.map +1 -0
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +9 -2
- package/library/InstanceVisibilitySelector.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +13 -3
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +33 -5
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/VisibilityOptionRow.d.ts +6 -0
- package/library/VisibilityOptionRow.d.ts.map +1 -1
- package/library/VisibilityOptionRow.js +11 -1
- package/library/VisibilityOptionRow.js.map +1 -1
- package/library/VisibilitySelector.d.ts.map +1 -1
- package/library/VisibilitySelector.js +14 -2
- package/library/VisibilitySelector.js.map +1 -1
- package/library/index.d.ts +4 -2
- package/library/index.d.ts.map +1 -1
- package/library/index.js +2 -1
- package/library/index.js.map +1 -1
- package/library/useCanSetPublicVisibility.d.ts +30 -0
- package/library/useCanSetPublicVisibility.d.ts.map +1 -0
- package/library/useCanSetPublicVisibility.js +41 -0
- package/library/useCanSetPublicVisibility.js.map +1 -0
- package/library/visibilityLevels.d.ts +32 -1
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +34 -14
- package/library/visibilityLevels.js.map +1 -1
- package/mcp-server/McpServerDetailView.js +2 -2
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/index.d.ts +0 -1
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +0 -1
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/internal/definitionDrift.d.ts +14 -12
- package/mcp-server/internal/definitionDrift.d.ts.map +1 -1
- package/mcp-server/internal/definitionDrift.js +22 -19
- package/mcp-server/internal/definitionDrift.js.map +1 -1
- package/mcp-server/useMcpServerConnect.d.ts +8 -5
- package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerConnect.js +2 -1
- package/mcp-server/useMcpServerConnect.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.js +7 -2
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
- package/mcp-server/useUpdateMcpServer.d.ts +3 -3
- package/mcp-server/useUpdateMcpServer.js +3 -3
- package/models/ModelSelector.d.ts +33 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +37 -4
- package/models/ModelSelector.js.map +1 -1
- package/models/index.d.ts +2 -0
- package/models/index.d.ts.map +1 -1
- package/models/index.js +1 -0
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +16 -0
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +13 -0
- package/models/registry.js.map +1 -1
- package/models/thinking-mode.d.ts +27 -0
- package/models/thinking-mode.d.ts.map +1 -0
- package/models/thinking-mode.js +32 -0
- package/models/thinking-mode.js.map +1 -0
- package/oauth-app/CreateOAuthAppForm.d.ts +2 -2
- package/oauth-app/CreateOAuthAppForm.d.ts.map +1 -1
- package/oauth-app/CreateOAuthAppForm.js +13 -4
- package/oauth-app/CreateOAuthAppForm.js.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.d.ts.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.js +50 -22
- package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
- package/organization/OrgPreferencesPanel.d.ts +32 -0
- package/organization/OrgPreferencesPanel.d.ts.map +1 -0
- package/organization/OrgPreferencesPanel.js +90 -0
- package/organization/OrgPreferencesPanel.js.map +1 -0
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +5 -4
- package/organization/OrgProfilePanel.js.map +1 -1
- package/organization/index.d.ts +2 -0
- package/organization/index.d.ts.map +1 -1
- package/organization/index.js +1 -0
- package/organization/index.js.map +1 -1
- package/package.json +4 -4
- package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -1
- package/platform-client/PlatformClientDetailPanel.js +12 -13
- package/platform-client/PlatformClientDetailPanel.js.map +1 -1
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +21 -10
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +13 -3
- package/schedule/ScheduleForm.js.map +1 -1
- package/session/NewSessionViewer.d.ts +10 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +2 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/run-config.d.ts +9 -0
- package/session/run-config.d.ts.map +1 -1
- package/session/run-config.js +5 -0
- package/session/run-config.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +27 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +51 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts +12 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +15 -24
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +14 -10
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useUpdateSession.d.ts +18 -24
- package/session/useUpdateSession.d.ts.map +1 -1
- package/session/useUpdateSession.js +18 -24
- package/session/useUpdateSession.js.map +1 -1
- package/settings/AccountPreferencesSection.d.ts +3 -0
- package/settings/AccountPreferencesSection.d.ts.map +1 -0
- package/settings/AccountPreferencesSection.js +10 -0
- package/settings/AccountPreferencesSection.js.map +1 -0
- package/settings/OrgPreferencesSection.d.ts +3 -0
- package/settings/OrgPreferencesSection.d.ts.map +1 -0
- package/settings/OrgPreferencesSection.js +13 -0
- package/settings/OrgPreferencesSection.js.map +1 -0
- package/settings/index.d.ts +2 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +2 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +17 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +2 -2
- package/src/agent/index.ts +0 -2
- package/src/agent/useUpdateAgent.ts +3 -3
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +5 -7
- package/src/agent-instance/__tests__/AgentInstanceDetailPanel.test.tsx +75 -0
- package/src/agent-instance/useUpdateAgentInstance.ts +1 -1
- package/src/attachment/AttachmentChipList.tsx +19 -8
- package/src/attachment/__tests__/AttachmentChipList.test.tsx +12 -0
- package/src/attachment/__tests__/useAttachments.test.ts +293 -0
- package/src/attachment/index.ts +1 -0
- package/src/attachment/useAttachments.ts +143 -11
- package/src/channel/AgentChannelsPanel.tsx +26 -1
- package/src/channel/ChannelRunConfigDialog.tsx +351 -0
- package/src/channel/__tests__/ChannelRunConfigDialog.test.tsx +210 -0
- package/src/channel/index.ts +2 -0
- package/src/channel/useSaveAgentChannel.ts +6 -3
- package/src/channel-app/ChannelAppDetailPanel.tsx +9 -9
- package/src/channel-app/__tests__/channelAppWhatsApp.test.tsx +2 -0
- package/src/channel-app/__tests__/channelAppWipeSafety.test.tsx +87 -0
- package/src/composer/ComposerToolbar.tsx +9 -0
- package/src/composer/SessionComposer.tsx +59 -16
- package/src/composer/__tests__/SessionComposer-thinkingMode.test.tsx +176 -0
- package/src/conversation/ConversationComposer.tsx +38 -4
- package/src/conversation/ConversationTemplatePickerDialog.tsx +441 -0
- package/src/conversation/ConversationsWorkbench.tsx +40 -4
- package/src/conversation/__tests__/ConversationComposer.test.tsx +56 -1
- package/src/conversation/__tests__/ConversationTemplatePickerDialog.test.tsx +224 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +45 -4
- package/src/conversation/index.ts +4 -0
- package/src/conversation/useConversationParticipation.ts +58 -6
- package/src/cursor-accounts/CursorAccountsConsole.tsx +40 -5
- package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +32 -0
- package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +58 -1
- package/src/cursor-accounts/cursor-account-coverage.ts +55 -0
- package/src/execution/useCreateAgentExecution.ts +19 -0
- package/src/identity-account/AccountPreferencesPanel.tsx +532 -0
- package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +267 -0
- package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +116 -0
- package/src/identity-account/__tests__/useMyIdentityAccount.test.tsx +121 -0
- package/src/identity-account/index.ts +11 -0
- package/src/identity-account/useAccountExecutionDefaults.ts +63 -0
- package/src/identity-account/useMyIdentityAccount.ts +75 -0
- package/src/identity-account/useUpdateIdentityAccount.ts +73 -0
- package/src/identity-provider/IdentityProviderDetailPanel.tsx +18 -15
- package/src/identity-provider/__tests__/IdentityProviderDetailPanel.test.tsx +80 -0
- package/src/index.ts +26 -4
- package/src/internal/StandingContextField.tsx +92 -0
- package/src/library/InstanceVisibilitySelector.tsx +9 -2
- package/src/library/ResourceVisibilityControl.tsx +46 -13
- package/src/library/VisibilityOptionRow.tsx +28 -6
- package/src/library/VisibilitySelector.tsx +14 -2
- package/src/library/__tests__/VisibilitySelector.test.tsx +48 -1
- package/src/library/__tests__/visibilityLevels.test.ts +67 -1
- package/src/library/index.ts +6 -1
- package/src/library/useCanSetPublicVisibility.ts +56 -0
- package/src/library/visibilityLevels.ts +65 -14
- package/src/mcp-server/McpServerDetailView.tsx +2 -2
- package/src/mcp-server/__tests__/useMcpServerConnect.asyncLane.test.tsx +117 -0
- package/src/mcp-server/index.ts +0 -2
- package/src/mcp-server/internal/definitionDrift.ts +22 -20
- package/src/mcp-server/useMcpServerConnect.ts +9 -6
- package/src/mcp-server/useMcpServerOAuthConnect.ts +7 -2
- package/src/mcp-server/useUpdateMcpServer.ts +3 -3
- package/src/models/ModelSelector.tsx +94 -1
- package/src/models/index.ts +2 -0
- package/src/models/registry.ts +29 -0
- package/src/models/thinking-mode.ts +48 -0
- package/src/oauth-app/CreateOAuthAppForm.tsx +58 -3
- package/src/oauth-app/OAuthAppDetailPanel.tsx +103 -20
- package/src/oauth-app/__tests__/OAuthAppDetailPanel.test.tsx +94 -0
- package/src/organization/OrgPreferencesPanel.tsx +221 -0
- package/src/organization/OrgProfilePanel.tsx +5 -4
- package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +145 -0
- package/src/organization/__tests__/OrgProfilePanel.test.tsx +108 -0
- package/src/organization/index.ts +2 -0
- package/src/platform-client/PlatformClientDetailPanel.tsx +14 -13
- package/src/platform-client/__tests__/PlatformClientDetailPanel.test.tsx +82 -0
- package/src/schedule/ScheduleDetailView.tsx +29 -4
- package/src/schedule/ScheduleForm.tsx +14 -1
- package/src/session/NewSessionViewer.tsx +12 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +151 -0
- package/src/session/run-config.ts +16 -0
- package/src/session/useNewSessionFlow.ts +82 -9
- package/src/session/useSessionConversation.ts +32 -40
- package/src/session/useSessionPageFlow.ts +14 -11
- package/src/session/useUpdateSession.ts +18 -24
- package/src/settings/AccountPreferencesSection.tsx +25 -0
- package/src/settings/OrgPreferencesSection.tsx +35 -0
- package/src/settings/__tests__/settings-nav.test.ts +27 -0
- package/src/settings/index.ts +2 -0
- package/src/settings/settings-nav.ts +18 -0
- package/src/workflow/ApprovalFormBuilder.tsx +20 -27
- package/src/workflow/WorkflowDetailView.tsx +2 -2
- package/src/workflow/__tests__/WorkflowInstanceDetailPanel.test.tsx +83 -0
- package/src/workflow/index.ts +0 -2
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +6 -7
- package/src/workflow/useUpdateWorkflow.ts +3 -3
- package/styles.css +1 -1
- package/workflow/ApprovalFormBuilder.d.ts.map +1 -1
- package/workflow/ApprovalFormBuilder.js +10 -5
- package/workflow/ApprovalFormBuilder.js.map +1 -1
- package/workflow/WorkflowDetailView.js +2 -2
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/index.d.ts +0 -1
- package/workflow/index.d.ts.map +1 -1
- package/workflow/index.js +0 -1
- package/workflow/index.js.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -7
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/useUpdateWorkflow.d.ts +3 -3
- package/workflow/useUpdateWorkflow.js +3 -3
- package/agent/internal/agentToInput.d.ts +0 -15
- package/agent/internal/agentToInput.d.ts.map +0 -1
- package/agent/internal/agentToInput.js +0 -84
- package/agent/internal/agentToInput.js.map +0 -1
- package/mcp-server/internal/mcpServerToInput.d.ts +0 -12
- package/mcp-server/internal/mcpServerToInput.d.ts.map +0 -1
- package/mcp-server/internal/mcpServerToInput.js +0 -84
- package/mcp-server/internal/mcpServerToInput.js.map +0 -1
- package/session/session-spec-converters.d.ts +0 -24
- package/session/session-spec-converters.d.ts.map +0 -1
- package/session/session-spec-converters.js +0 -72
- package/session/session-spec-converters.js.map +0 -1
- package/src/agent/internal/__tests__/agentToInput.test.ts +0 -145
- package/src/agent/internal/agentToInput.ts +0 -102
- package/src/mcp-server/__tests__/mcpServerToInput.test.ts +0 -201
- package/src/mcp-server/internal/mcpServerToInput.ts +0 -98
- package/src/session/__tests__/session-spec-converters.test.ts +0 -183
- package/src/session/session-spec-converters.ts +0 -86
- package/src/workflow/internal/workflowToInput.ts +0 -96
- package/workflow/internal/workflowToInput.d.ts +0 -12
- package/workflow/internal/workflowToInput.d.ts.map +0 -1
- package/workflow/internal/workflowToInput.js +0 -73
- package/workflow/internal/workflowToInput.js.map +0 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type FormEvent, useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage, toOrganizationUpdateInput } from "@stigmer/sdk";
|
|
6
|
+
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
7
|
+
import { useOrganization } from "./useOrganization.js";
|
|
8
|
+
import { useUpdateOrganization } from "./useUpdateOrganization.js";
|
|
9
|
+
import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
|
|
10
|
+
import { StandingContextField } from "../internal/StandingContextField.js";
|
|
11
|
+
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
12
|
+
|
|
13
|
+
/** Props for {@link OrgPreferencesPanel}. */
|
|
14
|
+
export interface OrgPreferencesPanelProps {
|
|
15
|
+
/** The ID of the organization whose preferences to display and edit. */
|
|
16
|
+
readonly orgId: string;
|
|
17
|
+
/** Fired with the updated resource after a successful save. */
|
|
18
|
+
readonly onUpdated?: (org: Organization) => void;
|
|
19
|
+
/** Additional CSS class names for the root container. */
|
|
20
|
+
readonly className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Self-contained editor for an {@link Organization}'s declared
|
|
25
|
+
* preferences (`spec.preferences.standing_context`).
|
|
26
|
+
*
|
|
27
|
+
* The declared text is snapshotted into every eligible agent execution
|
|
28
|
+
* run by the organization's members (first-party human operators only)
|
|
29
|
+
* and delivered to the agent as background context. Editing requires
|
|
30
|
+
* `can_edit` on the organization; viewers without it get a read-only
|
|
31
|
+
* rendering. On save, calls `organization.update()` with the complete
|
|
32
|
+
* mapped input (full-spec-replace safety) and fires `onUpdated`.
|
|
33
|
+
*
|
|
34
|
+
* All visual properties flow through `--stgm-*` design tokens. Zero
|
|
35
|
+
* dependencies on Console routing, auth context, or layout — platform
|
|
36
|
+
* builders can embed it directly:
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <OrgPreferencesPanel orgId="org-id-123" />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function OrgPreferencesPanel({
|
|
44
|
+
orgId,
|
|
45
|
+
onUpdated,
|
|
46
|
+
className,
|
|
47
|
+
}: OrgPreferencesPanelProps) {
|
|
48
|
+
const baseId = useId();
|
|
49
|
+
const {
|
|
50
|
+
organization,
|
|
51
|
+
isLoading: isFetching,
|
|
52
|
+
error: fetchError,
|
|
53
|
+
refetch,
|
|
54
|
+
} = useOrganization(orgId || null);
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
update,
|
|
58
|
+
isUpdating,
|
|
59
|
+
error: updateError,
|
|
60
|
+
clearError,
|
|
61
|
+
} = useUpdateOrganization();
|
|
62
|
+
|
|
63
|
+
// Fail-open: OSS local mode has no IAM service, so editing stays
|
|
64
|
+
// available there; cloud gets a genuine server verdict.
|
|
65
|
+
const { allowed: canEdit } = useCheckPermission(
|
|
66
|
+
orgId ? { kind: "organization", id: orgId } : null,
|
|
67
|
+
"can_edit",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const [standingContext, setStandingContext] = useState("");
|
|
71
|
+
|
|
72
|
+
const serverStandingContext =
|
|
73
|
+
organization?.spec?.preferences?.standingContext ?? "";
|
|
74
|
+
|
|
75
|
+
// Sync the form field when server data changes.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!organization) return;
|
|
78
|
+
setStandingContext(
|
|
79
|
+
organization.spec?.preferences?.standingContext ?? "",
|
|
80
|
+
);
|
|
81
|
+
}, [organization]);
|
|
82
|
+
|
|
83
|
+
const hasChanges = useMemo(
|
|
84
|
+
() => standingContext.trim() !== serverStandingContext,
|
|
85
|
+
[standingContext, serverStandingContext],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const canSubmit = canEdit && hasChanges && !isUpdating;
|
|
89
|
+
|
|
90
|
+
const handleDiscard = useCallback(() => {
|
|
91
|
+
setStandingContext(serverStandingContext);
|
|
92
|
+
clearError();
|
|
93
|
+
}, [serverStandingContext, clearError]);
|
|
94
|
+
|
|
95
|
+
const handleSubmit = useCallback(
|
|
96
|
+
async (e: FormEvent) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
if (!canSubmit || !organization) return;
|
|
99
|
+
|
|
100
|
+
clearError();
|
|
101
|
+
try {
|
|
102
|
+
// update() is a full-spec replace: spread the complete mapped input
|
|
103
|
+
// so unedited spec fields survive, and override only preferences.
|
|
104
|
+
const updated = await update({
|
|
105
|
+
...toOrganizationUpdateInput(organization),
|
|
106
|
+
preferences: { standingContext: standingContext.trim() || undefined },
|
|
107
|
+
});
|
|
108
|
+
refetch();
|
|
109
|
+
onUpdated?.(updated);
|
|
110
|
+
} catch {
|
|
111
|
+
// error state is managed by useUpdateOrganization
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[canSubmit, organization, standingContext, update, clearError, refetch, onUpdated],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
// Loading state
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
if (isFetching && !organization) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
className={cn("stg:space-y-4", className)}
|
|
125
|
+
aria-busy="true"
|
|
126
|
+
aria-label="Loading organization preferences"
|
|
127
|
+
>
|
|
128
|
+
<div className="stg:bg-muted-subtle stg:h-28 stg:animate-pulse stg:rounded" />
|
|
129
|
+
<div className="stg:bg-muted-subtle stg:h-8 stg:w-32 stg:animate-pulse stg:rounded" />
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -----------------------------------------------------------------------
|
|
135
|
+
// Fetch error
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
if (fetchError) {
|
|
139
|
+
return (
|
|
140
|
+
<div className={cn("stg:space-y-3", className)} role="alert">
|
|
141
|
+
<p className="stg:text-destructive stg:text-sm">
|
|
142
|
+
{getUserMessage(fetchError)}
|
|
143
|
+
</p>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={refetch}
|
|
147
|
+
className={cn(
|
|
148
|
+
"stg:rounded-md stg:px-3 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
149
|
+
"stg:bg-primary stg:text-primary-foreground stg:hover:bg-primary-hover",
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
Retry
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!organization) return null;
|
|
159
|
+
|
|
160
|
+
// -----------------------------------------------------------------------
|
|
161
|
+
// Render
|
|
162
|
+
// -----------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<form onSubmit={handleSubmit} className={cn("stg:space-y-4", className)}>
|
|
166
|
+
<StandingContextField
|
|
167
|
+
id={`${baseId}-standing-context`}
|
|
168
|
+
value={standingContext}
|
|
169
|
+
onChange={setStandingContext}
|
|
170
|
+
disabled={isUpdating}
|
|
171
|
+
readOnly={!canEdit}
|
|
172
|
+
placeholder={
|
|
173
|
+
"e.g. We deploy to us-east-1. Our stack is Go and TypeScript. Prefer concise answers."
|
|
174
|
+
}
|
|
175
|
+
helperText="Shared with agents as background context — not instructions. Applies to executions started by signed-in members of this organization and is visible on their execution records."
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
{!canEdit && (
|
|
179
|
+
<p className="stg:text-[0.65rem] stg:text-muted-foreground">
|
|
180
|
+
Only organization admins can edit these preferences.
|
|
181
|
+
</p>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{updateError && (
|
|
185
|
+
<p className="stg:text-destructive stg:text-[0.65rem]" role="alert">
|
|
186
|
+
{getUserMessage(updateError)}
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{canEdit && (
|
|
191
|
+
<div className="stg:flex stg:items-center stg:gap-2">
|
|
192
|
+
<button
|
|
193
|
+
type="submit"
|
|
194
|
+
disabled={!canSubmit}
|
|
195
|
+
className={cn(
|
|
196
|
+
"stg:inline-flex stg:items-center stg:gap-1.5 stg:rounded-md stg:px-3 stg:py-1.5 stg:text-xs stg:font-medium",
|
|
197
|
+
"stg:bg-primary stg:text-primary-foreground stg:hover:bg-primary-hover",
|
|
198
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-40",
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
{isUpdating && <SpinnerIcon size={12} />}
|
|
202
|
+
Save changes
|
|
203
|
+
</button>
|
|
204
|
+
|
|
205
|
+
{hasChanges && !isUpdating && (
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={handleDiscard}
|
|
209
|
+
className={cn(
|
|
210
|
+
"stg:rounded-md stg:px-3 stg:py-1.5 stg:text-xs",
|
|
211
|
+
"stg:text-muted-foreground stg:hover:text-foreground stg:hover:bg-accent-hover",
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
Discard
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</form>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { type FormEvent, useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
-
import { getUserMessage } from "@stigmer/sdk";
|
|
5
|
+
import { getUserMessage, toOrganizationUpdateInput } from "@stigmer/sdk";
|
|
6
6
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
7
7
|
import { useOrganization } from "./useOrganization.js";
|
|
8
8
|
import { useUpdateOrganization } from "./useUpdateOrganization.js";
|
|
@@ -116,10 +116,12 @@ export function OrgProfilePanel({
|
|
|
116
116
|
|
|
117
117
|
clearError();
|
|
118
118
|
try {
|
|
119
|
+
// update() is a full-spec replace: spread the complete mapped input
|
|
120
|
+
// so unedited spec fields (e.g. preferences) survive the save, and
|
|
121
|
+
// override only the fields this form edits.
|
|
119
122
|
const updated = await update({
|
|
123
|
+
...toOrganizationUpdateInput(organization),
|
|
120
124
|
name: name.trim(),
|
|
121
|
-
slug: serverSlug,
|
|
122
|
-
org: serverSlug,
|
|
123
125
|
description: description.trim() || undefined,
|
|
124
126
|
logoUrl: logoUrl.trim() || undefined,
|
|
125
127
|
});
|
|
@@ -135,7 +137,6 @@ export function OrgProfilePanel({
|
|
|
135
137
|
name,
|
|
136
138
|
description,
|
|
137
139
|
logoUrl,
|
|
138
|
-
serverSlug,
|
|
139
140
|
update,
|
|
140
141
|
clearError,
|
|
141
142
|
refetch,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
OrganizationSchema,
|
|
6
|
+
type Organization,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
8
|
+
import type { OrganizationInput } from "@stigmer/sdk";
|
|
9
|
+
import { StigmerContext } from "../../context";
|
|
10
|
+
import { DeploymentModeContext } from "../../deployment-mode";
|
|
11
|
+
import { OrgPreferencesPanel } from "../OrgPreferencesPanel";
|
|
12
|
+
|
|
13
|
+
const ORG: Organization = create(OrganizationSchema, {
|
|
14
|
+
metadata: { id: "acme", name: "Acme Corp", slug: "acme", org: "acme" },
|
|
15
|
+
spec: {
|
|
16
|
+
description: "We make everything.",
|
|
17
|
+
logoUrl: "https://acme.example/logo.png",
|
|
18
|
+
preferences: { standingContext: "We deploy to us-east-1." },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function createMockStigmer(overrides?: {
|
|
23
|
+
update?: ReturnType<typeof vi.fn>;
|
|
24
|
+
canEdit?: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
return {
|
|
27
|
+
organization: {
|
|
28
|
+
get: vi.fn(async () => ORG),
|
|
29
|
+
update: overrides?.update ?? vi.fn(async () => ORG),
|
|
30
|
+
},
|
|
31
|
+
iamPolicy: {
|
|
32
|
+
checkMyPermission: vi.fn(async () => ({
|
|
33
|
+
isAuthorized: overrides?.canEdit ?? true,
|
|
34
|
+
})),
|
|
35
|
+
},
|
|
36
|
+
} as never;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderPanel(client: unknown) {
|
|
40
|
+
return render(
|
|
41
|
+
<StigmerContext.Provider value={client as never}>
|
|
42
|
+
<DeploymentModeContext.Provider value="local">
|
|
43
|
+
<OrgPreferencesPanel orgId="acme" />
|
|
44
|
+
</DeploymentModeContext.Provider>
|
|
45
|
+
</StigmerContext.Provider>,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
afterEach(cleanup);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Waits until the form has synced from server data (the panel copies the
|
|
53
|
+
* fetched value into local state in a passive effect, one flush after the
|
|
54
|
+
* field first renders) — interacting earlier races the sync.
|
|
55
|
+
*/
|
|
56
|
+
async function findSyncedField(value: string) {
|
|
57
|
+
const field = await screen.findByLabelText("Standing context");
|
|
58
|
+
await waitFor(() => expect(field).toHaveProperty("value", value));
|
|
59
|
+
return field;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("OrgPreferencesPanel", () => {
|
|
63
|
+
it("loads and displays the declared standing context", async () => {
|
|
64
|
+
renderPanel(createMockStigmer());
|
|
65
|
+
|
|
66
|
+
await findSyncedField("We deploy to us-east-1.");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("saves the full mapped input — unedited profile fields survive (wipe-bug guard)", async () => {
|
|
70
|
+
const update = vi.fn(async (_input: OrganizationInput) => ORG);
|
|
71
|
+
renderPanel(createMockStigmer({ update }));
|
|
72
|
+
|
|
73
|
+
const field = await findSyncedField("We deploy to us-east-1.");
|
|
74
|
+
fireEvent.change(field, { target: { value: "Prefer terse answers." } });
|
|
75
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
76
|
+
|
|
77
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
78
|
+
const input = update.mock.calls[0]![0];
|
|
79
|
+
|
|
80
|
+
expect(input.preferences).toEqual({
|
|
81
|
+
standingContext: "Prefer terse answers.",
|
|
82
|
+
});
|
|
83
|
+
// Fields this form never renders must round-trip untouched.
|
|
84
|
+
expect(input.name).toBe("Acme Corp");
|
|
85
|
+
expect(input.description).toBe("We make everything.");
|
|
86
|
+
expect(input.logoUrl).toBe("https://acme.example/logo.png");
|
|
87
|
+
expect(input.org).toBe("acme");
|
|
88
|
+
expect(input.slug).toBe("acme");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("clears the standing context when the text is emptied", async () => {
|
|
92
|
+
const update = vi.fn(async (_input: OrganizationInput) => ORG);
|
|
93
|
+
renderPanel(createMockStigmer({ update }));
|
|
94
|
+
|
|
95
|
+
const field = await findSyncedField("We deploy to us-east-1.");
|
|
96
|
+
fireEvent.change(field, { target: { value: "" } });
|
|
97
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
98
|
+
|
|
99
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
100
|
+
expect(update.mock.calls[0]![0].preferences).toEqual({
|
|
101
|
+
standingContext: undefined,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("discard resets the field to the server value", async () => {
|
|
106
|
+
renderPanel(createMockStigmer());
|
|
107
|
+
|
|
108
|
+
const field = await findSyncedField("We deploy to us-east-1.");
|
|
109
|
+
fireEvent.change(field, { target: { value: "Edited but regretted" } });
|
|
110
|
+
fireEvent.click(screen.getByRole("button", { name: "Discard" }));
|
|
111
|
+
|
|
112
|
+
expect(field).toHaveProperty("value", "We deploy to us-east-1.");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders read-only with an explanation when the caller lacks can_edit", async () => {
|
|
116
|
+
renderPanel(createMockStigmer({ canEdit: false }));
|
|
117
|
+
|
|
118
|
+
const field = await screen.findByLabelText("Standing context");
|
|
119
|
+
await waitFor(() =>
|
|
120
|
+
expect(field).toHaveProperty("readOnly", true),
|
|
121
|
+
);
|
|
122
|
+
expect(
|
|
123
|
+
screen.getByText("Only organization admins can edit these preferences."),
|
|
124
|
+
).toBeTruthy();
|
|
125
|
+
expect(screen.queryByRole("button", { name: "Save changes" })).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("shows the fetch error with a retry action when loading fails", async () => {
|
|
129
|
+
const client = {
|
|
130
|
+
organization: {
|
|
131
|
+
get: vi.fn(async () => {
|
|
132
|
+
throw new Error("boom");
|
|
133
|
+
}),
|
|
134
|
+
update: vi.fn(),
|
|
135
|
+
},
|
|
136
|
+
iamPolicy: {
|
|
137
|
+
checkMyPermission: vi.fn(async () => ({ isAuthorized: true })),
|
|
138
|
+
},
|
|
139
|
+
} as never;
|
|
140
|
+
renderPanel(client);
|
|
141
|
+
|
|
142
|
+
expect(await screen.findByRole("alert")).toBeTruthy();
|
|
143
|
+
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
OrganizationSchema,
|
|
6
|
+
type Organization,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
8
|
+
import type { OrganizationInput } from "@stigmer/sdk";
|
|
9
|
+
import { StigmerContext } from "../../context";
|
|
10
|
+
import { DeploymentModeContext } from "../../deployment-mode";
|
|
11
|
+
import { OrgProfilePanel } from "../OrgProfilePanel";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Regression suite for the full-spec-replace wipe bug (oss#293 Phase 1):
|
|
15
|
+
* `organization.update()` wholesale replaces the stored spec, so a profile
|
|
16
|
+
* save that sends only the edited fields silently wipes every other mutable
|
|
17
|
+
* spec field — most visibly `spec.preferences.standing_context` set via the
|
|
18
|
+
* CLI or the preferences page. The panel must spread the complete mapped
|
|
19
|
+
* input and override only what it edits.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const ORG: Organization = create(OrganizationSchema, {
|
|
23
|
+
metadata: {
|
|
24
|
+
id: "acme",
|
|
25
|
+
name: "Acme Corp",
|
|
26
|
+
slug: "acme",
|
|
27
|
+
org: "acme",
|
|
28
|
+
},
|
|
29
|
+
spec: {
|
|
30
|
+
description: "We make everything.",
|
|
31
|
+
logoUrl: "https://acme.example/logo.png",
|
|
32
|
+
preferences: { standingContext: "We deploy to us-east-1." },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function createMockStigmer(overrides?: {
|
|
37
|
+
update?: ReturnType<typeof vi.fn>;
|
|
38
|
+
}) {
|
|
39
|
+
return {
|
|
40
|
+
organization: {
|
|
41
|
+
get: vi.fn(async () => ORG),
|
|
42
|
+
update: overrides?.update ?? vi.fn(async () => ORG),
|
|
43
|
+
},
|
|
44
|
+
} as never;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Deployment mode "local" keeps the IdentityProvidersSummary sub-panel
|
|
48
|
+
// inert (identity_provider is cloud-only), so no extra client stubs are
|
|
49
|
+
// needed — the suite tests the form, not the summary.
|
|
50
|
+
function renderPanel(client: unknown) {
|
|
51
|
+
return render(
|
|
52
|
+
<StigmerContext.Provider value={client as never}>
|
|
53
|
+
<DeploymentModeContext.Provider value="local">
|
|
54
|
+
<OrgProfilePanel orgId="acme" />
|
|
55
|
+
</DeploymentModeContext.Provider>
|
|
56
|
+
</StigmerContext.Provider>,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
afterEach(cleanup);
|
|
61
|
+
|
|
62
|
+
describe("OrgProfilePanel save payload", () => {
|
|
63
|
+
it("round-trips unedited spec fields — preferences survive a profile save", async () => {
|
|
64
|
+
const update = vi.fn(async (_input: OrganizationInput) => ORG);
|
|
65
|
+
const client = createMockStigmer({ update });
|
|
66
|
+
renderPanel(client);
|
|
67
|
+
|
|
68
|
+
// Wait for the server-sync effect to fill the form before editing —
|
|
69
|
+
// interacting on first appearance races the sync.
|
|
70
|
+
const description = await screen.findByLabelText("Description");
|
|
71
|
+
await waitFor(() =>
|
|
72
|
+
expect(description).toHaveProperty("value", "We make everything."),
|
|
73
|
+
);
|
|
74
|
+
fireEvent.change(description, { target: { value: "New description" } });
|
|
75
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
76
|
+
|
|
77
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
78
|
+
const input = update.mock.calls[0]![0];
|
|
79
|
+
|
|
80
|
+
// The edited field.
|
|
81
|
+
expect(input.description).toBe("New description");
|
|
82
|
+
// The unedited fields the form does not render — the wipe-bug guard.
|
|
83
|
+
expect(input.preferences).toEqual({
|
|
84
|
+
standingContext: "We deploy to us-east-1.",
|
|
85
|
+
});
|
|
86
|
+
expect(input.logoUrl).toBe("https://acme.example/logo.png");
|
|
87
|
+
// Addressing fields for the update pipeline's org+slug lookup.
|
|
88
|
+
expect(input.org).toBe("acme");
|
|
89
|
+
expect(input.slug).toBe("acme");
|
|
90
|
+
expect(input.name).toBe("Acme Corp");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("sends the edited name and keeps discard/dirty semantics intact", async () => {
|
|
94
|
+
const update = vi.fn(async (_input: OrganizationInput) => ORG);
|
|
95
|
+
renderPanel(createMockStigmer({ update }));
|
|
96
|
+
|
|
97
|
+
const name = await screen.findByLabelText("Name");
|
|
98
|
+
await waitFor(() => expect(name).toHaveProperty("value", "Acme Corp"));
|
|
99
|
+
fireEvent.change(name, { target: { value: "Acme Corporation" } });
|
|
100
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
101
|
+
|
|
102
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
103
|
+
expect(update.mock.calls[0]![0].name).toBe("Acme Corporation");
|
|
104
|
+
expect(update.mock.calls[0]![0].preferences).toEqual({
|
|
105
|
+
standingContext: "We deploy to us-east-1.",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -16,5 +16,7 @@ export { CreateOrganizationForm } from "./CreateOrganizationForm.js";
|
|
|
16
16
|
export type { CreateOrganizationFormProps } from "./CreateOrganizationForm.js";
|
|
17
17
|
export { OrgProfilePanel } from "./OrgProfilePanel.js";
|
|
18
18
|
export type { OrgProfilePanelProps } from "./OrgProfilePanel.js";
|
|
19
|
+
export { OrgPreferencesPanel } from "./OrgPreferencesPanel.js";
|
|
20
|
+
export type { OrgPreferencesPanelProps } from "./OrgPreferencesPanel.js";
|
|
19
21
|
export { OrgSwitcher } from "./OrgSwitcher.js";
|
|
20
22
|
export type { OrgSwitcherProps } from "./OrgSwitcher.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { type FormEvent, type KeyboardEvent, useCallback, useId, useState } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import { UNSTYLED_FIELDSET } from "../internal/element-resets.js";
|
|
6
|
-
import { getUserMessage } from "@stigmer/sdk";
|
|
6
|
+
import { getUserMessage, toPlatformClientUpdateInput } from "@stigmer/sdk";
|
|
7
7
|
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
8
8
|
import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";
|
|
9
9
|
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
@@ -176,21 +176,22 @@ export function PlatformClientDetailPanel({
|
|
|
176
176
|
e.preventDefault();
|
|
177
177
|
clearUpdateError();
|
|
178
178
|
try {
|
|
179
|
+
// Full-spec-replace safety: spread the complete mapped input and
|
|
180
|
+
// override only the edited fields, so unlisted spec fields (e.g.
|
|
181
|
+
// environment_refs, the embedded-assistant credential binding)
|
|
182
|
+
// survive the save. Cleared fields are set to undefined
|
|
183
|
+
// explicitly — omitting them would carry the stale mapped value.
|
|
179
184
|
const updated = await update({
|
|
180
|
-
|
|
181
|
-
slug: meta?.slug,
|
|
182
|
-
org: meta?.org ?? "",
|
|
185
|
+
...toPlatformClientUpdateInput(platformClient),
|
|
183
186
|
neverExpires,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
187
|
-
}),
|
|
187
|
+
expiresAt:
|
|
188
|
+
!neverExpires && expiresAt ? new Date(expiresAt) : undefined,
|
|
188
189
|
autoProvisionAccounts: autoProvision,
|
|
189
190
|
autoGrantOnOrg: autoGrant,
|
|
190
|
-
|
|
191
|
-
autoGrantRole !== IamRole.iam_role_unspecified
|
|
192
|
-
autoGrantRole
|
|
193
|
-
|
|
191
|
+
autoGrantRole:
|
|
192
|
+
autoGrant && autoGrantRole !== IamRole.iam_role_unspecified
|
|
193
|
+
? autoGrantRole
|
|
194
|
+
: undefined,
|
|
194
195
|
allowedOrigins: origins,
|
|
195
196
|
});
|
|
196
197
|
setMode("view");
|
|
@@ -200,7 +201,7 @@ export function PlatformClientDetailPanel({
|
|
|
200
201
|
}
|
|
201
202
|
},
|
|
202
203
|
[
|
|
203
|
-
|
|
204
|
+
platformClient,
|
|
204
205
|
neverExpires,
|
|
205
206
|
expiresAt,
|
|
206
207
|
autoProvision,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
5
|
+
import {
|
|
6
|
+
PlatformClientSchema,
|
|
7
|
+
type PlatformClient,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
9
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
10
|
+
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
11
|
+
import type { PlatformClientInput } from "@stigmer/sdk";
|
|
12
|
+
import { StigmerContext } from "../../context";
|
|
13
|
+
import { PlatformClientDetailPanel } from "../PlatformClientDetailPanel";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Regression suite for the full-spec-replace wipe bug: before the
|
|
17
|
+
* generated-mapper migration, this panel hand-built its update input and
|
|
18
|
+
* NEVER sent `environment_refs` — saving any edit silently wiped the
|
|
19
|
+
* client's credential-delivery environment bindings. The panel must
|
|
20
|
+
* spread `toPlatformClientUpdateInput` and override only what it edits.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PLATFORM_CLIENT: PlatformClient = create(PlatformClientSchema, {
|
|
24
|
+
metadata: {
|
|
25
|
+
id: "pc-1",
|
|
26
|
+
name: "Embed Client",
|
|
27
|
+
slug: "embed-client",
|
|
28
|
+
org: "acme",
|
|
29
|
+
},
|
|
30
|
+
spec: {
|
|
31
|
+
clientId: "client-abc",
|
|
32
|
+
expiresAt: timestampFromDate(new Date("2027-06-01T00:00:00Z")),
|
|
33
|
+
neverExpires: false,
|
|
34
|
+
autoProvisionAccounts: true,
|
|
35
|
+
autoGrantOnOrg: true,
|
|
36
|
+
autoGrantRole: IamRole.admin,
|
|
37
|
+
allowedOrigins: ["https://embed.acme.example"],
|
|
38
|
+
environmentRefs: [
|
|
39
|
+
{ org: "acme", slug: "prod", kind: ApiResourceKind.environment },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function renderPanel(update: ReturnType<typeof vi.fn>) {
|
|
45
|
+
const client = {
|
|
46
|
+
platformclient: { update },
|
|
47
|
+
} as never;
|
|
48
|
+
return render(
|
|
49
|
+
<StigmerContext.Provider value={client}>
|
|
50
|
+
<PlatformClientDetailPanel platformClient={PLATFORM_CLIENT} />
|
|
51
|
+
</StigmerContext.Provider>,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
afterEach(cleanup);
|
|
56
|
+
|
|
57
|
+
describe("PlatformClientDetailPanel save payload", () => {
|
|
58
|
+
it("round-trips environment_refs on an origins-only edit (the wipe bug)", async () => {
|
|
59
|
+
const update = vi.fn(async (_input: PlatformClientInput) => PLATFORM_CLIENT);
|
|
60
|
+
renderPanel(update);
|
|
61
|
+
|
|
62
|
+
fireEvent.click(screen.getByRole("button", { name: "Edit" }));
|
|
63
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
64
|
+
|
|
65
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
66
|
+
const input = update.mock.calls[0]![0];
|
|
67
|
+
|
|
68
|
+
// The wipe-bug guard: the form does not render environment bindings,
|
|
69
|
+
// yet they must survive the save.
|
|
70
|
+
expect(input.environmentRefs).toEqual([
|
|
71
|
+
{ org: "acme", slug: "prod", kind: ApiResourceKind.environment },
|
|
72
|
+
]);
|
|
73
|
+
// Form-owned fields round-trip from the edit state.
|
|
74
|
+
expect(input.allowedOrigins).toEqual(["https://embed.acme.example"]);
|
|
75
|
+
expect(input.autoGrantRole).toBe(IamRole.admin);
|
|
76
|
+
expect(input.expiresAt).toBeInstanceOf(Date);
|
|
77
|
+
// Addressing fields for the update pipeline's org+slug lookup.
|
|
78
|
+
expect(input.org).toBe("acme");
|
|
79
|
+
expect(input.slug).toBe("embed-client");
|
|
80
|
+
expect(input.name).toBe("Embed Client");
|
|
81
|
+
});
|
|
82
|
+
});
|