@kelpie/ui 0.7.0 → 0.9.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 +59 -6
- package/dist/api/resource.d.ts.map +1 -1
- package/dist/api/resource.js +106 -24
- package/dist/api/resource.js.map +1 -1
- package/dist/api/resources/companies.d.ts +2 -0
- package/dist/api/resources/companies.d.ts.map +1 -1
- package/dist/api/resources/companies.js +5 -1
- package/dist/api/resources/companies.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/decisions.d.ts +2 -0
- package/dist/api/resources/decisions.d.ts.map +1 -1
- package/dist/api/resources/decisions.js +1 -0
- package/dist/api/resources/decisions.js.map +1 -1
- package/dist/api/resources/emailDomainLinker.d.ts +16 -0
- package/dist/api/resources/emailDomainLinker.d.ts.map +1 -0
- package/dist/api/resources/emailDomainLinker.js +16 -0
- package/dist/api/resources/emailDomainLinker.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 +44 -17
- package/dist/api/resources/forms.js.map +1 -1
- package/dist/api/resources/handbookPages.d.ts +1 -1
- package/dist/api/resources/handbookPages.js +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/listMembers.d.ts +27 -0
- package/dist/api/resources/listMembers.d.ts.map +1 -0
- package/dist/api/resources/listMembers.js +108 -0
- package/dist/api/resources/listMembers.js.map +1 -0
- package/dist/api/resources/lists.d.ts +14 -0
- package/dist/api/resources/lists.d.ts.map +1 -0
- package/dist/api/resources/lists.js +34 -0
- package/dist/api/resources/lists.js.map +1 -0
- 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/people.d.ts.map +1 -1
- package/dist/api/resources/people.js +4 -1
- package/dist/api/resources/people.js.map +1 -1
- package/dist/api/resources/roles.d.ts +2 -0
- package/dist/api/resources/roles.d.ts.map +1 -1
- package/dist/api/resources/roles.js +1 -1
- package/dist/api/resources/roles.js.map +1 -1
- package/dist/api/resources/sampleData.d.ts +17 -0
- package/dist/api/resources/sampleData.d.ts.map +1 -0
- package/dist/api/resources/sampleData.js +31 -0
- package/dist/api/resources/sampleData.js.map +1 -0
- package/dist/api/resources/webhooks.d.ts.map +1 -1
- package/dist/api/resources/webhooks.js +7 -22
- package/dist/api/resources/webhooks.js.map +1 -1
- package/dist/app/KelpieApp.d.ts.map +1 -1
- package/dist/app/KelpieApp.js +9 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +3 -1
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/ColumnPicker.d.ts +24 -0
- package/dist/components/ColumnPicker.d.ts.map +1 -0
- package/dist/components/ColumnPicker.js +56 -0
- package/dist/components/ColumnPicker.js.map +1 -0
- 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/DataTable.d.ts +16 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTable.js +51 -10
- package/dist/components/DataTable.js.map +1 -1
- package/dist/components/DecisionsPanel.d.ts.map +1 -1
- package/dist/components/DecisionsPanel.js +4 -3
- package/dist/components/DecisionsPanel.js.map +1 -1
- package/dist/components/EntitySearch.d.ts.map +1 -1
- package/dist/components/EntitySearch.js +11 -5
- package/dist/components/EntitySearch.js.map +1 -1
- 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/ListsPanel.d.ts +14 -0
- package/dist/components/ListsPanel.d.ts.map +1 -0
- package/dist/components/ListsPanel.js +56 -0
- package/dist/components/ListsPanel.js.map +1 -0
- package/dist/components/NotesPanel.d.ts.map +1 -1
- package/dist/components/NotesPanel.js +6 -5
- package/dist/components/NotesPanel.js.map +1 -1
- package/dist/components/Paginator.d.ts +26 -0
- package/dist/components/Paginator.d.ts.map +1 -0
- package/dist/components/Paginator.js +16 -0
- package/dist/components/Paginator.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/PlanPanel.d.ts.map +1 -1
- package/dist/components/PlanPanel.js +2 -1
- package/dist/components/PlanPanel.js.map +1 -1
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +4 -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/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/listView.d.ts +15 -0
- package/dist/lib/listView.d.ts.map +1 -0
- package/dist/lib/listView.js +66 -0
- package/dist/lib/listView.js.map +1 -0
- package/dist/lib/sort.d.ts +43 -0
- package/dist/lib/sort.d.ts.map +1 -0
- package/dist/lib/sort.js +102 -0
- package/dist/lib/sort.js.map +1 -0
- package/dist/pages/CompaniesPage.d.ts.map +1 -1
- package/dist/pages/CompaniesPage.js +88 -3
- package/dist/pages/CompaniesPage.js.map +1 -1
- package/dist/pages/CompanyDetail.d.ts.map +1 -1
- package/dist/pages/CompanyDetail.js +40 -15
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +20 -1
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DealsPage.d.ts.map +1 -1
- package/dist/pages/DealsPage.js +134 -14
- package/dist/pages/DealsPage.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +52 -8
- 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/FormsPage.d.ts.map +1 -1
- package/dist/pages/FormsPage.js +53 -2
- package/dist/pages/FormsPage.js.map +1 -1
- package/dist/pages/FundraisingPage.d.ts.map +1 -1
- package/dist/pages/FundraisingPage.js +116 -14
- package/dist/pages/FundraisingPage.js.map +1 -1
- package/dist/pages/HiringPage.d.ts.map +1 -1
- package/dist/pages/HiringPage.js +27 -4
- package/dist/pages/HiringPage.js.map +1 -1
- package/dist/pages/ListDetail.d.ts +11 -0
- package/dist/pages/ListDetail.d.ts.map +1 -0
- package/dist/pages/ListDetail.js +214 -0
- package/dist/pages/ListDetail.js.map +1 -0
- package/dist/pages/ListsPage.d.ts +9 -0
- package/dist/pages/ListsPage.d.ts.map +1 -0
- package/dist/pages/ListsPage.js +125 -0
- package/dist/pages/ListsPage.js.map +1 -0
- package/dist/pages/OpportunitiesPage.d.ts.map +1 -1
- package/dist/pages/OpportunitiesPage.js +97 -14
- package/dist/pages/OpportunitiesPage.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +64 -3
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +19 -1
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/PartnershipsPage.d.ts.map +1 -1
- package/dist/pages/PartnershipsPage.js +111 -14
- package/dist/pages/PartnershipsPage.js.map +1 -1
- package/dist/pages/PeoplePage.d.ts.map +1 -1
- package/dist/pages/PeoplePage.js +119 -3
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +67 -5
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +88 -17
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +13 -1
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/RoleDetail.d.ts.map +1 -1
- package/dist/pages/RoleDetail.js +5 -4
- 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 +126 -13
- 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/admin/WebhookDeliveries.d.ts.map +1 -1
- package/dist/pages/admin/WebhookDeliveries.js +3 -2
- package/dist/pages/admin/WebhookDeliveries.js.map +1 -1
- package/dist/pages/admin/WebhooksPage.d.ts.map +1 -1
- package/dist/pages/admin/WebhooksPage.js +3 -2
- package/dist/pages/admin/WebhooksPage.js.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.js +45 -2
- package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
- 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 +57 -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/handbook/HandbookPage.js +1 -1
- package/dist/pages/hiringDirectory.js +2 -2
- package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js +15 -2
- package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
- package/dist/pages/positionDirectory.js +2 -2
- package/package.json +2 -2
- package/src/api/resource.ts +155 -32
- package/src/api/resources/companies.ts +7 -1
- package/src/api/resources/consentPurposes.ts +83 -0
- package/src/api/resources/customFields.ts +88 -0
- package/src/api/resources/decisions.ts +3 -0
- package/src/api/resources/emailDomainLinker.ts +43 -0
- package/src/api/resources/enquiries.ts +118 -0
- package/src/api/resources/forms.ts +59 -19
- package/src/api/resources/handbookPages.ts +1 -1
- package/src/api/resources/importJobs.ts +9 -0
- package/src/api/resources/listMembers.ts +173 -0
- package/src/api/resources/lists.ts +61 -0
- package/src/api/resources/opportunities.ts +3 -0
- package/src/api/resources/people.ts +4 -1
- package/src/api/resources/roles.ts +3 -1
- package/src/api/resources/sampleData.ts +57 -0
- package/src/api/resources/webhooks.ts +8 -24
- package/src/app/KelpieApp.tsx +16 -0
- package/src/components/ActivitiesPanel.tsx +3 -10
- package/src/components/ColumnPicker.tsx +124 -0
- package/src/components/CustomFieldsPanel.tsx +330 -0
- package/src/components/DataTable.tsx +89 -10
- package/src/components/DecisionsPanel.tsx +4 -12
- package/src/components/EntitySearch.tsx +12 -5
- package/src/components/FormsPanel.tsx +86 -0
- package/src/components/ListsPanel.tsx +176 -0
- package/src/components/NotesPanel.tsx +6 -14
- package/src/components/Paginator.tsx +78 -0
- package/src/components/PlanAttention.tsx +20 -3
- package/src/components/PlanPanel.tsx +2 -10
- package/src/components/Shell.tsx +4 -0
- package/src/components/useHasCustomFields.ts +18 -0
- package/src/index.ts +10 -1
- package/src/lib/listView.ts +149 -0
- package/src/lib/sort.ts +143 -0
- package/src/pages/CompaniesPage.tsx +125 -11
- package/src/pages/CompanyDetail.tsx +98 -63
- package/src/pages/DealDetail.tsx +34 -0
- package/src/pages/DealsPage.tsx +178 -24
- package/src/pages/DecisionsPage.tsx +75 -18
- 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/FormsPage.tsx +81 -17
- package/src/pages/FundraisingPage.tsx +152 -24
- package/src/pages/HiringPage.tsx +39 -11
- package/src/pages/ListDetail.tsx +470 -0
- package/src/pages/ListsPage.tsx +281 -0
- package/src/pages/OpportunitiesPage.tsx +130 -24
- package/src/pages/OpportunityDetail.tsx +168 -2
- package/src/pages/PartnershipDetail.tsx +35 -0
- package/src/pages/PartnershipsPage.tsx +149 -24
- package/src/pages/PeoplePage.tsx +157 -11
- package/src/pages/PersonDetail.tsx +157 -6
- package/src/pages/PlanningPage.tsx +259 -29
- package/src/pages/RaiseDetail.tsx +24 -0
- package/src/pages/RoleDetail.tsx +21 -26
- package/src/pages/SearchPage.tsx +1 -0
- package/src/pages/admin/DataPage.tsx +324 -13
- package/src/pages/admin/FieldsPage.tsx +455 -0
- package/src/pages/admin/PrivacyPage.tsx +461 -0
- package/src/pages/admin/WebhookDeliveries.tsx +10 -15
- package/src/pages/admin/WebhooksPage.tsx +14 -19
- package/src/pages/admin/WorkspaceSettingsPage.tsx +134 -2
- 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 +112 -26
- package/src/pages/forms/fieldList.ts +68 -5
- package/src/pages/forms/template.ts +103 -0
- package/src/pages/handbook/HandbookPage.tsx +1 -1
- package/src/pages/hiringDirectory.ts +2 -2
- package/src/pages/onboarding/WorkspaceStepPage.tsx +31 -3
- package/src/pages/positionDirectory.ts +2 -2
- package/src/styles.css +3 -3
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import type { Workspace } from '@kelpie/schemas'
|
|
2
|
-
import { useState } from 'react'
|
|
1
|
+
import type { Company, Workspace } from '@kelpie/schemas'
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
3
3
|
import type { FormEvent, ReactNode } from 'react'
|
|
4
4
|
import { useNavigate } from 'react-router'
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
useCompanies,
|
|
8
|
+
useCreateCompany,
|
|
9
|
+
useUpdateCompany,
|
|
10
|
+
} from '../../api/resources/companies.ts'
|
|
6
11
|
import { useSession } from '../../api/resources/session.ts'
|
|
7
12
|
import {
|
|
8
13
|
useDeleteWorkspace,
|
|
9
14
|
useUpdateWorkspace,
|
|
10
15
|
useWorkspace,
|
|
11
16
|
} from '../../api/resources/workspace.ts'
|
|
17
|
+
import { EntitySearch } from '../../components/EntitySearch.tsx'
|
|
18
|
+
import type { SearchOption } from '../../components/EntitySearch.tsx'
|
|
12
19
|
import { PageHeader } from '../../components/PageHeader.tsx'
|
|
13
20
|
import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
|
|
14
21
|
|
|
@@ -190,11 +197,136 @@ function WorkspaceSettingsForm({
|
|
|
190
197
|
)}
|
|
191
198
|
</form>
|
|
192
199
|
|
|
200
|
+
<OwnCompanySection canEdit={canEdit} />
|
|
201
|
+
|
|
193
202
|
{session?.role === 'owner' && <DangerZone slug={workspace.slug} />}
|
|
194
203
|
</div>
|
|
195
204
|
)
|
|
196
205
|
}
|
|
197
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Mark one or more Companies as "us" — the workspace's own organisation.
|
|
209
|
+
*
|
|
210
|
+
* Backed by `companies.is_own`. Zero or many rows may carry it (a parent and a
|
|
211
|
+
* subsidiary is the intended case), so the surface is a list plus an add
|
|
212
|
+
* picker rather than a single-select. Picking a name that does not exist
|
|
213
|
+
* creates the Company with `is_own: true` in one call.
|
|
214
|
+
*/
|
|
215
|
+
function OwnCompanySection({ canEdit }: { readonly canEdit: boolean }): React.JSX.Element {
|
|
216
|
+
const own = useCompanies({ isOwn: true })
|
|
217
|
+
const [search, setSearch] = useState('')
|
|
218
|
+
// The picker's options: workspace companies that are NOT already marked.
|
|
219
|
+
// Server-side search on the term the user types.
|
|
220
|
+
const searchable = useCompanies({
|
|
221
|
+
isOwn: false,
|
|
222
|
+
term: search.trim().length > 0 ? search.trim() : undefined,
|
|
223
|
+
})
|
|
224
|
+
const update = useUpdateCompany()
|
|
225
|
+
const create = useCreateCompany()
|
|
226
|
+
|
|
227
|
+
const options: SearchOption[] = useMemo(
|
|
228
|
+
() =>
|
|
229
|
+
searchable.records.map((company) => ({
|
|
230
|
+
id: company.id,
|
|
231
|
+
label: company.name,
|
|
232
|
+
meta: company.domain ?? undefined,
|
|
233
|
+
})),
|
|
234
|
+
[searchable.records],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
function mark(companyId: string): void {
|
|
238
|
+
update.run({ id: companyId, changes: { isOwn: true } })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function unmark(companyId: string): void {
|
|
242
|
+
update.run({ id: companyId, changes: { isOwn: false } })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createAndMark(name: string): void {
|
|
246
|
+
// The API defaults every other field; `is_own: true` lands the row in the
|
|
247
|
+
// list this section shows and nowhere else.
|
|
248
|
+
create.run({ name, isOwn: true })
|
|
249
|
+
setSearch('')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const busy = update.isPending || create.isPending
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<section className="space-y-3 rounded-lg border border-border bg-surface p-4">
|
|
256
|
+
<div>
|
|
257
|
+
<h2 className="text-[13px] font-semibold text-ink">This company</h2>
|
|
258
|
+
<p className="mt-1 max-w-xl text-[12px] text-ink-muted">
|
|
259
|
+
Mark the company (or companies) in the CRM that represent this workspace itself. Used
|
|
260
|
+
later to separate own records from prospects.
|
|
261
|
+
</p>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{own.isLoading ? (
|
|
265
|
+
<p className="text-[12px] text-ink-faint">Loading…</p>
|
|
266
|
+
) : own.records.length === 0 ? (
|
|
267
|
+
<p className="text-[12px] text-ink-faint">Nothing marked yet.</p>
|
|
268
|
+
) : (
|
|
269
|
+
<ul className="divide-y divide-border rounded-md border border-border">
|
|
270
|
+
{own.records.map((company: Company) => (
|
|
271
|
+
<li
|
|
272
|
+
key={company.id}
|
|
273
|
+
className="flex items-center justify-between gap-3 px-3 py-2.5"
|
|
274
|
+
>
|
|
275
|
+
<div className="min-w-0">
|
|
276
|
+
<div className="truncate text-[13px] font-medium text-ink">{company.name}</div>
|
|
277
|
+
{company.domain !== null && company.domain.length > 0 && (
|
|
278
|
+
<div className="truncate text-[11px] text-ink-faint">{company.domain}</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
{canEdit && (
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={() => {
|
|
285
|
+
unmark(company.id)
|
|
286
|
+
}}
|
|
287
|
+
disabled={busy}
|
|
288
|
+
className="shrink-0 rounded-md border border-border bg-surface-raised px-2.5 py-1 text-[11px] font-medium text-ink hover:border-danger hover:text-danger disabled:opacity-50"
|
|
289
|
+
>
|
|
290
|
+
Unmark
|
|
291
|
+
</button>
|
|
292
|
+
)}
|
|
293
|
+
</li>
|
|
294
|
+
))}
|
|
295
|
+
</ul>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{canEdit && (
|
|
299
|
+
<div className="max-w-md">
|
|
300
|
+
<label className="mb-1.5 block text-[12px] font-medium text-ink">
|
|
301
|
+
{own.records.length === 0 ? 'Pick or create' : 'Add another'}
|
|
302
|
+
</label>
|
|
303
|
+
<EntitySearch
|
|
304
|
+
options={options}
|
|
305
|
+
value=""
|
|
306
|
+
onChange={mark}
|
|
307
|
+
onQueryChange={setSearch}
|
|
308
|
+
onCreate={createAndMark}
|
|
309
|
+
createLabel={(query) => `Create “${query}” and mark it`}
|
|
310
|
+
placeholder="Search companies…"
|
|
311
|
+
emptyMessage="No matches — type a new name to create one"
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{update.error !== null && (
|
|
317
|
+
<div className="max-w-xl">
|
|
318
|
+
<ErrorPanel error={update.error} />
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
{create.error !== null && (
|
|
322
|
+
<div className="max-w-xl">
|
|
323
|
+
<ErrorPanel error={create.error} />
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</section>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
198
330
|
/**
|
|
199
331
|
* Deleting the workspace, behind the workspace's own name.
|
|
200
332
|
*
|
|
@@ -28,6 +28,7 @@ const ROUTES: Readonly<Record<RecordTargetType, string | undefined>> = {
|
|
|
28
28
|
opportunity: '/opportunities',
|
|
29
29
|
partnership: '/partnerships',
|
|
30
30
|
raise: '/fundraising',
|
|
31
|
+
enquiry: '/enquiries',
|
|
31
32
|
candidate: undefined,
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -38,6 +39,7 @@ const TARGET_TYPE_LABELS: Readonly<Record<RecordTargetType, string>> = {
|
|
|
38
39
|
opportunity: 'Opportunity',
|
|
39
40
|
partnership: 'Partnership',
|
|
40
41
|
raise: 'Fundraising',
|
|
42
|
+
enquiry: 'Enquiry',
|
|
41
43
|
candidate: 'Candidate',
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -147,6 +149,7 @@ function plural(count: number, singular: string, pluralForm = `${singular}s`): s
|
|
|
147
149
|
const OPEN_PIPELINE_NOUNS: Readonly<
|
|
148
150
|
Record<PipelineKind, { readonly one: string; readonly many: string }>
|
|
149
151
|
> = {
|
|
152
|
+
enquiry: { one: 'enquiry', many: 'enquiries' },
|
|
150
153
|
deal: { one: 'deal', many: 'deals' },
|
|
151
154
|
opportunity: { one: 'opportunity', many: 'opportunities' },
|
|
152
155
|
raise: { one: 'raise', many: 'raises' },
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { FORM_FIELD_MAP_TARGET_LABELS } from '@kelpie/schemas'
|
|
2
|
+
import type { FormFieldInput } from '@kelpie/schemas'
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { unusedCrmPresets } from './fieldList.ts'
|
|
6
|
+
import type { EditableField } from './fieldList.ts'
|
|
7
|
+
import { SUBMISSION_FIELD_PRESETS } from './template.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The "Add field" button and its menu of ready-made fields.
|
|
11
|
+
*
|
|
12
|
+
* Two groups. CRM presets carry a mapping and appear only while that mapping is
|
|
13
|
+
* free, because a form may carry each CRM target once. Submission-only fields
|
|
14
|
+
* always appear and may repeat. The menu owns nothing but its own open state:
|
|
15
|
+
* the field list lives with the builder, which is what `onAdd` hands the chosen
|
|
16
|
+
* preset back to.
|
|
17
|
+
*/
|
|
18
|
+
export interface AddFieldMenuProps {
|
|
19
|
+
readonly fields: readonly EditableField[]
|
|
20
|
+
readonly onAdd: (field: FormFieldInput) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AddFieldMenu({ fields, onAdd }: AddFieldMenuProps): React.JSX.Element {
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
26
|
+
const crmPresets = unusedCrmPresets(fields)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!open) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function onPointerDown(event: MouseEvent): void {
|
|
34
|
+
if (containerRef.current?.contains(event.target as Node) !== true) {
|
|
35
|
+
setOpen(false)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
40
|
+
if (event.key === 'Escape') {
|
|
41
|
+
setOpen(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
document.addEventListener('mousedown', onPointerDown)
|
|
46
|
+
document.addEventListener('keydown', onKeyDown)
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('mousedown', onPointerDown)
|
|
50
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
51
|
+
}
|
|
52
|
+
}, [open])
|
|
53
|
+
|
|
54
|
+
function add(field: FormFieldInput): void {
|
|
55
|
+
onAdd(field)
|
|
56
|
+
setOpen(false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="relative" ref={containerRef}>
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => {
|
|
64
|
+
setOpen((current) => !current)
|
|
65
|
+
}}
|
|
66
|
+
aria-expanded={open}
|
|
67
|
+
aria-haspopup="menu"
|
|
68
|
+
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover"
|
|
69
|
+
>
|
|
70
|
+
+ Add field
|
|
71
|
+
</button>
|
|
72
|
+
{open && (
|
|
73
|
+
<div
|
|
74
|
+
role="menu"
|
|
75
|
+
aria-label="Field to add"
|
|
76
|
+
className="absolute right-0 z-20 mt-1.5 max-h-80 w-64 overflow-y-auto rounded-md border border-border bg-surface-raised py-1"
|
|
77
|
+
>
|
|
78
|
+
{crmPresets.length > 0 && (
|
|
79
|
+
<>
|
|
80
|
+
<p className="px-3 pb-1 pt-1.5 text-[10px] font-semibold uppercase tracking-wide text-ink-faint">
|
|
81
|
+
Writes to the CRM
|
|
82
|
+
</p>
|
|
83
|
+
{crmPresets.map((preset) => (
|
|
84
|
+
<button
|
|
85
|
+
key={preset.mapTo}
|
|
86
|
+
type="button"
|
|
87
|
+
role="menuitem"
|
|
88
|
+
onClick={() => {
|
|
89
|
+
add(preset)
|
|
90
|
+
}}
|
|
91
|
+
className="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-[13px] text-ink transition hover:bg-surface-sunken"
|
|
92
|
+
>
|
|
93
|
+
<span>{preset.label}</span>
|
|
94
|
+
<span className="shrink-0 text-[11px] text-ink-faint">
|
|
95
|
+
{FORM_FIELD_MAP_TARGET_LABELS[preset.mapTo]}
|
|
96
|
+
</span>
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
<p className="px-3 pb-1 pt-1.5 text-[10px] font-semibold uppercase tracking-wide text-ink-faint">
|
|
102
|
+
Submission only
|
|
103
|
+
</p>
|
|
104
|
+
{SUBMISSION_FIELD_PRESETS.map((preset) => (
|
|
105
|
+
<button
|
|
106
|
+
key={preset.menuLabel}
|
|
107
|
+
type="button"
|
|
108
|
+
role="menuitem"
|
|
109
|
+
onClick={() => {
|
|
110
|
+
add(preset.field)
|
|
111
|
+
}}
|
|
112
|
+
className="w-full px-3 py-1.5 text-left text-[13px] text-ink transition hover:bg-surface-sunken"
|
|
113
|
+
>
|
|
114
|
+
{preset.menuLabel}
|
|
115
|
+
</button>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Form } from '@kelpie/schemas'
|
|
2
|
+
import { useEffect, useId, useState } from 'react'
|
|
2
3
|
|
|
3
4
|
import { useFormEmbed } from '../../api/resources/forms.ts'
|
|
4
5
|
import { CopyButton } from '../../components/CopyButton.tsx'
|
|
@@ -12,14 +13,21 @@ import { SectionHeader } from '../../components/SectionHeader.tsx'
|
|
|
12
13
|
* embed page is served by the service, so its address depends on where the
|
|
13
14
|
* service is reached, which the server knows from the request and the browser
|
|
14
15
|
* only happens to share while the two are same-origin.
|
|
16
|
+
*
|
|
17
|
+
* Each iframe snippet has a Preview that opens the bare embed document in a
|
|
18
|
+
* modal — fields only, the same document the snippet iframes — fixed height for
|
|
19
|
+
* the plain iframe, auto-resize for the script variant.
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
22
|
export interface EmbedPanelProps {
|
|
18
23
|
readonly form: Form
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
type PreviewMode = 'fixed' | 'resize'
|
|
27
|
+
|
|
21
28
|
export function EmbedPanel({ form }: EmbedPanelProps): React.JSX.Element {
|
|
22
29
|
const { snippets, isLoading, error } = useFormEmbed(form.id)
|
|
30
|
+
const [preview, setPreview] = useState<PreviewMode | null>(null)
|
|
23
31
|
|
|
24
32
|
if (error !== null) {
|
|
25
33
|
return <ErrorPanel error={error} />
|
|
@@ -33,7 +41,7 @@ export function EmbedPanel({ form }: EmbedPanelProps): React.JSX.Element {
|
|
|
33
41
|
<div className="max-w-2xl space-y-5">
|
|
34
42
|
<SectionHeader
|
|
35
43
|
title="Embed"
|
|
36
|
-
description="
|
|
44
|
+
description="Hosted page is the standalone URL with Kelpie chrome. The iframe snippets load a bare form — fields only — so they sit inside your own site."
|
|
37
45
|
/>
|
|
38
46
|
|
|
39
47
|
<div>
|
|
@@ -57,12 +65,31 @@ export function EmbedPanel({ form }: EmbedPanelProps): React.JSX.Element {
|
|
|
57
65
|
label="iframe"
|
|
58
66
|
hint="One tag, no JavaScript, fixed height."
|
|
59
67
|
value={snippets.iframeSnippet}
|
|
68
|
+
onPreview={() => {
|
|
69
|
+
setPreview('fixed')
|
|
70
|
+
}}
|
|
60
71
|
/>
|
|
61
72
|
<Snippet
|
|
62
73
|
label="iframe with auto-resize"
|
|
63
74
|
hint="Adds a listener that follows the page as fields appear and as the thank-you replaces the form."
|
|
64
75
|
value={snippets.scriptSnippet}
|
|
76
|
+
onPreview={() => {
|
|
77
|
+
setPreview('resize')
|
|
78
|
+
}}
|
|
65
79
|
/>
|
|
80
|
+
|
|
81
|
+
{preview !== null && (
|
|
82
|
+
<EmbedPreviewModal
|
|
83
|
+
formId={form.id}
|
|
84
|
+
formName={form.name}
|
|
85
|
+
formTitle={form.title}
|
|
86
|
+
url={snippets.embedUrl}
|
|
87
|
+
mode={preview}
|
|
88
|
+
onClose={() => {
|
|
89
|
+
setPreview(null)
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
66
93
|
</div>
|
|
67
94
|
)
|
|
68
95
|
}
|
|
@@ -71,16 +98,27 @@ function Snippet({
|
|
|
71
98
|
label,
|
|
72
99
|
hint,
|
|
73
100
|
value,
|
|
101
|
+
onPreview,
|
|
74
102
|
}: {
|
|
75
103
|
readonly label: string
|
|
76
104
|
readonly hint: string
|
|
77
105
|
readonly value: string
|
|
106
|
+
readonly onPreview: () => void
|
|
78
107
|
}): React.JSX.Element {
|
|
79
108
|
return (
|
|
80
109
|
<div>
|
|
81
110
|
<div className="mb-1.5 flex items-center justify-between gap-2">
|
|
82
111
|
<span className="text-[12px] font-medium text-ink">{label}</span>
|
|
83
|
-
<
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onPreview}
|
|
116
|
+
className="rounded-md bg-accent px-3 py-1.5 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover"
|
|
117
|
+
>
|
|
118
|
+
Preview
|
|
119
|
+
</button>
|
|
120
|
+
<CopyButton value={value} label={`Copy the ${label} snippet`} />
|
|
121
|
+
</div>
|
|
84
122
|
</div>
|
|
85
123
|
<textarea
|
|
86
124
|
readOnly
|
|
@@ -93,3 +131,126 @@ function Snippet({
|
|
|
93
131
|
</div>
|
|
94
132
|
)
|
|
95
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Frames the hosted embed the way each snippet would: fixed 720px for the plain
|
|
137
|
+
* iframe, or listening for the height postMessage when previewing auto-resize.
|
|
138
|
+
*/
|
|
139
|
+
function EmbedPreviewModal({
|
|
140
|
+
formId,
|
|
141
|
+
formName,
|
|
142
|
+
formTitle,
|
|
143
|
+
url,
|
|
144
|
+
mode,
|
|
145
|
+
onClose,
|
|
146
|
+
}: {
|
|
147
|
+
readonly formId: string
|
|
148
|
+
readonly formName: string
|
|
149
|
+
readonly formTitle: string
|
|
150
|
+
readonly url: string
|
|
151
|
+
readonly mode: PreviewMode
|
|
152
|
+
readonly onClose: () => void
|
|
153
|
+
}): React.JSX.Element {
|
|
154
|
+
const titleId = useId()
|
|
155
|
+
const heading = formTitle.trim().length > 0 ? formTitle : formName
|
|
156
|
+
const [height, setHeight] = useState(720)
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (mode !== 'resize') {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function onMessage(event: MessageEvent): void {
|
|
164
|
+
const data = event.data as { kelpie?: string; formId?: string; height?: number } | null
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
data === null ||
|
|
168
|
+
typeof data !== 'object' ||
|
|
169
|
+
data.kelpie !== 'height' ||
|
|
170
|
+
data.formId !== formId ||
|
|
171
|
+
typeof data.height !== 'number' ||
|
|
172
|
+
!Number.isFinite(data.height)
|
|
173
|
+
) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setHeight(Math.max(320, Math.ceil(data.height)))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
window.addEventListener('message', onMessage)
|
|
181
|
+
|
|
182
|
+
return () => {
|
|
183
|
+
window.removeEventListener('message', onMessage)
|
|
184
|
+
}
|
|
185
|
+
}, [formId, mode])
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
189
|
+
if (event.key === 'Escape') {
|
|
190
|
+
onClose()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
window.addEventListener('keydown', onKeyDown)
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
198
|
+
}
|
|
199
|
+
}, [onClose])
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
className="fixed inset-0 z-50 flex items-end justify-center bg-ink/40 p-4 sm:items-center"
|
|
204
|
+
role="dialog"
|
|
205
|
+
aria-modal="true"
|
|
206
|
+
aria-labelledby={titleId}
|
|
207
|
+
onClick={onClose}
|
|
208
|
+
>
|
|
209
|
+
<div
|
|
210
|
+
className="animate-slide-in flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden rounded-md border border-border bg-surface-raised shadow-lg"
|
|
211
|
+
onClick={(event) => {
|
|
212
|
+
event.stopPropagation()
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-border px-4 py-3">
|
|
216
|
+
<div className="min-w-0">
|
|
217
|
+
<h2 id={titleId} className="truncate text-[14px] font-semibold text-ink">
|
|
218
|
+
Preview · {heading}
|
|
219
|
+
</h2>
|
|
220
|
+
<p className="mt-0.5 text-[11px] text-ink-faint">
|
|
221
|
+
{mode === 'resize'
|
|
222
|
+
? 'Bare iframe embed with auto-resize — fields only, height follows the form.'
|
|
223
|
+
: 'Bare iframe embed — fields only, fixed height.'}
|
|
224
|
+
</p>
|
|
225
|
+
</div>
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={onClose}
|
|
229
|
+
className="shrink-0 rounded-md px-2 py-1 text-[12px] font-medium text-ink-muted transition hover:text-ink"
|
|
230
|
+
>
|
|
231
|
+
Close
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div className="min-h-0 flex-1 overflow-y-auto bg-[#f4f4f5] p-4">
|
|
236
|
+
<div className="overflow-hidden rounded-md border border-[#d4d4d8] bg-white shadow-sm">
|
|
237
|
+
<iframe
|
|
238
|
+
src={url}
|
|
239
|
+
title={`${heading} preview`}
|
|
240
|
+
style={{
|
|
241
|
+
width: '100%',
|
|
242
|
+
border: 0,
|
|
243
|
+
height: mode === 'fixed' ? '720px' : `${String(height)}px`,
|
|
244
|
+
colorScheme: 'normal',
|
|
245
|
+
}}
|
|
246
|
+
className="block bg-transparent"
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
<p className="mt-2 text-center text-[11px] text-ink-faint">
|
|
250
|
+
Simulated host page — the iframe uses its own neutral styles, not Kelpie's.
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|