@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.
- package/dist/api/resources/activities.d.ts +2 -1
- package/dist/api/resources/activities.d.ts.map +1 -1
- package/dist/api/resources/activities.js +6 -2
- package/dist/api/resources/activities.js.map +1 -1
- package/dist/api/resources/conversions.d.ts +16 -0
- package/dist/api/resources/conversions.d.ts.map +1 -0
- package/dist/api/resources/conversions.js +81 -0
- package/dist/api/resources/conversions.js.map +1 -0
- package/dist/api/resources/enquiries.d.ts +2 -7
- package/dist/api/resources/enquiries.d.ts.map +1 -1
- package/dist/api/resources/enquiries.js +2 -30
- package/dist/api/resources/enquiries.js.map +1 -1
- package/dist/api/resources/importJobs.d.ts +2 -2
- package/dist/api/resources/importJobs.d.ts.map +1 -1
- package/dist/api/resources/importJobs.js.map +1 -1
- package/dist/app/KelpieApp.js +1 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts +2 -2
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +7 -5
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/ApiKeysPanel.d.ts +0 -7
- package/dist/components/ApiKeysPanel.d.ts.map +1 -1
- package/dist/components/ApiKeysPanel.js +83 -16
- package/dist/components/ApiKeysPanel.js.map +1 -1
- package/dist/components/ConvertRecordDialog.d.ts +30 -0
- package/dist/components/ConvertRecordDialog.d.ts.map +1 -0
- package/dist/components/ConvertRecordDialog.js +50 -0
- package/dist/components/ConvertRecordDialog.js.map +1 -0
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +19 -7
- package/dist/components/Shell.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +15 -2
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +1 -7
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiryDetail.d.ts.map +1 -1
- package/dist/pages/EnquiryDetail.js +17 -25
- package/dist/pages/EnquiryDetail.js.map +1 -1
- package/dist/pages/ListDetail.d.ts.map +1 -1
- package/dist/pages/ListDetail.js +1 -14
- package/dist/pages/ListDetail.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +15 -2
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +15 -2
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +15 -2
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/account/SecurityPage.js +2 -2
- package/dist/pages/account/SecurityPage.js.map +1 -1
- package/dist/pages/admin/DataPage.d.ts.map +1 -1
- package/dist/pages/admin/DataPage.js +3 -3
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/PrivacyPage.js +3 -3
- package/dist/pages/admin/PrivacyPage.js.map +1 -1
- package/dist/pages/admin/TeamPage.js +1 -1
- package/dist/pages/admin/TeamPage.js.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.js +2 -12
- package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/auth/AuthForm.d.ts +18 -0
- package/dist/pages/auth/AuthForm.d.ts.map +1 -1
- package/dist/pages/auth/AuthForm.js +17 -0
- package/dist/pages/auth/AuthForm.js.map +1 -1
- package/dist/pages/auth/AuthMethods.d.ts +12 -0
- package/dist/pages/auth/AuthMethods.d.ts.map +1 -0
- package/dist/pages/auth/AuthMethods.js +20 -0
- package/dist/pages/auth/AuthMethods.js.map +1 -0
- package/dist/pages/auth/SignInPage.d.ts +7 -0
- package/dist/pages/auth/SignInPage.d.ts.map +1 -1
- package/dist/pages/auth/SignInPage.js +3 -8
- package/dist/pages/auth/SignInPage.js.map +1 -1
- package/dist/pages/auth/SignUpPage.d.ts.map +1 -1
- package/dist/pages/auth/SignUpPage.js +2 -1
- package/dist/pages/auth/SignUpPage.js.map +1 -1
- package/dist/pages/auth/nextPath.d.ts +15 -0
- package/dist/pages/auth/nextPath.d.ts.map +1 -0
- package/dist/pages/auth/nextPath.js +17 -0
- package/dist/pages/auth/nextPath.js.map +1 -0
- package/dist/pages/forms/AddFieldMenu.js +2 -2
- package/dist/pages/forms/AddFieldMenu.js.map +1 -1
- package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
- package/dist/pages/forms/FieldsEditor.js +22 -10
- package/dist/pages/forms/FieldsEditor.js.map +1 -1
- package/dist/pages/forms/MapTargetSearch.d.ts +7 -0
- package/dist/pages/forms/MapTargetSearch.d.ts.map +1 -0
- package/dist/pages/forms/MapTargetSearch.js +32 -0
- package/dist/pages/forms/MapTargetSearch.js.map +1 -0
- package/dist/pages/forms/fieldList.d.ts +6 -7
- package/dist/pages/forms/fieldList.d.ts.map +1 -1
- package/dist/pages/forms/fieldList.js +14 -16
- package/dist/pages/forms/fieldList.js.map +1 -1
- package/dist/pages/forms/template.d.ts.map +1 -1
- package/dist/pages/forms/template.js +7 -0
- package/dist/pages/forms/template.js.map +1 -1
- package/dist/registry/context.d.ts +3 -1
- package/dist/registry/context.d.ts.map +1 -1
- package/dist/registry/context.js +4 -0
- package/dist/registry/context.js.map +1 -1
- package/dist/registry/contributions.d.ts +24 -0
- package/dist/registry/contributions.d.ts.map +1 -1
- package/dist/registry/contributions.js.map +1 -1
- package/dist/registry/registry.d.ts +4 -1
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +6 -0
- package/dist/registry/registry.js.map +1 -1
- package/package.json +2 -2
- package/src/api/resources/activities.ts +10 -2
- package/src/api/resources/conversions.ts +125 -0
- package/src/api/resources/enquiries.ts +9 -35
- package/src/api/resources/importJobs.ts +3 -2
- package/src/app/KelpieApp.tsx +1 -0
- package/src/components/ActivitiesPanel.tsx +20 -14
- package/src/components/ApiKeysPanel.tsx +206 -21
- package/src/components/ConvertRecordDialog.tsx +210 -0
- package/src/components/Shell.tsx +91 -36
- package/src/index.ts +7 -0
- package/src/pages/DealDetail.tsx +35 -0
- package/src/pages/DecisionsPage.tsx +0 -14
- package/src/pages/EnquiryDetail.tsx +37 -90
- package/src/pages/ListDetail.tsx +0 -47
- package/src/pages/OpportunityDetail.tsx +35 -0
- package/src/pages/PartnershipDetail.tsx +35 -0
- package/src/pages/RaiseDetail.tsx +35 -0
- package/src/pages/account/SecurityPage.tsx +6 -1
- package/src/pages/admin/DataPage.tsx +14 -9
- package/src/pages/admin/PrivacyPage.tsx +4 -4
- package/src/pages/admin/TeamPage.tsx +1 -2
- package/src/pages/admin/WorkspaceSettingsPage.tsx +1 -31
- package/src/pages/auth/AuthForm.tsx +38 -0
- package/src/pages/auth/AuthMethods.tsx +29 -0
- package/src/pages/auth/SignInPage.tsx +3 -8
- package/src/pages/auth/SignUpPage.tsx +7 -0
- package/src/pages/auth/nextPath.ts +17 -0
- package/src/pages/forms/AddFieldMenu.tsx +2 -2
- package/src/pages/forms/FieldsEditor.tsx +45 -20
- package/src/pages/forms/MapTargetSearch.tsx +69 -0
- package/src/pages/forms/fieldList.ts +34 -16
- package/src/pages/forms/template.ts +7 -0
- package/src/registry/context.ts +6 -0
- package/src/registry/contributions.ts +26 -0
- 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
|
+
}
|
package/src/components/Shell.tsx
CHANGED
|
@@ -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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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,
|
package/src/pages/DealDetail.tsx
CHANGED
|
@@ -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
|
)}
|