@kelpie/ui 0.8.0 → 0.10.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/resource.d.ts +1 -1
- package/dist/api/resource.d.ts.map +1 -1
- package/dist/api/resource.js.map +1 -1
- package/dist/api/resources/consentPurposes.d.ts +13 -0
- package/dist/api/resources/consentPurposes.d.ts.map +1 -0
- package/dist/api/resources/consentPurposes.js +39 -0
- package/dist/api/resources/consentPurposes.js.map +1 -0
- package/dist/api/resources/customFields.d.ts +15 -0
- package/dist/api/resources/customFields.d.ts.map +1 -0
- package/dist/api/resources/customFields.js +41 -0
- package/dist/api/resources/customFields.js.map +1 -0
- package/dist/api/resources/enquiries.d.ts +39 -0
- package/dist/api/resources/enquiries.d.ts.map +1 -0
- package/dist/api/resources/enquiries.js +68 -0
- package/dist/api/resources/enquiries.js.map +1 -0
- package/dist/api/resources/forms.d.ts +21 -1
- package/dist/api/resources/forms.d.ts.map +1 -1
- package/dist/api/resources/forms.js +41 -0
- package/dist/api/resources/forms.js.map +1 -1
- package/dist/api/resources/importJobs.d.ts +5 -0
- package/dist/api/resources/importJobs.d.ts.map +1 -1
- package/dist/api/resources/importJobs.js +3 -0
- package/dist/api/resources/importJobs.js.map +1 -1
- package/dist/api/resources/opportunities.d.ts +2 -0
- package/dist/api/resources/opportunities.d.ts.map +1 -1
- package/dist/api/resources/opportunities.js +1 -0
- package/dist/api/resources/opportunities.js.map +1 -1
- package/dist/api/resources/sampleData.d.ts.map +1 -1
- package/dist/api/resources/sampleData.js +1 -0
- package/dist/api/resources/sampleData.js.map +1 -1
- package/dist/app/KelpieApp.d.ts.map +1 -1
- package/dist/app/KelpieApp.js +7 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +1 -0
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/CustomFieldsPanel.d.ts +23 -0
- package/dist/components/CustomFieldsPanel.d.ts.map +1 -0
- package/dist/components/CustomFieldsPanel.js +116 -0
- package/dist/components/CustomFieldsPanel.js.map +1 -0
- package/dist/components/FormsPanel.d.ts +18 -0
- package/dist/components/FormsPanel.d.ts.map +1 -0
- package/dist/components/FormsPanel.js +37 -0
- package/dist/components/FormsPanel.js.map +1 -0
- package/dist/components/PlanAttention.d.ts +3 -2
- package/dist/components/PlanAttention.d.ts.map +1 -1
- package/dist/components/PlanAttention.js +7 -3
- package/dist/components/PlanAttention.js.map +1 -1
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +3 -0
- package/dist/components/Shell.js.map +1 -1
- package/dist/components/useHasCustomFields.d.ts +13 -0
- package/dist/components/useHasCustomFields.d.ts.map +1 -0
- package/dist/components/useHasCustomFields.js +16 -0
- package/dist/components/useHasCustomFields.js.map +1 -0
- package/dist/pages/CompanyDetail.d.ts.map +1 -1
- package/dist/pages/CompanyDetail.js +24 -2
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +18 -1
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +2 -0
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiriesPage.d.ts +2 -0
- package/dist/pages/EnquiriesPage.d.ts.map +1 -0
- package/dist/pages/EnquiriesPage.js +251 -0
- package/dist/pages/EnquiriesPage.js.map +1 -0
- package/dist/pages/EnquiryDetail.d.ts +2 -0
- package/dist/pages/EnquiryDetail.d.ts.map +1 -0
- package/dist/pages/EnquiryDetail.js +207 -0
- package/dist/pages/EnquiryDetail.js.map +1 -0
- package/dist/pages/EnquiryStageSettingsPage.d.ts +3 -0
- package/dist/pages/EnquiryStageSettingsPage.d.ts.map +1 -0
- package/dist/pages/EnquiryStageSettingsPage.js +34 -0
- package/dist/pages/EnquiryStageSettingsPage.js.map +1 -0
- package/dist/pages/FormDetail.d.ts.map +1 -1
- package/dist/pages/FormDetail.js +19 -1
- package/dist/pages/FormDetail.js.map +1 -1
- package/dist/pages/ListDetail.d.ts.map +1 -1
- package/dist/pages/ListDetail.js +27 -1
- package/dist/pages/ListDetail.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +62 -3
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +17 -1
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/PeoplePage.d.ts.map +1 -1
- package/dist/pages/PeoplePage.js +17 -0
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +63 -3
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +3 -0
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +11 -1
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/RoleDetail.js +1 -1
- package/dist/pages/RoleDetail.js.map +1 -1
- package/dist/pages/SearchPage.d.ts.map +1 -1
- package/dist/pages/SearchPage.js +1 -0
- package/dist/pages/SearchPage.js.map +1 -1
- package/dist/pages/admin/DataPage.d.ts.map +1 -1
- package/dist/pages/admin/DataPage.js +18 -3
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/FieldsPage.d.ts +2 -0
- package/dist/pages/admin/FieldsPage.d.ts.map +1 -0
- package/dist/pages/admin/FieldsPage.js +164 -0
- package/dist/pages/admin/FieldsPage.js.map +1 -0
- package/dist/pages/admin/PrivacyPage.d.ts +9 -0
- package/dist/pages/admin/PrivacyPage.d.ts.map +1 -0
- package/dist/pages/admin/PrivacyPage.js +170 -0
- package/dist/pages/admin/PrivacyPage.js.map +1 -0
- package/dist/pages/dashboard/attention.d.ts.map +1 -1
- package/dist/pages/dashboard/attention.js +3 -0
- package/dist/pages/dashboard/attention.js.map +1 -1
- package/dist/pages/forms/AddFieldMenu.d.ts +17 -0
- package/dist/pages/forms/AddFieldMenu.d.ts.map +1 -0
- package/dist/pages/forms/AddFieldMenu.js +43 -0
- package/dist/pages/forms/AddFieldMenu.js.map +1 -0
- package/dist/pages/forms/EmbedPanel.d.ts +4 -0
- package/dist/pages/forms/EmbedPanel.d.ts.map +1 -1
- package/dist/pages/forms/EmbedPanel.js +62 -3
- package/dist/pages/forms/EmbedPanel.js.map +1 -1
- package/dist/pages/forms/FieldsEditor.d.ts.map +1 -1
- package/dist/pages/forms/FieldsEditor.js +179 -36
- package/dist/pages/forms/FieldsEditor.js.map +1 -1
- package/dist/pages/forms/FormActions.d.ts +6 -0
- package/dist/pages/forms/FormActions.d.ts.map +1 -0
- package/dist/pages/forms/FormActions.js +149 -0
- package/dist/pages/forms/FormActions.js.map +1 -0
- package/dist/pages/forms/FormSettings.d.ts.map +1 -1
- package/dist/pages/forms/FormSettings.js +15 -19
- package/dist/pages/forms/FormSettings.js.map +1 -1
- package/dist/pages/forms/SubmissionDetail.d.ts +2 -0
- package/dist/pages/forms/SubmissionDetail.d.ts.map +1 -0
- package/dist/pages/forms/SubmissionDetail.js +76 -0
- package/dist/pages/forms/SubmissionDetail.js.map +1 -0
- package/dist/pages/forms/SubmissionsTable.d.ts.map +1 -1
- package/dist/pages/forms/SubmissionsTable.js +56 -8
- package/dist/pages/forms/SubmissionsTable.js.map +1 -1
- package/dist/pages/forms/fieldList.d.ts +11 -5
- package/dist/pages/forms/fieldList.d.ts.map +1 -1
- package/dist/pages/forms/fieldList.js +66 -6
- package/dist/pages/forms/fieldList.js.map +1 -1
- package/dist/pages/forms/template.d.ts +17 -0
- package/dist/pages/forms/template.d.ts.map +1 -1
- package/dist/pages/forms/template.js +93 -0
- package/dist/pages/forms/template.js.map +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
- package/package.json +2 -2
- package/src/api/resource.ts +6 -1
- package/src/api/resources/consentPurposes.ts +83 -0
- package/src/api/resources/customFields.ts +88 -0
- package/src/api/resources/enquiries.ts +118 -0
- package/src/api/resources/forms.ts +58 -0
- package/src/api/resources/importJobs.ts +9 -0
- package/src/api/resources/opportunities.ts +3 -0
- package/src/api/resources/sampleData.ts +1 -0
- package/src/app/KelpieApp.tsx +12 -0
- package/src/components/ActivitiesPanel.tsx +1 -0
- package/src/components/CustomFieldsPanel.tsx +330 -0
- package/src/components/FormsPanel.tsx +86 -0
- package/src/components/PlanAttention.tsx +20 -3
- package/src/components/Shell.tsx +3 -0
- package/src/components/useHasCustomFields.ts +18 -0
- package/src/pages/CompanyDetail.tsx +36 -1
- package/src/pages/DealDetail.tsx +31 -0
- package/src/pages/DecisionsPage.tsx +2 -0
- package/src/pages/EnquiriesPage.tsx +434 -0
- package/src/pages/EnquiryDetail.tsx +554 -0
- package/src/pages/EnquiryStageSettingsPage.tsx +63 -0
- package/src/pages/FormDetail.tsx +23 -1
- package/src/pages/ListDetail.tsx +62 -0
- package/src/pages/OpportunityDetail.tsx +165 -2
- package/src/pages/PartnershipDetail.tsx +32 -0
- package/src/pages/PeoplePage.tsx +20 -0
- package/src/pages/PersonDetail.tsx +152 -3
- package/src/pages/PlanningPage.tsx +3 -0
- package/src/pages/RaiseDetail.tsx +21 -0
- package/src/pages/RoleDetail.tsx +1 -1
- package/src/pages/SearchPage.tsx +1 -0
- package/src/pages/admin/DataPage.tsx +51 -3
- package/src/pages/admin/FieldsPage.tsx +455 -0
- package/src/pages/admin/PrivacyPage.tsx +461 -0
- package/src/pages/dashboard/attention.ts +3 -0
- package/src/pages/forms/AddFieldMenu.tsx +121 -0
- package/src/pages/forms/EmbedPanel.tsx +163 -2
- package/src/pages/forms/FieldsEditor.tsx +539 -165
- package/src/pages/forms/FormActions.tsx +552 -0
- package/src/pages/forms/FormSettings.tsx +29 -70
- package/src/pages/forms/SubmissionDetail.tsx +249 -0
- package/src/pages/forms/SubmissionsTable.tsx +109 -16
- package/src/pages/forms/fieldList.ts +68 -5
- package/src/pages/forms/template.ts +103 -0
- package/src/pages/onboarding/WorkspaceStepPage.tsx +1 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import type { Enquiry, EnquiryInput, PipelineStage } from '@kelpie/schemas'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import type { FormEvent } from 'react'
|
|
4
|
+
import { Link, useNavigate, useParams } from 'react-router'
|
|
5
|
+
|
|
6
|
+
import { usePatch } from '../api/resource.ts'
|
|
7
|
+
import type { PatchResult } from '../api/resource.ts'
|
|
8
|
+
import { useCompanies, useCompany } from '../api/resources/companies.ts'
|
|
9
|
+
import {
|
|
10
|
+
useConvertEnquiry,
|
|
11
|
+
useDeleteEnquiry,
|
|
12
|
+
useEnquiry,
|
|
13
|
+
useUpdateEnquiry,
|
|
14
|
+
} from '../api/resources/enquiries.ts'
|
|
15
|
+
import { useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
16
|
+
import { useMembers } from '../api/resources/members.ts'
|
|
17
|
+
import { usePeople } from '../api/resources/people.ts'
|
|
18
|
+
import { usePipelineStages } from '../api/resources/pipelineStages.ts'
|
|
19
|
+
import { useRecordPlanItems } from '../api/resources/planItems.ts'
|
|
20
|
+
import { ActivitiesPanel, LatestActivity } from '../components/ActivitiesPanel.tsx'
|
|
21
|
+
import { AgentTasks } from '../components/AgentTasks.tsx'
|
|
22
|
+
import { Chip } from '../components/Chip.tsx'
|
|
23
|
+
import type { ChipTone } from '../components/Chip.tsx'
|
|
24
|
+
import { DecisionsPanel } from '../components/DecisionsPanel.tsx'
|
|
25
|
+
import { DeleteRecord } from '../components/DeleteRecord.tsx'
|
|
26
|
+
import { EntitySearch } from '../components/EntitySearch.tsx'
|
|
27
|
+
import { FormsPanel } from '../components/FormsPanel.tsx'
|
|
28
|
+
import { CustomFieldsPanel } from '../components/CustomFieldsPanel.tsx'
|
|
29
|
+
import { useHasCustomFields } from '../components/useHasCustomFields.ts'
|
|
30
|
+
import { InlineEdit } from '../components/InlineEdit.tsx'
|
|
31
|
+
import { ListsPanel } from '../components/ListsPanel.tsx'
|
|
32
|
+
import { NotesPanel } from '../components/NotesPanel.tsx'
|
|
33
|
+
import { PlanAttention } from '../components/PlanAttention.tsx'
|
|
34
|
+
import { PlanPanel } from '../components/PlanPanel.tsx'
|
|
35
|
+
import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryState.tsx'
|
|
36
|
+
import { RecordTabs } from '../components/RecordTabs.tsx'
|
|
37
|
+
import type { RecordTabDescriptor } from '../components/RecordTabs.tsx'
|
|
38
|
+
import { SectionHeader } from '../components/SectionHeader.tsx'
|
|
39
|
+
import { SidebarField } from '../components/SidebarField.tsx'
|
|
40
|
+
import { SummaryBlock } from '../components/SummaryBlock.tsx'
|
|
41
|
+
import { useRecordTabs } from '../registry/context.ts'
|
|
42
|
+
import { inSlotOrder } from '../registry/registry.ts'
|
|
43
|
+
import { toTags } from './fields.ts'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One enquiry.
|
|
47
|
+
*
|
|
48
|
+
* Same shape as `OpportunityDetail` minus the target date, plus a
|
|
49
|
+
* Convert-to-Deal action that becomes a link to the created deal once the
|
|
50
|
+
* enquiry has been converted.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const STAGE_TONES: Readonly<Record<string, ChipTone>> = {
|
|
54
|
+
new: 'accent',
|
|
55
|
+
in_progress: 'warning',
|
|
56
|
+
closed: 'neutral',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function EnquiryDetail(): React.JSX.Element {
|
|
60
|
+
const { id } = useParams()
|
|
61
|
+
const navigate = useNavigate()
|
|
62
|
+
const { record, isLoading, isNotFound, error } = useEnquiry(id)
|
|
63
|
+
const deleteEnquiry = useDeleteEnquiry()
|
|
64
|
+
const convertEnquiry = useConvertEnquiry()
|
|
65
|
+
const moduleTabs = inSlotOrder(useRecordTabs('enquiry'))
|
|
66
|
+
const hasCustomFields = useHasCustomFields('enquiry')
|
|
67
|
+
const [activeTab, setActiveTab] = useState('overview')
|
|
68
|
+
const [showConvert, setShowConvert] = useState(false)
|
|
69
|
+
const formSubmissions = useFormSubmissionsForRecord('enquiry', id)
|
|
70
|
+
|
|
71
|
+
if (isNotFound) {
|
|
72
|
+
return <NotFoundPanel label="Enquiry" backTo="/enquiries" />
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error !== null) {
|
|
76
|
+
return <ErrorPanel error={error} />
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isLoading || record === undefined || id === undefined) {
|
|
80
|
+
return <LoadingPanel label="Loading enquiry…" />
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tabs: readonly RecordTabDescriptor<string>[] = [
|
|
84
|
+
{ id: 'overview', label: 'Overview' },
|
|
85
|
+
...(hasCustomFields ? [{ id: 'fields', label: 'Fields' }] : []),
|
|
86
|
+
{ id: 'plan', label: 'Plan' },
|
|
87
|
+
{ id: 'activity', label: 'Activity' },
|
|
88
|
+
{ id: 'notes', label: 'Notes' },
|
|
89
|
+
{ id: 'decisions', label: 'Decisions' },
|
|
90
|
+
{ id: 'lists', label: 'Lists' },
|
|
91
|
+
...(formSubmissions.records.length === 0
|
|
92
|
+
? []
|
|
93
|
+
: [{ id: 'forms', label: 'Forms', count: formSubmissions.records.length }]),
|
|
94
|
+
...moduleTabs.map((tab) => ({ id: tab.id, label: tab.label })),
|
|
95
|
+
]
|
|
96
|
+
const active = tabs.some((tab) => tab.id === activeTab) ? activeTab : 'overview'
|
|
97
|
+
const moduleTab = moduleTabs.find((tab) => tab.id === active)
|
|
98
|
+
|
|
99
|
+
const convertBlockedReason =
|
|
100
|
+
record.convertedDealId !== null
|
|
101
|
+
? null
|
|
102
|
+
: record.companyId === null
|
|
103
|
+
? 'Link a company first'
|
|
104
|
+
: null
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="animate-fade-in mx-auto max-w-6xl">
|
|
108
|
+
<Link
|
|
109
|
+
to="/enquiries"
|
|
110
|
+
className="mb-4 inline-flex text-[12px] font-medium text-ink-muted transition hover:text-accent"
|
|
111
|
+
>
|
|
112
|
+
← Enquiries
|
|
113
|
+
</Link>
|
|
114
|
+
|
|
115
|
+
<div className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_280px]">
|
|
116
|
+
<div className="min-w-0 space-y-8">
|
|
117
|
+
<EnquiryHeading enquiry={record} />
|
|
118
|
+
|
|
119
|
+
<div className="flex justify-end gap-2">
|
|
120
|
+
{record.convertedDealId === null ? (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => {
|
|
124
|
+
if (convertBlockedReason === null) {
|
|
125
|
+
setShowConvert(true)
|
|
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
|
+
)}
|
|
142
|
+
<AgentTasks targetType="enquiry" targetId={record.id} targetLabel={record.name} />
|
|
143
|
+
<DeleteRecord
|
|
144
|
+
recordLabel="Enquiry"
|
|
145
|
+
recordName={record.name}
|
|
146
|
+
isPending={deleteEnquiry.isPending}
|
|
147
|
+
error={deleteEnquiry.error}
|
|
148
|
+
onConfirm={() => {
|
|
149
|
+
deleteEnquiry
|
|
150
|
+
.runAsync(record.id)
|
|
151
|
+
.then(() => navigate('/enquiries'))
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{convertEnquiry.error !== null && <ErrorPanel error={convertEnquiry.error} />}
|
|
158
|
+
{showConvert && (
|
|
159
|
+
<ConvertConfirm
|
|
160
|
+
enquiry={record}
|
|
161
|
+
isPending={convertEnquiry.isPending}
|
|
162
|
+
onCancel={() => {
|
|
163
|
+
setShowConvert(false)
|
|
164
|
+
}}
|
|
165
|
+
onConfirm={() => {
|
|
166
|
+
convertEnquiry
|
|
167
|
+
.runAsync(record.id)
|
|
168
|
+
.then((deal) => {
|
|
169
|
+
setShowConvert(false)
|
|
170
|
+
|
|
171
|
+
return navigate(`/deals/${deal.id}`)
|
|
172
|
+
})
|
|
173
|
+
.catch(() => undefined)
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<RecordTabs
|
|
179
|
+
tabs={tabs}
|
|
180
|
+
active={active}
|
|
181
|
+
onChange={setActiveTab}
|
|
182
|
+
ariaLabel="Enquiry sections"
|
|
183
|
+
>
|
|
184
|
+
{active === 'overview' && <EnquiryOverview enquiry={record} />}
|
|
185
|
+
{active === 'fields' && <EnquiryFields enquiry={record} />}
|
|
186
|
+
{active === 'plan' && <PlanPanel targetType="enquiry" targetId={record.id} />}
|
|
187
|
+
{active === 'activity' && (
|
|
188
|
+
<ActivitiesPanel targetType="enquiry" targetId={record.id} />
|
|
189
|
+
)}
|
|
190
|
+
{active === 'notes' && <NotesPanel targetType="enquiry" targetId={record.id} />}
|
|
191
|
+
{active === 'decisions' && (
|
|
192
|
+
<DecisionsPanel targetType="enquiry" targetId={record.id} />
|
|
193
|
+
)}
|
|
194
|
+
{active === 'lists' && <ListsPanel targetType="enquiry" targetId={record.id} />}
|
|
195
|
+
{active === 'forms' && <FormsPanel targetType="enquiry" targetId={record.id} />}
|
|
196
|
+
{moduleTab?.render({ objectType: 'enquiry', recordId: record.id })}
|
|
197
|
+
</RecordTabs>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<aside className="space-y-4 text-[12px] lg:sticky lg:top-6">
|
|
201
|
+
<EnquirySidebar enquiry={record} />
|
|
202
|
+
<EnquiryContacts enquiry={record} />
|
|
203
|
+
</aside>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function useEnquiryPatch(enquiry: Enquiry): PatchResult<EnquiryInput> {
|
|
210
|
+
return usePatch(useUpdateEnquiry, enquiry)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function EnquiryHeading({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
|
|
214
|
+
const { patch, error } = useEnquiryPatch(enquiry)
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="min-w-0 flex-1">
|
|
218
|
+
{error !== null && (
|
|
219
|
+
<div className="mb-2">
|
|
220
|
+
<ErrorPanel error={error} />
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
<InlineEdit
|
|
224
|
+
value={enquiry.name}
|
|
225
|
+
onChange={(name) => {
|
|
226
|
+
patch({ name })
|
|
227
|
+
}}
|
|
228
|
+
displayClassName="text-[22px] font-semibold tracking-tight text-ink not-italic"
|
|
229
|
+
emptyLabel="Untitled enquiry"
|
|
230
|
+
/>
|
|
231
|
+
<div className="mt-1">
|
|
232
|
+
<InlineEdit
|
|
233
|
+
value={enquiry.source}
|
|
234
|
+
onChange={(source) => {
|
|
235
|
+
patch({ source })
|
|
236
|
+
}}
|
|
237
|
+
displayClassName="text-[13px] text-ink-muted not-italic"
|
|
238
|
+
emptyLabel="Add source…"
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function EnquiryOverview({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
|
|
246
|
+
const { patch, error } = useEnquiryPatch(enquiry)
|
|
247
|
+
const planItems = useRecordPlanItems('enquiry', enquiry.id)
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div className="space-y-8">
|
|
251
|
+
{error !== null && <ErrorPanel error={error} />}
|
|
252
|
+
<SummaryBlock
|
|
253
|
+
value={enquiry.summary}
|
|
254
|
+
onChange={(summary) => {
|
|
255
|
+
patch({ summary })
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
<PlanAttention items={planItems.records} isLoading={planItems.isLoading} />
|
|
260
|
+
|
|
261
|
+
<LatestActivity targetType="enquiry" targetId={enquiry.id} />
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function EnquirySidebar({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
|
|
267
|
+
const { patch, error } = useEnquiryPatch(enquiry)
|
|
268
|
+
const stages = usePipelineStages('enquiry')
|
|
269
|
+
const members = useMembers()
|
|
270
|
+
const currentCompany = useCompany(enquiry.companyId ?? undefined)
|
|
271
|
+
|
|
272
|
+
const [companySearch, setCompanySearch] = useState('')
|
|
273
|
+
const searchableCompanies = useCompanies({
|
|
274
|
+
term: companySearch.trim().length > 0 ? companySearch.trim() : undefined,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const orderedStages = [...stages.records].sort(
|
|
278
|
+
(a: PipelineStage, b: PipelineStage) => a.sortOrder - b.sortOrder,
|
|
279
|
+
)
|
|
280
|
+
const currentStage = orderedStages.find((stage) => stage.id === enquiry.stageId)
|
|
281
|
+
|
|
282
|
+
const companyOptions = [
|
|
283
|
+
...(currentCompany.record === undefined
|
|
284
|
+
? []
|
|
285
|
+
: [{ id: currentCompany.record.id, label: currentCompany.record.name }]),
|
|
286
|
+
...searchableCompanies.records
|
|
287
|
+
.filter((company) => company.id !== enquiry.companyId)
|
|
288
|
+
.map((company) => ({
|
|
289
|
+
id: company.id,
|
|
290
|
+
label: company.name,
|
|
291
|
+
meta: company.domain ?? undefined,
|
|
292
|
+
})),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<section className="rounded-md border border-border p-3">
|
|
297
|
+
{error !== null && (
|
|
298
|
+
<div className="mb-2">
|
|
299
|
+
<ErrorPanel error={error} />
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
<div className="mb-2">
|
|
303
|
+
<div className="mb-0.5 text-[10px] font-semibold tracking-wide text-ink-faint uppercase">
|
|
304
|
+
Stage
|
|
305
|
+
</div>
|
|
306
|
+
<InlineEdit
|
|
307
|
+
value={enquiry.stageId}
|
|
308
|
+
onChange={(stageId) => {
|
|
309
|
+
patch({ stageId })
|
|
310
|
+
}}
|
|
311
|
+
options={orderedStages.map((stage) => ({ value: stage.id, label: stage.label }))}
|
|
312
|
+
display={
|
|
313
|
+
<Chip tone={STAGE_TONES[currentStage?.slug ?? ''] ?? 'accent'}>
|
|
314
|
+
<span className="text-[10px]">{currentStage?.label ?? enquiry.stageId}</span>
|
|
315
|
+
</Chip>
|
|
316
|
+
}
|
|
317
|
+
displayClassName="not-italic inline-flex"
|
|
318
|
+
className="!w-auto"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<SidebarField label="Organisation">
|
|
323
|
+
<EntitySearch
|
|
324
|
+
options={companyOptions}
|
|
325
|
+
value={enquiry.companyId ?? ''}
|
|
326
|
+
onChange={(companyId) => {
|
|
327
|
+
patch({ companyId: companyId.length > 0 ? companyId : null })
|
|
328
|
+
}}
|
|
329
|
+
onQueryChange={setCompanySearch}
|
|
330
|
+
placeholder="Search companies…"
|
|
331
|
+
size="sm"
|
|
332
|
+
/>
|
|
333
|
+
{currentCompany.record !== undefined && (
|
|
334
|
+
<Link
|
|
335
|
+
to={`/companies/${currentCompany.record.id}`}
|
|
336
|
+
className="mt-1 inline-block text-[11px] text-accent hover:underline"
|
|
337
|
+
>
|
|
338
|
+
Open company
|
|
339
|
+
</Link>
|
|
340
|
+
)}
|
|
341
|
+
</SidebarField>
|
|
342
|
+
<SidebarField label="Owner">
|
|
343
|
+
<EntitySearch
|
|
344
|
+
options={members.members.map((member) => ({
|
|
345
|
+
id: member.id,
|
|
346
|
+
label: member.name,
|
|
347
|
+
meta: member.email,
|
|
348
|
+
}))}
|
|
349
|
+
value={enquiry.ownerId ?? ''}
|
|
350
|
+
onChange={(ownerId) => {
|
|
351
|
+
patch({ ownerId })
|
|
352
|
+
}}
|
|
353
|
+
placeholder="Search owners…"
|
|
354
|
+
size="sm"
|
|
355
|
+
/>
|
|
356
|
+
</SidebarField>
|
|
357
|
+
<SidebarField label="Tags">
|
|
358
|
+
<InlineEdit
|
|
359
|
+
value={enquiry.tags.join(', ')}
|
|
360
|
+
onChange={(value) => {
|
|
361
|
+
patch({ tags: toTags(value) })
|
|
362
|
+
}}
|
|
363
|
+
display={
|
|
364
|
+
enquiry.tags.length > 0 ? (
|
|
365
|
+
<span className="flex flex-wrap gap-1">
|
|
366
|
+
{enquiry.tags.map((tag) => (
|
|
367
|
+
<Chip key={tag}>
|
|
368
|
+
<span className="text-[10px]">{tag}</span>
|
|
369
|
+
</Chip>
|
|
370
|
+
))}
|
|
371
|
+
</span>
|
|
372
|
+
) : undefined
|
|
373
|
+
}
|
|
374
|
+
emptyLabel="Add tags…"
|
|
375
|
+
displayClassName="not-italic"
|
|
376
|
+
/>
|
|
377
|
+
</SidebarField>
|
|
378
|
+
</section>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function EnquiryFields({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
|
|
383
|
+
const { patch, error } = useEnquiryPatch(enquiry)
|
|
384
|
+
return (
|
|
385
|
+
<div className="space-y-4">
|
|
386
|
+
{error !== null && <ErrorPanel error={error} />}
|
|
387
|
+
<CustomFieldsPanel
|
|
388
|
+
objectType="enquiry"
|
|
389
|
+
values={enquiry.customFields}
|
|
390
|
+
onPatch={(customFields) => {
|
|
391
|
+
patch({ customFields })
|
|
392
|
+
}}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function EnquiryContacts({ enquiry }: { readonly enquiry: Enquiry }): React.JSX.Element {
|
|
399
|
+
const { patch, error } = useEnquiryPatch(enquiry)
|
|
400
|
+
|
|
401
|
+
const [adding, setAdding] = useState(false)
|
|
402
|
+
const [personId, setPersonId] = useState('')
|
|
403
|
+
const [search, setSearch] = useState('')
|
|
404
|
+
|
|
405
|
+
const directory = usePeople({ limit: 200 })
|
|
406
|
+
const searchable = usePeople({ term: search.trim().length > 0 ? search.trim() : undefined })
|
|
407
|
+
const nameById = new Map(directory.records.map((person) => [person.id, person.name]))
|
|
408
|
+
const linked = new Set(enquiry.personIds)
|
|
409
|
+
|
|
410
|
+
function reset(): void {
|
|
411
|
+
setAdding(false)
|
|
412
|
+
setPersonId('')
|
|
413
|
+
setSearch('')
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function submit(event: FormEvent): void {
|
|
417
|
+
event.preventDefault()
|
|
418
|
+
|
|
419
|
+
if (personId.length === 0 || linked.has(personId)) {
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
patch({ personIds: [...enquiry.personIds, personId] })
|
|
424
|
+
reset()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<section className="rounded-md border border-border">
|
|
429
|
+
<div className="border-b border-border px-3.5 py-2.5">
|
|
430
|
+
<SectionHeader
|
|
431
|
+
title="Contacts"
|
|
432
|
+
onAdd={() => {
|
|
433
|
+
setAdding((current) => !current)
|
|
434
|
+
}}
|
|
435
|
+
addLabel="Add contact"
|
|
436
|
+
compact
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{error !== null && (
|
|
441
|
+
<div className="px-3.5 py-2">
|
|
442
|
+
<ErrorPanel error={error} />
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
|
|
446
|
+
<ul className="divide-y divide-border">
|
|
447
|
+
{enquiry.personIds.length === 0 && !adding && (
|
|
448
|
+
<li className="px-3.5 py-4 text-[12px] text-ink-faint">No contacts yet.</li>
|
|
449
|
+
)}
|
|
450
|
+
{enquiry.personIds.map((linkedPersonId) => (
|
|
451
|
+
<li
|
|
452
|
+
key={linkedPersonId}
|
|
453
|
+
className="flex items-start justify-between gap-2 px-3.5 py-2.5"
|
|
454
|
+
>
|
|
455
|
+
<Link
|
|
456
|
+
to={`/people/${linkedPersonId}`}
|
|
457
|
+
className="text-[13px] font-medium text-ink hover:text-accent"
|
|
458
|
+
>
|
|
459
|
+
{nameById.get(linkedPersonId) ?? linkedPersonId}
|
|
460
|
+
</Link>
|
|
461
|
+
<button
|
|
462
|
+
type="button"
|
|
463
|
+
onClick={() => {
|
|
464
|
+
patch({
|
|
465
|
+
personIds: enquiry.personIds.filter(
|
|
466
|
+
(candidate) => candidate !== linkedPersonId,
|
|
467
|
+
),
|
|
468
|
+
})
|
|
469
|
+
}}
|
|
470
|
+
className="text-[11px] font-medium text-danger hover:underline"
|
|
471
|
+
>
|
|
472
|
+
Remove
|
|
473
|
+
</button>
|
|
474
|
+
</li>
|
|
475
|
+
))}
|
|
476
|
+
</ul>
|
|
477
|
+
|
|
478
|
+
{adding && (
|
|
479
|
+
<form onSubmit={submit} className="space-y-2 border-t border-border bg-surface px-3.5 py-3">
|
|
480
|
+
<EntitySearch
|
|
481
|
+
options={searchable.records
|
|
482
|
+
.filter((person) => !linked.has(person.id))
|
|
483
|
+
.map((person) => ({
|
|
484
|
+
id: person.id,
|
|
485
|
+
label: person.name,
|
|
486
|
+
meta: person.email ?? undefined,
|
|
487
|
+
}))}
|
|
488
|
+
value={personId}
|
|
489
|
+
onChange={setPersonId}
|
|
490
|
+
onQueryChange={setSearch}
|
|
491
|
+
placeholder="Search people…"
|
|
492
|
+
emptyMessage="No people match"
|
|
493
|
+
required
|
|
494
|
+
/>
|
|
495
|
+
<div className="flex justify-end gap-2">
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
498
|
+
onClick={reset}
|
|
499
|
+
className="rounded-md px-2.5 py-1 text-[12px] font-medium text-ink-muted hover:text-ink"
|
|
500
|
+
>
|
|
501
|
+
Cancel
|
|
502
|
+
</button>
|
|
503
|
+
<button
|
|
504
|
+
type="submit"
|
|
505
|
+
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg hover:bg-accent-hover"
|
|
506
|
+
>
|
|
507
|
+
Add
|
|
508
|
+
</button>
|
|
509
|
+
</div>
|
|
510
|
+
</form>
|
|
511
|
+
)}
|
|
512
|
+
</section>
|
|
513
|
+
)
|
|
514
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCreatePipelineStage,
|
|
3
|
+
usePipelineStages,
|
|
4
|
+
useRemovePipelineStage,
|
|
5
|
+
useUpdatePipelineStage,
|
|
6
|
+
} from '../api/resources/pipelineStages.ts'
|
|
7
|
+
import { ErrorPanel, LoadingPanel } from '../components/QueryState.tsx'
|
|
8
|
+
import { StageSettings } from '../components/StageSettings.tsx'
|
|
9
|
+
|
|
10
|
+
/** The enquiry pipeline's stage settings, on the shared `StageSettings` editor. */
|
|
11
|
+
export function EnquiryStageSettingsPage(): React.JSX.Element {
|
|
12
|
+
const stages = usePipelineStages('enquiry')
|
|
13
|
+
const createStage = useCreatePipelineStage()
|
|
14
|
+
const updateStage = useUpdatePipelineStage()
|
|
15
|
+
const removeStage = useRemovePipelineStage()
|
|
16
|
+
|
|
17
|
+
if (stages.error !== null) {
|
|
18
|
+
return <ErrorPanel error={stages.error} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (stages.isLoading) {
|
|
22
|
+
return <LoadingPanel label="Loading stages…" />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ordered = [...stages.records].sort((a, b) => a.sortOrder - b.sortOrder)
|
|
26
|
+
const writeError = createStage.error ?? updateStage.error ?? removeStage.error
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{writeError !== null && (
|
|
31
|
+
<div className="mx-auto mb-3 max-w-xl">
|
|
32
|
+
<ErrorPanel error={writeError} />
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<StageSettings
|
|
36
|
+
title="Enquiry stages"
|
|
37
|
+
backTo="/enquiries"
|
|
38
|
+
backLabel="Enquiries"
|
|
39
|
+
recordNoun="enquiry"
|
|
40
|
+
stages={ordered}
|
|
41
|
+
onRename={(id, label) => {
|
|
42
|
+
updateStage.run({ id, changes: { label } })
|
|
43
|
+
}}
|
|
44
|
+
onReorder={(activeId, overId) => {
|
|
45
|
+
const position = ordered.findIndex((stage) => stage.id === overId)
|
|
46
|
+
|
|
47
|
+
if (position >= 0) {
|
|
48
|
+
updateStage.run({ id: activeId, changes: { sortOrder: position } })
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
onToggleOpen={(id, open) => {
|
|
52
|
+
updateStage.run({ id, changes: { open } })
|
|
53
|
+
}}
|
|
54
|
+
onAdd={(label) => {
|
|
55
|
+
createStage.run({ kind: 'enquiry', label })
|
|
56
|
+
}}
|
|
57
|
+
onRemove={(id, moveToId) => {
|
|
58
|
+
removeStage.run({ id, moveToId })
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
}
|
package/src/pages/FormDetail.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { ErrorPanel, LoadingPanel, NotFoundPanel } from '../components/QueryStat
|
|
|
10
10
|
import { RecordTabs } from '../components/RecordTabs.tsx'
|
|
11
11
|
import { EmbedPanel } from './forms/EmbedPanel.tsx'
|
|
12
12
|
import { FieldsEditor } from './forms/FieldsEditor.tsx'
|
|
13
|
+
import { FormActions } from './forms/FormActions.tsx'
|
|
13
14
|
import { FormSettings } from './forms/FormSettings.tsx'
|
|
14
15
|
import { SubmissionsTable } from './forms/SubmissionsTable.tsx'
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ import { SubmissionsTable } from './forms/SubmissionsTable.tsx'
|
|
|
21
22
|
* committing per keystroke would rewrite every field on every character.
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
|
-
type FormTab = 'submissions' | 'fields' | 'settings' | 'embed'
|
|
25
|
+
type FormTab = 'submissions' | 'fields' | 'actions' | 'settings' | 'embed'
|
|
25
26
|
|
|
26
27
|
export function FormDetail(): React.JSX.Element {
|
|
27
28
|
const { id } = useParams()
|
|
@@ -72,6 +73,7 @@ export function FormDetail(): React.JSX.Element {
|
|
|
72
73
|
tabs={[
|
|
73
74
|
{ id: 'submissions', label: 'Submissions', count: submissions.records.length },
|
|
74
75
|
{ id: 'fields', label: 'Fields', count: record.fields.length },
|
|
76
|
+
{ id: 'actions', label: 'Actions', count: actionCount(record) },
|
|
75
77
|
{ id: 'settings', label: 'Settings' },
|
|
76
78
|
{ id: 'embed', label: 'Embed' },
|
|
77
79
|
]}
|
|
@@ -81,6 +83,7 @@ export function FormDetail(): React.JSX.Element {
|
|
|
81
83
|
>
|
|
82
84
|
{tab === 'submissions' && <SubmissionsTable form={record} submissions={submissions} />}
|
|
83
85
|
{tab === 'fields' && <FieldsEditor form={record} />}
|
|
86
|
+
{tab === 'actions' && <FormActions form={record} />}
|
|
84
87
|
{tab === 'settings' && <FormSettings form={record} />}
|
|
85
88
|
{tab === 'embed' && <EmbedPanel form={record} />}
|
|
86
89
|
</RecordTabs>
|
|
@@ -88,6 +91,25 @@ export function FormDetail(): React.JSX.Element {
|
|
|
88
91
|
)
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* How many post-submit actions the form is configured to run. Toggled
|
|
96
|
+
* triggers, tag lists (each counted once when non-empty), lists, and attach
|
|
97
|
+
* targets all contribute. The tab count is a hint, not a total — a reader
|
|
98
|
+
* sees "3" and knows the form does more than just capture answers.
|
|
99
|
+
*/
|
|
100
|
+
function actionCount(form: Form): number {
|
|
101
|
+
return (
|
|
102
|
+
(form.createDeal ? 1 : 0) +
|
|
103
|
+
(form.createOpportunity ? 1 : 0) +
|
|
104
|
+
(form.createPartnership ? 1 : 0) +
|
|
105
|
+
(form.createEnquiry ? 1 : 0) +
|
|
106
|
+
(form.personTags.length > 0 ? 1 : 0) +
|
|
107
|
+
(form.companyTags.length > 0 ? 1 : 0) +
|
|
108
|
+
form.listIds.length +
|
|
109
|
+
form.attachTargets.length
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
/** The title block: name, description, and the two things a reader checks first. */
|
|
92
114
|
function FormHeading({ form }: { readonly form: Form }): React.JSX.Element {
|
|
93
115
|
const updateForm = useUpdateForm()
|