@portaki/module-sections 1.0.1 → 1.2.3
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 +16 -8
- package/portaki.catalog.json +124 -0
- package/portaki.module.json +53 -17
- package/src/components/SectionsGuestView.tsx +2 -2
- package/src/index.tsx +1 -17
- package/src/manifest.test.ts +8 -0
- package/src/module.test.tsx +20 -0
- package/src/portaki.gateway.tsx +3 -0
- package/src/portaki.module.tsx +4 -0
- package/src/sections-module-definition.ts +223 -0
- package/vitest.config.ts +3 -0
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portaki/module-sections",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.3",
|
|
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
|
-
"scripts": {
|
|
9
|
-
"lint": "echo \"no lint yet\""
|
|
10
|
-
},
|
|
11
8
|
"dependencies": {
|
|
12
|
-
"@portaki/
|
|
13
|
-
"@portaki/sdk": "^0.5.0",
|
|
9
|
+
"@portaki/sdk": "^2.0.4",
|
|
14
10
|
"@tiptap/core": "^3.0.0",
|
|
15
11
|
"@tiptap/html": "^3.0.0",
|
|
16
12
|
"@tiptap/starter-kit": "^3.0.0",
|
|
@@ -18,8 +14,14 @@
|
|
|
18
14
|
"react-dom": "^19.1.0"
|
|
19
15
|
},
|
|
20
16
|
"devDependencies": {
|
|
17
|
+
"@portaki/cli": "^0.1.2",
|
|
18
|
+
"@portaki/sdk-test-support": "^2.0.4",
|
|
19
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
20
|
+
"@testing-library/react": "^16.3.0",
|
|
21
21
|
"@types/react": "^19",
|
|
22
|
-
"
|
|
22
|
+
"jsdom": "^26.0.0",
|
|
23
|
+
"typescript": "^5",
|
|
24
|
+
"vitest": "^3.0.5"
|
|
23
25
|
},
|
|
24
26
|
"repository": {
|
|
25
27
|
"type": "git",
|
|
@@ -28,5 +30,11 @@
|
|
|
28
30
|
},
|
|
29
31
|
"publishConfig": {
|
|
30
32
|
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "portaki build --entry src/portaki.gateway.tsx",
|
|
36
|
+
"lint": "echo \"no lint yet\"",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest"
|
|
31
39
|
}
|
|
32
|
-
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/PortakiApp/portaki-sdk/main/schema/module.v1.json",
|
|
3
|
+
"id": "sections",
|
|
4
|
+
"name": {
|
|
5
|
+
"fr": "Sections éditoriales",
|
|
6
|
+
"en": "Editorial sections"
|
|
7
|
+
},
|
|
8
|
+
"description": {
|
|
9
|
+
"fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
|
|
10
|
+
"en": "Rich content blocks (TipTap) for the guest welcome book."
|
|
11
|
+
},
|
|
12
|
+
"version": "1.2.3",
|
|
13
|
+
"releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
|
|
14
|
+
"changelog": [
|
|
15
|
+
{
|
|
16
|
+
"version": "1.2.0",
|
|
17
|
+
"date": "2026-05-18",
|
|
18
|
+
"notes": {
|
|
19
|
+
"fr": "Release catalogue modules 1.2.0.",
|
|
20
|
+
"en": "Module catalog release 1.2.0."
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"date": "2026-05-16",
|
|
26
|
+
"notes": {
|
|
27
|
+
"fr": "Première version : sections TipTap, gateway hôte et invité.",
|
|
28
|
+
"en": "Initial release: TipTap sections, host and guest gateway."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"author": {
|
|
33
|
+
"name": "Portaki",
|
|
34
|
+
"url": "https://portaki.app",
|
|
35
|
+
"type": "official"
|
|
36
|
+
},
|
|
37
|
+
"icon": "file-text",
|
|
38
|
+
"type": "official",
|
|
39
|
+
"tags": [
|
|
40
|
+
"editorial",
|
|
41
|
+
"content",
|
|
42
|
+
"sections"
|
|
43
|
+
],
|
|
44
|
+
"portakiVersionMin": "1.0.0",
|
|
45
|
+
"requiresHostSdk": "2.0.0",
|
|
46
|
+
"database": {
|
|
47
|
+
"schemaVersion": "1.0.1"
|
|
48
|
+
},
|
|
49
|
+
"license": "AGPL-3.0",
|
|
50
|
+
"repository": "https://github.com/PortakiApp/portaki-modules",
|
|
51
|
+
"scopes": [
|
|
52
|
+
"stay:read",
|
|
53
|
+
"property:read",
|
|
54
|
+
"host:property:read",
|
|
55
|
+
"host:property:write"
|
|
56
|
+
],
|
|
57
|
+
"queries": [
|
|
58
|
+
{
|
|
59
|
+
"name": "sections.list",
|
|
60
|
+
"description": {
|
|
61
|
+
"fr": "Liste des sections du logement.",
|
|
62
|
+
"en": "Property editorial sections."
|
|
63
|
+
},
|
|
64
|
+
"scope": "property:read"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"commands": [
|
|
68
|
+
{
|
|
69
|
+
"name": "sections.section.save",
|
|
70
|
+
"description": {
|
|
71
|
+
"fr": "Créer ou mettre à jour une section.",
|
|
72
|
+
"en": "Create or update a section."
|
|
73
|
+
},
|
|
74
|
+
"scope": "host:property:write"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "sections.section.delete",
|
|
78
|
+
"description": {
|
|
79
|
+
"fr": "Supprimer une section.",
|
|
80
|
+
"en": "Delete a section."
|
|
81
|
+
},
|
|
82
|
+
"scope": "host:property:write"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "sections.reorder",
|
|
86
|
+
"description": {
|
|
87
|
+
"fr": "Réordonner les sections.",
|
|
88
|
+
"en": "Reorder sections."
|
|
89
|
+
},
|
|
90
|
+
"scope": "host:property:write"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"hostSurfaces": [
|
|
94
|
+
{
|
|
95
|
+
"type": "property-workspace-tab",
|
|
96
|
+
"pathSegment": "sections",
|
|
97
|
+
"label": {
|
|
98
|
+
"fr": "Sections",
|
|
99
|
+
"en": "Sections"
|
|
100
|
+
},
|
|
101
|
+
"icon": "file-text"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"config": {
|
|
105
|
+
"defaults": {},
|
|
106
|
+
"fields": []
|
|
107
|
+
},
|
|
108
|
+
"catalog": {
|
|
109
|
+
"tagline": {
|
|
110
|
+
"fr": "Composez votre carnet section par section.",
|
|
111
|
+
"en": "Build your welcome book section by section."
|
|
112
|
+
},
|
|
113
|
+
"npmPackage": "@portaki/module-sections",
|
|
114
|
+
"npmUrl": "https://www.npmjs.com/package/@portaki/module-sections"
|
|
115
|
+
},
|
|
116
|
+
"runtime": {
|
|
117
|
+
"backend": "wasm",
|
|
118
|
+
"guest": "remote-esm"
|
|
119
|
+
},
|
|
120
|
+
"artifacts": {
|
|
121
|
+
"wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
|
|
122
|
+
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.2.3"
|
|
123
|
+
}
|
|
124
|
+
}
|
package/portaki.module.json
CHANGED
|
@@ -9,7 +9,26 @@
|
|
|
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.2.1",
|
|
13
|
+
"releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
|
|
14
|
+
"changelog": [
|
|
15
|
+
{
|
|
16
|
+
"version": "1.2.0",
|
|
17
|
+
"date": "2026-05-18",
|
|
18
|
+
"notes": {
|
|
19
|
+
"fr": "Release catalogue modules 1.2.0.",
|
|
20
|
+
"en": "Module catalog release 1.2.0."
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"date": "2026-05-16",
|
|
26
|
+
"notes": {
|
|
27
|
+
"fr": "Première version : sections TipTap, gateway hôte et invité.",
|
|
28
|
+
"en": "Initial release: TipTap sections, host and guest gateway."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
],
|
|
13
32
|
"author": {
|
|
14
33
|
"name": "Portaki",
|
|
15
34
|
"url": "https://portaki.app",
|
|
@@ -17,58 +36,68 @@
|
|
|
17
36
|
},
|
|
18
37
|
"icon": "file-text",
|
|
19
38
|
"type": "official",
|
|
20
|
-
"tags": [
|
|
39
|
+
"tags": [
|
|
40
|
+
"editorial",
|
|
41
|
+
"content",
|
|
42
|
+
"sections"
|
|
43
|
+
],
|
|
21
44
|
"portakiVersionMin": "1.0.0",
|
|
22
|
-
"requiresHostSdk": "0.
|
|
45
|
+
"requiresHostSdk": "2.0.0",
|
|
46
|
+
"database": {
|
|
47
|
+
"schemaVersion": "1.0.1"
|
|
48
|
+
},
|
|
23
49
|
"license": "AGPL-3.0",
|
|
24
50
|
"repository": "https://github.com/PortakiApp/portaki-modules",
|
|
25
51
|
"scopes": [
|
|
26
|
-
"stay:read",
|
|
27
|
-
"property:read",
|
|
28
52
|
"host:property:read",
|
|
29
|
-
"host:property:write"
|
|
53
|
+
"host:property:write",
|
|
54
|
+
"property:read",
|
|
55
|
+
"stay:read"
|
|
30
56
|
],
|
|
31
57
|
"queries": [
|
|
32
58
|
{
|
|
33
59
|
"name": "sections.list",
|
|
60
|
+
"scope": "property:read",
|
|
34
61
|
"description": {
|
|
35
62
|
"fr": "Liste des sections du logement.",
|
|
36
63
|
"en": "Property editorial sections."
|
|
37
|
-
}
|
|
38
|
-
"scope": "property:read"
|
|
64
|
+
}
|
|
39
65
|
}
|
|
40
66
|
],
|
|
41
67
|
"commands": [
|
|
42
68
|
{
|
|
43
69
|
"name": "sections.section.save",
|
|
70
|
+
"scope": "host:property:write",
|
|
44
71
|
"description": {
|
|
45
72
|
"fr": "Créer ou mettre à jour une section.",
|
|
46
73
|
"en": "Create or update a section."
|
|
47
|
-
}
|
|
48
|
-
"scope": "host:property:write"
|
|
74
|
+
}
|
|
49
75
|
},
|
|
50
76
|
{
|
|
51
77
|
"name": "sections.section.delete",
|
|
78
|
+
"scope": "host:property:write",
|
|
52
79
|
"description": {
|
|
53
80
|
"fr": "Supprimer une section.",
|
|
54
81
|
"en": "Delete a section."
|
|
55
|
-
}
|
|
56
|
-
"scope": "host:property:write"
|
|
82
|
+
}
|
|
57
83
|
},
|
|
58
84
|
{
|
|
59
85
|
"name": "sections.reorder",
|
|
86
|
+
"scope": "host:property:write",
|
|
60
87
|
"description": {
|
|
61
88
|
"fr": "Réordonner les sections.",
|
|
62
89
|
"en": "Reorder sections."
|
|
63
|
-
}
|
|
64
|
-
"scope": "host:property:write"
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
],
|
|
67
93
|
"hostSurfaces": [
|
|
68
94
|
{
|
|
69
95
|
"type": "property-workspace-tab",
|
|
70
96
|
"pathSegment": "sections",
|
|
71
|
-
"label": {
|
|
97
|
+
"label": {
|
|
98
|
+
"fr": "Sections",
|
|
99
|
+
"en": "Sections"
|
|
100
|
+
},
|
|
72
101
|
"icon": "file-text"
|
|
73
102
|
}
|
|
74
103
|
],
|
|
@@ -82,7 +111,14 @@
|
|
|
82
111
|
"en": "Build your welcome book section by section."
|
|
83
112
|
},
|
|
84
113
|
"npmPackage": "@portaki/module-sections",
|
|
85
|
-
"npmUrl": "https://www.npmjs.com/package/@portaki/module-sections"
|
|
86
|
-
|
|
114
|
+
"npmUrl": "https://www.npmjs.com/package/@portaki/module-sections"
|
|
115
|
+
},
|
|
116
|
+
"runtime": {
|
|
117
|
+
"backend": "wasm",
|
|
118
|
+
"guest": "remote-esm"
|
|
119
|
+
},
|
|
120
|
+
"artifacts": {
|
|
121
|
+
"wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
|
|
122
|
+
"guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.2.3"
|
|
87
123
|
}
|
|
88
124
|
}
|
|
@@ -4,8 +4,8 @@ import type { JSONContent } from '@tiptap/core'
|
|
|
4
4
|
import { generateHTML } from '@tiptap/html'
|
|
5
5
|
import StarterKit from '@tiptap/starter-kit'
|
|
6
6
|
import { usePortakiQuery } from '@portaki/sdk'
|
|
7
|
-
import type { LangCode } from '@portaki/
|
|
8
|
-
import { ModuleSection } from '@portaki/
|
|
7
|
+
import type { LangCode } from '@portaki/sdk'
|
|
8
|
+
import { ModuleSection } from '@portaki/sdk'
|
|
9
9
|
|
|
10
10
|
type SectionRow = {
|
|
11
11
|
id: string
|
package/src/index.tsx
CHANGED
|
@@ -1,17 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { definePortakiModule } from '@portaki/module-sdk'
|
|
3
|
-
|
|
4
|
-
import { SectionsGuestView } from './components/SectionsGuestView'
|
|
5
|
-
|
|
6
|
-
export default definePortakiModule({
|
|
7
|
-
id: 'sections',
|
|
8
|
-
label: { fr: 'Sections', en: 'Sections' },
|
|
9
|
-
description: {
|
|
10
|
-
fr: 'Contenus éditoriaux du carnet d’accueil.',
|
|
11
|
-
en: 'Welcome book editorial content.',
|
|
12
|
-
},
|
|
13
|
-
version: '1.0.0',
|
|
14
|
-
icon: 'file-text',
|
|
15
|
-
navSlot: 'section',
|
|
16
|
-
render: ({ lang }: ModuleContext) => <SectionsGuestView lang={lang} />,
|
|
17
|
-
})
|
|
1
|
+
export { default } from './portaki.module'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { waitFor } from '@testing-library/react'
|
|
3
|
+
import { assertGuestSurface, renderGuestModule } from '@portaki/sdk-test-support'
|
|
4
|
+
|
|
5
|
+
import moduleDef from './index'
|
|
6
|
+
|
|
7
|
+
describe('@portaki/module-sections', () => {
|
|
8
|
+
it('exposes a valid guest module definition', () => {
|
|
9
|
+
assertGuestSurface(moduleDef)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders without crashing', async () => {
|
|
13
|
+
const view =
|
|
14
|
+
moduleDef.surface === 'host' ? renderHostModule(moduleDef) : renderGuestModule(moduleDef)
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(view.container).toBeTruthy()
|
|
17
|
+
})
|
|
18
|
+
view.unmount()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineModule,
|
|
3
|
+
index,
|
|
4
|
+
int,
|
|
5
|
+
jsonb,
|
|
6
|
+
moduleSchema,
|
|
7
|
+
table,
|
|
8
|
+
tenantId,
|
|
9
|
+
tenantPropertyIndex,
|
|
10
|
+
text,
|
|
11
|
+
timestamptz,
|
|
12
|
+
uuid,
|
|
13
|
+
uuidPrimaryKey,
|
|
14
|
+
type ModuleContext,
|
|
15
|
+
type PortakiFullModule,
|
|
16
|
+
} from '@portaki/sdk'
|
|
17
|
+
import type { ReactNode } from 'react'
|
|
18
|
+
|
|
19
|
+
const sectionsSchema = moduleSchema([
|
|
20
|
+
table('items', 't_e_module_sections_item', {
|
|
21
|
+
columns: [
|
|
22
|
+
uuidPrimaryKey(),
|
|
23
|
+
uuid('propertyId'),
|
|
24
|
+
tenantId(),
|
|
25
|
+
int('sortOrder', { defaultSql: '0' }),
|
|
26
|
+
text('titleFr', { defaultSql: "''" }),
|
|
27
|
+
text('titleEn', { defaultSql: "''" }),
|
|
28
|
+
jsonb('contentFr', { nullable: true }),
|
|
29
|
+
jsonb('contentEn', { nullable: true }),
|
|
30
|
+
timestamptz('createdAt', { nullable: false, defaultSql: 'now()' }),
|
|
31
|
+
timestamptz('updatedAt', { nullable: false, defaultSql: 'now()' }),
|
|
32
|
+
],
|
|
33
|
+
indexes: [tenantPropertyIndex()],
|
|
34
|
+
}),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
function stringParam(params: Record<string, unknown>, key: string): string | null {
|
|
38
|
+
const value = params[key]
|
|
39
|
+
if (value == null) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
return String(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function intParam(params: Record<string, unknown>, key: string, defaultValue: number): number {
|
|
46
|
+
const value = params[key]
|
|
47
|
+
if (value == null) {
|
|
48
|
+
return defaultValue
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'number') {
|
|
51
|
+
return value
|
|
52
|
+
}
|
|
53
|
+
return Number.parseInt(String(value), 10)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function defineSectionsModule(render: (ctx: ModuleContext) => ReactNode): PortakiFullModule {
|
|
57
|
+
return defineModule({
|
|
58
|
+
id: 'sections',
|
|
59
|
+
label: { fr: 'Sections', en: 'Sections' },
|
|
60
|
+
description: {
|
|
61
|
+
fr: 'Contenus éditoriaux du carnet d’accueil.',
|
|
62
|
+
en: 'Welcome book editorial content.',
|
|
63
|
+
},
|
|
64
|
+
icon: 'file-text',
|
|
65
|
+
version: '1.2.1',
|
|
66
|
+
schemaVersion: '1.0.1',
|
|
67
|
+
schema: sectionsSchema,
|
|
68
|
+
navSlot: 'section',
|
|
69
|
+
|
|
70
|
+
queries: {
|
|
71
|
+
'sections.list': {
|
|
72
|
+
scope: 'property:read',
|
|
73
|
+
description: {
|
|
74
|
+
fr: 'Liste des sections du logement.',
|
|
75
|
+
en: 'Property editorial sections.',
|
|
76
|
+
},
|
|
77
|
+
async handler(ctx) {
|
|
78
|
+
const rows = await ctx.db
|
|
79
|
+
.from('items')
|
|
80
|
+
.where({ tenantId: ctx.tenantId, propertyId: ctx.propertyId })
|
|
81
|
+
.many()
|
|
82
|
+
rows.sort((a, b) => {
|
|
83
|
+
const orderA = Number(a.sortOrder ?? 0)
|
|
84
|
+
const orderB = Number(b.sortOrder ?? 0)
|
|
85
|
+
if (orderA !== orderB) {
|
|
86
|
+
return orderA - orderB
|
|
87
|
+
}
|
|
88
|
+
const createdA = String(a.createdAt ?? '')
|
|
89
|
+
const createdB = String(b.createdAt ?? '')
|
|
90
|
+
return createdA.localeCompare(createdB)
|
|
91
|
+
})
|
|
92
|
+
const sections = rows.map((row) => {
|
|
93
|
+
const section: Record<string, unknown> = {
|
|
94
|
+
id: row.id,
|
|
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 }
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
commands: {
|
|
112
|
+
'sections.section.save': {
|
|
113
|
+
scope: 'host:property:write',
|
|
114
|
+
description: {
|
|
115
|
+
fr: 'Créer ou mettre à jour une section.',
|
|
116
|
+
en: 'Create or update a section.',
|
|
117
|
+
},
|
|
118
|
+
async handler(ctx, params) {
|
|
119
|
+
const filters = { tenantId: ctx.tenantId, propertyId: ctx.propertyId }
|
|
120
|
+
const idRaw = stringParam(params, 'id')
|
|
121
|
+
const id =
|
|
122
|
+
idRaw == null || idRaw.trim() === '' ? crypto.randomUUID() : idRaw.trim()
|
|
123
|
+
let titleFr = stringParam(params, 'titleFr')
|
|
124
|
+
let titleEn = stringParam(params, 'titleEn')
|
|
125
|
+
if (titleFr == null || titleFr.trim() === '') {
|
|
126
|
+
throw new Error('title_fr_required')
|
|
127
|
+
}
|
|
128
|
+
titleFr = titleFr.trim()
|
|
129
|
+
if (titleEn == null || titleEn.trim() === '') {
|
|
130
|
+
titleEn = titleFr
|
|
131
|
+
} else {
|
|
132
|
+
titleEn = titleEn.trim()
|
|
133
|
+
}
|
|
134
|
+
const contentFr = params.contentFr ?? null
|
|
135
|
+
const contentEn = params.contentEn ?? null
|
|
136
|
+
const existing = await ctx.db
|
|
137
|
+
.from('items')
|
|
138
|
+
.where({ ...filters, id })
|
|
139
|
+
.one()
|
|
140
|
+
let sortOrder = intParam(params, 'sortOrder', -1)
|
|
141
|
+
if (sortOrder < 0) {
|
|
142
|
+
const all = await ctx.db.from('items').where(filters).many()
|
|
143
|
+
const max = all
|
|
144
|
+
.filter((row) => String(row.id) !== id)
|
|
145
|
+
.reduce((acc, row) => Math.max(acc, Number(row.sortOrder ?? 0)), -1)
|
|
146
|
+
sortOrder = max + 1
|
|
147
|
+
}
|
|
148
|
+
const now = new Date().toISOString()
|
|
149
|
+
const payload = {
|
|
150
|
+
titleFr,
|
|
151
|
+
titleEn,
|
|
152
|
+
contentFr,
|
|
153
|
+
contentEn,
|
|
154
|
+
sortOrder,
|
|
155
|
+
updatedAt: now,
|
|
156
|
+
}
|
|
157
|
+
if (existing != null) {
|
|
158
|
+
await ctx.db.from('items').where({ ...filters, id }).update(payload)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
await ctx.db.from('items').insert({
|
|
162
|
+
id,
|
|
163
|
+
...filters,
|
|
164
|
+
...payload,
|
|
165
|
+
createdAt: now,
|
|
166
|
+
})
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
'sections.section.delete': {
|
|
170
|
+
scope: 'host:property:write',
|
|
171
|
+
description: {
|
|
172
|
+
fr: 'Supprimer une section.',
|
|
173
|
+
en: 'Delete a section.',
|
|
174
|
+
},
|
|
175
|
+
async handler(ctx, params) {
|
|
176
|
+
const idRaw = stringParam(params, 'id')
|
|
177
|
+
if (idRaw == null || idRaw.trim() === '') {
|
|
178
|
+
throw new Error('id_required')
|
|
179
|
+
}
|
|
180
|
+
const deleted = await ctx.db
|
|
181
|
+
.from('items')
|
|
182
|
+
.where({
|
|
183
|
+
id: idRaw.trim(),
|
|
184
|
+
tenantId: ctx.tenantId,
|
|
185
|
+
propertyId: ctx.propertyId,
|
|
186
|
+
})
|
|
187
|
+
.delete()
|
|
188
|
+
if (deleted === 0) {
|
|
189
|
+
throw new Error('section_not_found')
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
'sections.reorder': {
|
|
194
|
+
scope: 'host:property:write',
|
|
195
|
+
description: {
|
|
196
|
+
fr: 'Réordonner les sections.',
|
|
197
|
+
en: 'Reorder sections.',
|
|
198
|
+
},
|
|
199
|
+
async handler(ctx, params) {
|
|
200
|
+
const raw = params.orderedIds
|
|
201
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
202
|
+
throw new Error('ordered_ids_required')
|
|
203
|
+
}
|
|
204
|
+
const filters = { tenantId: ctx.tenantId, propertyId: ctx.propertyId }
|
|
205
|
+
const now = new Date().toISOString()
|
|
206
|
+
let order = 0
|
|
207
|
+
for (const item of raw) {
|
|
208
|
+
if (item == null) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
const id = String(item)
|
|
212
|
+
await ctx.db
|
|
213
|
+
.from('items')
|
|
214
|
+
.where({ ...filters, id })
|
|
215
|
+
.update({ sortOrder: order++, updatedAt: now })
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
render,
|
|
222
|
+
})
|
|
223
|
+
}
|
package/vitest.config.ts
ADDED