@kelpie/ui 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/api/resources/activities.d.ts +2 -1
  2. package/dist/api/resources/activities.d.ts.map +1 -1
  3. package/dist/api/resources/activities.js +6 -2
  4. package/dist/api/resources/activities.js.map +1 -1
  5. package/dist/api/resources/conversions.d.ts +16 -0
  6. package/dist/api/resources/conversions.d.ts.map +1 -0
  7. package/dist/api/resources/conversions.js +81 -0
  8. package/dist/api/resources/conversions.js.map +1 -0
  9. package/dist/api/resources/enquiries.d.ts +2 -7
  10. package/dist/api/resources/enquiries.d.ts.map +1 -1
  11. package/dist/api/resources/enquiries.js +2 -30
  12. package/dist/api/resources/enquiries.js.map +1 -1
  13. package/dist/api/resources/importJobs.d.ts +2 -2
  14. package/dist/api/resources/importJobs.d.ts.map +1 -1
  15. package/dist/api/resources/importJobs.js.map +1 -1
  16. package/dist/app/KelpieApp.js +1 -1
  17. package/dist/app/KelpieApp.js.map +1 -1
  18. package/dist/components/ActivitiesPanel.d.ts +2 -2
  19. package/dist/components/ActivitiesPanel.d.ts.map +1 -1
  20. package/dist/components/ActivitiesPanel.js +7 -5
  21. package/dist/components/ActivitiesPanel.js.map +1 -1
  22. package/dist/components/ApiKeysPanel.d.ts +0 -7
  23. package/dist/components/ApiKeysPanel.d.ts.map +1 -1
  24. package/dist/components/ApiKeysPanel.js +83 -16
  25. package/dist/components/ApiKeysPanel.js.map +1 -1
  26. package/dist/components/ConvertRecordDialog.d.ts +30 -0
  27. package/dist/components/ConvertRecordDialog.d.ts.map +1 -0
  28. package/dist/components/ConvertRecordDialog.js +50 -0
  29. package/dist/components/ConvertRecordDialog.js.map +1 -0
  30. package/dist/components/Shell.d.ts.map +1 -1
  31. package/dist/components/Shell.js +19 -7
  32. package/dist/components/Shell.js.map +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/pages/DealDetail.d.ts.map +1 -1
  38. package/dist/pages/DealDetail.js +15 -2
  39. package/dist/pages/DealDetail.js.map +1 -1
  40. package/dist/pages/DecisionsPage.d.ts.map +1 -1
  41. package/dist/pages/DecisionsPage.js +1 -7
  42. package/dist/pages/DecisionsPage.js.map +1 -1
  43. package/dist/pages/EnquiryDetail.d.ts.map +1 -1
  44. package/dist/pages/EnquiryDetail.js +17 -25
  45. package/dist/pages/EnquiryDetail.js.map +1 -1
  46. package/dist/pages/ListDetail.d.ts.map +1 -1
  47. package/dist/pages/ListDetail.js +1 -14
  48. package/dist/pages/ListDetail.js.map +1 -1
  49. package/dist/pages/OpportunityDetail.d.ts.map +1 -1
  50. package/dist/pages/OpportunityDetail.js +15 -2
  51. package/dist/pages/OpportunityDetail.js.map +1 -1
  52. package/dist/pages/PartnershipDetail.d.ts.map +1 -1
  53. package/dist/pages/PartnershipDetail.js +15 -2
  54. package/dist/pages/PartnershipDetail.js.map +1 -1
  55. package/dist/pages/RaiseDetail.d.ts.map +1 -1
  56. package/dist/pages/RaiseDetail.js +15 -2
  57. package/dist/pages/RaiseDetail.js.map +1 -1
  58. package/dist/pages/account/SecurityPage.js +2 -2
  59. package/dist/pages/account/SecurityPage.js.map +1 -1
  60. package/dist/pages/admin/DataPage.d.ts.map +1 -1
  61. package/dist/pages/admin/DataPage.js +3 -3
  62. package/dist/pages/admin/DataPage.js.map +1 -1
  63. package/dist/pages/admin/PrivacyPage.js +3 -3
  64. package/dist/pages/admin/PrivacyPage.js.map +1 -1
  65. package/dist/pages/admin/TeamPage.js +1 -1
  66. package/dist/pages/admin/TeamPage.js.map +1 -1
  67. package/dist/pages/admin/WorkspaceSettingsPage.js +2 -12
  68. package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
  69. package/dist/pages/auth/AuthForm.d.ts +18 -0
  70. package/dist/pages/auth/AuthForm.d.ts.map +1 -1
  71. package/dist/pages/auth/AuthForm.js +17 -0
  72. package/dist/pages/auth/AuthForm.js.map +1 -1
  73. package/dist/pages/auth/AuthMethods.d.ts +12 -0
  74. package/dist/pages/auth/AuthMethods.d.ts.map +1 -0
  75. package/dist/pages/auth/AuthMethods.js +20 -0
  76. package/dist/pages/auth/AuthMethods.js.map +1 -0
  77. package/dist/pages/auth/SignInPage.d.ts +7 -0
  78. package/dist/pages/auth/SignInPage.d.ts.map +1 -1
  79. package/dist/pages/auth/SignInPage.js +3 -8
  80. package/dist/pages/auth/SignInPage.js.map +1 -1
  81. package/dist/pages/auth/SignUpPage.d.ts.map +1 -1
  82. package/dist/pages/auth/SignUpPage.js +2 -1
  83. package/dist/pages/auth/SignUpPage.js.map +1 -1
  84. package/dist/pages/auth/nextPath.d.ts +15 -0
  85. package/dist/pages/auth/nextPath.d.ts.map +1 -0
  86. package/dist/pages/auth/nextPath.js +17 -0
  87. package/dist/pages/auth/nextPath.js.map +1 -0
  88. package/dist/pages/forms/AddFieldMenu.js +2 -2
  89. package/dist/pages/forms/AddFieldMenu.js.map +1 -1
  90. package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
  91. package/dist/pages/forms/FieldsEditor.js +22 -10
  92. package/dist/pages/forms/FieldsEditor.js.map +1 -1
  93. package/dist/pages/forms/MapTargetSearch.d.ts +7 -0
  94. package/dist/pages/forms/MapTargetSearch.d.ts.map +1 -0
  95. package/dist/pages/forms/MapTargetSearch.js +32 -0
  96. package/dist/pages/forms/MapTargetSearch.js.map +1 -0
  97. package/dist/pages/forms/fieldList.d.ts +6 -7
  98. package/dist/pages/forms/fieldList.d.ts.map +1 -1
  99. package/dist/pages/forms/fieldList.js +14 -16
  100. package/dist/pages/forms/fieldList.js.map +1 -1
  101. package/dist/pages/forms/template.d.ts.map +1 -1
  102. package/dist/pages/forms/template.js +7 -0
  103. package/dist/pages/forms/template.js.map +1 -1
  104. package/dist/registry/context.d.ts +3 -1
  105. package/dist/registry/context.d.ts.map +1 -1
  106. package/dist/registry/context.js +4 -0
  107. package/dist/registry/context.js.map +1 -1
  108. package/dist/registry/contributions.d.ts +24 -0
  109. package/dist/registry/contributions.d.ts.map +1 -1
  110. package/dist/registry/contributions.js.map +1 -1
  111. package/dist/registry/registry.d.ts +4 -1
  112. package/dist/registry/registry.d.ts.map +1 -1
  113. package/dist/registry/registry.js +6 -0
  114. package/dist/registry/registry.js.map +1 -1
  115. package/package.json +2 -2
  116. package/src/api/resources/activities.ts +10 -2
  117. package/src/api/resources/conversions.ts +125 -0
  118. package/src/api/resources/enquiries.ts +9 -35
  119. package/src/api/resources/importJobs.ts +3 -2
  120. package/src/app/KelpieApp.tsx +1 -0
  121. package/src/components/ActivitiesPanel.tsx +20 -14
  122. package/src/components/ApiKeysPanel.tsx +206 -21
  123. package/src/components/ConvertRecordDialog.tsx +210 -0
  124. package/src/components/Shell.tsx +91 -36
  125. package/src/index.ts +7 -0
  126. package/src/pages/DealDetail.tsx +35 -0
  127. package/src/pages/DecisionsPage.tsx +0 -14
  128. package/src/pages/EnquiryDetail.tsx +37 -90
  129. package/src/pages/ListDetail.tsx +0 -47
  130. package/src/pages/OpportunityDetail.tsx +35 -0
  131. package/src/pages/PartnershipDetail.tsx +35 -0
  132. package/src/pages/RaiseDetail.tsx +35 -0
  133. package/src/pages/account/SecurityPage.tsx +6 -1
  134. package/src/pages/admin/DataPage.tsx +14 -9
  135. package/src/pages/admin/PrivacyPage.tsx +4 -4
  136. package/src/pages/admin/TeamPage.tsx +1 -2
  137. package/src/pages/admin/WorkspaceSettingsPage.tsx +1 -31
  138. package/src/pages/auth/AuthForm.tsx +38 -0
  139. package/src/pages/auth/AuthMethods.tsx +29 -0
  140. package/src/pages/auth/SignInPage.tsx +3 -8
  141. package/src/pages/auth/SignUpPage.tsx +7 -0
  142. package/src/pages/auth/nextPath.ts +17 -0
  143. package/src/pages/forms/AddFieldMenu.tsx +2 -2
  144. package/src/pages/forms/FieldsEditor.tsx +45 -20
  145. package/src/pages/forms/MapTargetSearch.tsx +69 -0
  146. package/src/pages/forms/fieldList.ts +34 -16
  147. package/src/pages/forms/template.ts +7 -0
  148. package/src/registry/context.ts +6 -0
  149. package/src/registry/contributions.ts +26 -0
  150. package/src/registry/registry.ts +12 -0
@@ -0,0 +1,210 @@
1
+ import {
2
+ PIPELINE_KINDS,
3
+ PIPELINE_KIND_LABELS,
4
+ } from '@kelpie/schemas'
5
+ import type { ConvertPipelineRecordInput, PipelineKind } from '@kelpie/schemas'
6
+ import { useState } from 'react'
7
+ import { Link } from 'react-router'
8
+
9
+ import { useCompanies } from '../api/resources/companies.ts'
10
+ import type { MutationResult } from '../api/resource.ts'
11
+ import {
12
+ detailPathForPipelineKind,
13
+ type ConvertPipelineRecordArguments,
14
+ type ConvertedPipelineRecord,
15
+ } from '../api/resources/conversions.ts'
16
+
17
+ const COMPANY_REQUIRED_TARGETS = new Set<PipelineKind>(['deal', 'raise', 'partnership'])
18
+
19
+ export interface ConvertRecordDialogProps {
20
+ readonly sourceKind: PipelineKind
21
+ readonly recordName: string
22
+ readonly companyId: string | null
23
+ readonly isPending: boolean
24
+ readonly onCancel: () => void
25
+ readonly onConfirm: (body: ConvertPipelineRecordInput) => void
26
+ }
27
+
28
+ export function ConvertRecordDialog({
29
+ sourceKind,
30
+ recordName,
31
+ companyId,
32
+ isPending,
33
+ onCancel,
34
+ onConfirm,
35
+ }: ConvertRecordDialogProps): React.JSX.Element {
36
+ const targetOptions = PIPELINE_KINDS.filter((kind) => kind !== sourceKind)
37
+ const [targetType, setTargetType] = useState<PipelineKind>(targetOptions[0] ?? 'deal')
38
+ const [kind, setKind] = useState('')
39
+ const [selectedCompanyId, setSelectedCompanyId] = useState(companyId ?? '')
40
+ const companies = useCompanies({ limit: 100 })
41
+
42
+ const needsCompany = COMPANY_REQUIRED_TARGETS.has(targetType) && companyId === null
43
+ const needsKind = targetType === 'opportunity' || targetType === 'partnership'
44
+
45
+ return (
46
+ <div className="rounded-md border border-border bg-surface-raised p-4">
47
+ <p className="text-[13px] text-ink">
48
+ Convert <span className="font-semibold">{recordName}</span> to another record type. This
49
+ creates a new record, moves notes / activity / plans / decisions to it, copies linked people,
50
+ and leaves this record in place with a link.
51
+ </p>
52
+
53
+ <div className="mt-4 space-y-3">
54
+ <label className="block text-[12px] font-medium text-ink-muted">
55
+ Convert to
56
+ <select
57
+ value={targetType}
58
+ onChange={(event) => {
59
+ setTargetType(event.target.value as PipelineKind)
60
+ }}
61
+ className="mt-1 block w-full rounded-md border border-border bg-surface px-2.5 py-1.5 text-[13px] text-ink"
62
+ >
63
+ {targetOptions.map((kind) => (
64
+ <option key={kind} value={kind}>
65
+ {PIPELINE_KIND_LABELS[kind]}
66
+ </option>
67
+ ))}
68
+ </select>
69
+ </label>
70
+
71
+ {needsKind && (
72
+ <label className="block text-[12px] font-medium text-ink-muted">
73
+ Kind
74
+ <input
75
+ value={kind}
76
+ onChange={(event) => {
77
+ setKind(event.target.value)
78
+ }}
79
+ className="mt-1 block w-full rounded-md border border-border bg-surface px-2.5 py-1.5 text-[13px] text-ink"
80
+ placeholder={targetType === 'opportunity' ? 'Grant' : 'Integration'}
81
+ />
82
+ </label>
83
+ )}
84
+
85
+ {needsCompany && (
86
+ <label className="block text-[12px] font-medium text-ink-muted">
87
+ Company
88
+ <select
89
+ value={selectedCompanyId}
90
+ onChange={(event) => {
91
+ setSelectedCompanyId(event.target.value)
92
+ }}
93
+ className="mt-1 block w-full rounded-md border border-border bg-surface px-2.5 py-1.5 text-[13px] text-ink"
94
+ >
95
+ <option value="">Choose a company…</option>
96
+ {companies.records.map((company) => (
97
+ <option key={company.id} value={company.id}>
98
+ {company.name}
99
+ </option>
100
+ ))}
101
+ </select>
102
+ </label>
103
+ )}
104
+ </div>
105
+
106
+ <div className="mt-4 flex justify-end gap-2">
107
+ <button
108
+ type="button"
109
+ onClick={onCancel}
110
+ disabled={isPending}
111
+ className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink disabled:opacity-50"
112
+ >
113
+ Cancel
114
+ </button>
115
+ <button
116
+ type="button"
117
+ disabled={isPending || (needsCompany && selectedCompanyId.length === 0)}
118
+ onClick={() => {
119
+ onConfirm({
120
+ targetType,
121
+ ...(needsKind && kind.length > 0 ? { kind } : {}),
122
+ ...(needsCompany ? { companyId: selectedCompanyId } : {}),
123
+ })
124
+ }}
125
+ className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-50"
126
+ >
127
+ {isPending ? 'Converting…' : 'Convert'}
128
+ </button>
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ export interface ConvertRecordActionProps {
135
+ readonly sourceKind: PipelineKind
136
+ readonly recordId: string
137
+ readonly recordName: string
138
+ readonly companyId: string | null
139
+ readonly convertedTo: { readonly targetType: PipelineKind; readonly targetId: string } | null
140
+ readonly convert: MutationResult<ConvertPipelineRecordArguments, ConvertedPipelineRecord>
141
+ readonly onConverted: (record: ConvertedPipelineRecord, targetType: PipelineKind) => void
142
+ readonly showDialog: boolean
143
+ readonly onOpenDialog: () => void
144
+ readonly onCloseDialog: () => void
145
+ }
146
+
147
+ export function ConvertRecordButton({
148
+ convertedTo,
149
+ convert,
150
+ onOpenDialog,
151
+ }: Pick<
152
+ ConvertRecordActionProps,
153
+ 'convertedTo' | 'convert' | 'onOpenDialog'
154
+ >): React.JSX.Element {
155
+ if (convertedTo !== null) {
156
+ return (
157
+ <Link
158
+ to={detailPathForPipelineKind(convertedTo.targetType, convertedTo.targetId)}
159
+ className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-accent transition hover:border-accent hover:underline"
160
+ >
161
+ Converted — view {PIPELINE_KIND_LABELS[convertedTo.targetType].toLowerCase()}
162
+ </Link>
163
+ )
164
+ }
165
+
166
+ return (
167
+ <button
168
+ type="button"
169
+ onClick={onOpenDialog}
170
+ disabled={convert.isPending}
171
+ className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-ink-muted transition hover:border-accent hover:text-accent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-border disabled:hover:text-ink-muted"
172
+ >
173
+ Convert…
174
+ </button>
175
+ )
176
+ }
177
+
178
+ export function ConvertRecordAction({
179
+ sourceKind,
180
+ recordId,
181
+ recordName,
182
+ companyId,
183
+ convert,
184
+ onConverted,
185
+ showDialog,
186
+ onCloseDialog,
187
+ }: ConvertRecordActionProps): React.JSX.Element | null {
188
+ if (!showDialog) {
189
+ return null
190
+ }
191
+
192
+ return (
193
+ <ConvertRecordDialog
194
+ sourceKind={sourceKind}
195
+ recordName={recordName}
196
+ companyId={companyId}
197
+ isPending={convert.isPending}
198
+ onCancel={onCloseDialog}
199
+ onConfirm={(body) => {
200
+ convert
201
+ .runAsync({ id: recordId, body })
202
+ .then((record) => {
203
+ onCloseDialog()
204
+ onConverted(record, body.targetType)
205
+ })
206
+ .catch(() => undefined)
207
+ }}
208
+ />
209
+ )
210
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
- import { NavLink, Outlet, useLocation, useNavigate } from 'react-router'
2
+ import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router'
3
3
 
4
4
  import { useAccount, useTheme } from '../api/resources/account.ts'
5
5
  import { useLogOut } from '../api/resources/session.ts'
@@ -49,11 +49,21 @@ const CORE_NAV: readonly NavItem[] = [
49
49
  ]
50
50
 
51
51
  /**
52
- * `nav.primary` is one flat slot (`modules.md`); the mockup renders it as two
53
- * visual groups, an unheaded top section and a headed "CRM" section. This is
54
- * presentation only, so it stays here rather than becoming a second slot.
52
+ * `nav.primary` is one flat slot (`modules.md`); the shell renders it as visual
53
+ * groups: an unheaded top section, an unheaded directory pair (People,
54
+ * Companies), a headed "CRM" section for pipeline objects, then a headed
55
+ * "Tools" section for Lists and Forms. Presentation only — it stays here rather
56
+ * than becoming extra slots.
55
57
  */
56
58
  const TOP_LEVEL_NAV_IDS: ReadonlySet<string> = new Set(['dashboard', 'handbook', 'planning', 'decisions'])
59
+ const DIRECTORY_NAV_IDS: ReadonlySet<string> = new Set(['people', 'companies'])
60
+ const TOOLS_NAV_IDS: ReadonlySet<string> = new Set(['lists', 'forms'])
61
+
62
+ const ADMIN_ENTRY_PATH = '/admin/workspace'
63
+
64
+ function isAdminRoute(pathname: string): boolean {
65
+ return pathname.startsWith('/admin/')
66
+ }
57
67
 
58
68
  function linkClass({ isActive }: { isActive: boolean }): string {
59
69
  return [
@@ -118,8 +128,14 @@ export function Shell(): React.JSX.Element {
118
128
 
119
129
  const navItems = useVisibleNavItems(CORE_NAV, moduleNav)
120
130
  const topLevelItems = navItems.filter((item) => TOP_LEVEL_NAV_IDS.has(item.id))
121
- const crmItems = navItems.filter((item) => !TOP_LEVEL_NAV_IDS.has(item.id))
131
+ const nonTopLevelItems = navItems.filter((item) => !TOP_LEVEL_NAV_IDS.has(item.id))
132
+ const directoryItems = nonTopLevelItems.filter((item) => DIRECTORY_NAV_IDS.has(item.id))
133
+ const toolsItems = nonTopLevelItems.filter((item) => TOOLS_NAV_IDS.has(item.id))
134
+ const crmItems = nonTopLevelItems.filter(
135
+ (item) => !DIRECTORY_NAV_IDS.has(item.id) && !TOOLS_NAV_IDS.has(item.id),
136
+ )
122
137
  const adminItems = useVisibleNavItems(CORE_ADMIN_NAV, moduleAdminNav)
138
+ const inAdminNav = isAdminRoute(location.pathname)
123
139
 
124
140
  return (
125
141
  <div className="flex min-h-screen bg-surface">
@@ -133,40 +149,79 @@ export function Shell(): React.JSX.Element {
133
149
  </div>
134
150
 
135
151
  <nav className="flex flex-1 flex-col gap-5 overflow-y-auto px-2 pb-4">
136
- <div className="flex flex-col gap-0.5">
137
- {topLevelItems.map((item) => (
138
- <NavLink key={item.id} to={item.to} className={linkClass}>
139
- {item.label}
140
- </NavLink>
141
- ))}
142
- </div>
152
+ {inAdminNav ? (
153
+ <>
154
+ <Link
155
+ to="/dashboard"
156
+ className="inline-flex px-2 text-[12px] font-medium text-sidebar-muted transition-colors hover:text-sidebar-ink"
157
+ >
158
+ ← Back
159
+ </Link>
143
160
 
144
- <div>
145
- <div className="mb-1 px-2 text-[11px] text-sidebar-muted">CRM</div>
146
- <div className="flex flex-col gap-0.5">
147
- {crmItems.map((item) => (
148
- <NavLink key={item.id} to={item.to} className={linkClass}>
149
- {item.label}
150
- </NavLink>
151
- ))}
152
- </div>
153
- </div>
161
+ <div>
162
+ <div className="mb-1 px-2 text-[11px] text-sidebar-muted">Admin</div>
163
+ <div className="flex flex-col gap-0.5">
164
+ {adminItems.map((item) => (
165
+ <NavLink key={item.id} to={item.to} className={linkClass}>
166
+ {item.label}
167
+ </NavLink>
168
+ ))}
169
+ </div>
170
+ </div>
171
+ </>
172
+ ) : (
173
+ <>
174
+ <div className="flex flex-col gap-0.5">
175
+ {topLevelItems.map((item) => (
176
+ <NavLink key={item.id} to={item.to} className={linkClass}>
177
+ {item.label}
178
+ </NavLink>
179
+ ))}
180
+ </div>
154
181
 
155
- {/*
156
- Shown to every member. Both pages read for anyone in the workspace
157
- and only their controls are admin-gated, so hiding the section would
158
- keep a member from seeing who else is on their team.
159
- */}
160
- <div>
161
- <div className="mb-1 px-2 text-[11px] text-sidebar-muted">Admin</div>
162
- <div className="flex flex-col gap-0.5">
163
- {adminItems.map((item) => (
164
- <NavLink key={item.id} to={item.to} className={linkClass}>
165
- {item.label}
182
+ {directoryItems.length > 0 && (
183
+ <div className="flex flex-col gap-0.5">
184
+ {directoryItems.map((item) => (
185
+ <NavLink key={item.id} to={item.to} className={linkClass}>
186
+ {item.label}
187
+ </NavLink>
188
+ ))}
189
+ </div>
190
+ )}
191
+
192
+ {crmItems.length > 0 && (
193
+ <div>
194
+ <div className="mb-1 px-2 text-[11px] text-sidebar-muted">CRM</div>
195
+ <div className="flex flex-col gap-0.5">
196
+ {crmItems.map((item) => (
197
+ <NavLink key={item.id} to={item.to} className={linkClass}>
198
+ {item.label}
199
+ </NavLink>
200
+ ))}
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {toolsItems.length > 0 && (
206
+ <div>
207
+ <div className="mb-1 px-2 text-[11px] text-sidebar-muted">Tools</div>
208
+ <div className="flex flex-col gap-0.5">
209
+ {toolsItems.map((item) => (
210
+ <NavLink key={item.id} to={item.to} className={linkClass}>
211
+ {item.label}
212
+ </NavLink>
213
+ ))}
214
+ </div>
215
+ </div>
216
+ )}
217
+
218
+ <div className="flex flex-col gap-0.5">
219
+ <NavLink to={ADMIN_ENTRY_PATH} className={linkClass}>
220
+ Admin
166
221
  </NavLink>
167
- ))}
168
- </div>
169
- </div>
222
+ </div>
223
+ </>
224
+ )}
170
225
  </nav>
171
226
  </aside>
172
227
 
package/src/index.ts CHANGED
@@ -132,6 +132,10 @@ export { DecisionsPage } from './pages/DecisionsPage.tsx'
132
132
  export { PeoplePage } from './pages/PeoplePage.tsx'
133
133
  export { PersonDetail } from './pages/PersonDetail.tsx'
134
134
  export { PlanningPage } from './pages/PlanningPage.tsx'
135
+ // The controls a module contributing a sign-in method builds its button from,
136
+ // so it matches the form beside it without copying class names.
137
+ export { AuthDivider, AuthLinkButton } from './pages/auth/AuthForm.tsx'
138
+ export { safeNext } from './pages/auth/nextPath.ts'
135
139
  export { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage.tsx'
136
140
  export { JoinPage } from './pages/auth/JoinPage.tsx'
137
141
  export { ResetPasswordPage } from './pages/auth/ResetPasswordPage.tsx'
@@ -165,6 +169,8 @@ export type { ThemePreference } from './lib/theme.ts'
165
169
 
166
170
  export { EXTENSIBLE_RECORD_TYPES, NAV_SLOTS } from './registry/contributions.ts'
167
171
  export type {
172
+ AuthMethod,
173
+ AuthMethodContext,
168
174
  DashboardCard,
169
175
  ExtensibleRecordType,
170
176
  NavItem,
@@ -182,6 +188,7 @@ export { NO_UI_MODULES, UiModuleError, inSlotOrder, registerUiModules } from './
182
188
  export type { UiExtensions, UiModule, UiModuleContext } from './registry/registry.ts'
183
189
 
184
190
  export {
191
+ useAuthMethods,
185
192
  useDashboardCards,
186
193
  useModuleRoutes,
187
194
  useNavItems,
@@ -6,6 +6,10 @@ import { Link, useNavigate, useParams } from 'react-router'
6
6
  import { usePatch } from '../api/resource.ts'
7
7
  import type { PatchResult } from '../api/resource.ts'
8
8
  import { useCompanies, useCompany } from '../api/resources/companies.ts'
9
+ import {
10
+ detailPathForPipelineKind,
11
+ useConvertPipelineRecord,
12
+ } from '../api/resources/conversions.ts'
9
13
  import { useDeal, useDeleteDeal, useUpdateDeal } from '../api/resources/deals.ts'
10
14
  import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
11
15
  import { useMembers } from '../api/resources/members.ts'
@@ -16,6 +20,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
16
20
  import { AgentTasks } from '../components/AgentTasks.tsx'
17
21
  import { Chip } from '../components/Chip.tsx'
18
22
  import type { ChipTone } from '../components/Chip.tsx'
23
+ import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
19
24
  import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
20
25
  import { DeleteRecord } from '../components/DeleteRecord.tsx'
21
26
  import { EntitySearch } from '../components/EntitySearch.tsx'
@@ -56,10 +61,12 @@ export function DealDetail(): React.JSX.Element {
56
61
  const { id } = useParams()
57
62
  const navigate = useNavigate()
58
63
  const { record, isLoading, isNotFound, error } = useDeal(id)
64
+ const convertRecord = useConvertPipelineRecord('deal')
59
65
  const deleteDeal = useDeleteDeal()
60
66
  const moduleTabs = inSlotOrder(useRecordTabs('deal'))
61
67
  const hasCustomFields = useHasCustomFields('deal')
62
68
  const [activeTab, setActiveTab] = useState('overview')
69
+ const [showConvert, setShowConvert] = useState(false)
63
70
  const formSubmissions = useFormSubmissionsForRecord('deal', id)
64
71
 
65
72
  if (isNotFound) {
@@ -89,6 +96,7 @@ export function DealDetail(): React.JSX.Element {
89
96
  ]
90
97
  const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
91
98
  const moduleTab = moduleTabs.find((tab) => tab.id === active)
99
+ const convertedTo = record.convertedTo
92
100
 
93
101
  return (
94
102
  <div className="animate-fade-in mx-auto max-w-6xl">
@@ -104,6 +112,13 @@ export function DealDetail(): React.JSX.Element {
104
112
  <DealHeading deal={record} />
105
113
 
106
114
  <div className="flex justify-end gap-2">
115
+ <ConvertRecordButton
116
+ convertedTo={convertedTo}
117
+ convert={convertRecord}
118
+ onOpenDialog={() => {
119
+ setShowConvert(true)
120
+ }}
121
+ />
107
122
  <AgentTasks targetType="deal" targetId={record.id} targetLabel={record.name} />
108
123
  <DeleteRecord
109
124
  recordLabel="Deal"
@@ -119,6 +134,26 @@ export function DealDetail(): React.JSX.Element {
119
134
  />
120
135
  </div>
121
136
 
137
+ {convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
138
+ <ConvertRecordAction
139
+ sourceKind="deal"
140
+ recordId={record.id}
141
+ recordName={record.name}
142
+ companyId={record.companyId}
143
+ convertedTo={convertedTo}
144
+ convert={convertRecord}
145
+ showDialog={showConvert}
146
+ onOpenDialog={() => {
147
+ setShowConvert(true)
148
+ }}
149
+ onCloseDialog={() => {
150
+ setShowConvert(false)
151
+ }}
152
+ onConverted={(created, targetType) => {
153
+ navigate(detailPathForPipelineKind(targetType, created.id))
154
+ }}
155
+ />
156
+
122
157
  <RecordTabs tabs={tabs} active={active} onChange={setActiveTab} ariaLabel="Deal sections">
123
158
  {active === 'overview' && <DealOverview deal={record} />}
124
159
  {active === 'fields' && (
@@ -60,7 +60,6 @@ const TARGET_ROUTES: Readonly<Partial<Record<RecordTargetType, string>>> = {
60
60
 
61
61
  interface TargetDirectory {
62
62
  nameFor(decision: Decision): string | undefined
63
- readonly isComplete: boolean
64
63
  }
65
64
 
66
65
  function useTargetDirectory(): TargetDirectory {
@@ -95,13 +94,6 @@ function useTargetDirectory(): TargetDirectory {
95
94
 
96
95
  return {
97
96
  nameFor: (decision) => nameById.get(decision.targetId),
98
- isComplete:
99
- !people.hasNext &&
100
- !companies.hasNext &&
101
- !deals.hasNext &&
102
- !opportunities.hasNext &&
103
- !raises.hasNext &&
104
- !partnerships.hasNext,
105
97
  }
106
98
  }
107
99
 
@@ -258,12 +250,6 @@ export function DecisionsPage(): React.JSX.Element {
258
250
  onSortChange={setSort}
259
251
  visibleColumnKeys={listView.visibleKeys}
260
252
  />
261
- {!directory.isComplete && (
262
- <p className="mt-2 text-[11px] text-ink-faint">
263
- The workspace holds more records than one page returns, so some rows name only their
264
- record type.
265
- </p>
266
- )}
267
253
  <Paginator list={decisions} />
268
254
  </>
269
255
  )}