@portaki/module-sections 1.0.0 → 1.2.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 CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@portaki/module-sections",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
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/module-sdk": "0.5.0",
9
+ "@portaki/module-sdk": "^1.0.0",
13
10
  "@portaki/sdk": "^0.5.0",
14
11
  "@tiptap/core": "^3.0.0",
15
12
  "@tiptap/html": "^3.0.0",
@@ -19,7 +16,11 @@
19
16
  },
20
17
  "devDependencies": {
21
18
  "@types/react": "^19",
22
- "typescript": "^5"
19
+ "typescript": "^5",
20
+ "vitest": "^3.0.5",
21
+ "@testing-library/react": "^16.3.0",
22
+ "@testing-library/jest-dom": "^6.6.3",
23
+ "jsdom": "^26.0.0"
23
24
  },
24
25
  "repository": {
25
26
  "type": "git",
@@ -28,5 +29,10 @@
28
29
  },
29
30
  "publishConfig": {
30
31
  "access": "public"
32
+ },
33
+ "scripts": {
34
+ "lint": "echo \"no lint yet\"",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest"
31
37
  }
32
- }
38
+ }
@@ -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.0.0",
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,9 +36,16 @@
17
36
  },
18
37
  "icon": "file-text",
19
38
  "type": "official",
20
- "tags": ["editorial", "content", "sections"],
39
+ "tags": [
40
+ "editorial",
41
+ "content",
42
+ "sections"
43
+ ],
21
44
  "portakiVersionMin": "1.0.0",
22
- "requiresHostSdk": "0.6.0",
45
+ "requiresHostSdk": "1.1.0",
46
+ "database": {
47
+ "schemaVersion": "1.0.0"
48
+ },
23
49
  "license": "AGPL-3.0",
24
50
  "repository": "https://github.com/PortakiApp/portaki-modules",
25
51
  "scopes": [
@@ -68,7 +94,10 @@
68
94
  {
69
95
  "type": "property-workspace-tab",
70
96
  "pathSegment": "sections",
71
- "label": { "fr": "Sections", "en": "Sections" },
97
+ "label": {
98
+ "fr": "Sections",
99
+ "en": "Sections"
100
+ },
72
101
  "icon": "file-text"
73
102
  }
74
103
  ],
@@ -83,6 +112,15 @@
83
112
  },
84
113
  "npmPackage": "@portaki/module-sections",
85
114
  "npmUrl": "https://www.npmjs.com/package/@portaki/module-sections",
86
- "javaArtifact": "app.portaki.module:sections-backend:0.1.0"
115
+ "javaArtifact": "app.portaki.module:sections-backend"
116
+ },
117
+ "runtime": {
118
+ "backend": "wasm",
119
+ "guest": "remote-esm"
120
+ },
121
+ "artifacts": {
122
+ "wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections:1.2.0",
123
+ "jarMaven": "app.portaki.module:sections-backend:1.2.0",
124
+ "guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.2.0"
87
125
  }
88
126
  }
@@ -33,17 +33,17 @@ function contentToHtml(raw: unknown): string {
33
33
  }
34
34
 
35
35
  export function SectionsGuestView({ lang }: { lang: LangCode }) {
36
- const { data, isLoading, isError } = usePortakiQuery<ListResponse>('sections.list', {})
36
+ const { data, loading, error } = usePortakiQuery<ListResponse>('sections.list', {})
37
37
 
38
- if (isLoading) {
38
+ if (loading) {
39
39
  return (
40
- <ModuleSection>
40
+ <ModuleSection title={lang === 'fr' ? 'Sections' : 'Sections'}>
41
41
  <p className="text-sm opacity-70">{lang === 'fr' ? 'Chargement…' : 'Loading…'}</p>
42
42
  </ModuleSection>
43
43
  )
44
44
  }
45
45
 
46
- if (isError || !data?.sections?.length) {
46
+ if (error != null || !data?.sections?.length) {
47
47
  return null
48
48
  }
49
49
 
package/src/index.tsx CHANGED
@@ -10,7 +10,7 @@ export default definePortakiModule({
10
10
  fr: 'Contenus éditoriaux du carnet d’accueil.',
11
11
  en: 'Welcome book editorial content.',
12
12
  },
13
- version: '1.0.0',
13
+ version: '1.2.0',
14
14
  icon: 'file-text',
15
15
  navSlot: 'section',
16
16
  render: ({ lang }: ModuleContext) => <SectionsGuestView lang={lang} />,
@@ -0,0 +1,8 @@
1
+ import { describe, it } from 'vitest'
2
+ import { validateSiblingManifest } from '@portaki/module-test-support'
3
+
4
+ describe('portaki.module.json', () => {
5
+ it('matches module.v1 schema', () => {
6
+ validateSiblingManifest(import.meta.url)
7
+ })
8
+ })
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { waitFor } from '@testing-library/react'
3
+ import { assertGuestSurface, renderGuestModule } from '@portaki/module-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,3 @@
1
+ import { portakiModuleVitestConfig } from '@portaki/module-test-support/vitest'
2
+
3
+ export default portakiModuleVitestConfig(import.meta.url)
package/backend/pom.xml DELETED
@@ -1,57 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project xmlns="http://maven.apache.org/POM/4.0.0"
3
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5
- <modelVersion>4.0.0</modelVersion>
6
-
7
- <groupId>app.portaki.module</groupId>
8
- <artifactId>sections-backend</artifactId>
9
- <version>0.1.0</version>
10
- <packaging>jar</packaging>
11
-
12
- <name>Sections module (Java backend)</name>
13
- <description>Gateway handlers and persistence for the Portaki sections editorial module.</description>
14
-
15
- <properties>
16
- <java.version>21</java.version>
17
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18
- </properties>
19
-
20
- <dependencies>
21
- <dependency>
22
- <groupId>app.portaki</groupId>
23
- <artifactId>portaki-module-sdk</artifactId>
24
- <version>0.6.0</version>
25
- </dependency>
26
- <dependency>
27
- <groupId>org.springframework</groupId>
28
- <artifactId>spring-context</artifactId>
29
- <version>6.2.5</version>
30
- <scope>provided</scope>
31
- </dependency>
32
- <dependency>
33
- <groupId>org.springframework</groupId>
34
- <artifactId>spring-jdbc</artifactId>
35
- <version>6.2.5</version>
36
- <scope>provided</scope>
37
- </dependency>
38
- <dependency>
39
- <groupId>com.fasterxml.jackson.core</groupId>
40
- <artifactId>jackson-databind</artifactId>
41
- <version>2.18.2</version>
42
- </dependency>
43
- </dependencies>
44
-
45
- <build>
46
- <plugins>
47
- <plugin>
48
- <groupId>org.apache.maven.plugins</groupId>
49
- <artifactId>maven-compiler-plugin</artifactId>
50
- <version>3.14.0</version>
51
- <configuration>
52
- <release>${java.version}</release>
53
- </configuration>
54
- </plugin>
55
- </plugins>
56
- </build>
57
- </project>
@@ -1,251 +0,0 @@
1
- package app.portaki.module.sections;
2
-
3
- import java.sql.ResultSet;
4
- import java.sql.SQLException;
5
- import java.time.Instant;
6
- import java.util.HashMap;
7
- import java.util.List;
8
- import java.util.Map;
9
- import java.util.UUID;
10
-
11
- import org.springframework.jdbc.core.JdbcTemplate;
12
- import org.springframework.stereotype.Component;
13
-
14
- import com.fasterxml.jackson.core.JsonProcessingException;
15
- import com.fasterxml.jackson.databind.JsonNode;
16
- import com.fasterxml.jackson.databind.ObjectMapper;
17
-
18
- import app.portaki.sdk.gateway.GatewayModuleContext;
19
- import app.portaki.sdk.gateway.PortakiCommandHandler;
20
- import app.portaki.sdk.gateway.PortakiQueryHandler;
21
- import app.portaki.sdk.module.PortakiModule;
22
-
23
- @Component
24
- @PortakiModule("sections")
25
- public class SectionsGatewayModule {
26
-
27
- private final JdbcTemplate jdbc;
28
- private final ObjectMapper objectMapper;
29
-
30
- public SectionsGatewayModule(JdbcTemplate jdbc, ObjectMapper objectMapper) {
31
- this.jdbc = jdbc;
32
- this.objectMapper = objectMapper;
33
- }
34
-
35
- @PortakiQueryHandler(value = "sections.list", scope = "property:read")
36
- public Map<String, Object> listSections(Map<String, Object> params, GatewayModuleContext ctx) {
37
- UUID tenantId = ctx.tenantIdInternal();
38
- UUID propertyId = UUID.fromString(ctx.propertyId());
39
- List<Map<String, Object>> rows =
40
- jdbc.query(
41
- """
42
- SELECT id, sort_order, title_fr, title_en, content_fr, content_en, updated_at
43
- FROM t_e_module_sections_item
44
- WHERE tenant_id = ? AND property_id = ?
45
- ORDER BY sort_order ASC, created_at ASC
46
- """,
47
- (rs, rowNum) -> toSectionRow(rs),
48
- tenantId,
49
- propertyId);
50
- return Map.of("sections", rows);
51
- }
52
-
53
- @PortakiCommandHandler(value = "sections.section.save", scope = "host:property:write")
54
- public void saveSection(Map<String, Object> params, GatewayModuleContext ctx) {
55
- UUID tenantId = ctx.tenantIdInternal();
56
- UUID propertyId = UUID.fromString(ctx.propertyId());
57
- String idRaw = stringParam(params, "id");
58
- UUID id = idRaw == null || idRaw.isBlank() ? UUID.randomUUID() : UUID.fromString(idRaw);
59
- String titleFr = stringParam(params, "titleFr");
60
- String titleEn = stringParam(params, "titleEn");
61
- JsonNode contentFr = readJsonParam(params, "contentFr");
62
- JsonNode contentEn = readJsonParam(params, "contentEn");
63
- int sortOrder = intParam(params, "sortOrder", nextSortOrder(tenantId, propertyId, id));
64
-
65
- if (titleFr == null || titleFr.isBlank()) {
66
- throw new IllegalArgumentException("title_fr_required");
67
- }
68
- if (titleEn == null || titleEn.isBlank()) {
69
- titleEn = titleFr;
70
- }
71
-
72
- boolean exists =
73
- Boolean.TRUE.equals(
74
- jdbc.queryForObject(
75
- """
76
- SELECT EXISTS(
77
- SELECT 1 FROM t_e_module_sections_item
78
- WHERE id = ? AND tenant_id = ? AND property_id = ?
79
- )
80
- """,
81
- Boolean.class,
82
- id,
83
- tenantId,
84
- propertyId));
85
-
86
- if (exists) {
87
- jdbc.update(
88
- """
89
- UPDATE t_e_module_sections_item
90
- SET title_fr = ?, title_en = ?, content_fr = ?::jsonb, content_en = ?::jsonb,
91
- sort_order = ?, updated_at = ?
92
- WHERE id = ? AND tenant_id = ? AND property_id = ?
93
- """,
94
- titleFr,
95
- titleEn,
96
- jsonOrNull(contentFr),
97
- jsonOrNull(contentEn),
98
- sortOrder,
99
- Instant.now(),
100
- id,
101
- tenantId,
102
- propertyId);
103
- } else {
104
- jdbc.update(
105
- """
106
- INSERT INTO t_e_module_sections_item (
107
- id, tenant_id, property_id, sort_order, title_fr, title_en,
108
- content_fr, content_en, created_at, updated_at
109
- ) VALUES (?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?, ?)
110
- """,
111
- id,
112
- tenantId,
113
- propertyId,
114
- sortOrder,
115
- titleFr,
116
- titleEn,
117
- jsonOrNull(contentFr),
118
- jsonOrNull(contentEn),
119
- Instant.now(),
120
- Instant.now());
121
- }
122
-
123
- }
124
-
125
- @PortakiCommandHandler(value = "sections.section.delete", scope = "host:property:write")
126
- public void deleteSection(Map<String, Object> params, GatewayModuleContext ctx) {
127
- UUID tenantId = ctx.tenantIdInternal();
128
- UUID propertyId = UUID.fromString(ctx.propertyId());
129
- String idRaw = stringParam(params, "id");
130
- if (idRaw == null || idRaw.isBlank()) {
131
- throw new IllegalArgumentException("id_required");
132
- }
133
- UUID id = UUID.fromString(idRaw);
134
- int deleted =
135
- jdbc.update(
136
- """
137
- DELETE FROM t_e_module_sections_item
138
- WHERE id = ? AND tenant_id = ? AND property_id = ?
139
- """,
140
- id,
141
- tenantId,
142
- propertyId);
143
- if (deleted == 0) {
144
- throw new IllegalArgumentException("section_not_found");
145
- }
146
- }
147
-
148
- @PortakiCommandHandler(value = "sections.reorder", scope = "host:property:write")
149
- public void reorder(Map<String, Object> params, GatewayModuleContext ctx) {
150
- UUID tenantId = ctx.tenantIdInternal();
151
- UUID propertyId = UUID.fromString(ctx.propertyId());
152
- Object raw = params.get("orderedIds");
153
- if (!(raw instanceof List<?> list) || list.isEmpty()) {
154
- throw new IllegalArgumentException("ordered_ids_required");
155
- }
156
- int order = 0;
157
- for (Object item : list) {
158
- if (item == null) {
159
- continue;
160
- }
161
- UUID id = UUID.fromString(item.toString());
162
- jdbc.update(
163
- """
164
- UPDATE t_e_module_sections_item
165
- SET sort_order = ?, updated_at = ?
166
- WHERE id = ? AND tenant_id = ? AND property_id = ?
167
- """,
168
- order++,
169
- Instant.now(),
170
- id,
171
- tenantId,
172
- propertyId);
173
- }
174
- }
175
-
176
- private Map<String, Object> toSectionRow(ResultSet rs) throws SQLException {
177
- Map<String, Object> row = new HashMap<>();
178
- row.put("id", rs.getObject("id", UUID.class).toString());
179
- row.put("sortOrder", rs.getInt("sort_order"));
180
- row.put("titleFr", rs.getString("title_fr"));
181
- row.put("titleEn", rs.getString("title_en"));
182
- row.put("contentFr", parseJsonColumn(rs.getString("content_fr")));
183
- row.put("contentEn", parseJsonColumn(rs.getString("content_en")));
184
- Instant updatedAt = rs.getObject("updated_at", Instant.class);
185
- if (updatedAt != null) {
186
- row.put("updatedAt", updatedAt.toString());
187
- }
188
- return row;
189
- }
190
-
191
- private Object parseJsonColumn(String raw) {
192
- if (raw == null || raw.isBlank()) {
193
- return null;
194
- }
195
- try {
196
- return objectMapper.readValue(raw, Object.class);
197
- } catch (JsonProcessingException e) {
198
- return null;
199
- }
200
- }
201
-
202
- private int nextSortOrder(UUID tenantId, UUID propertyId, UUID exceptId) {
203
- Integer max =
204
- jdbc.queryForObject(
205
- """
206
- SELECT COALESCE(MAX(sort_order), -1)
207
- FROM t_e_module_sections_item
208
- WHERE tenant_id = ? AND property_id = ? AND id <> ?
209
- """,
210
- Integer.class,
211
- tenantId,
212
- propertyId,
213
- exceptId);
214
- return max == null ? 0 : max + 1;
215
- }
216
-
217
- private static String stringParam(Map<String, Object> params, String key) {
218
- Object v = params.get(key);
219
- return v == null ? null : v.toString();
220
- }
221
-
222
- private static int intParam(Map<String, Object> params, String key, int defaultValue) {
223
- Object v = params.get(key);
224
- if (v == null) {
225
- return defaultValue;
226
- }
227
- if (v instanceof Number n) {
228
- return n.intValue();
229
- }
230
- return Integer.parseInt(v.toString());
231
- }
232
-
233
- private JsonNode readJsonParam(Map<String, Object> params, String key) {
234
- Object v = params.get(key);
235
- if (v == null) {
236
- return null;
237
- }
238
- return objectMapper.valueToTree(v);
239
- }
240
-
241
- private String jsonOrNull(JsonNode node) {
242
- if (node == null || node.isNull()) {
243
- return null;
244
- }
245
- try {
246
- return objectMapper.writeValueAsString(node);
247
- } catch (JsonProcessingException e) {
248
- throw new IllegalArgumentException("invalid_json", e);
249
- }
250
- }
251
- }
@@ -1,8 +0,0 @@
1
- package app.portaki.module.sections;
2
-
3
- import org.springframework.context.annotation.ComponentScan;
4
- import org.springframework.context.annotation.Configuration;
5
-
6
- @Configuration
7
- @ComponentScan(basePackageClasses = SectionsGatewayModule.class)
8
- public class SectionsModuleBackendConfiguration {}
@@ -1,3 +0,0 @@
1
- artifactId=sections-backend
2
- groupId=app.portaki.module
3
- version=0.1.0
@@ -1,2 +0,0 @@
1
- app/portaki/module/sections/SectionsGatewayModule.class
2
- app/portaki/module/sections/SectionsModuleBackendConfiguration.class
@@ -1,2 +0,0 @@
1
- /Users/cyrilcolinet/Documents/Projects/PortakiApp/Repositories/portaki-modules/modules/sections/backend/src/main/java/app/portaki/module/sections/SectionsGatewayModule.java
2
- /Users/cyrilcolinet/Documents/Projects/PortakiApp/Repositories/portaki-modules/modules/sections/backend/src/main/java/app/portaki/module/sections/SectionsModuleBackendConfiguration.java
package/db/README.md DELETED
@@ -1,8 +0,0 @@
1
- # Schéma `sections`
2
-
3
- - **Propriété** : module `sections` (`app.portaki.module.sections`).
4
- - **Source de vérité** : `schema.sql` dans ce dossier.
5
- - **Application aujourd’hui** : copie versionnée dans `portaki-api/.../db/migration/V58__module_sections_items.sql` (Flyway unique sur la base core).
6
- - **Cible** : job **Atlas** / `portaki-module-migrator` (schéma Postgres dédié ou préfixe module) — voir `portaki-internal-docs/MODULE_PLATFORM_PREPARATION.md`.
7
-
8
- Ne pas ajouter d’entités JPA pour ce module dans `portaki-api` : persistance dans `backend/` uniquement.
package/db/schema.sql DELETED
@@ -1,18 +0,0 @@
1
- -- Canonical DDL for module `sections` (review in portaki-modules).
2
- -- Runtime: copied to portaki-api Flyway until Atlas module migrator exists.
3
-
4
- CREATE TABLE t_e_module_sections_item (
5
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6
- tenant_id UUID NOT NULL REFERENCES t_e_tenants (id),
7
- property_id UUID NOT NULL REFERENCES t_e_properties (id) ON DELETE CASCADE,
8
- sort_order INT NOT NULL DEFAULT 0,
9
- title_fr TEXT NOT NULL DEFAULT '',
10
- title_en TEXT NOT NULL DEFAULT '',
11
- content_fr JSONB,
12
- content_en JSONB,
13
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
14
- updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
15
- );
16
-
17
- CREATE INDEX t_e_module_sections_item_property_idx
18
- ON t_e_module_sections_item (tenant_id, property_id);