@codemation/next-host 0.0.1

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 (206) hide show
  1. package/README.md +25 -0
  2. package/app/(shell)/credentials/page.tsx +5 -0
  3. package/app/(shell)/dashboard/page.tsx +14 -0
  4. package/app/(shell)/layout.tsx +11 -0
  5. package/app/(shell)/page.tsx +5 -0
  6. package/app/(shell)/users/page.tsx +5 -0
  7. package/app/(shell)/workflows/[workflowId]/page.tsx +19 -0
  8. package/app/(shell)/workflows/page.tsx +5 -0
  9. package/app/api/[[...path]]/route.ts +40 -0
  10. package/app/api/auth/[...nextauth]/route.ts +3 -0
  11. package/app/globals.css +997 -0
  12. package/app/invite/[token]/page.tsx +10 -0
  13. package/app/layout.tsx +65 -0
  14. package/app/login/layout.tsx +25 -0
  15. package/app/login/page.tsx +22 -0
  16. package/components.json +21 -0
  17. package/docs/FORMS.md +46 -0
  18. package/docs/TAILWIND_SHADCN_MIGRATION.md +89 -0
  19. package/eslint.config.mjs +56 -0
  20. package/middleware.ts +29 -0
  21. package/next-env.d.ts +6 -0
  22. package/next.config.ts +34 -0
  23. package/package.json +76 -0
  24. package/postcss.config.mjs +7 -0
  25. package/public/canvas-icons/builtin/openai.svg +5 -0
  26. package/src/api/CodemationApiClient.ts +107 -0
  27. package/src/api/CodemationApiHttpError.ts +17 -0
  28. package/src/auth/CodemationNextAuthConfigResolver.ts +14 -0
  29. package/src/auth/CodemationNextAuthOAuthProviderDescriptorMapper.ts +30 -0
  30. package/src/auth/CodemationNextAuthOAuthProviderSnapshotResolver.ts +17 -0
  31. package/src/auth/CodemationNextAuthProviderCatalog.ts +107 -0
  32. package/src/auth/codemationEdgeAuth.ts +25 -0
  33. package/src/auth/codemationNextAuth.ts +32 -0
  34. package/src/components/Codemation.tsx +6 -0
  35. package/src/components/CodemationDataTable.tsx +37 -0
  36. package/src/components/CodemationDialog.tsx +137 -0
  37. package/src/components/CodemationFormattedDateTime.tsx +46 -0
  38. package/src/components/GoogleColorGIcon.tsx +39 -0
  39. package/src/components/OauthProviderIcon.tsx +33 -0
  40. package/src/components/PasswordStrengthMeter.tsx +59 -0
  41. package/src/components/forms/index.ts +28 -0
  42. package/src/components/json/JsonMonacoEditor.tsx +75 -0
  43. package/src/components/oauthProviderIconData.ts +17 -0
  44. package/src/components/ui/alert.tsx +56 -0
  45. package/src/components/ui/badge.tsx +40 -0
  46. package/src/components/ui/button.tsx +64 -0
  47. package/src/components/ui/card.tsx +70 -0
  48. package/src/components/ui/collapsible.tsx +26 -0
  49. package/src/components/ui/dialog.tsx +137 -0
  50. package/src/components/ui/dropdown-menu.tsx +238 -0
  51. package/src/components/ui/form.tsx +147 -0
  52. package/src/components/ui/input.tsx +19 -0
  53. package/src/components/ui/label.tsx +26 -0
  54. package/src/components/ui/scroll-area.tsx +47 -0
  55. package/src/components/ui/select.tsx +169 -0
  56. package/src/components/ui/separator.tsx +28 -0
  57. package/src/components/ui/switch.tsx +28 -0
  58. package/src/components/ui/table.tsx +72 -0
  59. package/src/components/ui/tabs.tsx +76 -0
  60. package/src/components/ui/textarea.tsx +18 -0
  61. package/src/components/ui/toggle.tsx +41 -0
  62. package/src/features/credentials/components/CredentialConfirmDialog.tsx +58 -0
  63. package/src/features/credentials/components/CredentialDialog.tsx +252 -0
  64. package/src/features/credentials/components/CredentialDialogFeedback.tsx +36 -0
  65. package/src/features/credentials/components/CredentialDialogFieldRows.tsx +257 -0
  66. package/src/features/credentials/components/CredentialDialogFormSections.tsx +230 -0
  67. package/src/features/credentials/components/CredentialEnvFieldStatusRow.tsx +64 -0
  68. package/src/features/credentials/components/CredentialFieldCopyButton.tsx +48 -0
  69. package/src/features/credentials/components/CredentialsScreenHealthBadge.tsx +21 -0
  70. package/src/features/credentials/components/CredentialsScreenInstancesTable.tsx +108 -0
  71. package/src/features/credentials/components/CredentialsScreenTestFailureAlert.tsx +33 -0
  72. package/src/features/credentials/hooks/useCredentialCreateDialog.ts +33 -0
  73. package/src/features/credentials/hooks/useCredentialDialogSession.ts +616 -0
  74. package/src/features/credentials/hooks/useCredentialsScreen.ts +213 -0
  75. package/src/features/credentials/lib/credentialFieldHelpers.ts +35 -0
  76. package/src/features/credentials/lib/credentialFormTypes.ts +1 -0
  77. package/src/features/credentials/lib/credentialInstanceTestPayloadParser.ts +10 -0
  78. package/src/features/credentials/screens/CredentialsScreen.tsx +187 -0
  79. package/src/features/invite/screens/InviteAcceptScreen.tsx +190 -0
  80. package/src/features/users/components/UsersInviteDialog.tsx +121 -0
  81. package/src/features/users/components/UsersRegenerateDialog.tsx +81 -0
  82. package/src/features/users/components/UsersScreenUserStatusBadge.tsx +19 -0
  83. package/src/features/users/schemas/usersInviteFormSchema.ts +7 -0
  84. package/src/features/users/screens/UsersScreen.tsx +240 -0
  85. package/src/features/workflows/components/WorkflowListFolderSection.tsx +91 -0
  86. package/src/features/workflows/components/WorkflowListItemCard.tsx +67 -0
  87. package/src/features/workflows/components/WorkflowListRoot.tsx +39 -0
  88. package/src/features/workflows/components/WorkflowsListTree.tsx +28 -0
  89. package/src/features/workflows/components/canvas/CanvasNodeChromeTooltip.tsx +96 -0
  90. package/src/features/workflows/components/canvas/CanvasNodeIconSlot.tsx +25 -0
  91. package/src/features/workflows/components/canvas/VisibleNodeStatusResolver.tsx +84 -0
  92. package/src/features/workflows/components/canvas/WorkflowCanvas.tsx +248 -0
  93. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNode.tsx +182 -0
  94. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAccents.tsx +73 -0
  95. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentBottomSourceHandles.tsx +43 -0
  96. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeAgentLabels.tsx +47 -0
  97. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeCard.tsx +202 -0
  98. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeHandles.tsx +77 -0
  99. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeLabelBelow.tsx +51 -0
  100. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeMainGlyph.tsx +64 -0
  101. package/src/features/workflows/components/canvas/WorkflowCanvasCodemationNodeToolbar.tsx +95 -0
  102. package/src/features/workflows/components/canvas/WorkflowCanvasLoadingPlaceholder.tsx +69 -0
  103. package/src/features/workflows/components/canvas/WorkflowCanvasNodeIcon.tsx +102 -0
  104. package/src/features/workflows/components/canvas/WorkflowCanvasSimpleIconGlyph.tsx +21 -0
  105. package/src/features/workflows/components/canvas/WorkflowCanvasStraightCountEdge.tsx +33 -0
  106. package/src/features/workflows/components/canvas/WorkflowCanvasStructureSignature.tsx +7 -0
  107. package/src/features/workflows/components/canvas/WorkflowCanvasSymmetricForkEdge.tsx +32 -0
  108. package/src/features/workflows/components/canvas/WorkflowCanvasToolbarIconButton.tsx +95 -0
  109. package/src/features/workflows/components/canvas/lib/WorkflowCanvasBuiltinIconRegistry.ts +26 -0
  110. package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeCountResolver.ts +51 -0
  111. package/src/features/workflows/components/canvas/lib/WorkflowCanvasEdgeStyleResolver.ts +35 -0
  112. package/src/features/workflows/components/canvas/lib/WorkflowCanvasLabelLayoutEstimator.ts +42 -0
  113. package/src/features/workflows/components/canvas/lib/WorkflowCanvasOverlapResolver.ts +78 -0
  114. package/src/features/workflows/components/canvas/lib/WorkflowCanvasPortOrderResolver.ts +25 -0
  115. package/src/features/workflows/components/canvas/lib/WorkflowCanvasRoundedOrthogonalPathPlanner.ts +56 -0
  116. package/src/features/workflows/components/canvas/lib/WorkflowCanvasSiIconRegistry.ts +18 -0
  117. package/src/features/workflows/components/canvas/lib/WorkflowCanvasSymmetricForkPathPlanner.ts +43 -0
  118. package/src/features/workflows/components/canvas/lib/layoutWorkflow.ts +315 -0
  119. package/src/features/workflows/components/canvas/lib/workflowCanvasEdgeGeometry.ts +3 -0
  120. package/src/features/workflows/components/canvas/lib/workflowCanvasEmbeddedStyles.ts +62 -0
  121. package/src/features/workflows/components/canvas/lib/workflowCanvasFlowTypes.ts +10 -0
  122. package/src/features/workflows/components/canvas/lib/workflowCanvasNodeData.ts +41 -0
  123. package/src/features/workflows/components/canvas/lib/workflowCanvasNodeGeometry.ts +99 -0
  124. package/src/features/workflows/components/canvas/workflowCanvasNodeChrome.tsx +46 -0
  125. package/src/features/workflows/components/realtime/RealtimeContext.tsx +14 -0
  126. package/src/features/workflows/components/realtime/WorkflowRealtimeProvider.tsx +15 -0
  127. package/src/features/workflows/components/workflowDetail/NodeCredentialBindingRow.tsx +209 -0
  128. package/src/features/workflows/components/workflowDetail/NodeCredentialBindingsSection.tsx +227 -0
  129. package/src/features/workflows/components/workflowDetail/NodePropertiesConfigSection.tsx +51 -0
  130. package/src/features/workflows/components/workflowDetail/NodePropertiesPanelHeader.tsx +50 -0
  131. package/src/features/workflows/components/workflowDetail/NodePropertiesSlidePanel.tsx +134 -0
  132. package/src/features/workflows/components/workflowDetail/WorkflowActivationErrorDialog.tsx +71 -0
  133. package/src/features/workflows/components/workflowDetail/WorkflowActivationHeaderControl.tsx +64 -0
  134. package/src/features/workflows/components/workflowDetail/WorkflowDetailIcons.tsx +52 -0
  135. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspector.tsx +110 -0
  136. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorDetailBody.tsx +213 -0
  137. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorPanes.tsx +239 -0
  138. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorSidebarResizer.tsx +31 -0
  139. package/src/features/workflows/components/workflowDetail/WorkflowExecutionInspectorTreePanel.tsx +133 -0
  140. package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentGroupingPresenter.tsx +31 -0
  141. package/src/features/workflows/components/workflowDetail/WorkflowInspectorAttachmentList.tsx +118 -0
  142. package/src/features/workflows/components/workflowDetail/WorkflowInspectorBinaryView.tsx +15 -0
  143. package/src/features/workflows/components/workflowDetail/WorkflowInspectorErrorView.tsx +107 -0
  144. package/src/features/workflows/components/workflowDetail/WorkflowInspectorJsonView.tsx +114 -0
  145. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreePresenter.tsx +132 -0
  146. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyTreeViewRenderer.tsx +147 -0
  147. package/src/features/workflows/components/workflowDetail/WorkflowInspectorPrettyView.tsx +65 -0
  148. package/src/features/workflows/components/workflowDetail/WorkflowInspectorViews.tsx +5 -0
  149. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryAttachmentRow.tsx +74 -0
  150. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorBinaryUploadRow.tsx +69 -0
  151. package/src/features/workflows/components/workflowDetail/WorkflowJsonEditorDialog.tsx +254 -0
  152. package/src/features/workflows/components/workflowDetail/WorkflowRunsList.tsx +89 -0
  153. package/src/features/workflows/components/workflowDetail/WorkflowRunsSidebar.tsx +50 -0
  154. package/src/features/workflows/hooks/canvas/useWorkflowCanvasVisibleNodeStatuses.ts +14 -0
  155. package/src/features/workflows/hooks/realtime/realtime.tsx +271 -0
  156. package/src/features/workflows/hooks/realtime/runQueryPolling.ts +34 -0
  157. package/src/features/workflows/hooks/realtime/useWorkflowRealtimeInfrastructure.ts +541 -0
  158. package/src/features/workflows/hooks/realtime/useWorkflowRealtimeShowDisconnectedBadge.ts +9 -0
  159. package/src/features/workflows/hooks/workflowDetail/useWorkflowDetailController.tsx +1300 -0
  160. package/src/features/workflows/lib/realtime/realtimeApi.ts +78 -0
  161. package/src/features/workflows/lib/realtime/realtimeClientBridge.ts +52 -0
  162. package/src/features/workflows/lib/realtime/realtimeDomainTypes.ts +191 -0
  163. package/src/features/workflows/lib/realtime/realtimeQueryKeys.ts +15 -0
  164. package/src/features/workflows/lib/realtime/realtimeRunMutations.ts +167 -0
  165. package/src/features/workflows/lib/realtime/workflowTypes.ts +5 -0
  166. package/src/features/workflows/lib/workflowDetail/PersistedWorkflowSnapshotMapper.ts +205 -0
  167. package/src/features/workflows/lib/workflowDetail/WorkflowActivationHttpErrorFormat.ts +32 -0
  168. package/src/features/workflows/lib/workflowDetail/WorkflowDetailPresenter.ts +1017 -0
  169. package/src/features/workflows/lib/workflowDetail/WorkflowDetailUrlCodec.ts +70 -0
  170. package/src/features/workflows/lib/workflowDetail/workflowDetailTypes.ts +152 -0
  171. package/src/features/workflows/lib/workflowDetailTreeStyles.ts +65 -0
  172. package/src/features/workflows/screens/WorkflowDetailScreen.tsx +236 -0
  173. package/src/features/workflows/screens/WorkflowDetailScreenInspectorPanel.tsx +55 -0
  174. package/src/features/workflows/screens/WorkflowsList.tsx +35 -0
  175. package/src/features/workflows/screens/WorkflowsScreen.tsx +31 -0
  176. package/src/index.ts +1 -0
  177. package/src/lib/utils.ts +6 -0
  178. package/src/middleware/CodemationNextHostMiddlewarePathRules.ts +31 -0
  179. package/src/providers/CodemationSessionProvider.tsx +23 -0
  180. package/src/providers/Providers.tsx +36 -0
  181. package/src/providers/RealtimeBoundary.tsx +17 -0
  182. package/src/providers/WhitelabelProvider.tsx +22 -0
  183. package/src/server/CodemationAuthPrismaClient.ts +21 -0
  184. package/src/server/CodemationNextHost.ts +379 -0
  185. package/src/shell/AppLayout.tsx +141 -0
  186. package/src/shell/AppLayoutNavItems.tsx +129 -0
  187. package/src/shell/AppLayoutPageHeader.tsx +79 -0
  188. package/src/shell/AppLayoutSidebarBrand.tsx +33 -0
  189. package/src/shell/AppMainContent.tsx +17 -0
  190. package/src/shell/AppShellHeaderActions.tsx +12 -0
  191. package/src/shell/AppShellHeaderActionsAuthenticated.tsx +51 -0
  192. package/src/shell/CodemationNextClientShell.tsx +17 -0
  193. package/src/shell/CredentialsSignInRedirectResolver.ts +21 -0
  194. package/src/shell/LoginPageClient.tsx +231 -0
  195. package/src/shell/WorkflowDetailChromeContext.tsx +42 -0
  196. package/src/shell/WorkflowFolderTreeBuilder.ts +62 -0
  197. package/src/shell/WorkflowFolderUi.ts +42 -0
  198. package/src/shell/WorkflowSidebarNavFolder.tsx +112 -0
  199. package/src/shell/WorkflowSidebarNavTree.tsx +68 -0
  200. package/src/shell/appLayoutPageTitle.ts +16 -0
  201. package/src/shell/appLayoutSidebarIcons.tsx +108 -0
  202. package/src/whitelabel/CodemationWhitelabelSnapshot.ts +4 -0
  203. package/src/whitelabel/CodemationWhitelabelSnapshotFactory.ts +18 -0
  204. package/tsconfig.json +40 -0
  205. package/tsconfig.tsbuildinfo +1 -0
  206. package/vitest.config.ts +34 -0
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { CodemationDialog } from "@/components/CodemationDialog";
5
+ import { Input } from "@/components/ui/input";
6
+ import {
7
+ Form,
8
+ FormControl,
9
+ FormField,
10
+ FormItem,
11
+ FormLabel,
12
+ FormMessage,
13
+ useForm,
14
+ zodResolver,
15
+ } from "@/components/forms";
16
+ import { usersInviteFormSchema, type UsersInviteFormValues } from "../schemas/usersInviteFormSchema";
17
+
18
+ type UsersInviteDialogProps = Readonly<{
19
+ errorMessage: string | null;
20
+ successUrl: string | null;
21
+ isSubmitting: boolean;
22
+ copyFeedback: boolean;
23
+ onSubmit: (email: string) => void | Promise<void>;
24
+ onCopy: () => void;
25
+ onClose: () => void;
26
+ }>;
27
+
28
+ export function UsersInviteDialog({
29
+ errorMessage,
30
+ successUrl,
31
+ isSubmitting,
32
+ copyFeedback,
33
+ onSubmit,
34
+ onCopy,
35
+ onClose,
36
+ }: UsersInviteDialogProps) {
37
+ const form = useForm<UsersInviteFormValues>({
38
+ resolver: zodResolver(usersInviteFormSchema),
39
+ defaultValues: { email: "" },
40
+ });
41
+
42
+ return (
43
+ <CodemationDialog
44
+ onClose={onClose}
45
+ testId="users-invite-dialog"
46
+ size="narrow"
47
+ contentClassName="max-h-[min(90vh,640px)]"
48
+ >
49
+ <CodemationDialog.Title>Invite user</CodemationDialog.Title>
50
+ {successUrl ? (
51
+ <>
52
+ <CodemationDialog.Content className="space-y-3">
53
+ <p className="m-0 text-muted-foreground" data-testid="users-invite-success-message">
54
+ Share this link; it expires in seven days.
55
+ </p>
56
+ <Input
57
+ type="text"
58
+ readOnly
59
+ value={successUrl}
60
+ data-testid="users-invite-link-field"
61
+ className="font-mono text-xs"
62
+ />
63
+ <div className="flex flex-wrap gap-2">
64
+ <Button type="button" variant="secondary" data-testid="users-invite-copy-link" onClick={onCopy}>
65
+ {copyFeedback ? "Copied" : "Copy link"}
66
+ </Button>
67
+ </div>
68
+ </CodemationDialog.Content>
69
+ <CodemationDialog.Actions>
70
+ <Button type="button" variant="outline" data-testid="users-invite-cancel" onClick={onClose}>
71
+ Done
72
+ </Button>
73
+ </CodemationDialog.Actions>
74
+ </>
75
+ ) : (
76
+ <Form {...form}>
77
+ <form
78
+ data-testid="users-invite-form"
79
+ onSubmit={form.handleSubmit((values) => void onSubmit(values.email))}
80
+ className="flex min-h-0 flex-1 flex-col overflow-hidden"
81
+ >
82
+ <CodemationDialog.Content className="space-y-3">
83
+ <FormField
84
+ control={form.control}
85
+ name="email"
86
+ render={({ field }) => (
87
+ <FormItem>
88
+ <FormLabel>Email</FormLabel>
89
+ <FormControl>
90
+ <Input
91
+ type="email"
92
+ data-testid="users-invite-email-input"
93
+ placeholder="colleague@company.com"
94
+ autoComplete="off"
95
+ {...field}
96
+ />
97
+ </FormControl>
98
+ <FormMessage />
99
+ </FormItem>
100
+ )}
101
+ />
102
+ {errorMessage ? (
103
+ <div className="text-sm text-destructive" data-testid="users-invite-error" role="alert">
104
+ {errorMessage}
105
+ </div>
106
+ ) : null}
107
+ </CodemationDialog.Content>
108
+ <CodemationDialog.Actions>
109
+ <Button type="button" variant="outline" data-testid="users-invite-cancel" onClick={onClose}>
110
+ Cancel
111
+ </Button>
112
+ <Button type="submit" data-testid="users-invite-submit" disabled={isSubmitting}>
113
+ {isSubmitting ? "Sending…" : "Create invite"}
114
+ </Button>
115
+ </CodemationDialog.Actions>
116
+ </form>
117
+ </Form>
118
+ )}
119
+ </CodemationDialog>
120
+ );
121
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { CodemationDialog } from "@/components/CodemationDialog";
5
+ import { Input } from "@/components/ui/input";
6
+
7
+ type UsersRegenerateDialogProps = Readonly<{
8
+ email: string;
9
+ newUrl: string | null;
10
+ errorMessage: string | null;
11
+ isSubmitting: boolean;
12
+ copyFeedback: boolean;
13
+ onConfirm: () => void;
14
+ onCopy: () => void;
15
+ onClose: () => void;
16
+ }>;
17
+
18
+ export function UsersRegenerateDialog({
19
+ email,
20
+ newUrl,
21
+ errorMessage,
22
+ isSubmitting,
23
+ copyFeedback,
24
+ onConfirm,
25
+ onCopy,
26
+ onClose,
27
+ }: UsersRegenerateDialogProps) {
28
+ return (
29
+ <CodemationDialog
30
+ onClose={onClose}
31
+ testId="users-regenerate-dialog"
32
+ size="narrow"
33
+ contentClassName="max-h-[min(90vh,640px)]"
34
+ >
35
+ <CodemationDialog.Title>Regenerate invite link</CodemationDialog.Title>
36
+ <CodemationDialog.Content className="space-y-3">
37
+ {newUrl ? (
38
+ <>
39
+ <p className="m-0 text-muted-foreground" data-testid="users-regenerate-success-message">
40
+ New link for {email}. Previous links stop working.
41
+ </p>
42
+ <Input
43
+ type="text"
44
+ readOnly
45
+ value={newUrl}
46
+ data-testid="users-regenerate-link-field"
47
+ className="font-mono text-xs"
48
+ />
49
+ <div className="flex flex-wrap gap-2">
50
+ <Button type="button" variant="secondary" data-testid="users-regenerate-copy-link" onClick={onCopy}>
51
+ {copyFeedback ? "Copied" : "Copy link"}
52
+ </Button>
53
+ </div>
54
+ </>
55
+ ) : (
56
+ <>
57
+ <p className="m-0 text-muted-foreground" data-testid="users-regenerate-confirm-text">
58
+ Generate a new seven-day link for <strong data-testid="users-regenerate-email">{email}</strong>? The
59
+ current invite link will no longer work.
60
+ </p>
61
+ {errorMessage ? (
62
+ <div className="text-sm text-destructive" data-testid="users-regenerate-error">
63
+ {errorMessage}
64
+ </div>
65
+ ) : null}
66
+ </>
67
+ )}
68
+ </CodemationDialog.Content>
69
+ <CodemationDialog.Actions>
70
+ <Button type="button" variant="outline" data-testid="users-regenerate-cancel" onClick={onClose}>
71
+ {newUrl ? "Close" : "Cancel"}
72
+ </Button>
73
+ {!newUrl ? (
74
+ <Button type="button" data-testid="users-regenerate-confirm" disabled={isSubmitting} onClick={onConfirm}>
75
+ {isSubmitting ? "Working…" : "Regenerate"}
76
+ </Button>
77
+ ) : null}
78
+ </CodemationDialog.Actions>
79
+ </CodemationDialog>
80
+ );
81
+ }
@@ -0,0 +1,19 @@
1
+ import type { UserAccountStatus } from "@codemation/host-src/application/contracts/userDirectoryContracts.types";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export function UsersScreenUserStatusBadge(props: Readonly<{ userId: string; status: UserAccountStatus }>) {
7
+ const { status, userId } = props;
8
+ const className =
9
+ status === "active"
10
+ ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200"
11
+ : status === "invited"
12
+ ? "border-blue-500/30 bg-blue-500/10 text-blue-900 dark:text-blue-200"
13
+ : "text-muted-foreground";
14
+ return (
15
+ <Badge variant="outline" className={cn(className)} data-testid={`user-status-badge-${userId}`}>
16
+ {status}
17
+ </Badge>
18
+ );
19
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+
3
+ export const usersInviteFormSchema = z.object({
4
+ email: z.string().email("Enter a valid email address"),
5
+ });
6
+
7
+ export type UsersInviteFormValues = z.infer<typeof usersInviteFormSchema>;
@@ -0,0 +1,240 @@
1
+ "use client";
2
+
3
+ import type {
4
+ UserAccountDto,
5
+ UserAccountStatus,
6
+ } from "@codemation/host-src/application/contracts/userDirectoryContracts.types";
7
+ import { useCallback, useEffect, useState } from "react";
8
+
9
+ import { Button } from "@/components/ui/button";
10
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
11
+ import { TableCell, TableRow } from "@/components/ui/table";
12
+ import { CodemationDataTable } from "../../../components/CodemationDataTable";
13
+ import { CodemationFormattedDateTime } from "../../../components/CodemationFormattedDateTime";
14
+ import {
15
+ useInviteUserMutation,
16
+ useRegenerateUserInviteMutation,
17
+ useUpdateUserAccountStatusMutation,
18
+ useUserAccountsQuery,
19
+ } from "../../workflows/hooks/realtime/realtime";
20
+ import { UsersInviteDialog } from "../components/UsersInviteDialog";
21
+ import { UsersRegenerateDialog } from "../components/UsersRegenerateDialog";
22
+ import { UsersScreenUserStatusBadge } from "../components/UsersScreenUserStatusBadge";
23
+
24
+ export function UsersScreen() {
25
+ const usersQuery = useUserAccountsQuery();
26
+ const inviteMutation = useInviteUserMutation();
27
+ const regenerateMutation = useRegenerateUserInviteMutation();
28
+ const statusMutation = useUpdateUserAccountStatusMutation();
29
+ const users = usersQuery.data ?? [];
30
+
31
+ const [inviteOpen, setInviteOpen] = useState(false);
32
+ const [inviteError, setInviteError] = useState<string | null>(null);
33
+ const [inviteSuccessUrl, setInviteSuccessUrl] = useState<string | null>(null);
34
+ const [copyFeedback, setCopyFeedback] = useState(false);
35
+
36
+ const [regenerateDialog, setRegenerateDialog] = useState<UserAccountDto | null>(null);
37
+ const [regeneratedUrl, setRegeneratedUrl] = useState<string | null>(null);
38
+
39
+ const closeInvite = useCallback(() => {
40
+ setInviteOpen(false);
41
+ setInviteError(null);
42
+ setInviteSuccessUrl(null);
43
+ setCopyFeedback(false);
44
+ }, []);
45
+
46
+ const openInvite = useCallback(() => {
47
+ setInviteOpen(true);
48
+ setInviteError(null);
49
+ setInviteSuccessUrl(null);
50
+ setCopyFeedback(false);
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ if (!copyFeedback) return;
55
+ const t = window.setTimeout(() => setCopyFeedback(false), 2000);
56
+ return () => window.clearTimeout(t);
57
+ }, [copyFeedback]);
58
+
59
+ const submitInvite = async (email: string): Promise<void> => {
60
+ setInviteError(null);
61
+ try {
62
+ const result = await inviteMutation.mutateAsync(email.trim());
63
+ setInviteSuccessUrl(result.inviteUrl);
64
+ } catch (e) {
65
+ setInviteError(e instanceof Error ? e.message : String(e));
66
+ }
67
+ };
68
+
69
+ const copyInviteUrl = async (url: string): Promise<void> => {
70
+ try {
71
+ await navigator.clipboard.writeText(url);
72
+ setCopyFeedback(true);
73
+ } catch {
74
+ setInviteError("Could not copy to clipboard.");
75
+ }
76
+ };
77
+
78
+ const runRegenerate = async (user: UserAccountDto): Promise<void> => {
79
+ setInviteError(null);
80
+ try {
81
+ const result = await regenerateMutation.mutateAsync(user.id);
82
+ setRegeneratedUrl(result.inviteUrl);
83
+ } catch (e) {
84
+ setInviteError(e instanceof Error ? e.message : String(e));
85
+ }
86
+ };
87
+
88
+ const onStatusChange = async (user: UserAccountDto, status: UserAccountStatus): Promise<void> => {
89
+ if (status === "invited" || user.status === status) return;
90
+ try {
91
+ await statusMutation.mutateAsync({ userId: user.id, status });
92
+ } catch {
93
+ /* query error surfaces via mutation state if needed */
94
+ }
95
+ };
96
+
97
+ const loading = usersQuery.isLoading;
98
+ const loadError = usersQuery.isError;
99
+
100
+ return (
101
+ <div data-testid="users-screen" className="flex flex-col gap-6">
102
+ <div className="flex flex-wrap items-start justify-between gap-4">
103
+ <p className="m-0 max-w-2xl text-sm text-muted-foreground">
104
+ Invite teammates with a secure link. Invites expire after seven days; you can regenerate a link for any
105
+ invited account.
106
+ </p>
107
+ <Button type="button" onClick={openInvite} data-testid="users-invite-open">
108
+ Invite user
109
+ </Button>
110
+ </div>
111
+
112
+ {loadError && (
113
+ <div
114
+ className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive"
115
+ role="alert"
116
+ data-testid="users-load-error"
117
+ >
118
+ Failed to load users.
119
+ </div>
120
+ )}
121
+
122
+ {loading ? (
123
+ <div className="text-sm text-muted-foreground" data-testid="users-loading">
124
+ Loading…
125
+ </div>
126
+ ) : users.length === 0 ? (
127
+ <div
128
+ className="rounded-lg border border-dashed border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground"
129
+ data-testid="users-empty"
130
+ >
131
+ No users yet. Invite someone to get started.
132
+ </div>
133
+ ) : (
134
+ <CodemationDataTable
135
+ tableTestId="users-table"
136
+ columns={[
137
+ { key: "email", header: "Email" },
138
+ { key: "loginMethods", header: "Sign-in methods" },
139
+ { key: "status", header: "Status" },
140
+ { key: "inviteExpiry", header: "Invite expires" },
141
+ { key: "actions", header: "Actions" },
142
+ ]}
143
+ >
144
+ {users.map((user) => {
145
+ const loginMethods = user.loginMethods ?? [];
146
+ return (
147
+ <TableRow key={user.id} data-testid={`user-row-${user.id}`}>
148
+ <TableCell>
149
+ <span data-testid={`user-email-${user.id}`}>{user.email}</span>
150
+ </TableCell>
151
+ <TableCell>
152
+ <span data-testid={`user-login-methods-${user.id}`}>
153
+ {loginMethods.length > 0 ? loginMethods.join(", ") : "—"}
154
+ </span>
155
+ </TableCell>
156
+ <TableCell>
157
+ <UsersScreenUserStatusBadge userId={user.id} status={user.status} />
158
+ </TableCell>
159
+ <TableCell>
160
+ <CodemationFormattedDateTime
161
+ isoUtc={user.inviteExpiresAt}
162
+ dataTestId={`user-invite-expires-${user.id}`}
163
+ />
164
+ </TableCell>
165
+ <TableCell>
166
+ <div className="flex flex-wrap items-center gap-2">
167
+ {user.status === "invited" && (
168
+ <Button
169
+ type="button"
170
+ size="sm"
171
+ data-testid={`user-regenerate-invite-${user.id}`}
172
+ onClick={() => {
173
+ setInviteError(null);
174
+ setRegeneratedUrl(null);
175
+ setRegenerateDialog(user);
176
+ }}
177
+ disabled={regenerateMutation.isPending}
178
+ >
179
+ Regenerate link
180
+ </Button>
181
+ )}
182
+ {user.status !== "invited" && (
183
+ <div className="inline-flex items-center gap-2">
184
+ <span className="sr-only" data-testid={`user-status-label-${user.id}`}>
185
+ Account status
186
+ </span>
187
+ <Select
188
+ value={user.status}
189
+ onValueChange={(value) => void onStatusChange(user, value as UserAccountStatus)}
190
+ disabled={statusMutation.isPending}
191
+ >
192
+ <SelectTrigger className="h-8 w-[140px]" data-testid={`user-account-status-${user.id}`}>
193
+ <SelectValue />
194
+ </SelectTrigger>
195
+ <SelectContent>
196
+ <SelectItem value="active">active</SelectItem>
197
+ <SelectItem value="inactive">inactive</SelectItem>
198
+ </SelectContent>
199
+ </Select>
200
+ </div>
201
+ )}
202
+ </div>
203
+ </TableCell>
204
+ </TableRow>
205
+ );
206
+ })}
207
+ </CodemationDataTable>
208
+ )}
209
+
210
+ {inviteOpen && (
211
+ <UsersInviteDialog
212
+ errorMessage={inviteError}
213
+ successUrl={inviteSuccessUrl}
214
+ isSubmitting={inviteMutation.isPending}
215
+ copyFeedback={copyFeedback}
216
+ onSubmit={(email) => void submitInvite(email)}
217
+ onCopy={() => inviteSuccessUrl && void copyInviteUrl(inviteSuccessUrl)}
218
+ onClose={closeInvite}
219
+ />
220
+ )}
221
+
222
+ {regenerateDialog && (
223
+ <UsersRegenerateDialog
224
+ email={regenerateDialog.email}
225
+ newUrl={regeneratedUrl}
226
+ errorMessage={inviteError}
227
+ isSubmitting={regenerateMutation.isPending}
228
+ copyFeedback={copyFeedback}
229
+ onConfirm={() => void runRegenerate(regenerateDialog)}
230
+ onCopy={() => regeneratedUrl && void copyInviteUrl(regeneratedUrl)}
231
+ onClose={() => {
232
+ setRegenerateDialog(null);
233
+ setRegeneratedUrl(null);
234
+ setInviteError(null);
235
+ }}
236
+ />
237
+ )}
238
+ </div>
239
+ );
240
+ }
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { ChevronRight, Folder } from "lucide-react";
4
+ import type { ReactNode } from "react";
5
+
6
+ import type { WorkflowSummary } from "../hooks/realtime/realtime";
7
+
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
10
+ import { cn } from "@/lib/utils";
11
+ import type { WorkflowFolderTreeNode } from "@/shell/WorkflowFolderTreeBuilder";
12
+ import { WorkflowFolderUi } from "@/shell/WorkflowFolderUi";
13
+
14
+ import { WorkflowListItemCard } from "./WorkflowListItemCard";
15
+
16
+ function folderTestId(folderPath: ReadonlyArray<string>): string {
17
+ return `workflows-folder-${folderPath.join("__")}`;
18
+ }
19
+
20
+ export function WorkflowListFolderSection(
21
+ args: Readonly<{
22
+ node: WorkflowFolderTreeNode;
23
+ folderPath: ReadonlyArray<string>;
24
+ depth: number;
25
+ pathname: string;
26
+ workflows: ReadonlyArray<WorkflowSummary>;
27
+ }>,
28
+ ): ReactNode {
29
+ const { node, folderPath, depth, pathname, workflows } = args;
30
+ const nextPath = [...folderPath, node.segment];
31
+ const totalInTree = WorkflowFolderUi.countWorkflowsInSubtree(node);
32
+ const defaultOpen = WorkflowFolderUi.computeDefaultFolderOpen(nextPath, pathname, workflows);
33
+
34
+ return (
35
+ <li className={cn("list-none", depth > 0 && "mt-1.5")}>
36
+ <Collapsible
37
+ defaultOpen={defaultOpen}
38
+ className="overflow-hidden rounded-lg border border-border/55 bg-card/80 shadow-none ring-1 ring-black/[0.03] dark:bg-card/60 dark:ring-white/[0.04]"
39
+ >
40
+ <CollapsibleTrigger
41
+ type="button"
42
+ data-testid={folderTestId(nextPath)}
43
+ className={cn(
44
+ "flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-sm outline-none transition-colors",
45
+ "text-foreground hover:bg-muted/50",
46
+ "focus-visible:bg-muted/45 focus-visible:ring-2 focus-visible:ring-ring/35",
47
+ "[&[data-state=open]>svg:first-child]:rotate-90",
48
+ )}
49
+ >
50
+ <ChevronRight
51
+ className="size-3.5 shrink-0 text-muted-foreground/90 transition-transform duration-200 ease-out"
52
+ aria-hidden
53
+ />
54
+ <Folder className="size-3.5 shrink-0 text-primary/85" strokeWidth={2} aria-hidden />
55
+ <span className="min-w-0 flex-1 font-medium tracking-tight text-foreground">{node.segment}</span>
56
+ <Badge
57
+ variant="secondary"
58
+ className="h-5 min-w-5 justify-center border border-border/50 bg-muted/60 px-1.5 font-mono text-[0.65rem] tabular-nums text-muted-foreground"
59
+ >
60
+ {totalInTree}
61
+ </Badge>
62
+ </CollapsibleTrigger>
63
+ <CollapsibleContent>
64
+ <div className="border-t border-border/45 bg-muted/[0.35] px-2 pb-2.5 pt-1.5 dark:bg-muted/15">
65
+ <ul className="m-0 grid list-none gap-0 p-0">
66
+ {node.workflows.map((workflow) => (
67
+ <li key={workflow.id} className="list-none">
68
+ <WorkflowListItemCard workflow={workflow} appearance="folder" />
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ {node.children.length > 0 ? (
73
+ <ul className="m-0 mt-2 list-none space-y-1.5 border-l border-border/55 pl-3">
74
+ {node.children.map((child) => (
75
+ <WorkflowListFolderSection
76
+ key={child.segment}
77
+ node={child}
78
+ folderPath={nextPath}
79
+ depth={depth + 1}
80
+ pathname={pathname}
81
+ workflows={workflows}
82
+ />
83
+ ))}
84
+ </ul>
85
+ ) : null}
86
+ </div>
87
+ </CollapsibleContent>
88
+ </Collapsible>
89
+ </li>
90
+ );
91
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { ChevronRight } from "lucide-react";
4
+ import Link from "next/link";
5
+
6
+ import type { ReactNode } from "react";
7
+
8
+ import type { WorkflowSummary } from "../hooks/realtime/realtime";
9
+
10
+ import { cn } from "@/lib/utils";
11
+
12
+ export function WorkflowListItemCard(
13
+ args: Readonly<{
14
+ workflow: WorkflowSummary;
15
+ appearance: "root" | "folder";
16
+ }>,
17
+ ): ReactNode {
18
+ const { workflow, appearance } = args;
19
+ const href = `/workflows/${encodeURIComponent(workflow.id)}`;
20
+ const pathLine = workflow.discoveryPathSegments.length > 0 ? workflow.discoveryPathSegments.join(" / ") : null;
21
+ return (
22
+ <div
23
+ className={cn(
24
+ "group relative -mx-1 rounded-md border border-transparent px-2.5 py-2 transition-[background-color,border-color,box-shadow]",
25
+ "hover:border-border/55 hover:bg-muted/50 hover:shadow-sm",
26
+ "focus-within:border-border/60 focus-within:bg-muted/45 focus-within:shadow-sm",
27
+ appearance === "root" && "hover:bg-muted/55",
28
+ appearance === "folder" && "hover:bg-muted/40",
29
+ )}
30
+ data-testid={`workflow-list-item-${workflow.id}`}
31
+ >
32
+ <div className="flex items-start gap-2.5">
33
+ <div className="min-w-0 flex-1">
34
+ <Link
35
+ href={href}
36
+ className={cn(
37
+ "inline-flex max-w-full items-baseline gap-1.5 text-pretty text-sm font-medium leading-snug text-foreground no-underline",
38
+ "transition-colors group-hover:text-primary",
39
+ "focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/45",
40
+ )}
41
+ data-testid={`workflow-open-${workflow.id}`}
42
+ >
43
+ <span className="truncate">{workflow.name}</span>
44
+ </Link>
45
+ <div className="mt-1 flex min-w-0 flex-wrap items-baseline gap-x-1.5 gap-y-0.5 text-[0.7rem] leading-snug text-muted-foreground">
46
+ <span className="shrink-0 font-mono text-[0.65rem] text-muted-foreground/80">{workflow.id}</span>
47
+ {pathLine !== null ? (
48
+ <>
49
+ <span className="select-none text-muted-foreground/35" aria-hidden>
50
+ ·
51
+ </span>
52
+ <span className="min-w-0 font-sans text-muted-foreground/75">{pathLine}</span>
53
+ </>
54
+ ) : null}
55
+ </div>
56
+ </div>
57
+ <ChevronRight
58
+ className={cn(
59
+ "mt-0.5 size-4 shrink-0 text-muted-foreground transition-[opacity,transform]",
60
+ "opacity-0 group-hover:translate-x-px group-hover:opacity-70",
61
+ )}
62
+ aria-hidden
63
+ />
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import type { WorkflowSummary } from "../hooks/realtime/realtime";
6
+
7
+ import type { WorkflowFolderTreeNode } from "@/shell/WorkflowFolderTreeBuilder";
8
+
9
+ import { WorkflowListFolderSection } from "./WorkflowListFolderSection";
10
+ import { WorkflowListItemCard } from "./WorkflowListItemCard";
11
+
12
+ export function WorkflowListRoot(
13
+ args: Readonly<{
14
+ node: WorkflowFolderTreeNode;
15
+ pathname: string;
16
+ workflows: ReadonlyArray<WorkflowSummary>;
17
+ }>,
18
+ ): ReactNode {
19
+ const { node, pathname, workflows } = args;
20
+ return (
21
+ <>
22
+ {node.workflows.map((workflow) => (
23
+ <li key={workflow.id} className="list-none">
24
+ <WorkflowListItemCard workflow={workflow} appearance="root" />
25
+ </li>
26
+ ))}
27
+ {node.children.map((child) => (
28
+ <WorkflowListFolderSection
29
+ key={child.segment}
30
+ node={child}
31
+ folderPath={[]}
32
+ depth={0}
33
+ pathname={pathname}
34
+ workflows={workflows}
35
+ />
36
+ ))}
37
+ </>
38
+ );
39
+ }