@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
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
FormFieldInput,
|
|
6
6
|
FormInput,
|
|
7
7
|
FormSubmission,
|
|
8
|
+
FormSubmissionLinkTarget,
|
|
8
9
|
} from '@kelpie/schemas'
|
|
9
10
|
import {
|
|
10
11
|
useMutation,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
|
|
15
16
|
import { isRecord } from '../json.ts'
|
|
16
17
|
import type { QueryParameters } from '../client.ts'
|
|
18
|
+
import { ApiError } from '../client.ts'
|
|
17
19
|
import { useApiClient } from '../context.ts'
|
|
18
20
|
import { toError } from '../errors.ts'
|
|
19
21
|
import { createResourceHooks, usePagedList } from '../resource.ts'
|
|
@@ -146,9 +148,63 @@ export function useFormSubmissions(formId: string | undefined): RecordListResult
|
|
|
146
148
|
})
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Submissions that touched one record, newest first.
|
|
153
|
+
*
|
|
154
|
+
* Powers the Forms tab on every record type a submission can name (person,
|
|
155
|
+
* company, position, deal, opportunity, partnership, enquiry). Returns an
|
|
156
|
+
* empty page for a record no submission has ever named — the server does no
|
|
157
|
+
* per-target existence check, so a stale id reads the same as a real one.
|
|
158
|
+
*/
|
|
159
|
+
export function useFormSubmissionsForRecord(
|
|
160
|
+
targetType: FormSubmissionLinkTarget | undefined,
|
|
161
|
+
targetId: string | undefined,
|
|
162
|
+
): RecordListResult<FormSubmission> {
|
|
163
|
+
return usePagedList<FormSubmission>({
|
|
164
|
+
queryKey: ['form-submissions', 'by-target', targetType ?? '', targetId ?? ''],
|
|
165
|
+
path: '/form-submissions',
|
|
166
|
+
decode: formSubmissionSchema.parse,
|
|
167
|
+
query: { target_type: targetType, target_id: targetId },
|
|
168
|
+
enabled: targetType !== undefined && targetId !== undefined,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One submission on a form.
|
|
174
|
+
*
|
|
175
|
+
* Nested under the form rather than a top-level collection: a submission id is
|
|
176
|
+
* only meaningful with its form (the field labels live there), and the path
|
|
177
|
+
* mirrors the list endpoint above it.
|
|
178
|
+
*/
|
|
179
|
+
export function useFormSubmission(
|
|
180
|
+
formId: string | undefined,
|
|
181
|
+
submissionId: string | undefined,
|
|
182
|
+
): RecordResult<FormSubmission> {
|
|
183
|
+
const client = useApiClient()
|
|
184
|
+
const result = useQuery({
|
|
185
|
+
queryKey: ['forms', 'submissions', formId ?? '', submissionId ?? ''],
|
|
186
|
+
queryFn: () =>
|
|
187
|
+
client.get(
|
|
188
|
+
`/forms/${formId ?? ''}/submissions/${submissionId ?? ''}`,
|
|
189
|
+
formSubmissionSchema.parse,
|
|
190
|
+
),
|
|
191
|
+
enabled: formId !== undefined && submissionId !== undefined,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
record: result.data,
|
|
196
|
+
isLoading: result.isPending && formId !== undefined && submissionId !== undefined,
|
|
197
|
+
isNotFound: result.error instanceof ApiError && result.error.status === 404,
|
|
198
|
+
error: toError(result.error),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
149
202
|
/** What a customer pastes into their site, built server-side from the request origin. */
|
|
150
203
|
export interface EmbedSnippets {
|
|
204
|
+
/** Standalone hosted page (brand chrome). */
|
|
151
205
|
readonly url: string
|
|
206
|
+
/** Bare document the iframe snippets load (fields only). */
|
|
207
|
+
readonly embedUrl: string
|
|
152
208
|
readonly iframeSnippet: string
|
|
153
209
|
readonly scriptSnippet: string
|
|
154
210
|
}
|
|
@@ -157,6 +213,7 @@ function decodeSnippets(value: unknown): EmbedSnippets {
|
|
|
157
213
|
if (
|
|
158
214
|
!isRecord(value) ||
|
|
159
215
|
typeof value.url !== 'string' ||
|
|
216
|
+
typeof value.embed_url !== 'string' ||
|
|
160
217
|
typeof value.iframe_snippet !== 'string' ||
|
|
161
218
|
typeof value.script_snippet !== 'string'
|
|
162
219
|
) {
|
|
@@ -165,6 +222,7 @@ function decodeSnippets(value: unknown): EmbedSnippets {
|
|
|
165
222
|
|
|
166
223
|
return {
|
|
167
224
|
url: value.url,
|
|
225
|
+
embedUrl: value.embed_url,
|
|
168
226
|
iframeSnippet: value.iframe_snippet,
|
|
169
227
|
scriptSnippet: value.script_snippet,
|
|
170
228
|
}
|
|
@@ -34,6 +34,11 @@ export interface CreateImportJobInput {
|
|
|
34
34
|
/** People import only: what to do with a row naming a company that is not here yet. */
|
|
35
35
|
readonly onMissingCompany: OnMissingCompany
|
|
36
36
|
readonly matchKeyId: string
|
|
37
|
+
/**
|
|
38
|
+
* People import only: the consent purpose a row's `consent_status` grants.
|
|
39
|
+
* Required whenever the map names `consent_status` or `consent_at`.
|
|
40
|
+
*/
|
|
41
|
+
readonly consentPurposeId?: string | null
|
|
37
42
|
/**
|
|
38
43
|
* Absent on the first upload, so the server derives one from the source preset
|
|
39
44
|
* and the file's own headers. The mapping screen sends the corrected map back
|
|
@@ -57,6 +62,10 @@ function jobForm(input: CreateImportJobInput): FormData {
|
|
|
57
62
|
form.set('column_map', JSON.stringify(input.columnMap))
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
if (input.consentPurposeId != null && input.consentPurposeId.length > 0) {
|
|
66
|
+
form.set('consent_purpose_id', input.consentPurposeId)
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
return form
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -41,6 +41,8 @@ export interface OpportunityFilters {
|
|
|
41
41
|
readonly companyIds?: readonly string[] | undefined
|
|
42
42
|
/** Opportunities sitting in any of these stages. Repeats on the wire. */
|
|
43
43
|
readonly stageIds?: readonly string[] | undefined
|
|
44
|
+
/** Opportunities any of these people are on. Repeats on the wire. */
|
|
45
|
+
readonly personIds?: readonly string[] | undefined
|
|
44
46
|
readonly limit?: number | undefined
|
|
45
47
|
/** `field` ascending, `-field` descending. Only `name`, `created_at`, `updated_at` are sortable. */
|
|
46
48
|
readonly sort?: string | undefined
|
|
@@ -52,6 +54,7 @@ function opportunityQuery(filters: OpportunityFilters): QueryParameters {
|
|
|
52
54
|
kind: filters.kinds,
|
|
53
55
|
company_id: filters.companyIds,
|
|
54
56
|
stage_id: filters.stageIds,
|
|
57
|
+
person_id: filters.personIds,
|
|
55
58
|
limit: filters.limit,
|
|
56
59
|
sort: filters.sort,
|
|
57
60
|
}
|
package/src/app/KelpieApp.tsx
CHANGED
|
@@ -11,12 +11,16 @@ import { DealsPage } from '../pages/DealsPage.tsx'
|
|
|
11
11
|
import { DecisionsPage } from '../pages/DecisionsPage.tsx'
|
|
12
12
|
import { SearchPage } from '../pages/SearchPage.tsx'
|
|
13
13
|
import { FormDetail } from '../pages/FormDetail.tsx'
|
|
14
|
+
import { SubmissionDetail } from '../pages/forms/SubmissionDetail.tsx'
|
|
14
15
|
import { FormsPage } from '../pages/FormsPage.tsx'
|
|
15
16
|
import { FundraisingPage } from '../pages/FundraisingPage.tsx'
|
|
16
17
|
import { HiringPage } from '../pages/HiringPage.tsx'
|
|
17
18
|
import { ListDetail } from '../pages/ListDetail.tsx'
|
|
18
19
|
import { ListsPage } from '../pages/ListsPage.tsx'
|
|
19
20
|
import { NotFoundPage } from '../pages/NotFoundPage.tsx'
|
|
21
|
+
import { EnquiriesPage } from '../pages/EnquiriesPage.tsx'
|
|
22
|
+
import { EnquiryDetail } from '../pages/EnquiryDetail.tsx'
|
|
23
|
+
import { EnquiryStageSettingsPage } from '../pages/EnquiryStageSettingsPage.tsx'
|
|
20
24
|
import { OpportunitiesPage } from '../pages/OpportunitiesPage.tsx'
|
|
21
25
|
import { OpportunityDetail } from '../pages/OpportunityDetail.tsx'
|
|
22
26
|
import { OpportunityStageSettingsPage } from '../pages/OpportunityStageSettingsPage.tsx'
|
|
@@ -36,6 +40,8 @@ import { ProfilePage } from '../pages/account/ProfilePage.tsx'
|
|
|
36
40
|
import { SecurityPage } from '../pages/account/SecurityPage.tsx'
|
|
37
41
|
import { ApiKeysPage } from '../pages/admin/ApiKeysPage.tsx'
|
|
38
42
|
import { DataPage } from '../pages/admin/DataPage.tsx'
|
|
43
|
+
import { FieldsPage } from '../pages/admin/FieldsPage.tsx'
|
|
44
|
+
import { PrivacyPage } from '../pages/admin/PrivacyPage.tsx'
|
|
39
45
|
import { McpPage } from '../pages/admin/McpPage.tsx'
|
|
40
46
|
import { ModulesPage } from '../pages/admin/ModulesPage.tsx'
|
|
41
47
|
import { TeamPage } from '../pages/admin/TeamPage.tsx'
|
|
@@ -155,6 +161,9 @@ function AppRoutes(): React.JSX.Element {
|
|
|
155
161
|
<Route path="opportunities" element={<OpportunitiesPage />} />
|
|
156
162
|
<Route path="opportunities/settings" element={<OpportunityStageSettingsPage />} />
|
|
157
163
|
<Route path="opportunities/:id" element={<OpportunityDetail />} />
|
|
164
|
+
<Route path="enquiries" element={<EnquiriesPage />} />
|
|
165
|
+
<Route path="enquiries/settings" element={<EnquiryStageSettingsPage />} />
|
|
166
|
+
<Route path="enquiries/:id" element={<EnquiryDetail />} />
|
|
158
167
|
<Route path="fundraising" element={<FundraisingPage />} />
|
|
159
168
|
<Route path="fundraising/settings" element={<RaiseStageSettingsPage />} />
|
|
160
169
|
<Route path="fundraising/:id" element={<RaiseDetail />} />
|
|
@@ -166,12 +175,15 @@ function AppRoutes(): React.JSX.Element {
|
|
|
166
175
|
<Route path="planning" element={<PlanningPage />} />
|
|
167
176
|
<Route path="decisions" element={<DecisionsPage />} />
|
|
168
177
|
<Route path="forms" element={<FormsPage />} />
|
|
178
|
+
<Route path="forms/:id/submissions/:submissionId" element={<SubmissionDetail />} />
|
|
169
179
|
<Route path="forms/:id" element={<FormDetail />} />
|
|
170
180
|
{/* The optional segment is the mockup's: /handbook opens the first page. */}
|
|
171
181
|
<Route path="handbook" element={<HandbookLayout />} />
|
|
172
182
|
<Route path="handbook/:pageId" element={<HandbookLayout />} />
|
|
173
183
|
<Route path="admin/workspace" element={<WorkspaceSettingsPage />} />
|
|
174
184
|
<Route path="admin/team" element={<TeamPage />} />
|
|
185
|
+
<Route path="admin/fields" element={<FieldsPage />} />
|
|
186
|
+
<Route path="admin/privacy" element={<PrivacyPage />} />
|
|
175
187
|
<Route path="admin/data" element={<DataPage />} />
|
|
176
188
|
<Route path="admin/api-keys" element={<ApiKeysPage />} />
|
|
177
189
|
<Route path="admin/mcp" element={<McpPage />} />
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { CUSTOM_FIELD_TYPES_WITH_OPTIONS } from '@kelpie/schemas'
|
|
2
|
+
import type {
|
|
3
|
+
CustomFieldDefinition,
|
|
4
|
+
CustomFieldObjectType,
|
|
5
|
+
CustomFieldType,
|
|
6
|
+
CustomFieldValue,
|
|
7
|
+
CustomFieldValues,
|
|
8
|
+
} from '@kelpie/schemas'
|
|
9
|
+
import { useMemo, useState } from 'react'
|
|
10
|
+
import { Link } from 'react-router'
|
|
11
|
+
|
|
12
|
+
import { useCustomFields } from '../api/resources/customFields.ts'
|
|
13
|
+
import { InlineEdit } from './InlineEdit.tsx'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders the workspace's custom fields for one record as a full-width panel,
|
|
17
|
+
* dropped into a detail page's Fields tab.
|
|
18
|
+
*
|
|
19
|
+
* Each field patches only its own key; the server merges the sent object into
|
|
20
|
+
* stored values in the same transaction.
|
|
21
|
+
*
|
|
22
|
+
* Currency is edited in major units in the box and stored as integer cents,
|
|
23
|
+
* mirroring how DealDetail handles `value_cents`. Multi-select uses a compact
|
|
24
|
+
* checkbox list because InlineEdit only handles one string in and out.
|
|
25
|
+
*
|
|
26
|
+
* `useHasCustomFields` lets a caller decide whether to render the tab at all —
|
|
27
|
+
* a workspace with no definitions should not see a Fields tab full of empty
|
|
28
|
+
* space with only a "go set one up" pointer.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export interface CustomFieldsPanelProps {
|
|
32
|
+
readonly objectType: CustomFieldObjectType
|
|
33
|
+
readonly values: CustomFieldValues
|
|
34
|
+
readonly onPatch: (change: Readonly<Record<string, CustomFieldValue | null>>) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const OPTIONS_TYPES: ReadonlySet<CustomFieldType> = new Set(CUSTOM_FIELD_TYPES_WITH_OPTIONS)
|
|
38
|
+
|
|
39
|
+
export function CustomFieldsPanel({
|
|
40
|
+
objectType,
|
|
41
|
+
values,
|
|
42
|
+
onPatch,
|
|
43
|
+
}: CustomFieldsPanelProps): React.JSX.Element {
|
|
44
|
+
const definitions = useCustomFields({ objectType, sort: 'sort_order', limit: 200 })
|
|
45
|
+
|
|
46
|
+
if (definitions.isLoading) {
|
|
47
|
+
return <p className="text-[13px] text-ink-muted">Loading fields…</p>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (definitions.records.length === 0) {
|
|
51
|
+
return (
|
|
52
|
+
<p className="text-[13px] text-ink-muted">
|
|
53
|
+
No custom fields yet. Add them at{' '}
|
|
54
|
+
<Link to="/admin/fields" className="text-accent hover:text-accent-hover">
|
|
55
|
+
Admin → Fields
|
|
56
|
+
</Link>
|
|
57
|
+
.
|
|
58
|
+
</p>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<dl className="grid gap-x-8 gap-y-5 sm:grid-cols-2">
|
|
64
|
+
{definitions.records.map((definition) => (
|
|
65
|
+
<CustomFieldControl
|
|
66
|
+
key={definition.id}
|
|
67
|
+
definition={definition}
|
|
68
|
+
value={values[definition.key]}
|
|
69
|
+
onChange={(next) => {
|
|
70
|
+
onPatch({ [definition.key]: next })
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</dl>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface CustomFieldControlProps {
|
|
79
|
+
readonly definition: CustomFieldDefinition
|
|
80
|
+
readonly value: CustomFieldValue | undefined
|
|
81
|
+
readonly onChange: (value: CustomFieldValue | null) => void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function CustomFieldControl({
|
|
85
|
+
definition,
|
|
86
|
+
value,
|
|
87
|
+
onChange,
|
|
88
|
+
}: CustomFieldControlProps): React.JSX.Element {
|
|
89
|
+
const type = definition.type as CustomFieldType
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<dt className="text-[10px] font-semibold tracking-wide text-ink-faint uppercase">
|
|
94
|
+
{definition.label}
|
|
95
|
+
</dt>
|
|
96
|
+
{definition.description.length > 0 && (
|
|
97
|
+
<p className="mt-0.5 text-[11px] text-ink-muted">{definition.description}</p>
|
|
98
|
+
)}
|
|
99
|
+
<dd className="mt-1 text-[13px] text-ink">
|
|
100
|
+
<ControlFor definition={definition} type={type} value={value} onChange={onChange} />
|
|
101
|
+
</dd>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ControlForProps {
|
|
107
|
+
readonly definition: CustomFieldDefinition
|
|
108
|
+
readonly type: CustomFieldType
|
|
109
|
+
readonly value: CustomFieldValue | undefined
|
|
110
|
+
readonly onChange: (value: CustomFieldValue | null) => void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ControlFor({
|
|
114
|
+
definition,
|
|
115
|
+
type,
|
|
116
|
+
value,
|
|
117
|
+
onChange,
|
|
118
|
+
}: ControlForProps): React.JSX.Element {
|
|
119
|
+
if (type === 'checkbox') {
|
|
120
|
+
return (
|
|
121
|
+
<label className="inline-flex cursor-pointer items-center gap-2">
|
|
122
|
+
<input
|
|
123
|
+
type="checkbox"
|
|
124
|
+
checked={value === true}
|
|
125
|
+
onChange={(event) => {
|
|
126
|
+
onChange(event.target.checked)
|
|
127
|
+
}}
|
|
128
|
+
className="rounded border-border"
|
|
129
|
+
/>
|
|
130
|
+
<span>{value === true ? 'Yes' : 'No'}</span>
|
|
131
|
+
</label>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (type === 'multi_select') {
|
|
136
|
+
return (
|
|
137
|
+
<MultiSelectField
|
|
138
|
+
options={definition.options}
|
|
139
|
+
selected={Array.isArray(value) ? (value as readonly string[]) : []}
|
|
140
|
+
onChange={onChange}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (type === 'currency') {
|
|
146
|
+
const currentCents =
|
|
147
|
+
typeof value === 'object' &&
|
|
148
|
+
value !== null &&
|
|
149
|
+
!Array.isArray(value) &&
|
|
150
|
+
'amountCents' in value
|
|
151
|
+
? value.amountCents
|
|
152
|
+
: null
|
|
153
|
+
const currentCurrency =
|
|
154
|
+
typeof value === 'object' &&
|
|
155
|
+
value !== null &&
|
|
156
|
+
!Array.isArray(value) &&
|
|
157
|
+
'currency' in value
|
|
158
|
+
? value.currency
|
|
159
|
+
: 'USD'
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<InlineEdit
|
|
163
|
+
type="number"
|
|
164
|
+
value={currentCents === null ? '' : String(currentCents / 100)}
|
|
165
|
+
onChange={(raw) => {
|
|
166
|
+
const trimmed = raw.trim()
|
|
167
|
+
if (trimmed.length === 0) {
|
|
168
|
+
onChange(null)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const parsed = Number(trimmed)
|
|
172
|
+
if (!Number.isFinite(parsed)) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
onChange({
|
|
176
|
+
amountCents: Math.round(parsed * 100),
|
|
177
|
+
currency: currentCurrency,
|
|
178
|
+
})
|
|
179
|
+
}}
|
|
180
|
+
emptyLabel="Add…"
|
|
181
|
+
/>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (OPTIONS_TYPES.has(type)) {
|
|
186
|
+
// A stored value that is no longer in the definition's options must still
|
|
187
|
+
// be visible until the reader chooses another; keep it in the list.
|
|
188
|
+
const stored = typeof value === 'string' ? value : ''
|
|
189
|
+
const options = optionsWithStale(definition.options, stored)
|
|
190
|
+
return (
|
|
191
|
+
<InlineEdit
|
|
192
|
+
value={stored}
|
|
193
|
+
options={options.map((option) => ({ value: option, label: option }))}
|
|
194
|
+
onChange={(next) => {
|
|
195
|
+
onChange(next.length === 0 ? null : next)
|
|
196
|
+
}}
|
|
197
|
+
emptyLabel="Choose…"
|
|
198
|
+
/>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (type === 'number') {
|
|
203
|
+
const current = typeof value === 'number' ? String(value) : ''
|
|
204
|
+
return (
|
|
205
|
+
<InlineEdit
|
|
206
|
+
type="number"
|
|
207
|
+
value={current}
|
|
208
|
+
onChange={(raw) => {
|
|
209
|
+
const trimmed = raw.trim()
|
|
210
|
+
if (trimmed.length === 0) {
|
|
211
|
+
onChange(null)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const parsed = Number(trimmed)
|
|
215
|
+
if (Number.isFinite(parsed)) {
|
|
216
|
+
onChange(parsed)
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
emptyLabel="Add…"
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const inputType: 'text' | 'date' | 'url' | 'email' =
|
|
225
|
+
type === 'date' ? 'date' : type === 'url' ? 'url' : 'text'
|
|
226
|
+
const multiline = type === 'long_text'
|
|
227
|
+
const current = typeof value === 'string' ? value : ''
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<InlineEdit
|
|
231
|
+
type={inputType}
|
|
232
|
+
multiline={multiline}
|
|
233
|
+
value={current}
|
|
234
|
+
onChange={(next) => {
|
|
235
|
+
onChange(next.length === 0 ? null : next)
|
|
236
|
+
}}
|
|
237
|
+
emptyLabel="Add…"
|
|
238
|
+
/>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface MultiSelectFieldProps {
|
|
243
|
+
readonly options: readonly string[]
|
|
244
|
+
readonly selected: readonly string[]
|
|
245
|
+
readonly onChange: (value: readonly string[] | null) => void
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function MultiSelectField({
|
|
249
|
+
options,
|
|
250
|
+
selected,
|
|
251
|
+
onChange,
|
|
252
|
+
}: MultiSelectFieldProps): React.JSX.Element {
|
|
253
|
+
const [open, setOpen] = useState(false)
|
|
254
|
+
const allOptions = useMemo(
|
|
255
|
+
() => optionsWithStale(options, ...selected),
|
|
256
|
+
[options, selected],
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const chips = selected.length === 0 ? null : (
|
|
260
|
+
<div className="flex flex-wrap gap-1">
|
|
261
|
+
{selected.map((entry) => (
|
|
262
|
+
<span
|
|
263
|
+
key={entry}
|
|
264
|
+
className="inline-flex items-center gap-1 rounded-full bg-surface-raised px-2 py-0.5 text-[11px]"
|
|
265
|
+
>
|
|
266
|
+
{entry}
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={() => {
|
|
270
|
+
const next = selected.filter((existing) => existing !== entry)
|
|
271
|
+
onChange(next.length === 0 ? null : next)
|
|
272
|
+
}}
|
|
273
|
+
className="text-ink-muted hover:text-ink"
|
|
274
|
+
aria-label={`Remove ${entry}`}
|
|
275
|
+
>
|
|
276
|
+
×
|
|
277
|
+
</button>
|
|
278
|
+
</span>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className="space-y-1">
|
|
285
|
+
{chips ?? <span className="text-ink-faint">None</span>}
|
|
286
|
+
<button
|
|
287
|
+
type="button"
|
|
288
|
+
onClick={() => {
|
|
289
|
+
setOpen((current) => !current)
|
|
290
|
+
}}
|
|
291
|
+
className="text-[12px] text-ink-muted hover:text-ink"
|
|
292
|
+
>
|
|
293
|
+
{open ? 'Close' : 'Edit'}
|
|
294
|
+
</button>
|
|
295
|
+
{open && (
|
|
296
|
+
<ul className="space-y-1 rounded-md border border-border bg-surface-raised p-2">
|
|
297
|
+
{allOptions.map((option) => {
|
|
298
|
+
const chosen = selected.includes(option)
|
|
299
|
+
return (
|
|
300
|
+
<li key={option}>
|
|
301
|
+
<label className="flex cursor-pointer items-center gap-2 text-[12px]">
|
|
302
|
+
<input
|
|
303
|
+
type="checkbox"
|
|
304
|
+
checked={chosen}
|
|
305
|
+
onChange={() => {
|
|
306
|
+
const next = chosen
|
|
307
|
+
? selected.filter((existing) => existing !== option)
|
|
308
|
+
: [...selected, option]
|
|
309
|
+
onChange(next.length === 0 ? null : next)
|
|
310
|
+
}}
|
|
311
|
+
/>
|
|
312
|
+
<span>{option}</span>
|
|
313
|
+
</label>
|
|
314
|
+
</li>
|
|
315
|
+
)
|
|
316
|
+
})}
|
|
317
|
+
</ul>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function optionsWithStale(
|
|
324
|
+
options: readonly string[],
|
|
325
|
+
...stored: readonly string[]
|
|
326
|
+
): readonly string[] {
|
|
327
|
+
const seen = new Set(options)
|
|
328
|
+
const extras = stored.filter((entry) => entry.length > 0 && !seen.has(entry))
|
|
329
|
+
return extras.length === 0 ? options : [...options, ...extras]
|
|
330
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { FormSubmissionLinkTarget } from '@kelpie/schemas'
|
|
2
|
+
import { useMemo } from 'react'
|
|
3
|
+
import { Link } from 'react-router'
|
|
4
|
+
|
|
5
|
+
import { useTimezone } from '../api/resources/account.ts'
|
|
6
|
+
import { useForms, useFormSubmissionsForRecord } from '../api/resources/forms.ts'
|
|
7
|
+
import { formatDateTime } from '../lib/dates.ts'
|
|
8
|
+
import { ErrorPanel, LoadingPanel } from './QueryState.tsx'
|
|
9
|
+
import { SectionHeader } from './SectionHeader.tsx'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* "Which forms touched this record?" panel, for any record's detail page.
|
|
13
|
+
*
|
|
14
|
+
* A submission's FK to this record is what puts it here — the record was
|
|
15
|
+
* either created by the form, or matched from an earlier upsert. The panel
|
|
16
|
+
* does not distinguish the two (nothing on the row tells them apart), and
|
|
17
|
+
* the section header reflects that.
|
|
18
|
+
*
|
|
19
|
+
* Form names are pulled from `useForms`, which the sidebar already caches;
|
|
20
|
+
* an unresolved id (deleted form) falls back to the id itself so the row
|
|
21
|
+
* stays clickable.
|
|
22
|
+
*/
|
|
23
|
+
export function FormsPanel({
|
|
24
|
+
targetType,
|
|
25
|
+
targetId,
|
|
26
|
+
}: {
|
|
27
|
+
readonly targetType: FormSubmissionLinkTarget
|
|
28
|
+
readonly targetId: string
|
|
29
|
+
}): React.JSX.Element {
|
|
30
|
+
const submissions = useFormSubmissionsForRecord(targetType, targetId)
|
|
31
|
+
const forms = useForms({})
|
|
32
|
+
const timezone = useTimezone()
|
|
33
|
+
const formNameById = useMemo(
|
|
34
|
+
() => new Map(forms.records.map((form) => [form.id, form.name])),
|
|
35
|
+
[forms.records],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (submissions.error !== null) {
|
|
39
|
+
return <ErrorPanel error={submissions.error} />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (submissions.isLoading) {
|
|
43
|
+
return <LoadingPanel label="Loading form submissions…" />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<section>
|
|
48
|
+
<SectionHeader
|
|
49
|
+
title="Forms"
|
|
50
|
+
description="Every form submission that named this record."
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
{submissions.records.length === 0 ? (
|
|
54
|
+
<p className="text-[13px] text-ink-faint">No form submissions yet.</p>
|
|
55
|
+
) : (
|
|
56
|
+
<ul className="divide-y divide-border rounded-md border border-border">
|
|
57
|
+
{submissions.records.map((submission) => {
|
|
58
|
+
const formName = formNameById.get(submission.formId) ?? submission.formId
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<li key={submission.id} className="flex items-center justify-between gap-3 p-3">
|
|
62
|
+
<div className="min-w-0">
|
|
63
|
+
<Link
|
|
64
|
+
to={`/forms/${submission.formId}/submissions/${submission.id}`}
|
|
65
|
+
className="text-[13px] font-medium text-ink hover:text-accent"
|
|
66
|
+
>
|
|
67
|
+
{formName}
|
|
68
|
+
</Link>
|
|
69
|
+
<div className="text-[11px] text-ink-faint">
|
|
70
|
+
Submitted {formatDateTime(submission.submittedAt, timezone)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<Link
|
|
74
|
+
to={`/forms/${submission.formId}`}
|
|
75
|
+
className="shrink-0 text-[11px] font-medium text-ink-muted hover:text-accent"
|
|
76
|
+
>
|
|
77
|
+
Open form →
|
|
78
|
+
</Link>
|
|
79
|
+
</li>
|
|
80
|
+
)
|
|
81
|
+
})}
|
|
82
|
+
</ul>
|
|
83
|
+
)}
|
|
84
|
+
</section>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { PIPELINE_KIND_LABELS, PLAN_ITEM_STATUS_LABELS } from '@kelpie/schemas'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Deal,
|
|
4
|
+
Enquiry,
|
|
5
|
+
Opportunity,
|
|
6
|
+
Partnership,
|
|
7
|
+
PipelineKind,
|
|
8
|
+
PlanItem,
|
|
9
|
+
Raise,
|
|
10
|
+
} from '@kelpie/schemas'
|
|
3
11
|
import { Link } from 'react-router'
|
|
4
12
|
|
|
5
13
|
import { useMembers } from '../api/resources/members.ts'
|
|
@@ -20,6 +28,7 @@ import { SectionHeader } from './SectionHeader.tsx'
|
|
|
20
28
|
|
|
21
29
|
/** Where a plan item's record lives. */
|
|
22
30
|
const ROUTES: Readonly<Record<PipelineKind, string | undefined>> = {
|
|
31
|
+
enquiry: '/enquiries',
|
|
23
32
|
deal: '/deals',
|
|
24
33
|
opportunity: '/opportunities',
|
|
25
34
|
raise: '/fundraising',
|
|
@@ -93,12 +102,14 @@ export function RelatedPlanAttention({
|
|
|
93
102
|
opportunities = [],
|
|
94
103
|
raises = [],
|
|
95
104
|
partnerships = [],
|
|
105
|
+
enquiries = [],
|
|
96
106
|
isLoading,
|
|
97
107
|
}: {
|
|
98
108
|
readonly deals: readonly Deal[]
|
|
99
109
|
readonly opportunities?: readonly Opportunity[]
|
|
100
110
|
readonly raises?: readonly Raise[]
|
|
101
111
|
readonly partnerships?: readonly Partnership[]
|
|
112
|
+
readonly enquiries?: readonly Enquiry[]
|
|
102
113
|
readonly isLoading: boolean
|
|
103
114
|
}): React.JSX.Element {
|
|
104
115
|
const dealItems = usePlanItemsForRecords(
|
|
@@ -117,6 +128,10 @@ export function RelatedPlanAttention({
|
|
|
117
128
|
'partnership',
|
|
118
129
|
partnerships.map((partnership) => partnership.id),
|
|
119
130
|
)
|
|
131
|
+
const enquiryItems = usePlanItemsForRecords(
|
|
132
|
+
'enquiry',
|
|
133
|
+
enquiries.map((enquiry) => enquiry.id),
|
|
134
|
+
)
|
|
120
135
|
|
|
121
136
|
return (
|
|
122
137
|
<PlanAttention
|
|
@@ -125,11 +140,12 @@ export function RelatedPlanAttention({
|
|
|
125
140
|
...opportunityItems.records,
|
|
126
141
|
...raiseItems.records,
|
|
127
142
|
...partnershipItems.records,
|
|
143
|
+
...enquiryItems.records,
|
|
128
144
|
]}
|
|
129
145
|
showTarget
|
|
130
146
|
targetNames={
|
|
131
147
|
new Map(
|
|
132
|
-
[...deals, ...opportunities, ...raises, ...partnerships].map((record) => [
|
|
148
|
+
[...deals, ...opportunities, ...raises, ...partnerships, ...enquiries].map((record) => [
|
|
133
149
|
record.id,
|
|
134
150
|
record.name,
|
|
135
151
|
]),
|
|
@@ -140,7 +156,8 @@ export function RelatedPlanAttention({
|
|
|
140
156
|
dealItems.isLoading ||
|
|
141
157
|
opportunityItems.isLoading ||
|
|
142
158
|
raiseItems.isLoading ||
|
|
143
|
-
partnershipItems.isLoading
|
|
159
|
+
partnershipItems.isLoading ||
|
|
160
|
+
enquiryItems.isLoading
|
|
144
161
|
}
|
|
145
162
|
/>
|
|
146
163
|
)
|