@kelpie/ui 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/resources/attendances.d.ts +13 -0
- package/dist/api/resources/attendances.d.ts.map +1 -0
- package/dist/api/resources/attendances.js +34 -0
- package/dist/api/resources/attendances.js.map +1 -0
- package/dist/api/resources/eventAssociations.d.ts +12 -0
- package/dist/api/resources/eventAssociations.d.ts.map +1 -0
- package/dist/api/resources/eventAssociations.js +25 -0
- package/dist/api/resources/eventAssociations.js.map +1 -0
- package/dist/api/resources/events.d.ts +17 -0
- package/dist/api/resources/events.d.ts.map +1 -0
- package/dist/api/resources/events.js +40 -0
- package/dist/api/resources/events.js.map +1 -0
- package/dist/api/resources/handbookSeed.d.ts +11 -0
- package/dist/api/resources/handbookSeed.d.ts.map +1 -0
- package/dist/api/resources/handbookSeed.js +43 -0
- package/dist/api/resources/handbookSeed.js.map +1 -0
- package/dist/api/resources/moduleSettings.d.ts +10 -0
- package/dist/api/resources/moduleSettings.d.ts.map +1 -1
- package/dist/api/resources/moduleSettings.js +21 -0
- package/dist/api/resources/moduleSettings.js.map +1 -1
- package/dist/api/resources/planItems.d.ts +4 -4
- package/dist/api/resources/planItems.d.ts.map +1 -1
- package/dist/api/resources/planItems.js +1 -1
- package/dist/api/resources/planItems.js.map +1 -1
- package/dist/api/resources/publicConfig.js +3 -3
- package/dist/api/resources/sampleData.d.ts.map +1 -1
- package/dist/api/resources/sampleData.js +2 -0
- package/dist/api/resources/sampleData.js.map +1 -1
- package/dist/api/resources/session.d.ts +22 -1
- package/dist/api/resources/session.d.ts.map +1 -1
- package/dist/api/resources/session.js +43 -1
- package/dist/api/resources/session.js.map +1 -1
- package/dist/api/resources/workspace.d.ts.map +1 -1
- package/dist/api/resources/workspace.js +2 -3
- package/dist/api/resources/workspace.js.map +1 -1
- package/dist/app/KelpieApp.d.ts.map +1 -1
- package/dist/app/KelpieApp.js +5 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +2 -0
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/AddressesField.d.ts +23 -0
- package/dist/components/AddressesField.d.ts.map +1 -0
- package/dist/components/AddressesField.js +215 -0
- package/dist/components/AddressesField.js.map +1 -0
- package/dist/components/CountrySearch.d.ts +16 -0
- package/dist/components/CountrySearch.d.ts.map +1 -0
- package/dist/components/CountrySearch.js +30 -0
- package/dist/components/CountrySearch.js.map +1 -0
- package/dist/components/EnvironmentBanner.d.ts +0 -12
- package/dist/components/EnvironmentBanner.d.ts.map +1 -1
- package/dist/components/EnvironmentBanner.js +21 -6
- package/dist/components/EnvironmentBanner.js.map +1 -1
- package/dist/components/InlineEdit.d.ts +7 -1
- package/dist/components/InlineEdit.d.ts.map +1 -1
- package/dist/components/InlineEdit.js +8 -1
- package/dist/components/InlineEdit.js.map +1 -1
- package/dist/components/LinkedEventsPanel.d.ts +11 -0
- package/dist/components/LinkedEventsPanel.d.ts.map +1 -0
- package/dist/components/LinkedEventsPanel.js +18 -0
- package/dist/components/LinkedEventsPanel.js.map +1 -0
- package/dist/components/NotesPanel.d.ts +4 -6
- package/dist/components/NotesPanel.d.ts.map +1 -1
- package/dist/components/NotesPanel.js +39 -3
- package/dist/components/NotesPanel.js.map +1 -1
- package/dist/components/PlanAttention.d.ts.map +1 -1
- package/dist/components/PlanAttention.js +3 -2
- package/dist/components/PlanAttention.js.map +1 -1
- package/dist/components/PlanPanel.d.ts +3 -3
- package/dist/components/PlanPanel.d.ts.map +1 -1
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +8 -2
- package/dist/components/Shell.js.map +1 -1
- package/dist/components/SocialNetworkIcon.d.ts +17 -0
- package/dist/components/SocialNetworkIcon.d.ts.map +1 -0
- package/dist/components/SocialNetworkIcon.js +39 -0
- package/dist/components/SocialNetworkIcon.js.map +1 -0
- package/dist/components/SocialProfilesField.d.ts +8 -0
- package/dist/components/SocialProfilesField.d.ts.map +1 -1
- package/dist/components/SocialProfilesField.js +19 -5
- package/dist/components/SocialProfilesField.js.map +1 -1
- package/dist/components/TimezoneSearch.d.ts +21 -0
- package/dist/components/TimezoneSearch.d.ts.map +1 -0
- package/dist/components/TimezoneSearch.js +27 -0
- package/dist/components/TimezoneSearch.js.map +1 -0
- package/dist/components/WorkspaceSwitcher.d.ts +8 -0
- package/dist/components/WorkspaceSwitcher.d.ts.map +1 -0
- package/dist/components/WorkspaceSwitcher.js +67 -0
- package/dist/components/WorkspaceSwitcher.js.map +1 -0
- package/dist/components/environmentTone.d.ts +10 -0
- package/dist/components/environmentTone.d.ts.map +1 -0
- package/dist/components/environmentTone.js +35 -0
- package/dist/components/environmentTone.js.map +1 -0
- package/dist/index.d.ts +21 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/timezones.d.ts +17 -0
- package/dist/lib/timezones.d.ts.map +1 -0
- package/dist/lib/timezones.js +39 -0
- package/dist/lib/timezones.js.map +1 -0
- package/dist/pages/CompaniesPage.d.ts.map +1 -1
- package/dist/pages/CompaniesPage.js +12 -11
- package/dist/pages/CompaniesPage.js.map +1 -1
- package/dist/pages/CompanyDetail.d.ts.map +1 -1
- package/dist/pages/CompanyDetail.js +23 -12
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +5 -1
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +7 -0
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiryDetail.d.ts.map +1 -1
- package/dist/pages/EnquiryDetail.js +5 -1
- package/dist/pages/EnquiryDetail.js.map +1 -1
- package/dist/pages/EventDetail.d.ts +2 -0
- package/dist/pages/EventDetail.d.ts.map +1 -0
- package/dist/pages/EventDetail.js +332 -0
- package/dist/pages/EventDetail.js.map +1 -0
- package/dist/pages/EventsPage.d.ts +2 -0
- package/dist/pages/EventsPage.d.ts.map +1 -0
- package/dist/pages/EventsPage.js +176 -0
- package/dist/pages/EventsPage.js.map +1 -0
- package/dist/pages/ListDetail.d.ts.map +1 -1
- package/dist/pages/ListDetail.js +17 -0
- package/dist/pages/ListDetail.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +5 -1
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +5 -1
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/PeoplePage.d.ts.map +1 -1
- package/dist/pages/PeoplePage.js +13 -5
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +94 -28
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +6 -3
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +5 -1
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/RoleDetail.d.ts.map +1 -1
- package/dist/pages/RoleDetail.js +4 -1
- package/dist/pages/RoleDetail.js.map +1 -1
- package/dist/pages/SearchPage.d.ts.map +1 -1
- package/dist/pages/SearchPage.js +2 -1
- package/dist/pages/SearchPage.js.map +1 -1
- package/dist/pages/account/PreferencesPage.d.ts.map +1 -1
- package/dist/pages/account/PreferencesPage.js +2 -28
- package/dist/pages/account/PreferencesPage.js.map +1 -1
- package/dist/pages/admin/DataPage.js +10 -9
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/FieldsPage.js +1 -1
- package/dist/pages/admin/FieldsPage.js.map +1 -1
- package/dist/pages/admin/ModulesPage.d.ts.map +1 -1
- package/dist/pages/admin/ModulesPage.js +1 -0
- package/dist/pages/admin/ModulesPage.js.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.d.ts +12 -0
- package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.js +2 -21
- package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/auth/AuthLayout.d.ts +5 -4
- package/dist/pages/auth/AuthLayout.d.ts.map +1 -1
- package/dist/pages/auth/AuthLayout.js +7 -5
- package/dist/pages/auth/AuthLayout.js.map +1 -1
- package/dist/pages/auth/RegionSwitcher.d.ts +10 -0
- package/dist/pages/auth/RegionSwitcher.d.ts.map +1 -0
- package/dist/pages/auth/RegionSwitcher.js +28 -0
- package/dist/pages/auth/RegionSwitcher.js.map +1 -0
- package/dist/pages/auth/SignInPage.d.ts.map +1 -1
- package/dist/pages/auth/SignInPage.js +3 -1
- package/dist/pages/auth/SignInPage.js.map +1 -1
- package/dist/pages/auth/SignUpPage.d.ts.map +1 -1
- package/dist/pages/auth/SignUpPage.js +7 -0
- package/dist/pages/auth/SignUpPage.js.map +1 -1
- package/dist/pages/auth/regionHref.d.ts +6 -0
- package/dist/pages/auth/regionHref.d.ts.map +1 -0
- package/dist/pages/auth/regionHref.js +8 -0
- package/dist/pages/auth/regionHref.js.map +1 -0
- package/dist/pages/dashboard/DashboardPage.d.ts +8 -0
- package/dist/pages/dashboard/DashboardPage.d.ts.map +1 -1
- package/dist/pages/dashboard/DashboardPage.js +5 -13
- package/dist/pages/dashboard/DashboardPage.js.map +1 -1
- package/dist/pages/dashboard/attention.d.ts +1 -8
- package/dist/pages/dashboard/attention.d.ts.map +1 -1
- package/dist/pages/dashboard/attention.js +21 -54
- package/dist/pages/dashboard/attention.js.map +1 -1
- package/dist/pages/dashboard/sections.d.ts.map +1 -1
- package/dist/pages/dashboard/sections.js +3 -2
- package/dist/pages/dashboard/sections.js.map +1 -1
- package/dist/pages/forms/FormActions.d.ts.map +1 -1
- package/dist/pages/forms/FormActions.js +7 -3
- package/dist/pages/forms/FormActions.js.map +1 -1
- package/dist/pages/onboarding/HandbookStepPage.d.ts +4 -8
- package/dist/pages/onboarding/HandbookStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/HandbookStepPage.js +13 -13
- package/dist/pages/onboarding/HandbookStepPage.js.map +1 -1
- package/dist/pages/onboarding/InvitesStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/InvitesStepPage.js +13 -7
- package/dist/pages/onboarding/InvitesStepPage.js.map +1 -1
- package/dist/pages/onboarding/ModulesStepPage.d.ts +11 -0
- package/dist/pages/onboarding/ModulesStepPage.d.ts.map +1 -0
- package/dist/pages/onboarding/ModulesStepPage.js +80 -0
- package/dist/pages/onboarding/ModulesStepPage.js.map +1 -0
- package/dist/pages/onboarding/OnboardingNav.d.ts +17 -0
- package/dist/pages/onboarding/OnboardingNav.d.ts.map +1 -0
- package/dist/pages/onboarding/OnboardingNav.js +37 -0
- package/dist/pages/onboarding/OnboardingNav.js.map +1 -0
- package/dist/pages/onboarding/OrganisationStepPage.d.ts +13 -0
- package/dist/pages/onboarding/OrganisationStepPage.d.ts.map +1 -0
- package/dist/pages/onboarding/OrganisationStepPage.js +59 -0
- package/dist/pages/onboarding/OrganisationStepPage.js.map +1 -0
- package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js +25 -20
- package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
- package/dist/pages/onboarding/handbookTemplates.d.ts +19 -0
- package/dist/pages/onboarding/handbookTemplates.d.ts.map +1 -0
- package/dist/pages/onboarding/handbookTemplates.js +57 -0
- package/dist/pages/onboarding/handbookTemplates.js.map +1 -0
- package/dist/pages/onboarding/onboardingModules.d.ts +23 -0
- package/dist/pages/onboarding/onboardingModules.d.ts.map +1 -0
- package/dist/pages/onboarding/onboardingModules.js +153 -0
- package/dist/pages/onboarding/onboardingModules.js.map +1 -0
- package/dist/pages/onboarding/onboardingRerun.d.ts +21 -0
- package/dist/pages/onboarding/onboardingRerun.d.ts.map +1 -0
- package/dist/pages/onboarding/onboardingRerun.js +33 -0
- package/dist/pages/onboarding/onboardingRerun.js.map +1 -0
- package/package.json +4 -3
- package/src/api/resources/attendances.ts +54 -0
- package/src/api/resources/eventAssociations.ts +43 -0
- package/src/api/resources/events.ts +70 -0
- package/src/api/resources/handbookSeed.ts +63 -0
- package/src/api/resources/moduleSettings.ts +25 -0
- package/src/api/resources/planItems.ts +5 -5
- package/src/api/resources/publicConfig.ts +3 -3
- package/src/api/resources/sampleData.ts +2 -0
- package/src/api/resources/session.ts +59 -0
- package/src/api/resources/workspace.ts +2 -3
- package/src/app/KelpieApp.tsx +11 -3
- package/src/components/ActivitiesPanel.tsx +2 -0
- package/src/components/AddressesField.tsx +502 -0
- package/src/components/CountrySearch.tsx +71 -0
- package/src/components/EnvironmentBanner.tsx +23 -6
- package/src/components/InlineEdit.tsx +35 -1
- package/src/components/LinkedEventsPanel.tsx +50 -0
- package/src/components/NotesPanel.tsx +132 -15
- package/src/components/PlanAttention.tsx +5 -4
- package/src/components/PlanPanel.tsx +3 -3
- package/src/components/Shell.tsx +16 -1
- package/src/components/SocialNetworkIcon.tsx +88 -0
- package/src/components/SocialProfilesField.tsx +57 -10
- package/src/components/TimezoneSearch.tsx +67 -0
- package/src/components/WorkspaceSwitcher.tsx +136 -0
- package/src/components/environmentTone.ts +40 -0
- package/src/index.ts +42 -3
- package/src/lib/timezones.ts +43 -0
- package/src/pages/CompaniesPage.tsx +14 -13
- package/src/pages/CompanyDetail.tsx +59 -27
- package/src/pages/DealDetail.tsx +5 -0
- package/src/pages/DecisionsPage.tsx +7 -0
- package/src/pages/EnquiryDetail.tsx +7 -0
- package/src/pages/EventDetail.tsx +794 -0
- package/src/pages/EventsPage.tsx +382 -0
- package/src/pages/ListDetail.tsx +24 -0
- package/src/pages/OpportunityDetail.tsx +7 -0
- package/src/pages/PartnershipDetail.tsx +7 -0
- package/src/pages/PeoplePage.tsx +18 -5
- package/src/pages/PersonDetail.tsx +300 -100
- package/src/pages/PlanningPage.tsx +17 -14
- package/src/pages/RaiseDetail.tsx +5 -0
- package/src/pages/RoleDetail.tsx +8 -0
- package/src/pages/SearchPage.tsx +2 -1
- package/src/pages/account/PreferencesPage.tsx +2 -37
- package/src/pages/admin/DataPage.tsx +63 -21
- package/src/pages/admin/FieldsPage.tsx +1 -1
- package/src/pages/admin/ModulesPage.tsx +1 -0
- package/src/pages/admin/WorkspaceSettingsPage.tsx +10 -33
- package/src/pages/auth/AuthLayout.tsx +8 -4
- package/src/pages/auth/RegionSwitcher.tsx +56 -0
- package/src/pages/auth/SignInPage.tsx +10 -6
- package/src/pages/auth/SignUpPage.tsx +25 -0
- package/src/pages/auth/regionHref.ts +10 -0
- package/src/pages/dashboard/DashboardPage.tsx +16 -48
- package/src/pages/dashboard/attention.ts +24 -78
- package/src/pages/dashboard/sections.tsx +8 -2
- package/src/pages/forms/FormActions.tsx +14 -8
- package/src/pages/onboarding/HandbookStepPage.tsx +21 -24
- package/src/pages/onboarding/InvitesStepPage.tsx +27 -19
- package/src/pages/onboarding/ModulesStepPage.tsx +145 -0
- package/src/pages/onboarding/OnboardingNav.tsx +97 -0
- package/src/pages/onboarding/OrganisationStepPage.tsx +130 -0
- package/src/pages/onboarding/WorkspaceStepPage.tsx +53 -38
- package/src/pages/onboarding/handbookTemplates.ts +77 -0
- package/src/pages/onboarding/onboardingModules.ts +202 -0
- package/src/pages/onboarding/onboardingRerun.ts +41 -0
- package/src/styles.css +30 -0
|
@@ -3,16 +3,16 @@ import type {
|
|
|
3
3
|
DashboardPlanItem,
|
|
4
4
|
DashboardStaleContact,
|
|
5
5
|
DashboardTouchpoint,
|
|
6
|
-
|
|
6
|
+
DashboardUpcomingEvent,
|
|
7
7
|
RecordTargetType,
|
|
8
8
|
} from '@kelpie/schemas'
|
|
9
9
|
|
|
10
10
|
import type { ChipTone } from '../../components/Chip.tsx'
|
|
11
|
-
import { formatDay } from '../../lib/dates.ts'
|
|
11
|
+
import { formatDate, formatDay } from '../../lib/dates.ts'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Reading the workspace snapshot as a page: one attention list
|
|
15
|
-
*
|
|
14
|
+
* Reading the workspace snapshot as a page: one attention list from the
|
|
15
|
+
* signals the service already ranked.
|
|
16
16
|
*
|
|
17
17
|
* Pure functions over the decoded response, so the page renders and these are
|
|
18
18
|
* tested separately. Nothing here recomputes a signal: the service already
|
|
@@ -30,6 +30,8 @@ const ROUTES: Readonly<Record<RecordTargetType, string | undefined>> = {
|
|
|
30
30
|
raise: '/fundraising',
|
|
31
31
|
enquiry: '/enquiries',
|
|
32
32
|
candidate: undefined,
|
|
33
|
+
event: '/events',
|
|
34
|
+
attendance: undefined,
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const TARGET_TYPE_LABELS: Readonly<Record<RecordTargetType, string>> = {
|
|
@@ -41,6 +43,8 @@ const TARGET_TYPE_LABELS: Readonly<Record<RecordTargetType, string>> = {
|
|
|
41
43
|
raise: 'Fundraising',
|
|
42
44
|
enquiry: 'Enquiry',
|
|
43
45
|
candidate: 'Candidate',
|
|
46
|
+
event: 'Event',
|
|
47
|
+
attendance: 'Attendance',
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export function targetTypeLabel(targetType: RecordTargetType): string {
|
|
@@ -115,8 +119,22 @@ function staleContactRow(contact: DashboardStaleContact): AttentionRow {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
function upcomingEventRow(event: DashboardUpcomingEvent, timezone: string): AttentionRow {
|
|
123
|
+
const where = event.location.length > 0 ? event.location : event.format
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
id: `event-${event.id}`,
|
|
127
|
+
label: 'Upcoming event',
|
|
128
|
+
tone: 'accent',
|
|
129
|
+
meta: formatDate(event.startsAt, timezone),
|
|
130
|
+
title: event.name,
|
|
131
|
+
detail: `${String(event.attendeeCount)} registered · ${where}`,
|
|
132
|
+
href: `/events/${event.id}`,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
/**
|
|
119
|
-
* The
|
|
137
|
+
* The attention list, most urgent first.
|
|
120
138
|
*
|
|
121
139
|
* The groups are concatenated rather than sorted: each arrives from the API in
|
|
122
140
|
* its own order — plan items soonest first, contacts coldest first — and a
|
|
@@ -131,79 +149,7 @@ export function attentionRows(dashboard: Dashboard): readonly AttentionRow[] {
|
|
|
131
149
|
...touchpoints.filter((touchpoint) => touchpoint.overdue).map(touchpointRow),
|
|
132
150
|
...dashboard.dueSoonPlanItems.items.map((item) => planItemRow(item, false)),
|
|
133
151
|
...touchpoints.filter((touchpoint) => !touchpoint.overdue).map(touchpointRow),
|
|
152
|
+
...dashboard.upcomingEvents.items.map((item) => upcomingEventRow(item, dashboard.timezone)),
|
|
134
153
|
...dashboard.staleContacts.items.map(staleContactRow),
|
|
135
154
|
]
|
|
136
155
|
}
|
|
137
|
-
|
|
138
|
-
function plural(count: number, singular: string, pluralForm = `${singular}s`): string {
|
|
139
|
-
return `${String(count)} ${count === 1 ? singular : pluralForm}`
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* How a count of open records reads mid-sentence.
|
|
144
|
-
*
|
|
145
|
-
* Not `PIPELINE_KIND_LABELS`: that one is nav wording, where a Raise is filed
|
|
146
|
-
* under "Fundraising", and "3 fundraisings" is not a sentence. Written out
|
|
147
|
-
* rather than suffixed, because "opportunitys" is not one either.
|
|
148
|
-
*/
|
|
149
|
-
const OPEN_PIPELINE_NOUNS: Readonly<
|
|
150
|
-
Record<PipelineKind, { readonly one: string; readonly many: string }>
|
|
151
|
-
> = {
|
|
152
|
-
enquiry: { one: 'enquiry', many: 'enquiries' },
|
|
153
|
-
deal: { one: 'deal', many: 'deals' },
|
|
154
|
-
opportunity: { one: 'opportunity', many: 'opportunities' },
|
|
155
|
-
raise: { one: 'raise', many: 'raises' },
|
|
156
|
-
partnership: { one: 'partnership', many: 'partnerships' },
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The brief, built from the totals rather than from the rows.
|
|
161
|
-
*
|
|
162
|
-
* A list is capped by the request's limit and a total is not, so counting the
|
|
163
|
-
* rows on screen would understate every workspace with more than a page of them.
|
|
164
|
-
*/
|
|
165
|
-
export function briefLines(dashboard: Dashboard): readonly string[] {
|
|
166
|
-
const lines: string[] = []
|
|
167
|
-
|
|
168
|
-
if (dashboard.overduePlanItems.total > 0) {
|
|
169
|
-
lines.push(
|
|
170
|
-
`${plural(dashboard.overduePlanItems.total, 'plan item')} overdue — triage Planning first.`,
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (dashboard.dueSoonPlanItems.total > 0) {
|
|
175
|
-
lines.push(
|
|
176
|
-
`${plural(dashboard.dueSoonPlanItems.total, 'plan item')} due in the next ${plural(dashboard.upcomingDays, 'day')}.`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (dashboard.partnershipTouchpoints.total > 0) {
|
|
181
|
-
lines.push(
|
|
182
|
-
`${plural(dashboard.partnershipTouchpoints.total, 'partnership touchpoint')} at hand.`,
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (dashboard.staleContacts.total > 0) {
|
|
187
|
-
lines.push(
|
|
188
|
-
`${plural(dashboard.staleContacts.total, 'contact')} past the ${String(dashboard.staleContactDays)}-day touch threshold.`,
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const open = dashboard.pipelines.filter((pipeline) => pipeline.open > 0)
|
|
193
|
-
|
|
194
|
-
if (open.length > 0) {
|
|
195
|
-
lines.push(
|
|
196
|
-
`Open: ${open
|
|
197
|
-
.map((pipeline) => {
|
|
198
|
-
const noun = OPEN_PIPELINE_NOUNS[pipeline.kind]
|
|
199
|
-
|
|
200
|
-
return plural(pipeline.open, noun.one, noun.many)
|
|
201
|
-
})
|
|
202
|
-
.join(', ')}.`,
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return lines.length === 0
|
|
207
|
-
? ['Nothing urgent. Pipeline and relationships look quiet today.']
|
|
208
|
-
: lines
|
|
209
|
-
}
|
|
@@ -9,6 +9,7 @@ import type { ReactNode } from 'react'
|
|
|
9
9
|
import { Link } from 'react-router'
|
|
10
10
|
|
|
11
11
|
import { Chip } from '../../components/Chip.tsx'
|
|
12
|
+
import { MarkdownView } from '../../components/MarkdownView.tsx'
|
|
12
13
|
import { SectionHeader } from '../../components/SectionHeader.tsx'
|
|
13
14
|
import { formatDate, formatRelativeTime } from '../../lib/dates.ts'
|
|
14
15
|
import { targetHref, targetTypeLabel } from './attention.ts'
|
|
@@ -72,7 +73,10 @@ function EmptyOr({
|
|
|
72
73
|
export function AttentionList({ rows }: { readonly rows: readonly AttentionRow[] }): React.JSX.Element {
|
|
73
74
|
return (
|
|
74
75
|
<section>
|
|
75
|
-
<SectionHeader
|
|
76
|
+
<SectionHeader
|
|
77
|
+
title="Needs attention"
|
|
78
|
+
description="Overdue Plans, partnership touchpoints, and contacts going cold."
|
|
79
|
+
/>
|
|
76
80
|
<EmptyOr isEmpty={rows.length === 0} empty="Nothing urgent right now.">
|
|
77
81
|
<ul className="divide-y divide-border border-y border-border">
|
|
78
82
|
{rows.map((row) => (
|
|
@@ -201,7 +205,9 @@ export function NotesList({
|
|
|
201
205
|
{notes.map((note) => (
|
|
202
206
|
<li key={note.id} className="py-3">
|
|
203
207
|
<div className="flex items-start justify-between gap-2">
|
|
204
|
-
<
|
|
208
|
+
<div className="line-clamp-3 min-w-0 flex-1">
|
|
209
|
+
<MarkdownView source={note.body} />
|
|
210
|
+
</div>
|
|
205
211
|
{note.pinned && (
|
|
206
212
|
<span className="shrink-0 text-[10px] font-medium tracking-wide text-accent uppercase">
|
|
207
213
|
Pinned
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Form, FormAttachTarget, PipelineKind } from '@kelpie/schemas'
|
|
1
|
+
import { FORM_ATTACH_TARGET_TYPES, PLAN_ITEM_TARGET_TYPE_LABELS } from '@kelpie/schemas'
|
|
2
|
+
import type { Form, FormAttachTarget, FormAttachTargetType, PipelineKind } from '@kelpie/schemas'
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
4
|
|
|
5
5
|
import { useDeals } from '../../api/resources/deals.ts'
|
|
6
6
|
import { useEnquiries } from '../../api/resources/enquiries.ts'
|
|
7
|
+
import { useEvents } from '../../api/resources/events.ts'
|
|
7
8
|
import { useUpdateForm } from '../../api/resources/forms.ts'
|
|
8
9
|
import { useLists } from '../../api/resources/lists.ts'
|
|
9
10
|
import { useMembers } from '../../api/resources/members.ts'
|
|
@@ -400,12 +401,13 @@ interface AttachRecord {
|
|
|
400
401
|
readonly name: string
|
|
401
402
|
}
|
|
402
403
|
|
|
403
|
-
function useAttachOptions(kind:
|
|
404
|
+
function useAttachOptions(kind: FormAttachTargetType): readonly AttachRecord[] {
|
|
404
405
|
const deals = useDeals({}, { enabled: kind === 'deal' })
|
|
405
406
|
const opportunities = useOpportunities({}, { enabled: kind === 'opportunity' })
|
|
406
407
|
const raises = useRaises({}, { enabled: kind === 'raise' })
|
|
407
408
|
const partnerships = usePartnerships({}, { enabled: kind === 'partnership' })
|
|
408
409
|
const enquiries = useEnquiries({}, { enabled: kind === 'enquiry' })
|
|
410
|
+
const events = useEvents({}, { enabled: kind === 'event' })
|
|
409
411
|
|
|
410
412
|
switch (kind) {
|
|
411
413
|
case 'deal':
|
|
@@ -421,6 +423,8 @@ function useAttachOptions(kind: PipelineKind): readonly AttachRecord[] {
|
|
|
421
423
|
}))
|
|
422
424
|
case 'enquiry':
|
|
423
425
|
return enquiries.records.map((enquiry) => ({ id: enquiry.id, name: enquiry.name }))
|
|
426
|
+
case 'event':
|
|
427
|
+
return events.records.map((record) => ({ id: record.id, name: record.name }))
|
|
424
428
|
}
|
|
425
429
|
}
|
|
426
430
|
|
|
@@ -431,7 +435,7 @@ function AttachTargetsBlock({
|
|
|
431
435
|
readonly form: Form
|
|
432
436
|
readonly onChange: (next: readonly FormAttachTarget[]) => void
|
|
433
437
|
}): React.JSX.Element {
|
|
434
|
-
const [kind, setKind] = useState<
|
|
438
|
+
const [kind, setKind] = useState<FormAttachTargetType>('deal')
|
|
435
439
|
const [pick, setPick] = useState('')
|
|
436
440
|
const options = useAttachOptions(kind)
|
|
437
441
|
const chosen = form.attachTargets
|
|
@@ -459,7 +463,9 @@ function AttachTargetsBlock({
|
|
|
459
463
|
<div className="space-y-2 rounded-md border border-border p-4">
|
|
460
464
|
<div className="text-[13px] font-medium text-ink">Attach the submitter to a record</div>
|
|
461
465
|
<p className="text-[11px] text-ink-faint">
|
|
462
|
-
|
|
466
|
+
Pipeline records receive a <code>person_links</code> row. An Event
|
|
467
|
+
registers Attendance instead: the submitter is marked registered, and a
|
|
468
|
+
second submit of the same person is a no-op.
|
|
463
469
|
</p>
|
|
464
470
|
<ul className="divide-y divide-border">
|
|
465
471
|
{chosen.length === 0 && (
|
|
@@ -496,13 +502,13 @@ function AttachTargetsBlock({
|
|
|
496
502
|
className={inputClass}
|
|
497
503
|
value={kind}
|
|
498
504
|
onChange={(event) => {
|
|
499
|
-
setKind(event.target.value as
|
|
505
|
+
setKind(event.target.value as FormAttachTargetType)
|
|
500
506
|
setPick('')
|
|
501
507
|
}}
|
|
502
508
|
>
|
|
503
|
-
{
|
|
509
|
+
{FORM_ATTACH_TARGET_TYPES.map((option) => (
|
|
504
510
|
<option key={option} value={option}>
|
|
505
|
-
{option}
|
|
511
|
+
{PLAN_ITEM_TARGET_TYPE_LABELS[option]}
|
|
506
512
|
</option>
|
|
507
513
|
))}
|
|
508
514
|
</select>
|
|
@@ -1,45 +1,47 @@
|
|
|
1
|
-
import { useNavigate } from 'react-router'
|
|
1
|
+
import { Navigate, useNavigate, useSearchParams } from 'react-router'
|
|
2
2
|
|
|
3
3
|
import { useHandbookPages } from '../../api/resources/handbookPages.ts'
|
|
4
4
|
import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
|
|
5
|
-
import { PrimaryButton } from '../auth/AuthForm.tsx'
|
|
6
5
|
import { AuthLayout } from '../auth/AuthLayout.tsx'
|
|
6
|
+
import { OnboardingNav } from './OnboardingNav.tsx'
|
|
7
|
+
import { isOnboardingRerun } from './onboardingRerun.ts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Onboarding step
|
|
10
|
+
* Onboarding step 5: review the starter handbook.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the workspace, two steps ago. So this reads them back and shows what is there.
|
|
12
|
+
* Pages were seeded on step 2 when the reader picked an organisation type.
|
|
13
|
+
* This step reads them back and shows what agents will learn from.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* that creates nothing teaches a reader something false about the product, and
|
|
17
|
-
* the alternative — deleting the step — would drop the one screen that tells a
|
|
18
|
-
* new workspace its handbook is what agents read.
|
|
15
|
+
* A rerun skips this step: the pages are already in place.
|
|
19
16
|
*/
|
|
20
17
|
export function HandbookStepPage(): React.JSX.Element {
|
|
21
18
|
const navigate = useNavigate()
|
|
19
|
+
const [searchParams] = useSearchParams()
|
|
22
20
|
const { records: pages, isLoading, error } = useHandbookPages()
|
|
23
21
|
|
|
22
|
+
if (isOnboardingRerun(searchParams)) {
|
|
23
|
+
return <Navigate to="/dashboard" replace />
|
|
24
|
+
}
|
|
25
|
+
|
|
24
26
|
function finish(): void {
|
|
25
|
-
// The wizard ends on the dashboard, per `onboarding.md`. It is empty for a
|
|
26
|
-
// brand new workspace, which is the point: it says what there is to fill in.
|
|
27
27
|
navigate('/dashboard', { replace: true })
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const sortedPages = [...pages].sort((left, right) => left.sortOrder - right.sortOrder)
|
|
31
|
+
|
|
30
32
|
return (
|
|
31
33
|
<AuthLayout
|
|
32
|
-
step={
|
|
34
|
+
step={5}
|
|
33
35
|
title="Your starter handbook"
|
|
34
|
-
description="These markdown pages
|
|
36
|
+
description="These markdown pages were chosen for your organisation type. Agents read them to learn what your company is."
|
|
35
37
|
>
|
|
36
38
|
<div className="mt-5 space-y-4">
|
|
37
39
|
{error !== null && <ErrorPanel error={error} />}
|
|
38
40
|
{error === null && isLoading && <LoadingPanel label="Loading your handbook…" />}
|
|
39
41
|
|
|
40
|
-
{error === null && !isLoading &&
|
|
42
|
+
{error === null && !isLoading && sortedPages.length > 0 && (
|
|
41
43
|
<ul className="max-h-56 overflow-y-auto rounded-md border border-border bg-surface">
|
|
42
|
-
{
|
|
44
|
+
{sortedPages.map((page) => (
|
|
43
45
|
<li
|
|
44
46
|
key={page.id}
|
|
45
47
|
className="border-b border-border px-3 py-2 text-[13px] text-ink last:border-0"
|
|
@@ -50,25 +52,20 @@ export function HandbookStepPage(): React.JSX.Element {
|
|
|
50
52
|
</ul>
|
|
51
53
|
)}
|
|
52
54
|
|
|
53
|
-
{
|
|
54
|
-
assert one thing and show another. The handbook is seeded with the
|
|
55
|
-
workspace, so nothing here is the seeding having not happened. */}
|
|
56
|
-
{error === null && !isLoading && pages.length === 0 && (
|
|
55
|
+
{error === null && !isLoading && sortedPages.length === 0 && (
|
|
57
56
|
<p className="rounded-md border border-border bg-surface px-3 py-2 text-[13px] text-ink-muted">
|
|
58
57
|
This workspace has no handbook pages. That is not what a new workspace should look
|
|
59
58
|
like — the Handbook tab is where to add them.
|
|
60
59
|
</p>
|
|
61
60
|
)}
|
|
62
61
|
|
|
63
|
-
{
|
|
62
|
+
{sortedPages.length > 0 && (
|
|
64
63
|
<p className="text-[12px] text-ink-muted">
|
|
65
64
|
Every page starts as a stub. Write them from the Handbook tab whenever you are ready.
|
|
66
65
|
</p>
|
|
67
66
|
)}
|
|
68
67
|
|
|
69
|
-
{
|
|
70
|
-
button only moves the reader on. */}
|
|
71
|
-
<PrimaryButton label="Go to Kelpie" onClick={finish} />
|
|
68
|
+
<OnboardingNav step={5} nextLabel="Go to Kelpie" nextType="button" onNext={finish} />
|
|
72
69
|
</div>
|
|
73
70
|
</AuthLayout>
|
|
74
71
|
)
|
|
@@ -2,14 +2,16 @@ import { INVITABLE_ROLES } from '@kelpie/schemas'
|
|
|
2
2
|
import type { InvitableRole } from '@kelpie/schemas'
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import type { FormEvent } from 'react'
|
|
5
|
-
import { useNavigate } from 'react-router'
|
|
5
|
+
import { useNavigate, useSearchParams } from 'react-router'
|
|
6
6
|
|
|
7
7
|
import { useSendInvite } from '../../api/resources/invites.ts'
|
|
8
|
-
import { AUTH_INPUT_CLASS, SecondaryButton
|
|
8
|
+
import { AUTH_INPUT_CLASS, SecondaryButton } from '../auth/AuthForm.tsx'
|
|
9
9
|
import { AuthLayout } from '../auth/AuthLayout.tsx'
|
|
10
|
+
import { OnboardingNav } from './OnboardingNav.tsx'
|
|
11
|
+
import { isOnboardingRerun } from './onboardingRerun.ts'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* Onboarding step
|
|
14
|
+
* Onboarding step 4: invitations, against `POST /v1/workspaces/:id/invites`.
|
|
13
15
|
*
|
|
14
16
|
* One request per address, and they can fail one at a time — an address already
|
|
15
17
|
* invited answers `409` while its neighbours succeed. The mockup could ignore
|
|
@@ -38,9 +40,16 @@ function isOutstanding(row: InviteRow): boolean {
|
|
|
38
40
|
|
|
39
41
|
export function InvitesStepPage(): React.JSX.Element {
|
|
40
42
|
const navigate = useNavigate()
|
|
43
|
+
const [searchParams] = useSearchParams()
|
|
44
|
+
const rerun = isOnboardingRerun(searchParams)
|
|
41
45
|
const sendInvite = useSendInvite()
|
|
42
46
|
const [rows, setRows] = useState<readonly InviteRow[]>([emptyRow()])
|
|
43
47
|
|
|
48
|
+
/** First run reviews the handbook next; a rerun leaves those pages alone. */
|
|
49
|
+
function afterInvites(): void {
|
|
50
|
+
navigate(rerun ? '/dashboard' : '/onboarding/handbook', { replace: true })
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
function changeRow(index: number, changes: Partial<InviteRow>): void {
|
|
45
54
|
setRows((previous) =>
|
|
46
55
|
previous.map((row, at) => (at === index ? { ...row, ...changes } : row)),
|
|
@@ -80,7 +89,7 @@ export function InvitesStepPage(): React.JSX.Element {
|
|
|
80
89
|
sendOutstanding()
|
|
81
90
|
.then((allSent) => {
|
|
82
91
|
if (allSent) {
|
|
83
|
-
|
|
92
|
+
afterInvites()
|
|
84
93
|
}
|
|
85
94
|
})
|
|
86
95
|
.catch(() => undefined)
|
|
@@ -90,7 +99,7 @@ export function InvitesStepPage(): React.JSX.Element {
|
|
|
90
99
|
|
|
91
100
|
return (
|
|
92
101
|
<AuthLayout
|
|
93
|
-
step={
|
|
102
|
+
step={4}
|
|
94
103
|
title="Invite teammates"
|
|
95
104
|
description="Optional. You can always invite people later from Admin → Team."
|
|
96
105
|
>
|
|
@@ -117,20 +126,19 @@ export function InvitesStepPage(): React.JSX.Element {
|
|
|
117
126
|
Add another
|
|
118
127
|
</button>
|
|
119
128
|
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</div>
|
|
129
|
+
<OnboardingNav
|
|
130
|
+
step={4}
|
|
131
|
+
nextLabel={outstanding === 0 ? 'Next' : 'Send invitations'}
|
|
132
|
+
nextPendingLabel="Sending…"
|
|
133
|
+
isPending={sendInvite.isPending}
|
|
134
|
+
extra={
|
|
135
|
+
<SecondaryButton
|
|
136
|
+
label="Skip for now"
|
|
137
|
+
disabled={sendInvite.isPending}
|
|
138
|
+
onClick={afterInvites}
|
|
139
|
+
/>
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
134
142
|
</form>
|
|
135
143
|
</AuthLayout>
|
|
136
144
|
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { FormEvent } from 'react'
|
|
3
|
+
import { useNavigate, useSearchParams } from 'react-router'
|
|
4
|
+
|
|
5
|
+
import { useSetModuleEnabled } from '../../api/resources/moduleSettings.ts'
|
|
6
|
+
import { useInstallSampleData } from '../../api/resources/sampleData.ts'
|
|
7
|
+
import { useSession } from '../../api/resources/session.ts'
|
|
8
|
+
import { ErrorPanel, LoadingPanel } from '../../components/QueryState.tsx'
|
|
9
|
+
import { AuthLayout } from '../auth/AuthLayout.tsx'
|
|
10
|
+
import { organisationChoiceFromParam, organisationChoiceLabel } from './handbookTemplates.ts'
|
|
11
|
+
import type { OrganisationChoiceId } from './handbookTemplates.ts'
|
|
12
|
+
import { OnboardingNav } from './OnboardingNav.tsx'
|
|
13
|
+
import { defaultOnboardingModuleState, onboardingModulesForChoice } from './onboardingModules.ts'
|
|
14
|
+
import { isOnboardingRerun, ONBOARDING_ORG_PARAM, onboardingPath } from './onboardingRerun.ts'
|
|
15
|
+
|
|
16
|
+
function modulesStepDescription(org: OrganisationChoiceId): string {
|
|
17
|
+
if (org === 'startup' || org === 'later') {
|
|
18
|
+
return 'Pick the parts of Kelpie your team needs today. Turn the rest on whenever you like — nothing here is permanent.'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return `Suggested for ${organisationChoiceLabel(org)}. Turn the rest on whenever you like — nothing here is permanent.`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Onboarding step 3: which optional modules this workspace runs.
|
|
26
|
+
*
|
|
27
|
+
* The workspace already exists. This step writes each of the six choices
|
|
28
|
+
* through `PATCH /v1/workspaces/:id/modules/:moduleId`, then optionally
|
|
29
|
+
* installs sample data, then moves on to invites. Defaults and explanations
|
|
30
|
+
* follow the organisation type on `?org=`. Other toggleable modules stay
|
|
31
|
+
* on; Admin → Modules is where they change later.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export function ModulesStepPage(): React.JSX.Element {
|
|
35
|
+
const navigate = useNavigate()
|
|
36
|
+
const [searchParams] = useSearchParams()
|
|
37
|
+
const rerun = isOnboardingRerun(searchParams)
|
|
38
|
+
const org = organisationChoiceFromParam(searchParams.get(ONBOARDING_ORG_PARAM))
|
|
39
|
+
const modules = onboardingModulesForChoice(org)
|
|
40
|
+
const { session, isLoading } = useSession()
|
|
41
|
+
const setEnabled = useSetModuleEnabled()
|
|
42
|
+
const installSampleData = useInstallSampleData()
|
|
43
|
+
const [enabled, setEnabledState] = useState(() => defaultOnboardingModuleState(org))
|
|
44
|
+
const [seedSample, setSeedSample] = useState(false)
|
|
45
|
+
const [saving, setSaving] = useState(false)
|
|
46
|
+
|
|
47
|
+
function toggle(moduleId: (typeof modules)[number]['id'], next: boolean): void {
|
|
48
|
+
setEnabledState((previous) => ({ ...previous, [moduleId]: next }))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function submit(event: FormEvent): void {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
|
|
54
|
+
const workspaceId = session?.workspaceId
|
|
55
|
+
|
|
56
|
+
if (workspaceId === null || workspaceId === undefined) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setSaving(true)
|
|
61
|
+
|
|
62
|
+
persistChoices()
|
|
63
|
+
.then(async () => {
|
|
64
|
+
if (seedSample) {
|
|
65
|
+
await installSampleData.runAsync({ workspaceId })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
navigate(onboardingPath('/onboarding/invites', rerun, org), { replace: true })
|
|
69
|
+
})
|
|
70
|
+
.catch(() => undefined)
|
|
71
|
+
.finally(() => {
|
|
72
|
+
setSaving(false)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function persistChoices(): Promise<void> {
|
|
77
|
+
for (const choice of modules) {
|
|
78
|
+
await setEnabled.runAsync({
|
|
79
|
+
moduleId: choice.id,
|
|
80
|
+
enabled: enabled[choice.id] === true,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isPending = saving || setEnabled.isPending || installSampleData.isPending
|
|
86
|
+
const workspaceId = session?.workspaceId
|
|
87
|
+
const description = modulesStepDescription(org)
|
|
88
|
+
|
|
89
|
+
if (isLoading || workspaceId === null || workspaceId === undefined) {
|
|
90
|
+
return (
|
|
91
|
+
<AuthLayout step={3} title="What do you want to use?" description={description}>
|
|
92
|
+
<div className="mt-5">
|
|
93
|
+
<LoadingPanel label="Loading…" />
|
|
94
|
+
</div>
|
|
95
|
+
</AuthLayout>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<AuthLayout step={3} title="What do you want to use?" description={description}>
|
|
101
|
+
<form onSubmit={submit} className="mt-5 space-y-3">
|
|
102
|
+
<ul className="space-y-3">
|
|
103
|
+
{modules.map((choice) => (
|
|
104
|
+
<li key={choice.id}>
|
|
105
|
+
<label className="flex items-start gap-2 text-[12px] text-ink">
|
|
106
|
+
<input
|
|
107
|
+
type="checkbox"
|
|
108
|
+
checked={enabled[choice.id] === true}
|
|
109
|
+
onChange={(event) => {
|
|
110
|
+
toggle(choice.id, event.target.checked)
|
|
111
|
+
}}
|
|
112
|
+
className="mt-0.5 h-4 w-4 rounded border-border text-accent focus:ring-accent"
|
|
113
|
+
/>
|
|
114
|
+
<span>
|
|
115
|
+
<span className="font-medium">{choice.label}</span>
|
|
116
|
+
<span className="mt-0.5 block text-[11px] text-ink-muted">{choice.description}</span>
|
|
117
|
+
</span>
|
|
118
|
+
</label>
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
<label className="flex items-start gap-2 text-[12px] text-ink">
|
|
123
|
+
<input
|
|
124
|
+
type="checkbox"
|
|
125
|
+
checked={seedSample}
|
|
126
|
+
onChange={(event) => setSeedSample(event.target.checked)}
|
|
127
|
+
className="mt-0.5 h-4 w-4 rounded border-border text-accent focus:ring-accent"
|
|
128
|
+
/>
|
|
129
|
+
<span>
|
|
130
|
+
Fill it with sample data — a few companies, people, and records for the parts you
|
|
131
|
+
turned on, so you are not staring at an empty screen. Clear it out any time.
|
|
132
|
+
</span>
|
|
133
|
+
</label>
|
|
134
|
+
{setEnabled.error !== null && <ErrorPanel error={setEnabled.error} />}
|
|
135
|
+
{installSampleData.error !== null && <ErrorPanel error={installSampleData.error} />}
|
|
136
|
+
<OnboardingNav
|
|
137
|
+
step={3}
|
|
138
|
+
nextLabel="Next"
|
|
139
|
+
nextPendingLabel={installSampleData.isPending ? 'Installing sample data…' : 'Saving…'}
|
|
140
|
+
isPending={isPending}
|
|
141
|
+
/>
|
|
142
|
+
</form>
|
|
143
|
+
</AuthLayout>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { useNavigate, useSearchParams } from 'react-router'
|
|
3
|
+
|
|
4
|
+
import type { OnboardingStep } from '../auth/AuthLayout.tsx'
|
|
5
|
+
import { isOnboardingRerun, ONBOARDING_ORG_PARAM, onboardingPath } from './onboardingRerun.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Previous / Next row every onboarding step shares.
|
|
9
|
+
*
|
|
10
|
+
* Forward still commits on the step that owns the form. This only moves
|
|
11
|
+
* between the five routes. Step 1 has no previous onboarding page, so
|
|
12
|
+
* Previous stays off there. The `rerun` and `org` queries are kept on
|
|
13
|
+
* Previous so a finished workspace that walks the wizard again still skips
|
|
14
|
+
* handbook seeding, and the modules step still sees the organisation type.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const ONBOARDING_PATHS = {
|
|
18
|
+
1: '/onboarding/workspace',
|
|
19
|
+
2: '/onboarding/organisation',
|
|
20
|
+
3: '/onboarding/modules',
|
|
21
|
+
4: '/onboarding/invites',
|
|
22
|
+
5: '/onboarding/handbook',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
const PRIMARY_CLASS =
|
|
26
|
+
'flex-1 rounded-md bg-accent px-3.5 py-2 text-[12px] font-semibold text-accent-fg transition hover:bg-accent-hover disabled:opacity-50'
|
|
27
|
+
|
|
28
|
+
const SECONDARY_CLASS =
|
|
29
|
+
'flex-1 rounded-md border border-border bg-surface px-3.5 py-2 text-[12px] font-medium text-ink-muted transition hover:bg-surface-raised disabled:opacity-50'
|
|
30
|
+
|
|
31
|
+
export interface OnboardingNavProps {
|
|
32
|
+
readonly step: OnboardingStep
|
|
33
|
+
readonly nextLabel: string
|
|
34
|
+
/** Shown while the step's request is in flight. */
|
|
35
|
+
readonly nextPendingLabel?: string
|
|
36
|
+
readonly isPending?: boolean
|
|
37
|
+
/** `submit` inside a form; `button` when the step only navigates. */
|
|
38
|
+
readonly nextType?: 'submit' | 'button'
|
|
39
|
+
readonly onNext?: () => void
|
|
40
|
+
/** A third action under the row, such as Skip on invites. */
|
|
41
|
+
readonly extra?: ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function previousOnboardingPath(
|
|
45
|
+
step: OnboardingStep,
|
|
46
|
+
rerun = false,
|
|
47
|
+
org?: string | null,
|
|
48
|
+
): string | undefined {
|
|
49
|
+
if (step === 1) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return onboardingPath(ONBOARDING_PATHS[(step - 1) as OnboardingStep], rerun, org)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function OnboardingNav({
|
|
57
|
+
step,
|
|
58
|
+
nextLabel,
|
|
59
|
+
nextPendingLabel,
|
|
60
|
+
isPending = false,
|
|
61
|
+
nextType = 'submit',
|
|
62
|
+
onNext,
|
|
63
|
+
extra,
|
|
64
|
+
}: OnboardingNavProps): React.JSX.Element {
|
|
65
|
+
const navigate = useNavigate()
|
|
66
|
+
const [searchParams] = useSearchParams()
|
|
67
|
+
const rerun = isOnboardingRerun(searchParams)
|
|
68
|
+
const previousTo = previousOnboardingPath(step, rerun, searchParams.get(ONBOARDING_ORG_PARAM))
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col gap-2 pt-1">
|
|
72
|
+
<div className="flex gap-2">
|
|
73
|
+
{previousTo !== undefined && (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
disabled={isPending}
|
|
77
|
+
onClick={() => {
|
|
78
|
+
navigate(previousTo, { replace: true })
|
|
79
|
+
}}
|
|
80
|
+
className={SECONDARY_CLASS}
|
|
81
|
+
>
|
|
82
|
+
Previous
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
<button
|
|
86
|
+
type={nextType}
|
|
87
|
+
disabled={isPending}
|
|
88
|
+
onClick={onNext}
|
|
89
|
+
className={PRIMARY_CLASS}
|
|
90
|
+
>
|
|
91
|
+
{isPending && nextPendingLabel !== undefined ? nextPendingLabel : nextLabel}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
{extra}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|