@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
|
@@ -7,11 +7,14 @@ 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
9
|
import {
|
|
10
|
-
useConvertEnquiry,
|
|
11
10
|
useDeleteEnquiry,
|
|
12
11
|
useEnquiry,
|
|
13
12
|
useUpdateEnquiry,
|
|
14
13
|
} from '../api/resources/enquiries.ts'
|
|
14
|
+
import {
|
|
15
|
+
detailPathForPipelineKind,
|
|
16
|
+
useConvertPipelineRecord,
|
|
17
|
+
} from '../api/resources/conversions.ts'
|
|
15
18
|
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
16
19
|
import { useMembers } from '../api/resources/members.ts'
|
|
17
20
|
import { usePeople } from '../api/resources/people.ts'
|
|
@@ -21,6 +24,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
21
24
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
22
25
|
import { Chip } from '../components/Chip.tsx'
|
|
23
26
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
27
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
24
28
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
25
29
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
26
30
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -60,8 +64,8 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
60
64
|
const { id } = useParams()
|
|
61
65
|
const navigate = useNavigate()
|
|
62
66
|
const { record, isLoading, isNotFound, error } = useEnquiry(id)
|
|
67
|
+
const convertRecord = useConvertPipelineRecord('enquiry')
|
|
63
68
|
const deleteEnquiry = useDeleteEnquiry()
|
|
64
|
-
const convertEnquiry = useConvertEnquiry()
|
|
65
69
|
const moduleTabs = inSlotOrder(useRecordTabs('enquiry'))
|
|
66
70
|
const hasCustomFields = useHasCustomFields('enquiry')
|
|
67
71
|
const [activeTab, setActiveTab] = useState('overview')
|
|
@@ -96,12 +100,11 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
96
100
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
97
101
|
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
98
102
|
|
|
99
|
-
const
|
|
100
|
-
record.
|
|
101
|
-
|
|
102
|
-
: record.
|
|
103
|
-
|
|
104
|
-
: null
|
|
103
|
+
const convertedTo =
|
|
104
|
+
record.convertedTo ??
|
|
105
|
+
(record.convertedDealId !== null
|
|
106
|
+
? { targetType: 'deal' as const, targetId: record.convertedDealId }
|
|
107
|
+
: null)
|
|
105
108
|
|
|
106
109
|
return (
|
|
107
110
|
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
@@ -117,28 +120,13 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
117
120
|
<EnquiryHeading enquiry={record} />
|
|
118
121
|
|
|
119
122
|
<div className="flex justify-end gap-2">
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}}
|
|
128
|
-
disabled={convertBlockedReason !== null || convertEnquiry.isPending}
|
|
129
|
-
title={convertBlockedReason ?? undefined}
|
|
130
|
-
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"
|
|
131
|
-
>
|
|
132
|
-
Convert to deal
|
|
133
|
-
</button>
|
|
134
|
-
) : (
|
|
135
|
-
<Link
|
|
136
|
-
to={`/deals/${record.convertedDealId}`}
|
|
137
|
-
className="rounded-md border border-border px-2.5 py-1 text-[12px] font-medium text-accent transition hover:border-accent hover:underline"
|
|
138
|
-
>
|
|
139
|
-
Converted — view deal
|
|
140
|
-
</Link>
|
|
141
|
-
)}
|
|
123
|
+
<ConvertRecordButton
|
|
124
|
+
convertedTo={convertedTo}
|
|
125
|
+
convert={convertRecord}
|
|
126
|
+
onOpenDialog={() => {
|
|
127
|
+
setShowConvert(true)
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
142
130
|
<AgentTasks targetType="enquiry" targetId={record.id} targetLabel={record.name} />
|
|
143
131
|
<DeleteRecord
|
|
144
132
|
recordLabel="Enquiry"
|
|
@@ -154,26 +142,25 @@ export function EnquiryDetail(): React.JSX.Element {
|
|
|
154
142
|
/>
|
|
155
143
|
</div>
|
|
156
144
|
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)}
|
|
145
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
146
|
+
<ConvertRecordAction
|
|
147
|
+
sourceKind="enquiry"
|
|
148
|
+
recordId={record.id}
|
|
149
|
+
recordName={record.name}
|
|
150
|
+
companyId={record.companyId}
|
|
151
|
+
convertedTo={convertedTo}
|
|
152
|
+
convert={convertRecord}
|
|
153
|
+
showDialog={showConvert}
|
|
154
|
+
onOpenDialog={() => {
|
|
155
|
+
setShowConvert(true)
|
|
156
|
+
}}
|
|
157
|
+
onCloseDialog={() => {
|
|
158
|
+
setShowConvert(false)
|
|
159
|
+
}}
|
|
160
|
+
onConverted={(created, targetType) => {
|
|
161
|
+
navigate(detailPathForPipelineKind(targetType, created.id))
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
177
164
|
|
|
178
165
|
<RecordTabs
|
|
179
166
|
tabs={tabs}
|
|
@@ -512,43 +499,3 @@ function EnquiryContacts({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.
|
|
|
512
499
|
</section>
|
|
513
500
|
)
|
|
514
501
|
}
|
|
515
|
-
|
|
516
|
-
function ConvertConfirm({
|
|
517
|
-
enquiry,
|
|
518
|
-
isPending,
|
|
519
|
-
onCancel,
|
|
520
|
-
onConfirm,
|
|
521
|
-
}: {
|
|
522
|
-
readonly enquiry: Enquiry
|
|
523
|
-
readonly isPending: boolean
|
|
524
|
-
readonly onCancel: () => void
|
|
525
|
-
readonly onConfirm: () => void
|
|
526
|
-
}): React.JSX.Element {
|
|
527
|
-
return (
|
|
528
|
-
<div className="rounded-md border border-border bg-surface-raised p-4">
|
|
529
|
-
<p className="text-[13px] text-ink">
|
|
530
|
-
Convert <span className="font-semibold">{enquiry.name}</span> to a Deal. This creates a new
|
|
531
|
-
deal (name, company, owner and linked people carry over) and moves this enquiry to the
|
|
532
|
-
first closed stage.
|
|
533
|
-
</p>
|
|
534
|
-
<div className="mt-3 flex justify-end gap-2">
|
|
535
|
-
<button
|
|
536
|
-
type="button"
|
|
537
|
-
onClick={onCancel}
|
|
538
|
-
disabled={isPending}
|
|
539
|
-
className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink disabled:opacity-50"
|
|
540
|
-
>
|
|
541
|
-
Cancel
|
|
542
|
-
</button>
|
|
543
|
-
<button
|
|
544
|
-
type="button"
|
|
545
|
-
onClick={onConfirm}
|
|
546
|
-
disabled={isPending}
|
|
547
|
-
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-50"
|
|
548
|
-
>
|
|
549
|
-
{isPending ? 'Converting…' : 'Convert'}
|
|
550
|
-
</button>
|
|
551
|
-
</div>
|
|
552
|
-
</div>
|
|
553
|
-
)
|
|
554
|
-
}
|
package/src/pages/ListDetail.tsx
CHANGED
|
@@ -6,7 +6,6 @@ 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 } from '../api/resources/companies.ts'
|
|
9
|
-
import { useConsentPurposes } from '../api/resources/consentPurposes.ts'
|
|
10
9
|
import { useDeals } from '../api/resources/deals.ts'
|
|
11
10
|
import { useDeleteList, useList, useUpdateList } from '../api/resources/lists.ts'
|
|
12
11
|
import {
|
|
@@ -86,57 +85,11 @@ export function ListDetail(): React.JSX.Element {
|
|
|
86
85
|
|
|
87
86
|
<ListDescription list={record} />
|
|
88
87
|
|
|
89
|
-
{record.targetType === 'person' && <ListConsentPurpose list={record} />}
|
|
90
|
-
|
|
91
88
|
<ListMembers listId={record.id} targetType={record.targetType} />
|
|
92
89
|
</div>
|
|
93
90
|
)
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
function ListConsentPurpose({ list }: { readonly list: List }): React.JSX.Element {
|
|
97
|
-
const { patch, error } = useListPatch(list)
|
|
98
|
-
const purposes = useConsentPurposes({ sort: 'sort_order', limit: 200 })
|
|
99
|
-
const options = [
|
|
100
|
-
{ value: '', label: '— none —' },
|
|
101
|
-
...purposes.records.map((purpose) => ({ value: purpose.id, label: purpose.label })),
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div className="mb-6 rounded-md border border-border bg-surface-raised p-4 text-[13px]">
|
|
106
|
-
{error !== null && (
|
|
107
|
-
<div className="mb-2">
|
|
108
|
-
<ErrorPanel error={error} />
|
|
109
|
-
</div>
|
|
110
|
-
)}
|
|
111
|
-
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
|
112
|
-
<div>
|
|
113
|
-
<div className="text-[12px] font-medium text-ink">Consent purpose</div>
|
|
114
|
-
<p className="mt-0.5 text-[12px] text-ink-muted">
|
|
115
|
-
A form submit that adds a person to this list also grants this purpose. Manual and
|
|
116
|
-
API adds capture nothing.
|
|
117
|
-
</p>
|
|
118
|
-
</div>
|
|
119
|
-
<label className="shrink-0">
|
|
120
|
-
<select
|
|
121
|
-
value={list.consentPurposeId ?? ''}
|
|
122
|
-
onChange={(event) => {
|
|
123
|
-
const value = event.target.value
|
|
124
|
-
patch({ consentPurposeId: value.length === 0 ? null : value })
|
|
125
|
-
}}
|
|
126
|
-
className="rounded-md border border-border bg-transparent px-2 py-1 text-[12px] outline-none focus:border-accent"
|
|
127
|
-
>
|
|
128
|
-
{options.map((option) => (
|
|
129
|
-
<option key={option.value} value={option.value}>
|
|
130
|
-
{option.label}
|
|
131
|
-
</option>
|
|
132
|
-
))}
|
|
133
|
-
</select>
|
|
134
|
-
</label>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
93
|
function useListPatch(list: List): PatchResult<ListInput> {
|
|
141
94
|
return usePatch(useUpdateList, list)
|
|
142
95
|
}
|
|
@@ -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 { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
10
14
|
import { useMembers } from '../api/resources/members.ts'
|
|
11
15
|
import {
|
|
@@ -20,6 +24,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
20
24
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
21
25
|
import { Chip } from '../components/Chip.tsx'
|
|
22
26
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
27
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
23
28
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
24
29
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
25
30
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -61,10 +66,12 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
61
66
|
const { id } = useParams()
|
|
62
67
|
const navigate = useNavigate()
|
|
63
68
|
const { record, isLoading, isNotFound, error } = useOpportunity(id)
|
|
69
|
+
const convertRecord = useConvertPipelineRecord('opportunity')
|
|
64
70
|
const deleteOpportunity = useDeleteOpportunity()
|
|
65
71
|
const moduleTabs = inSlotOrder(useRecordTabs('opportunity'))
|
|
66
72
|
const hasCustomFields = useHasCustomFields('opportunity')
|
|
67
73
|
const [activeTab, setActiveTab] = useState('overview')
|
|
74
|
+
const [showConvert, setShowConvert] = useState(false)
|
|
68
75
|
const formSubmissions = useFormSubmissionsForRecord('opportunity', id)
|
|
69
76
|
|
|
70
77
|
if (isNotFound) {
|
|
@@ -94,6 +101,7 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
94
101
|
]
|
|
95
102
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
96
103
|
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
104
|
+
const convertedTo = record.convertedTo
|
|
97
105
|
|
|
98
106
|
return (
|
|
99
107
|
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
@@ -109,6 +117,13 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
109
117
|
<OpportunityHeading opportunity={record} />
|
|
110
118
|
|
|
111
119
|
<div className="flex justify-end gap-2">
|
|
120
|
+
<ConvertRecordButton
|
|
121
|
+
convertedTo={convertedTo}
|
|
122
|
+
convert={convertRecord}
|
|
123
|
+
onOpenDialog={() => {
|
|
124
|
+
setShowConvert(true)
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
112
127
|
<AgentTasks targetType="opportunity" targetId={record.id} targetLabel={record.name} />
|
|
113
128
|
<DeleteRecord
|
|
114
129
|
recordLabel="Opportunity"
|
|
@@ -124,6 +139,26 @@ export function OpportunityDetail(): React.JSX.Element {
|
|
|
124
139
|
/>
|
|
125
140
|
</div>
|
|
126
141
|
|
|
142
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
143
|
+
<ConvertRecordAction
|
|
144
|
+
sourceKind="opportunity"
|
|
145
|
+
recordId={record.id}
|
|
146
|
+
recordName={record.name}
|
|
147
|
+
companyId={record.companyId}
|
|
148
|
+
convertedTo={convertedTo}
|
|
149
|
+
convert={convertRecord}
|
|
150
|
+
showDialog={showConvert}
|
|
151
|
+
onOpenDialog={() => {
|
|
152
|
+
setShowConvert(true)
|
|
153
|
+
}}
|
|
154
|
+
onCloseDialog={() => {
|
|
155
|
+
setShowConvert(false)
|
|
156
|
+
}}
|
|
157
|
+
onConverted={(created, targetType) => {
|
|
158
|
+
navigate(detailPathForPipelineKind(targetType, created.id))
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
|
|
127
162
|
<RecordTabs
|
|
128
163
|
tabs={tabs}
|
|
129
164
|
active={active}
|
|
@@ -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 { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
10
14
|
import { useMembers } from '../api/resources/members.ts'
|
|
11
15
|
import {
|
|
@@ -20,6 +24,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
20
24
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
21
25
|
import { Chip } from '../components/Chip.tsx'
|
|
22
26
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
27
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
23
28
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
24
29
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
25
30
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -61,10 +66,12 @@ export function PartnershipDetail(): React.JSX.Element {
|
|
|
61
66
|
const { id } = useParams()
|
|
62
67
|
const navigate = useNavigate()
|
|
63
68
|
const { record, isLoading, isNotFound, error } = usePartnership(id)
|
|
69
|
+
const convertRecord = useConvertPipelineRecord('partnership')
|
|
64
70
|
const deletePartnership = useDeletePartnership()
|
|
65
71
|
const moduleTabs = inSlotOrder(useRecordTabs('partnership'))
|
|
66
72
|
const hasCustomFields = useHasCustomFields('partnership')
|
|
67
73
|
const [activeTab, setActiveTab] = useState('overview')
|
|
74
|
+
const [showConvert, setShowConvert] = useState(false)
|
|
68
75
|
const formSubmissions = useFormSubmissionsForRecord('partnership', id)
|
|
69
76
|
|
|
70
77
|
if (isNotFound) {
|
|
@@ -94,6 +101,7 @@ export function PartnershipDetail(): React.JSX.Element {
|
|
|
94
101
|
]
|
|
95
102
|
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
96
103
|
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
104
|
+
const convertedTo = record.convertedTo
|
|
97
105
|
|
|
98
106
|
return (
|
|
99
107
|
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
@@ -109,6 +117,13 @@ export function PartnershipDetail(): React.JSX.Element {
|
|
|
109
117
|
<PartnershipHeading partnership={record} />
|
|
110
118
|
|
|
111
119
|
<div className="flex justify-end gap-2">
|
|
120
|
+
<ConvertRecordButton
|
|
121
|
+
convertedTo={convertedTo}
|
|
122
|
+
convert={convertRecord}
|
|
123
|
+
onOpenDialog={() => {
|
|
124
|
+
setShowConvert(true)
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
112
127
|
<AgentTasks targetType="partnership" targetId={record.id} targetLabel={record.name} />
|
|
113
128
|
<DeleteRecord
|
|
114
129
|
recordLabel="Partnership"
|
|
@@ -124,6 +139,26 @@ export function PartnershipDetail(): React.JSX.Element {
|
|
|
124
139
|
/>
|
|
125
140
|
</div>
|
|
126
141
|
|
|
142
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
143
|
+
<ConvertRecordAction
|
|
144
|
+
sourceKind="partnership"
|
|
145
|
+
recordId={record.id}
|
|
146
|
+
recordName={record.name}
|
|
147
|
+
companyId={record.companyId}
|
|
148
|
+
convertedTo={convertedTo}
|
|
149
|
+
convert={convertRecord}
|
|
150
|
+
showDialog={showConvert}
|
|
151
|
+
onOpenDialog={() => {
|
|
152
|
+
setShowConvert(true)
|
|
153
|
+
}}
|
|
154
|
+
onCloseDialog={() => {
|
|
155
|
+
setShowConvert(false)
|
|
156
|
+
}}
|
|
157
|
+
onConverted={(created, targetType) => {
|
|
158
|
+
navigate(detailPathForPipelineKind(targetType, created.id))
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
|
|
127
162
|
<RecordTabs
|
|
128
163
|
tabs={tabs}
|
|
129
164
|
active={active}
|
|
@@ -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 { useMembers } from '../api/resources/members.ts'
|
|
10
14
|
import { usePeople } from '../api/resources/people.ts'
|
|
11
15
|
import { usePipelineStages } from '../api/resources/pipelineStages.ts'
|
|
@@ -15,6 +19,7 @@ import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.t
|
|
|
15
19
|
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
16
20
|
import { Chip } from '../components/Chip.tsx'
|
|
17
21
|
import type { ChipTone } from '../components/Chip.tsx'
|
|
22
|
+
import { ConvertRecordAction, ConvertRecordButton } from '../components/ConvertRecordDialog.tsx'
|
|
18
23
|
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
19
24
|
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
20
25
|
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
@@ -60,10 +65,12 @@ export function RaiseDetail(): React.JSX.Element {
|
|
|
60
65
|
const { id } = useParams()
|
|
61
66
|
const navigate = useNavigate()
|
|
62
67
|
const { record, isLoading, isNotFound, error } = useRaise(id)
|
|
68
|
+
const convertRecord = useConvertPipelineRecord('raise')
|
|
63
69
|
const deleteRaise = useDeleteRaise()
|
|
64
70
|
const moduleTabs = inSlotOrder(useRecordTabs('raise'))
|
|
65
71
|
const hasCustomFields = useHasCustomFields('raise')
|
|
66
72
|
const [activeTab, setActiveTab] = useState('overview')
|
|
73
|
+
const [showConvert, setShowConvert] = useState(false)
|
|
67
74
|
|
|
68
75
|
if (isNotFound) {
|
|
69
76
|
return <NotFoundPanel label="Raise" backTo="/fundraising" />
|
|
@@ -89,6 +96,7 @@ export function RaiseDetail(): 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 RaiseDetail(): React.JSX.Element {
|
|
|
104
112
|
<RaiseHeading raise={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="raise" targetId={record.id} targetLabel={record.name} />
|
|
108
123
|
<DeleteRecord
|
|
109
124
|
recordLabel="Raise"
|
|
@@ -119,6 +134,26 @@ export function RaiseDetail(): React.JSX.Element {
|
|
|
119
134
|
/>
|
|
120
135
|
</div>
|
|
121
136
|
|
|
137
|
+
{convertRecord.error !== null && <ErrorPanel error={convertRecord.error} />}
|
|
138
|
+
<ConvertRecordAction
|
|
139
|
+
sourceKind="raise"
|
|
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="Raise sections">
|
|
123
158
|
{active === 'overview' && <RaiseOverview raise={record} />}
|
|
124
159
|
{active === 'fields' && <RaiseFields raise={record} />}
|
|
@@ -88,7 +88,8 @@ function ChangePasswordForm(): React.JSX.Element {
|
|
|
88
88
|
<form onSubmit={submit} className="space-y-3">
|
|
89
89
|
<h2 className="text-[14px] font-semibold text-ink">Change password</h2>
|
|
90
90
|
<p className="max-w-md text-[12px] text-ink-muted">
|
|
91
|
-
Changing your password signs you out everywhere except this browser.
|
|
91
|
+
Changing your password signs you out everywhere except this browser. If you have never set
|
|
92
|
+
one, use <span className="font-medium text-ink">Forgot password</span> on the sign-in page.
|
|
92
93
|
</p>
|
|
93
94
|
|
|
94
95
|
<label className="block max-w-md">
|
|
@@ -216,6 +217,10 @@ function SessionRow({
|
|
|
216
217
|
<td className="px-4 py-3">
|
|
217
218
|
<div className="font-medium break-all text-ink">{session.device ?? 'Unknown device'}</div>
|
|
218
219
|
{session.current && <span className="text-[11px] font-medium text-success">This device</span>}
|
|
220
|
+
{/* Null for a password sign-in, which is the ordinary case and needs no label. */}
|
|
221
|
+
{session.signedInVia !== null && (
|
|
222
|
+
<div className="text-[11px] text-ink-faint">via {session.signedInVia}</div>
|
|
223
|
+
)}
|
|
219
224
|
</td>
|
|
220
225
|
{/* Always unknown today: the service records the User-Agent and nothing
|
|
221
226
|
derives a location from the request. The column stays because the field
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CONFLICT_MODE_LABELS,
|
|
3
|
+
EXPORT_OBJECT_LABELS,
|
|
4
|
+
EXPORT_OBJECTS,
|
|
3
5
|
IMPORT_OBJECTS,
|
|
4
6
|
IMPORT_SOURCES,
|
|
5
7
|
MATCH_KEYS,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
requiredColumns,
|
|
14
16
|
} from '@kelpie/schemas'
|
|
15
17
|
import type {
|
|
18
|
+
ExportObject,
|
|
16
19
|
ImportColumnMap,
|
|
17
20
|
ImportConflictMode,
|
|
18
21
|
ImportJob,
|
|
@@ -65,7 +68,7 @@ const STEPS: readonly { readonly id: WizardStep; readonly label: string }[] = [
|
|
|
65
68
|
]
|
|
66
69
|
|
|
67
70
|
export function DataPage(): ReactNode {
|
|
68
|
-
const [exportObject, setExportObject] = useState<
|
|
71
|
+
const [exportObject, setExportObject] = useState<ExportObject>('companies')
|
|
69
72
|
|
|
70
73
|
const [step, setStep] = useState<WizardStep>('source')
|
|
71
74
|
const [source, setSource] = useState<ImportSource>('custom')
|
|
@@ -133,7 +136,7 @@ export function DataPage(): ReactNode {
|
|
|
133
136
|
setProblem(null)
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
async function onDownload(target:
|
|
139
|
+
async function onDownload(target: ExportObject, template: boolean): Promise<void> {
|
|
137
140
|
setProblem(null)
|
|
138
141
|
saveCsv(await exportCsv.runAsync({ object: target, template }))
|
|
139
142
|
}
|
|
@@ -255,7 +258,7 @@ export function DataPage(): ReactNode {
|
|
|
255
258
|
<div className="animate-slide-in space-y-10">
|
|
256
259
|
<PageHeader
|
|
257
260
|
title="Import & export"
|
|
258
|
-
description="CSV for People, Companies, Positions, and
|
|
261
|
+
description="CSV for People, Companies, Positions, Deals, and other CRM objects. Custom field values export with each record; definitions export separately."
|
|
259
262
|
/>
|
|
260
263
|
|
|
261
264
|
{failure === null ? null : (
|
|
@@ -272,20 +275,20 @@ export function DataPage(): ReactNode {
|
|
|
272
275
|
<div>
|
|
273
276
|
<h2 className="text-[15px] font-semibold text-ink">Export</h2>
|
|
274
277
|
<p className="mt-1 text-[13px] text-ink-muted">
|
|
275
|
-
Download this workspace as CSV.
|
|
276
|
-
|
|
278
|
+
Download this workspace as CSV. Importable objects round-trip through the wizard; other
|
|
279
|
+
objects and custom field definitions export for backup and analysis.
|
|
277
280
|
</p>
|
|
278
281
|
</div>
|
|
279
282
|
<div className="flex flex-wrap items-end gap-3">
|
|
280
283
|
<Field label="Object">
|
|
281
284
|
<select
|
|
282
285
|
value={exportObject}
|
|
283
|
-
onChange={(event) => setExportObject(event.target.value as
|
|
286
|
+
onChange={(event) => setExportObject(event.target.value as ExportObject)}
|
|
284
287
|
className="w-48 rounded-md border border-border bg-surface-raised px-3 py-2 text-[13px] outline-none focus:border-accent"
|
|
285
288
|
>
|
|
286
|
-
{
|
|
289
|
+
{EXPORT_OBJECTS.map((item) => (
|
|
287
290
|
<option key={item} value={item}>
|
|
288
|
-
{
|
|
291
|
+
{EXPORT_OBJECT_LABELS[item]}
|
|
289
292
|
</option>
|
|
290
293
|
))}
|
|
291
294
|
</select>
|
|
@@ -377,7 +380,9 @@ export function DataPage(): ReactNode {
|
|
|
377
380
|
</button>
|
|
378
381
|
</div>
|
|
379
382
|
<p className="text-[12px] text-ink-muted">
|
|
380
|
-
Import order for linked data: Companies → People → Positions → Deals
|
|
383
|
+
Import order for linked data: Companies → People → Positions → pipeline records (Deals,
|
|
384
|
+
Opportunities, Enquiries, Partnerships, Fundraising). Custom field definitions can be
|
|
385
|
+
imported before the records that use them.
|
|
381
386
|
</p>
|
|
382
387
|
<button
|
|
383
388
|
type="submit"
|
|
@@ -132,14 +132,14 @@ function EmptyState({ onAdd, onSeedStarters, isSeeding }: EmptyStateProps): Reac
|
|
|
132
132
|
<p className="mt-1 text-ink-muted">
|
|
133
133
|
A purpose is what a person is agreeing to — being contacted at all, marketing, research.
|
|
134
134
|
Every workspace usually has at least a general <span className="font-medium">Contact</span>{' '}
|
|
135
|
-
purpose. Forms
|
|
135
|
+
purpose. Forms and imports that don’t need consent don’t need one.
|
|
136
136
|
</p>
|
|
137
137
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
138
138
|
<button
|
|
139
139
|
type="button"
|
|
140
140
|
onClick={onSeedStarters}
|
|
141
141
|
disabled={isSeeding}
|
|
142
|
-
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-
|
|
142
|
+
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
|
|
143
143
|
>
|
|
144
144
|
{isSeeding ? 'Adding…' : 'Add Contact & Marketing'}
|
|
145
145
|
</button>
|
|
@@ -268,7 +268,7 @@ function AddPurposeForm({ onDone }: AddPurposeFormProps): React.JSX.Element {
|
|
|
268
268
|
<button
|
|
269
269
|
type="submit"
|
|
270
270
|
disabled={create.isPending}
|
|
271
|
-
className="rounded-md bg-accent px-3 py-1 text-[12px] font-
|
|
271
|
+
className="rounded-md bg-accent px-3 py-1 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
|
|
272
272
|
>
|
|
273
273
|
{create.isPending ? 'Adding…' : 'Add purpose'}
|
|
274
274
|
</button>
|
|
@@ -389,7 +389,7 @@ function PurposeRow({ purpose }: PurposeRowProps): React.JSX.Element {
|
|
|
389
389
|
<button
|
|
390
390
|
type="submit"
|
|
391
391
|
disabled={update.isPending}
|
|
392
|
-
className="rounded-md bg-accent px-3 py-1 text-[12px] font-
|
|
392
|
+
className="rounded-md bg-accent px-3 py-1 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50"
|
|
393
393
|
>
|
|
394
394
|
{update.isPending ? 'Saving…' : 'Save'}
|
|
395
395
|
</button>
|
|
@@ -114,8 +114,7 @@ function InviteForm(): React.JSX.Element {
|
|
|
114
114
|
</div>
|
|
115
115
|
{sendInvite.error !== null && <ErrorPanel error={sendInvite.error} />}
|
|
116
116
|
<p className="text-[11px] text-ink-faint">
|
|
117
|
-
The invitation is emailed and lasts seven days.
|
|
118
|
-
member who has already joined.
|
|
117
|
+
The invitation is emailed and lasts seven days.
|
|
119
118
|
</p>
|
|
120
119
|
</form>
|
|
121
120
|
)
|