@portaki/module-sections 1.2.7 → 1.3.1
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/package.json +5 -1
- package/portaki.catalog.json +2 -2
- package/portaki.module.json +3 -3
- package/src/components/SectionsGuestView.tsx +3 -7
- package/src/host/SectionCreateModal.tsx +159 -0
- package/src/host/SectionsHostWorkspace.tsx +497 -0
- package/src/host/section-tiptap-presets.ts +156 -0
- package/src/host/sections-editor-design.ts +79 -0
- package/src/host/sections-host.types.ts +53 -0
- package/src/host/sections-toast-copy.ts +9 -0
- package/src/module.test.tsx +19 -4
- package/src/{sections-module-definition.ts → sections-module-definition.tsx} +131 -49
- package/src/test/mock-host-module-context.ts +35 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { SectionsEditorDesign } from './sections-host.types'
|
|
2
|
+
|
|
3
|
+
export const sectionsEditorDesign = {
|
|
4
|
+
id: 'sections-editor-v1',
|
|
5
|
+
moduleId: 'sections',
|
|
6
|
+
layout: 'sections-split-panel',
|
|
7
|
+
gateway: {
|
|
8
|
+
listQuery: 'sections.list',
|
|
9
|
+
saveCommand: 'sections.section.save',
|
|
10
|
+
deleteCommand: 'sections.section.delete',
|
|
11
|
+
reorderCommand: 'sections.reorder',
|
|
12
|
+
},
|
|
13
|
+
createModal: {
|
|
14
|
+
titleFr: 'Nouvelle section',
|
|
15
|
+
subtitleFr: 'Choisissez un modèle pour partir vite, ou créez une section vide.',
|
|
16
|
+
titleFieldLabelFr: 'Titre',
|
|
17
|
+
titleFieldHintFr: 'Visible par le voyageur, en haut de la section.',
|
|
18
|
+
footerHintFr: 'Vous pourrez renommer et réordonner ensuite.',
|
|
19
|
+
cancelLabelFr: 'Annuler',
|
|
20
|
+
submitLabelFr: 'Créer la section',
|
|
21
|
+
defaultTemplateId: 'welcome',
|
|
22
|
+
templates: [
|
|
23
|
+
{
|
|
24
|
+
id: 'welcome',
|
|
25
|
+
icon: '👋',
|
|
26
|
+
titleFr: 'Mot de bienvenue',
|
|
27
|
+
titleEn: 'Welcome message',
|
|
28
|
+
descriptionFr: "Un paragraphe d'accueil personnalisé",
|
|
29
|
+
contentPreset: 'welcome',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'beaches',
|
|
33
|
+
icon: '🏖️',
|
|
34
|
+
titleFr: 'Plages & criques',
|
|
35
|
+
titleEn: 'Beaches & coves',
|
|
36
|
+
descriptionFr: 'Liste de plages avec accès & distance',
|
|
37
|
+
contentPreset: 'beaches',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'restaurants',
|
|
41
|
+
icon: '🍝',
|
|
42
|
+
titleFr: 'Nos restaurants préférés',
|
|
43
|
+
titleEn: 'Our favourite restaurants',
|
|
44
|
+
descriptionFr: 'Recommandations par budget',
|
|
45
|
+
contentPreset: 'restaurants',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'transport',
|
|
49
|
+
icon: '🚂',
|
|
50
|
+
titleFr: 'Comment venir & repartir',
|
|
51
|
+
titleEn: 'Getting here & leaving',
|
|
52
|
+
descriptionFr: 'Train, bus, voiture, taxi',
|
|
53
|
+
contentPreset: 'transport',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'emergencies',
|
|
57
|
+
icon: '🆘',
|
|
58
|
+
titleFr: 'Urgences & numéros utiles',
|
|
59
|
+
titleEn: 'Emergencies & useful numbers',
|
|
60
|
+
descriptionFr: '112, médecin, police',
|
|
61
|
+
contentPreset: 'emergencies',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'empty',
|
|
65
|
+
icon: '📋',
|
|
66
|
+
titleFr: 'Vide · personnalisée',
|
|
67
|
+
titleEn: 'Empty · custom',
|
|
68
|
+
descriptionFr: "Démarrer d'une page blanche",
|
|
69
|
+
contentPreset: 'empty',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
emptyState: {
|
|
74
|
+
titleFr: 'Aucune section pour l’instant',
|
|
75
|
+
descriptionFr:
|
|
76
|
+
'Structurez le carnet d’accueil en blocs (plages, restaurants, consignes…) visibles par vos voyageurs dans le livret.',
|
|
77
|
+
primaryActionLabelFr: 'Créer une section',
|
|
78
|
+
},
|
|
79
|
+
} satisfies SectionsEditorDesign
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const SECTION_CONTENT_PRESET_IDS = [
|
|
2
|
+
'welcome',
|
|
3
|
+
'beaches',
|
|
4
|
+
'restaurants',
|
|
5
|
+
'transport',
|
|
6
|
+
'emergencies',
|
|
7
|
+
'empty',
|
|
8
|
+
] as const
|
|
9
|
+
|
|
10
|
+
export type SectionContentPresetId = (typeof SECTION_CONTENT_PRESET_IDS)[number]
|
|
11
|
+
|
|
12
|
+
export type SectionTemplateId = SectionContentPresetId
|
|
13
|
+
|
|
14
|
+
export type SectionsEditorEmptyStateDesign = {
|
|
15
|
+
titleFr: string
|
|
16
|
+
descriptionFr: string
|
|
17
|
+
primaryActionLabelFr: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SectionCreateModalDesign = {
|
|
21
|
+
titleFr: string
|
|
22
|
+
subtitleFr: string
|
|
23
|
+
titleFieldLabelFr: string
|
|
24
|
+
titleFieldHintFr: string
|
|
25
|
+
footerHintFr: string
|
|
26
|
+
cancelLabelFr: string
|
|
27
|
+
submitLabelFr: string
|
|
28
|
+
defaultTemplateId: SectionTemplateId
|
|
29
|
+
templates: readonly SectionTemplateDesign[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SectionTemplateDesign = {
|
|
33
|
+
id: SectionTemplateId
|
|
34
|
+
icon: string
|
|
35
|
+
titleFr: string
|
|
36
|
+
titleEn: string
|
|
37
|
+
descriptionFr: string
|
|
38
|
+
contentPreset: SectionContentPresetId
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SectionsEditorDesign = {
|
|
42
|
+
id: 'sections-editor-v1'
|
|
43
|
+
moduleId: 'sections'
|
|
44
|
+
layout: 'sections-split-panel'
|
|
45
|
+
gateway: {
|
|
46
|
+
listQuery: 'sections.list'
|
|
47
|
+
saveCommand: 'sections.section.save'
|
|
48
|
+
deleteCommand: 'sections.section.delete'
|
|
49
|
+
reorderCommand: 'sections.reorder'
|
|
50
|
+
}
|
|
51
|
+
createModal: SectionCreateModalDesign
|
|
52
|
+
emptyState: SectionsEditorEmptyStateDesign
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const sectionsToastCopy = {
|
|
2
|
+
loadError: 'Impossible de charger les sections.',
|
|
3
|
+
saveError: 'Enregistrement impossible.',
|
|
4
|
+
reorderError: 'Réordonnancement impossible.',
|
|
5
|
+
createSuccess: 'Section créée.',
|
|
6
|
+
createError: 'Création impossible.',
|
|
7
|
+
deleteSuccess: 'Section supprimée.',
|
|
8
|
+
deleteError: 'Suppression impossible.',
|
|
9
|
+
} as const
|
package/src/module.test.tsx
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { waitFor } from '@testing-library/react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assertGuestSurface,
|
|
5
|
+
renderGuestModule,
|
|
6
|
+
renderHostModule,
|
|
7
|
+
} from '@portaki/sdk-test-support'
|
|
4
8
|
|
|
5
9
|
import moduleDef from './index'
|
|
10
|
+
import { createSectionsTestHostContext } from './test/mock-host-module-context'
|
|
6
11
|
|
|
7
12
|
describe('@portaki/module-sections', () => {
|
|
8
13
|
it('exposes a valid guest module definition', () => {
|
|
9
14
|
assertGuestSurface(moduleDef)
|
|
10
15
|
})
|
|
11
16
|
|
|
12
|
-
it('renders without crashing', async () => {
|
|
13
|
-
const view =
|
|
14
|
-
|
|
17
|
+
it('renders guest surface without crashing', async () => {
|
|
18
|
+
const view = renderGuestModule(moduleDef)
|
|
19
|
+
await waitFor(() => {
|
|
20
|
+
expect(view.container).toBeTruthy()
|
|
21
|
+
})
|
|
22
|
+
view.unmount()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders host surface without crashing', async () => {
|
|
26
|
+
if (moduleDef.renderHost == null) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const view = renderHostModule(moduleDef, createSectionsTestHostContext())
|
|
15
30
|
await waitFor(() => {
|
|
16
31
|
expect(view.container).toBeTruthy()
|
|
17
32
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineModule,
|
|
3
|
-
index,
|
|
4
3
|
int,
|
|
5
4
|
jsonb,
|
|
6
5
|
moduleSchema,
|
|
@@ -11,11 +10,14 @@ import {
|
|
|
11
10
|
timestamptz,
|
|
12
11
|
uuid,
|
|
13
12
|
uuidPrimaryKey,
|
|
13
|
+
type HostModuleContext,
|
|
14
14
|
type ModuleContext,
|
|
15
15
|
type PortakiFullModule,
|
|
16
16
|
} from '@portaki/sdk'
|
|
17
17
|
import type { ReactNode } from 'react'
|
|
18
18
|
|
|
19
|
+
import { SectionsHostWorkspace } from './host/SectionsHostWorkspace'
|
|
20
|
+
|
|
19
21
|
const sectionsSchema = moduleSchema([
|
|
20
22
|
table('items', 't_e_module_sections_item', {
|
|
21
23
|
columns: [
|
|
@@ -23,17 +25,28 @@ const sectionsSchema = moduleSchema([
|
|
|
23
25
|
uuid('propertyId'),
|
|
24
26
|
workspaceId(),
|
|
25
27
|
int('sortOrder', { defaultSql: '0' }),
|
|
26
|
-
text('titleFr', { defaultSql: "''" }),
|
|
27
|
-
text('titleEn', { defaultSql: "''" }),
|
|
28
|
-
jsonb('contentFr', { nullable: true }),
|
|
29
|
-
jsonb('contentEn', { nullable: true }),
|
|
30
28
|
timestamptz('createdAt', { nullable: false, defaultSql: 'now()' }),
|
|
31
29
|
timestamptz('updatedAt', { nullable: false, defaultSql: 'now()' }),
|
|
32
30
|
],
|
|
33
31
|
indexes: [workspacePropertyIndex()],
|
|
34
32
|
}),
|
|
33
|
+
table('itemLocales', 't_e_module_sections_item_locale', {
|
|
34
|
+
columns: [
|
|
35
|
+
uuid('sectionId', { primaryKey: true }),
|
|
36
|
+
text('lang', { primaryKey: true }),
|
|
37
|
+
workspaceId(),
|
|
38
|
+
uuid('propertyId'),
|
|
39
|
+
text('title', { defaultSql: "''" }),
|
|
40
|
+
jsonb('content', { nullable: true }),
|
|
41
|
+
],
|
|
42
|
+
indexes: [workspacePropertyIndex()],
|
|
43
|
+
}),
|
|
35
44
|
])
|
|
36
45
|
|
|
46
|
+
const SECTION_LANGS = ['fr', 'en'] as const
|
|
47
|
+
|
|
48
|
+
type SectionLang = (typeof SECTION_LANGS)[number]
|
|
49
|
+
|
|
37
50
|
function stringParam(params: Record<string, unknown>, key: string): string | null {
|
|
38
51
|
const value = params[key]
|
|
39
52
|
if (value == null) {
|
|
@@ -53,6 +66,75 @@ function intParam(params: Record<string, unknown>, key: string, defaultValue: nu
|
|
|
53
66
|
return Number.parseInt(String(value), 10)
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
function localeTitleForLang(
|
|
70
|
+
lang: SectionLang,
|
|
71
|
+
titleFr: string,
|
|
72
|
+
titleEn: string,
|
|
73
|
+
): string {
|
|
74
|
+
if (lang === 'fr') {
|
|
75
|
+
return titleFr
|
|
76
|
+
}
|
|
77
|
+
return titleEn.trim() === '' ? titleFr : titleEn
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function localeContentForLang(
|
|
81
|
+
lang: SectionLang,
|
|
82
|
+
contentFr: unknown,
|
|
83
|
+
contentEn: unknown,
|
|
84
|
+
): unknown {
|
|
85
|
+
if (lang === 'fr') {
|
|
86
|
+
return contentFr ?? null
|
|
87
|
+
}
|
|
88
|
+
return contentEn ?? null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function upsertSectionLocale(
|
|
92
|
+
ctx: ModuleContext,
|
|
93
|
+
filters: { workspaceId: string; propertyId: string },
|
|
94
|
+
sectionId: string,
|
|
95
|
+
lang: SectionLang,
|
|
96
|
+
title: string,
|
|
97
|
+
content: unknown,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const where = { ...filters, sectionId, lang }
|
|
100
|
+
const existing = await ctx.db.from('itemLocales').where(where).one()
|
|
101
|
+
const payload = { title, content }
|
|
102
|
+
if (existing != null) {
|
|
103
|
+
await ctx.db.from('itemLocales').where(where).update(payload)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
await ctx.db.from('itemLocales').insert({
|
|
107
|
+
...where,
|
|
108
|
+
...payload,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sectionDtoFromLocales(
|
|
113
|
+
item: Record<string, unknown>,
|
|
114
|
+
locales: Record<string, unknown>[],
|
|
115
|
+
): Record<string, unknown> {
|
|
116
|
+
const byLang = new Map<string, Record<string, unknown>>()
|
|
117
|
+
for (const row of locales) {
|
|
118
|
+
byLang.set(String(row.lang), row)
|
|
119
|
+
}
|
|
120
|
+
const fr = byLang.get('fr')
|
|
121
|
+
const en = byLang.get('en')
|
|
122
|
+
const titleFr = String(fr?.title ?? '')
|
|
123
|
+
const titleEnRaw = String(en?.title ?? '')
|
|
124
|
+
const section: Record<string, unknown> = {
|
|
125
|
+
id: item.id,
|
|
126
|
+
sortOrder: item.sortOrder,
|
|
127
|
+
titleFr,
|
|
128
|
+
titleEn: titleEnRaw.trim() === '' ? titleFr : titleEnRaw,
|
|
129
|
+
contentFr: fr?.content ?? null,
|
|
130
|
+
contentEn: en?.content ?? null,
|
|
131
|
+
}
|
|
132
|
+
if (item.updatedAt != null) {
|
|
133
|
+
section.updatedAt = item.updatedAt
|
|
134
|
+
}
|
|
135
|
+
return section
|
|
136
|
+
}
|
|
137
|
+
|
|
56
138
|
export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode): PortakiFullModule {
|
|
57
139
|
return defineModule({
|
|
58
140
|
id: 'sections',
|
|
@@ -62,8 +144,8 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
62
144
|
en: 'Welcome book editorial content.',
|
|
63
145
|
},
|
|
64
146
|
icon: 'file-text',
|
|
65
|
-
version: '1.2.
|
|
66
|
-
schemaVersion: '1.0.
|
|
147
|
+
version: '1.2.7',
|
|
148
|
+
schemaVersion: '1.0.2',
|
|
67
149
|
schema: sectionsSchema,
|
|
68
150
|
navSlot: 'section',
|
|
69
151
|
|
|
@@ -75,10 +157,16 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
75
157
|
en: 'Property editorial sections.',
|
|
76
158
|
},
|
|
77
159
|
async handler(ctx) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
160
|
+
const filters = { workspaceId: ctx.workspaceId, propertyId: ctx.propertyId }
|
|
161
|
+
const rows = await ctx.db.from('items').where(filters).many()
|
|
162
|
+
const locales = await ctx.db.from('itemLocales').where(filters).many()
|
|
163
|
+
const localesBySection = new Map<string, Record<string, unknown>[]>()
|
|
164
|
+
for (const locale of locales) {
|
|
165
|
+
const sectionId = String(locale.sectionId)
|
|
166
|
+
const bucket = localesBySection.get(sectionId) ?? []
|
|
167
|
+
bucket.push(locale)
|
|
168
|
+
localesBySection.set(sectionId, bucket)
|
|
169
|
+
}
|
|
82
170
|
rows.sort((a, b) => {
|
|
83
171
|
const orderA = Number(a.sortOrder ?? 0)
|
|
84
172
|
const orderB = Number(b.sortOrder ?? 0)
|
|
@@ -89,21 +177,9 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
89
177
|
const createdB = String(b.createdAt ?? '')
|
|
90
178
|
return createdA.localeCompare(createdB)
|
|
91
179
|
})
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
sortOrder: row.sortOrder,
|
|
96
|
-
titleFr: row.titleFr,
|
|
97
|
-
titleEn: row.titleEn,
|
|
98
|
-
contentFr: row.contentFr ?? null,
|
|
99
|
-
contentEn: row.contentEn ?? null,
|
|
100
|
-
}
|
|
101
|
-
if (row.updatedAt != null) {
|
|
102
|
-
section.updatedAt = row.updatedAt
|
|
103
|
-
}
|
|
104
|
-
return section
|
|
105
|
-
})
|
|
106
|
-
return { sections }
|
|
180
|
+
return rows.map((row) =>
|
|
181
|
+
sectionDtoFromLocales(row, localesBySection.get(String(row.id)) ?? []),
|
|
182
|
+
)
|
|
107
183
|
},
|
|
108
184
|
},
|
|
109
185
|
},
|
|
@@ -146,24 +222,30 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
146
222
|
sortOrder = max + 1
|
|
147
223
|
}
|
|
148
224
|
const now = new Date().toISOString()
|
|
149
|
-
const
|
|
150
|
-
titleFr,
|
|
151
|
-
titleEn,
|
|
152
|
-
contentFr,
|
|
153
|
-
contentEn,
|
|
225
|
+
const itemPayload = {
|
|
154
226
|
sortOrder,
|
|
155
227
|
updatedAt: now,
|
|
156
228
|
}
|
|
157
229
|
if (existing != null) {
|
|
158
|
-
await ctx.db.from('items').where({ ...filters, id }).update(
|
|
159
|
-
|
|
230
|
+
await ctx.db.from('items').where({ ...filters, id }).update(itemPayload)
|
|
231
|
+
} else {
|
|
232
|
+
await ctx.db.from('items').insert({
|
|
233
|
+
id,
|
|
234
|
+
...filters,
|
|
235
|
+
...itemPayload,
|
|
236
|
+
createdAt: now,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
for (const lang of SECTION_LANGS) {
|
|
240
|
+
await upsertSectionLocale(
|
|
241
|
+
ctx,
|
|
242
|
+
filters,
|
|
243
|
+
id,
|
|
244
|
+
lang,
|
|
245
|
+
localeTitleForLang(lang, titleFr, titleEn),
|
|
246
|
+
localeContentForLang(lang, contentFr, contentEn),
|
|
247
|
+
)
|
|
160
248
|
}
|
|
161
|
-
await ctx.db.from('items').insert({
|
|
162
|
-
id,
|
|
163
|
-
...filters,
|
|
164
|
-
...payload,
|
|
165
|
-
createdAt: now,
|
|
166
|
-
})
|
|
167
249
|
},
|
|
168
250
|
},
|
|
169
251
|
'sections.section.delete': {
|
|
@@ -177,14 +259,13 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
177
259
|
if (idRaw == null || idRaw.trim() === '') {
|
|
178
260
|
throw new Error('id_required')
|
|
179
261
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.delete()
|
|
262
|
+
const id = idRaw.trim()
|
|
263
|
+
const filters = {
|
|
264
|
+
id,
|
|
265
|
+
workspaceId: ctx.workspaceId,
|
|
266
|
+
propertyId: ctx.propertyId,
|
|
267
|
+
}
|
|
268
|
+
const deleted = await ctx.db.from('items').where(filters).delete()
|
|
188
269
|
if (deleted === 0) {
|
|
189
270
|
throw new Error('section_not_found')
|
|
190
271
|
}
|
|
@@ -208,10 +289,10 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
208
289
|
if (item == null) {
|
|
209
290
|
continue
|
|
210
291
|
}
|
|
211
|
-
const
|
|
292
|
+
const sectionId = String(item)
|
|
212
293
|
await ctx.db
|
|
213
294
|
.from('items')
|
|
214
|
-
.where({ ...filters, id })
|
|
295
|
+
.where({ ...filters, id: sectionId })
|
|
215
296
|
.update({ sortOrder: order++, updatedAt: now })
|
|
216
297
|
}
|
|
217
298
|
},
|
|
@@ -219,5 +300,6 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
219
300
|
},
|
|
220
301
|
|
|
221
302
|
render,
|
|
303
|
+
renderHost: (ctx: HostModuleContext) => <SectionsHostWorkspace ctx={ctx} />,
|
|
222
304
|
})
|
|
223
305
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HostModuleContext, HostModuleShellComponents } from '@portaki/sdk'
|
|
2
|
+
import { createMockHostModuleContext } from '@portaki/sdk-test-support'
|
|
3
|
+
|
|
4
|
+
/** Stubs until published `@portaki/sdk-test-support` includes `shell` / `gateway`. */
|
|
5
|
+
export const mockHostShell: HostModuleShellComponents = {
|
|
6
|
+
NotionLikeEditor: () => null,
|
|
7
|
+
HostEmptyPlaceholder: () => null,
|
|
8
|
+
HostUnderlineTabs: () => null,
|
|
9
|
+
Button: () => null,
|
|
10
|
+
Input: () => null,
|
|
11
|
+
Dialog: ({ children }) => children ?? null,
|
|
12
|
+
DialogContent: ({ children }) => children ?? null,
|
|
13
|
+
DialogHeader: ({ children }) => children ?? null,
|
|
14
|
+
DialogTitle: ({ children }) => children ?? null,
|
|
15
|
+
DialogDescription: ({ children }) => children ?? null,
|
|
16
|
+
DialogFooter: ({ children }) => children ?? null,
|
|
17
|
+
cn: (...parts) => parts.filter(Boolean).join(' '),
|
|
18
|
+
toast: { success: () => {}, error: () => {} },
|
|
19
|
+
parseTipTapDoc: () => ({ type: 'doc', content: [{ type: 'paragraph' }] }),
|
|
20
|
+
serializeTipTapDoc: (doc) => JSON.stringify(doc),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createSectionsTestHostContext(
|
|
24
|
+
overrides: Partial<HostModuleContext> = {},
|
|
25
|
+
): HostModuleContext {
|
|
26
|
+
const base = createMockHostModuleContext(overrides)
|
|
27
|
+
return {
|
|
28
|
+
...base,
|
|
29
|
+
gateway: overrides.gateway ?? base.gateway ?? {
|
|
30
|
+
query: async <T,>() => [] as T,
|
|
31
|
+
command: async () => {},
|
|
32
|
+
},
|
|
33
|
+
shell: overrides.shell ?? base.shell ?? mockHostShell,
|
|
34
|
+
}
|
|
35
|
+
}
|