@portaki/module-sections 1.2.6 → 1.3.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/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portaki/module-sections",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Portaki module — editorial sections (TipTap)",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.tsx",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@portaki/sdk": "^
|
|
9
|
+
"@portaki/sdk": "^3.0.0",
|
|
10
10
|
"@tiptap/core": "^3.0.0",
|
|
11
11
|
"@tiptap/html": "^3.0.0",
|
|
12
12
|
"@tiptap/starter-kit": "^3.0.0",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"react-dom": "^19.1.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@portaki/cli": "^0.
|
|
18
|
-
"@portaki/sdk-test-support": "^
|
|
17
|
+
"@portaki/cli": "^1.0.0",
|
|
18
|
+
"@portaki/sdk-test-support": "^3.0.0",
|
|
19
19
|
"@testing-library/jest-dom": "^6.6.3",
|
|
20
20
|
"@testing-library/react": "^16.3.0",
|
|
21
21
|
"@types/react": "^19",
|
package/portaki.catalog.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
|
|
10
10
|
"en": "Rich content blocks (TipTap) for the guest welcome book."
|
|
11
11
|
},
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.3.0",
|
|
13
13
|
"releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
|
|
14
14
|
"changelog": [
|
|
15
15
|
{
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"sections"
|
|
43
43
|
],
|
|
44
44
|
"portakiVersionMin": "1.0.0",
|
|
45
|
-
"requiresHostSdk": "
|
|
45
|
+
"requiresHostSdk": "3.0.0",
|
|
46
46
|
"database": {
|
|
47
47
|
"schemaVersion": "1.0.1"
|
|
48
48
|
},
|
|
@@ -122,6 +122,6 @@
|
|
|
122
122
|
},
|
|
123
123
|
"artifacts": {
|
|
124
124
|
"wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
|
|
125
|
-
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.
|
|
125
|
+
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.3.0"
|
|
126
126
|
}
|
|
127
127
|
}
|
package/portaki.module.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
|
|
10
10
|
"en": "Rich content blocks (TipTap) for the guest welcome book."
|
|
11
11
|
},
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.3.0",
|
|
13
13
|
"releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
|
|
14
14
|
"changelog": [
|
|
15
15
|
{
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"sections"
|
|
43
43
|
],
|
|
44
44
|
"portakiVersionMin": "1.0.0",
|
|
45
|
-
"requiresHostSdk": "
|
|
45
|
+
"requiresHostSdk": "3.0.0",
|
|
46
46
|
"database": {
|
|
47
|
-
"schemaVersion": "1.0.
|
|
47
|
+
"schemaVersion": "1.0.2"
|
|
48
48
|
},
|
|
49
49
|
"license": "AGPL-3.0",
|
|
50
50
|
"repository": "https://github.com/PortakiApp/portaki-modules",
|
|
@@ -122,6 +122,6 @@
|
|
|
122
122
|
},
|
|
123
123
|
"artifacts": {
|
|
124
124
|
"wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
|
|
125
|
-
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.
|
|
125
|
+
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.3.0"
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -16,10 +16,6 @@ type SectionRow = {
|
|
|
16
16
|
contentEn: unknown
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type ListResponse = {
|
|
20
|
-
sections: SectionRow[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
const htmlExtensions = [StarterKit]
|
|
24
20
|
|
|
25
21
|
function contentToHtml(raw: unknown): string {
|
|
@@ -33,7 +29,7 @@ function contentToHtml(raw: unknown): string {
|
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
export function SectionsGuestView({ lang }: { lang: LangCode }) {
|
|
36
|
-
const { data, loading, error } = usePortakiQuery<
|
|
32
|
+
const { data, loading, error } = usePortakiQuery<SectionRow[]>('sections.list', {})
|
|
37
33
|
|
|
38
34
|
if (loading) {
|
|
39
35
|
return (
|
|
@@ -43,13 +39,13 @@ export function SectionsGuestView({ lang }: { lang: LangCode }) {
|
|
|
43
39
|
)
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
if (error != null || !data?.
|
|
42
|
+
if (error != null || !data?.length) {
|
|
47
43
|
return null
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
return (
|
|
51
47
|
<div className="space-y-10">
|
|
52
|
-
{data.
|
|
48
|
+
{data.map((section) => {
|
|
53
49
|
const title = lang === 'en' ? section.titleEn || section.titleFr : section.titleFr
|
|
54
50
|
const html = contentToHtml(lang === 'en' ? section.contentEn : section.contentFr)
|
|
55
51
|
return (
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineModule,
|
|
3
|
-
index,
|
|
4
3
|
int,
|
|
5
4
|
jsonb,
|
|
6
5
|
moduleSchema,
|
|
7
6
|
table,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
workspaceId,
|
|
8
|
+
workspacePropertyIndex,
|
|
10
9
|
text,
|
|
11
10
|
timestamptz,
|
|
12
11
|
uuid,
|
|
@@ -21,19 +20,30 @@ const sectionsSchema = moduleSchema([
|
|
|
21
20
|
columns: [
|
|
22
21
|
uuidPrimaryKey(),
|
|
23
22
|
uuid('propertyId'),
|
|
24
|
-
|
|
23
|
+
workspaceId(),
|
|
25
24
|
int('sortOrder', { defaultSql: '0' }),
|
|
26
|
-
text('titleFr', { defaultSql: "''" }),
|
|
27
|
-
text('titleEn', { defaultSql: "''" }),
|
|
28
|
-
jsonb('contentFr', { nullable: true }),
|
|
29
|
-
jsonb('contentEn', { nullable: true }),
|
|
30
25
|
timestamptz('createdAt', { nullable: false, defaultSql: 'now()' }),
|
|
31
26
|
timestamptz('updatedAt', { nullable: false, defaultSql: 'now()' }),
|
|
32
27
|
],
|
|
33
|
-
indexes: [
|
|
28
|
+
indexes: [workspacePropertyIndex()],
|
|
29
|
+
}),
|
|
30
|
+
table('itemLocales', 't_e_module_sections_item_locale', {
|
|
31
|
+
columns: [
|
|
32
|
+
uuid('sectionId', { primaryKey: true }),
|
|
33
|
+
text('lang', { primaryKey: true }),
|
|
34
|
+
workspaceId(),
|
|
35
|
+
uuid('propertyId'),
|
|
36
|
+
text('title', { defaultSql: "''" }),
|
|
37
|
+
jsonb('content', { nullable: true }),
|
|
38
|
+
],
|
|
39
|
+
indexes: [workspacePropertyIndex()],
|
|
34
40
|
}),
|
|
35
41
|
])
|
|
36
42
|
|
|
43
|
+
const SECTION_LANGS = ['fr', 'en'] as const
|
|
44
|
+
|
|
45
|
+
type SectionLang = (typeof SECTION_LANGS)[number]
|
|
46
|
+
|
|
37
47
|
function stringParam(params: Record<string, unknown>, key: string): string | null {
|
|
38
48
|
const value = params[key]
|
|
39
49
|
if (value == null) {
|
|
@@ -53,6 +63,75 @@ function intParam(params: Record<string, unknown>, key: string, defaultValue: nu
|
|
|
53
63
|
return Number.parseInt(String(value), 10)
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
function localeTitleForLang(
|
|
67
|
+
lang: SectionLang,
|
|
68
|
+
titleFr: string,
|
|
69
|
+
titleEn: string,
|
|
70
|
+
): string {
|
|
71
|
+
if (lang === 'fr') {
|
|
72
|
+
return titleFr
|
|
73
|
+
}
|
|
74
|
+
return titleEn.trim() === '' ? titleFr : titleEn
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function localeContentForLang(
|
|
78
|
+
lang: SectionLang,
|
|
79
|
+
contentFr: unknown,
|
|
80
|
+
contentEn: unknown,
|
|
81
|
+
): unknown {
|
|
82
|
+
if (lang === 'fr') {
|
|
83
|
+
return contentFr ?? null
|
|
84
|
+
}
|
|
85
|
+
return contentEn ?? null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function upsertSectionLocale(
|
|
89
|
+
ctx: ModuleContext,
|
|
90
|
+
filters: { workspaceId: string; propertyId: string },
|
|
91
|
+
sectionId: string,
|
|
92
|
+
lang: SectionLang,
|
|
93
|
+
title: string,
|
|
94
|
+
content: unknown,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const where = { ...filters, sectionId, lang }
|
|
97
|
+
const existing = await ctx.db.from('itemLocales').where(where).one()
|
|
98
|
+
const payload = { title, content }
|
|
99
|
+
if (existing != null) {
|
|
100
|
+
await ctx.db.from('itemLocales').where(where).update(payload)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
await ctx.db.from('itemLocales').insert({
|
|
104
|
+
...where,
|
|
105
|
+
...payload,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sectionDtoFromLocales(
|
|
110
|
+
item: Record<string, unknown>,
|
|
111
|
+
locales: Record<string, unknown>[],
|
|
112
|
+
): Record<string, unknown> {
|
|
113
|
+
const byLang = new Map<string, Record<string, unknown>>()
|
|
114
|
+
for (const row of locales) {
|
|
115
|
+
byLang.set(String(row.lang), row)
|
|
116
|
+
}
|
|
117
|
+
const fr = byLang.get('fr')
|
|
118
|
+
const en = byLang.get('en')
|
|
119
|
+
const titleFr = String(fr?.title ?? '')
|
|
120
|
+
const titleEnRaw = String(en?.title ?? '')
|
|
121
|
+
const section: Record<string, unknown> = {
|
|
122
|
+
id: item.id,
|
|
123
|
+
sortOrder: item.sortOrder,
|
|
124
|
+
titleFr,
|
|
125
|
+
titleEn: titleEnRaw.trim() === '' ? titleFr : titleEnRaw,
|
|
126
|
+
contentFr: fr?.content ?? null,
|
|
127
|
+
contentEn: en?.content ?? null,
|
|
128
|
+
}
|
|
129
|
+
if (item.updatedAt != null) {
|
|
130
|
+
section.updatedAt = item.updatedAt
|
|
131
|
+
}
|
|
132
|
+
return section
|
|
133
|
+
}
|
|
134
|
+
|
|
56
135
|
export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode): PortakiFullModule {
|
|
57
136
|
return defineModule({
|
|
58
137
|
id: 'sections',
|
|
@@ -62,8 +141,8 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
62
141
|
en: 'Welcome book editorial content.',
|
|
63
142
|
},
|
|
64
143
|
icon: 'file-text',
|
|
65
|
-
version: '1.2.
|
|
66
|
-
schemaVersion: '1.0.
|
|
144
|
+
version: '1.2.7',
|
|
145
|
+
schemaVersion: '1.0.2',
|
|
67
146
|
schema: sectionsSchema,
|
|
68
147
|
navSlot: 'section',
|
|
69
148
|
|
|
@@ -75,10 +154,16 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
75
154
|
en: 'Property editorial sections.',
|
|
76
155
|
},
|
|
77
156
|
async handler(ctx) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
157
|
+
const filters = { workspaceId: ctx.workspaceId, propertyId: ctx.propertyId }
|
|
158
|
+
const rows = await ctx.db.from('items').where(filters).many()
|
|
159
|
+
const locales = await ctx.db.from('itemLocales').where(filters).many()
|
|
160
|
+
const localesBySection = new Map<string, Record<string, unknown>[]>()
|
|
161
|
+
for (const locale of locales) {
|
|
162
|
+
const sectionId = String(locale.sectionId)
|
|
163
|
+
const bucket = localesBySection.get(sectionId) ?? []
|
|
164
|
+
bucket.push(locale)
|
|
165
|
+
localesBySection.set(sectionId, bucket)
|
|
166
|
+
}
|
|
82
167
|
rows.sort((a, b) => {
|
|
83
168
|
const orderA = Number(a.sortOrder ?? 0)
|
|
84
169
|
const orderB = Number(b.sortOrder ?? 0)
|
|
@@ -89,21 +174,9 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
89
174
|
const createdB = String(b.createdAt ?? '')
|
|
90
175
|
return createdA.localeCompare(createdB)
|
|
91
176
|
})
|
|
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 }
|
|
177
|
+
return rows.map((row) =>
|
|
178
|
+
sectionDtoFromLocales(row, localesBySection.get(String(row.id)) ?? []),
|
|
179
|
+
)
|
|
107
180
|
},
|
|
108
181
|
},
|
|
109
182
|
},
|
|
@@ -116,7 +189,7 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
116
189
|
en: 'Create or update a section.',
|
|
117
190
|
},
|
|
118
191
|
async handler(ctx, params) {
|
|
119
|
-
const filters = {
|
|
192
|
+
const filters = { workspaceId: ctx.workspaceId, propertyId: ctx.propertyId }
|
|
120
193
|
const idRaw = stringParam(params, 'id')
|
|
121
194
|
const id =
|
|
122
195
|
idRaw == null || idRaw.trim() === '' ? crypto.randomUUID() : idRaw.trim()
|
|
@@ -146,24 +219,30 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
146
219
|
sortOrder = max + 1
|
|
147
220
|
}
|
|
148
221
|
const now = new Date().toISOString()
|
|
149
|
-
const
|
|
150
|
-
titleFr,
|
|
151
|
-
titleEn,
|
|
152
|
-
contentFr,
|
|
153
|
-
contentEn,
|
|
222
|
+
const itemPayload = {
|
|
154
223
|
sortOrder,
|
|
155
224
|
updatedAt: now,
|
|
156
225
|
}
|
|
157
226
|
if (existing != null) {
|
|
158
|
-
await ctx.db.from('items').where({ ...filters, id }).update(
|
|
159
|
-
|
|
227
|
+
await ctx.db.from('items').where({ ...filters, id }).update(itemPayload)
|
|
228
|
+
} else {
|
|
229
|
+
await ctx.db.from('items').insert({
|
|
230
|
+
id,
|
|
231
|
+
...filters,
|
|
232
|
+
...itemPayload,
|
|
233
|
+
createdAt: now,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
for (const lang of SECTION_LANGS) {
|
|
237
|
+
await upsertSectionLocale(
|
|
238
|
+
ctx,
|
|
239
|
+
filters,
|
|
240
|
+
id,
|
|
241
|
+
lang,
|
|
242
|
+
localeTitleForLang(lang, titleFr, titleEn),
|
|
243
|
+
localeContentForLang(lang, contentFr, contentEn),
|
|
244
|
+
)
|
|
160
245
|
}
|
|
161
|
-
await ctx.db.from('items').insert({
|
|
162
|
-
id,
|
|
163
|
-
...filters,
|
|
164
|
-
...payload,
|
|
165
|
-
createdAt: now,
|
|
166
|
-
})
|
|
167
246
|
},
|
|
168
247
|
},
|
|
169
248
|
'sections.section.delete': {
|
|
@@ -177,14 +256,13 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
177
256
|
if (idRaw == null || idRaw.trim() === '') {
|
|
178
257
|
throw new Error('id_required')
|
|
179
258
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.delete()
|
|
259
|
+
const id = idRaw.trim()
|
|
260
|
+
const filters = {
|
|
261
|
+
id,
|
|
262
|
+
workspaceId: ctx.workspaceId,
|
|
263
|
+
propertyId: ctx.propertyId,
|
|
264
|
+
}
|
|
265
|
+
const deleted = await ctx.db.from('items').where(filters).delete()
|
|
188
266
|
if (deleted === 0) {
|
|
189
267
|
throw new Error('section_not_found')
|
|
190
268
|
}
|
|
@@ -201,17 +279,17 @@ export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode):
|
|
|
201
279
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
202
280
|
throw new Error('ordered_ids_required')
|
|
203
281
|
}
|
|
204
|
-
const filters = {
|
|
282
|
+
const filters = { workspaceId: ctx.workspaceId, propertyId: ctx.propertyId }
|
|
205
283
|
const now = new Date().toISOString()
|
|
206
284
|
let order = 0
|
|
207
285
|
for (const item of raw) {
|
|
208
286
|
if (item == null) {
|
|
209
287
|
continue
|
|
210
288
|
}
|
|
211
|
-
const
|
|
289
|
+
const sectionId = String(item)
|
|
212
290
|
await ctx.db
|
|
213
291
|
.from('items')
|
|
214
|
-
.where({ ...filters, id })
|
|
292
|
+
.where({ ...filters, id: sectionId })
|
|
215
293
|
.update({ sortOrder: order++, updatedAt: now })
|
|
216
294
|
}
|
|
217
295
|
},
|