@open-commune/web 0.1.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/README.md +41 -0
- package/app.config.ts +10 -0
- package/app.vue +13 -0
- package/assets/css/main.css +13 -0
- package/components/data/display/row/data-avatar.vue +23 -0
- package/components/data/display/row/data-badge.vue +34 -0
- package/components/data/display/row/data-color.vue +17 -0
- package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
- package/components/data/display/row/data-label.vue +37 -0
- package/components/data/display/row/data-last-update.vue +45 -0
- package/components/data/display/row/data-spacer.vue +4 -0
- package/components/data/display/row/data-summary.vue +44 -0
- package/components/data/display/row/data-text.vue +14 -0
- package/components/data/display/row/data-toggle.vue +16 -0
- package/components/data/display/row.vue +111 -0
- package/components/data/lists/data-list.vue +85 -0
- package/components/data/lists/data-lists-table.vue +155 -0
- package/components/elements/avatars/avatar-circular.vue +68 -0
- package/components/elements/avatars/avatar-with-text.vue +47 -0
- package/components/elements/badges/badge.vue +85 -0
- package/components/elements/buttons/primary.vue +32 -0
- package/components/elements/buttons/secondary.vue +35 -0
- package/components/elements/buttons/tertiary.vue +25 -0
- package/components/elements/dropdowns/dots.vue +75 -0
- package/components/elements/media/image.vue +82 -0
- package/components/elements/notifications/default.vue +90 -0
- package/components/forms/handlers/handler-collections.vue +189 -0
- package/components/forms/handlers/handler-publication-status.vue +193 -0
- package/components/forms/handlers/handler-records.vue +138 -0
- package/components/forms/handlers/handler-roles.vue +176 -0
- package/components/forms/handlers/handler-schedules.vue +218 -0
- package/components/forms/handlers/handler-steps.vue +147 -0
- package/components/forms/handlers/handler-tokens.vue +79 -0
- package/components/forms/inputs/input-color-picker.vue +159 -0
- package/components/forms/inputs/input-editor.vue +363 -0
- package/components/forms/inputs/input-files-media.vue +150 -0
- package/components/forms/inputs/input-files.vue +98 -0
- package/components/forms/inputs/input-json.vue +65 -0
- package/components/forms/inputs/input-location.vue +179 -0
- package/components/forms/inputs/input-text-area.vue +65 -0
- package/components/forms/inputs/input-text.vue +122 -0
- package/components/forms/inputs/input-toggle.vue +125 -0
- package/components/forms/inputs/media-section/media-editor.vue +95 -0
- package/components/forms/layouts/form.vue +203 -0
- package/components/forms/layouts/layout.vue +47 -0
- package/components/forms/layouts/modal.vue +96 -0
- package/components/forms/layouts/navigation.vue +48 -0
- package/components/forms/layouts/section.vue +118 -0
- package/components/forms/selects/select-collection-enum.vue +195 -0
- package/components/forms/selects/select-date.vue +245 -0
- package/components/forms/selects/select-enum.vue +186 -0
- package/components/forms/selects/select-record.vue +274 -0
- package/components/layout/headings/headings-collection.vue +29 -0
- package/components/layout/headings/headings-record.vue +33 -0
- package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
- package/components/layout/navigations/primary.vue +132 -0
- package/components/layout/notifications/notifications-list.vue +19 -0
- package/composables/core/domains/use-definitions.ts +57 -0
- package/composables/core/domains/use-entries.ts +62 -0
- package/composables/core/domains/use-media.ts +82 -0
- package/composables/core/use-core.ts +12 -0
- package/composables/use-ability.js +38 -0
- package/composables/use-models.js +27 -0
- package/composables/use-session.ts +80 -0
- package/composables/use-timer.js +62 -0
- package/composables/use-trpc.js +17 -0
- package/editor/index.ts +44 -0
- package/editor/views/CalloutView.vue +22 -0
- package/editor/views/DetailsContentView.vue +11 -0
- package/editor/views/DetailsSummaryView.vue +11 -0
- package/editor/views/DetailsView.vue +72 -0
- package/editor/views/EmbedView.vue +53 -0
- package/editor/views/FileView.vue +79 -0
- package/editor/views/ImageView.vue +56 -0
- package/editor/views/UploadView.vue +71 -0
- package/layouts/default.vue +21 -0
- package/layouts/entrance.vue +10 -0
- package/layouts/record.vue +21 -0
- package/middleware/00.auth.global.js +16 -0
- package/middleware/01.instance.global.js +21 -0
- package/middleware/03.verify-permissions.global.js +11 -0
- package/middleware/04.prefetch-page.global.js +18 -0
- package/middleware/05.headings-page.global.js +25 -0
- package/models/_commun.js +243 -0
- package/models/_factory.js +34 -0
- package/models/collection.js +87 -0
- package/models/entrance.js +42 -0
- package/models/media.js +82 -0
- package/models/organization.js +92 -0
- package/models/record.js +165 -0
- package/models/role.js +66 -0
- package/models/token.js +56 -0
- package/models/user.js +97 -0
- package/nuxt.config.ts +151 -0
- package/package.json +71 -0
- package/pages/[collection]/[record]/index.vue +88 -0
- package/pages/[collection]/index.vue +55 -0
- package/pages/_lab/editor.vue +149 -0
- package/pages/auth/callback.vue +31 -0
- package/pages/index/index.vue +85 -0
- package/pages/index/password/define/[token].vue +79 -0
- package/pages/index/password/recover/index.vue +72 -0
- package/pages/index/welcome/[token].vue +90 -0
- package/pages/index.vue +44 -0
- package/pages/management/index.vue +15 -0
- package/pages/management/members.vue +176 -0
- package/pages/management/organizations.vue +192 -0
- package/pages/management/roles.vue +183 -0
- package/pages/overview.vue +15 -0
- package/pages/settings.vue +197 -0
- package/plugins/01.session.client.ts +5 -0
- package/plugins/02.vue-query.ts +25 -0
- package/public/favicon.ico +0 -0
- package/public/logo-black.svg +1 -0
- package/public/logo-white.svg +1 -0
- package/server/api/trpc/[...trpc].ts +28 -0
- package/server/api/v1/content/deployment.get.ts +45 -0
- package/server/api/v1/content/records.get.ts +12 -0
- package/server/middleware/0.security.ts +19 -0
- package/server/middleware/1.cors.ts +31 -0
- package/server/middleware/2.session.ts +20 -0
- package/server/middleware/3.api-token.ts +20 -0
- package/server/plugins/core.ts +8 -0
- package/server/routes/_tasks/[name].post.ts +20 -0
- package/server/routes/health.get.ts +6 -0
- package/server/tasks/deploy.ts +26 -0
- package/server/types.d.ts +8 -0
- package/server/utils/core.ts +18 -0
- package/store/layout/headings.js +21 -0
- package/store/layout/modal.js +80 -0
- package/store/layout/notifications.js +52 -0
- package/store/layout/workspace.js +80 -0
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { globSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// Under Bun's isolated store, the ESM build of @vue/devtools-api only exists
|
|
6
|
+
// inside node_modules/.bun/@vue+devtools-api@<version>, where a plain node
|
|
7
|
+
// resolution never looks. We point Vite at it explicitly when we can find it.
|
|
8
|
+
//
|
|
9
|
+
// Best-effort ON PURPOSE: the path assumes this app sits inside its repository.
|
|
10
|
+
// It does not when the app is consumed as a remote Nuxt layer, or built from a
|
|
11
|
+
// tarball — the lookup must then fail quietly rather than break the build.
|
|
12
|
+
function findDevtoolsApiEsm(): string | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const [found] = globSync(
|
|
15
|
+
fileURLToPath(
|
|
16
|
+
new URL(
|
|
17
|
+
'../../node_modules/.bun/@vue+devtools-api@*/node_modules/@vue/devtools-api/lib/esm/index.js',
|
|
18
|
+
import.meta.url,
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
return found;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const devtoolsApiEsm = findDevtoolsApiEsm();
|
|
29
|
+
|
|
30
|
+
// Resolved from THIS file, not from `~`: when this application is consumed as
|
|
31
|
+
// a Nuxt layer, `~` points at the consuming app, and its own assets are not
|
|
32
|
+
// found.
|
|
33
|
+
const layerDir = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
|
|
35
|
+
// Le worker de dev empaquette les sources du core, donc le dossier de
|
|
36
|
+
// migrations Drizzle n'y est pas resolvable relativement — on l'epingle ici,
|
|
37
|
+
// ou le chemin est connu. (Repris de l'ancien apps/api/nitro.config.ts.)
|
|
38
|
+
process.env.COMMUN_MIGRATIONS_DIR ??= resolve(
|
|
39
|
+
fileURLToPath(new URL('.', import.meta.url)),
|
|
40
|
+
'../../packages/core/drizzle',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
44
|
+
export default defineNuxtConfig({
|
|
45
|
+
ssr: false,
|
|
46
|
+
// Reprise de l'ancien apps/api/nitro.config.ts.
|
|
47
|
+
compatibilityDate: '2026-05-19',
|
|
48
|
+
app: {
|
|
49
|
+
head: {
|
|
50
|
+
link: [
|
|
51
|
+
{
|
|
52
|
+
href: 'https://api.tiles.mapbox.com/mapbox-gl-js/v2.9.2/mapbox-gl.css',
|
|
53
|
+
rel: 'stylesheet',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
charset: 'utf-8',
|
|
57
|
+
viewport: 'width=device-width, initial-scale=1',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
runtimeConfig: {
|
|
61
|
+
// Repertoire de donnees de l'instance (surchargeable par COMMUN_DATA_DIR).
|
|
62
|
+
communDataDir: process.env.COMMUN_DATA_DIR ?? '',
|
|
63
|
+
public: {
|
|
64
|
+
// Vide par défaut (image d'instance : admin et API MÊME origine, appels
|
|
65
|
+
// relatifs `/api/trpc`) ; NUXT_ENV_API_URL reste prioritaire (dev, harness).
|
|
66
|
+
apiURL: process.env.NUXT_ENV_API_URL ?? '',
|
|
67
|
+
baseURL: process.env.NUXT_ENV_BASE_URL || 'http://localhost:3000',
|
|
68
|
+
mapBoxToken: process.env.NUXT_ENV_MAPBOX_TOKEN,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
// Le build du site est déclenché chaque nuit à 00:30 (heure LOCALE du
|
|
72
|
+
// server).
|
|
73
|
+
nitro: {
|
|
74
|
+
experimental: { tasks: true },
|
|
75
|
+
scheduledTasks: {
|
|
76
|
+
'30 0 * * *': ['deploy'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
css: [join(layerDir, 'assets/css/main.css')],
|
|
80
|
+
// Scan nested composables (composables/core/domains/*).
|
|
81
|
+
imports: { dirs: [join(layerDir, 'composables/**')] },
|
|
82
|
+
// Session auth lives in use-session + middleware/00.auth (no auth module).
|
|
83
|
+
modules: ['@nuxt/ui', '@pinia/nuxt', '@pinia-orm/nuxt', '@vueuse/nuxt'],
|
|
84
|
+
vite: {
|
|
85
|
+
resolve: {
|
|
86
|
+
// Sous le layout node_modules de Bun, Vite retombe sur l'entrée CJS
|
|
87
|
+
// (« does not provide an export named setupDevtoolsPlugin ») — on
|
|
88
|
+
// force l'entrée ESM via un chemin ABSOLU (un remplacement contenant
|
|
89
|
+
// la clé de l'alias boucle à l'infini → OOM).
|
|
90
|
+
alias: {
|
|
91
|
+
...(devtoolsApiEsm ? { '@vue/devtools-api': devtoolsApiEsm } : {}),
|
|
92
|
+
},
|
|
93
|
+
// One ProseMirror instance is mandatory (keyed plugins): bun's store
|
|
94
|
+
// gives @nuxt/ui and our direct @tiptap deps distinct copies.
|
|
95
|
+
dedupe: [
|
|
96
|
+
// Même raison, côté état : les modèles de l'admin tirent pinia-orm
|
|
97
|
+
// tardivement. (Disparaîtra avec la sortie de pinia-orm, groupe 7.)
|
|
98
|
+
'pinia',
|
|
99
|
+
'pinia-orm',
|
|
100
|
+
'@tanstack/vue-query',
|
|
101
|
+
'@trpc/client',
|
|
102
|
+
'@tiptap/core',
|
|
103
|
+
'@tiptap/pm',
|
|
104
|
+
'@tiptap/vue-3',
|
|
105
|
+
'prosemirror-state',
|
|
106
|
+
'prosemirror-model',
|
|
107
|
+
'prosemirror-view',
|
|
108
|
+
'prosemirror-transform',
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
// Prescribed by the Nuxt UI Editor docs (ProseMirror warning): share a
|
|
112
|
+
// single pre-bundled prosemirror across @nuxt/ui and our extensions.
|
|
113
|
+
optimizeDeps: {
|
|
114
|
+
include: [
|
|
115
|
+
// @open-commune/editor est un paquet de l'ESPACE DE TRAVAIL : Vite ne
|
|
116
|
+
// pré-empaquette pas ses dépendances au démarrage, il les découvre
|
|
117
|
+
// quand l'éditeur se monte — puis ré-optimise, ce qui périme les URL
|
|
118
|
+
// que la page tient déjà (504 « Outdated Optimize Dep », import
|
|
119
|
+
// dynamique en échec, page d'erreur 500). Un rechargement réparait,
|
|
120
|
+
// mais seulement après coup : on les déclare pour que le démarrage à
|
|
121
|
+
// froid soit propre.
|
|
122
|
+
'@tiptap/core',
|
|
123
|
+
'@tiptap/pm/model',
|
|
124
|
+
'@tiptap/pm/state',
|
|
125
|
+
'@tiptap/starter-kit',
|
|
126
|
+
'@tiptap/extension-code',
|
|
127
|
+
'@tiptap/extension-details',
|
|
128
|
+
'@tiptap/extension-file-handler',
|
|
129
|
+
'@tiptap/extension-highlight',
|
|
130
|
+
'@tiptap/extension-horizontal-rule',
|
|
131
|
+
'@tiptap/extension-image',
|
|
132
|
+
'@tiptap/extension-link',
|
|
133
|
+
'@tiptap/extension-list',
|
|
134
|
+
'@tiptap/extension-text-align',
|
|
135
|
+
'@tiptap/extension-text-style',
|
|
136
|
+
'@tiptap/extension-typography',
|
|
137
|
+
'@tiptap/extension-unique-id',
|
|
138
|
+
'@nuxt/ui > prosemirror-state',
|
|
139
|
+
'@nuxt/ui > prosemirror-transform',
|
|
140
|
+
'@nuxt/ui > prosemirror-model',
|
|
141
|
+
'@nuxt/ui > prosemirror-view',
|
|
142
|
+
'@nuxt/ui > prosemirror-gapcursor',
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
// optimizeDeps.esbuildOptions supprimé : Vite 8 optimise via Rolldown
|
|
146
|
+
// (option dépréciée, cible moderne par défaut).
|
|
147
|
+
build: {
|
|
148
|
+
target: 'esnext',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-commune/web",
|
|
3
|
+
"scripts": {
|
|
4
|
+
"build": "nuxt build",
|
|
5
|
+
"dev": "bun --bun nuxt dev",
|
|
6
|
+
"generate": "nuxt generate",
|
|
7
|
+
"preview": "nuxt preview",
|
|
8
|
+
"postinstall": "nuxt prepare",
|
|
9
|
+
"typecheck": "tsc --noEmit -p .nuxt/tsconfig.server.json"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@iconify/vue": "^5.0.1",
|
|
13
|
+
"@nuxt/kit": "^4.5.1",
|
|
14
|
+
"@nuxt/ui": "^4.3.0",
|
|
15
|
+
"@tiptap/pm": "^3.27.3",
|
|
16
|
+
"@tiptap/vue-3": "^3.27.3",
|
|
17
|
+
"@types/node": "^18",
|
|
18
|
+
"@vueuse/core": "^14.4.0",
|
|
19
|
+
"@vueuse/nuxt": "^14.4.0",
|
|
20
|
+
"nuxt": "^4.5.2",
|
|
21
|
+
"tailwindcss": "^4.3.2"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@casl/ability": "^7.0.1",
|
|
25
|
+
"@open-commune/core": "workspace:*",
|
|
26
|
+
"@open-commune/editor": "workspace:*",
|
|
27
|
+
"@pinia-orm/nuxt": "^2.0.0",
|
|
28
|
+
"@pinia/nuxt": "^1.0.1",
|
|
29
|
+
"@tanstack/vue-query": "^5.101.4",
|
|
30
|
+
"@trpc/client": "^11.17.0",
|
|
31
|
+
"@trpc/server": "^11.17.0",
|
|
32
|
+
"axios": "^1.6.7",
|
|
33
|
+
"consola": "^3.4.2",
|
|
34
|
+
"h3": "^1.15.11",
|
|
35
|
+
"luxon": "^3.4.4",
|
|
36
|
+
"mapbox-gl": "^3.2.0",
|
|
37
|
+
"ofetch": "^1.3.3",
|
|
38
|
+
"pinia-orm": "^2.0.0",
|
|
39
|
+
"zod": "^4.4.3"
|
|
40
|
+
},
|
|
41
|
+
"description": "The Open Commune application: the admin interface and the server, on a single origin.",
|
|
42
|
+
"license": "AGPL-3.0-or-later",
|
|
43
|
+
"version": "0.1.3",
|
|
44
|
+
"type": "module",
|
|
45
|
+
"main": "./nuxt.config.ts",
|
|
46
|
+
"files": [
|
|
47
|
+
"app.vue",
|
|
48
|
+
"app.config.ts",
|
|
49
|
+
"nuxt.config.ts",
|
|
50
|
+
"assets",
|
|
51
|
+
"components",
|
|
52
|
+
"composables",
|
|
53
|
+
"editor",
|
|
54
|
+
"layouts",
|
|
55
|
+
"middleware",
|
|
56
|
+
"models",
|
|
57
|
+
"pages",
|
|
58
|
+
"plugins",
|
|
59
|
+
"public",
|
|
60
|
+
"server",
|
|
61
|
+
"store"
|
|
62
|
+
],
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://github.com/open-commune/open-commune.git",
|
|
69
|
+
"directory": "apps/web"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- <alert
|
|
4
|
+
v-if="alertSort"
|
|
5
|
+
:sort="alertSort"
|
|
6
|
+
:title="ALERTS_MESSAGES[alertSort].title"
|
|
7
|
+
:description="ALERTS_MESSAGES[alertSort].description"
|
|
8
|
+
/> -->
|
|
9
|
+
<forms-layout
|
|
10
|
+
v-if="componentOptions"
|
|
11
|
+
v-bind="{ record, ...componentOptions }"
|
|
12
|
+
class="mt-4"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup>
|
|
18
|
+
// import alert from '@/components/template/content-section/Alert'
|
|
19
|
+
import formsLayout from '~/components/forms/layouts/layout'
|
|
20
|
+
|
|
21
|
+
definePageMeta({
|
|
22
|
+
permissions: ['read:records'],
|
|
23
|
+
preFetch: ['Record'],
|
|
24
|
+
layout: 'record'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Prepare constants
|
|
28
|
+
// const ALERTS_MESSAGES = {
|
|
29
|
+
// info: {
|
|
30
|
+
// title: 'Cet évènement est validé',
|
|
31
|
+
// description: 'Nos équipes ont validé cet évènemnt, il sera prochainement publié en ligne.'
|
|
32
|
+
// },
|
|
33
|
+
// warn: {
|
|
34
|
+
// title: 'Cet évènement est en attente de validation',
|
|
35
|
+
// description: 'Avant d\'être publié, cet évènement doit être validé par nos équipes.'
|
|
36
|
+
// },
|
|
37
|
+
// success: {
|
|
38
|
+
// title: 'Cet évènement est publié',
|
|
39
|
+
// description: 'Vous n\'avez plus rien à faire, vous pouvez consulter cet évènement en ligne.'
|
|
40
|
+
// }
|
|
41
|
+
// }
|
|
42
|
+
|
|
43
|
+
const { Collection, Record } = useModels()
|
|
44
|
+
const $route = useRoute()
|
|
45
|
+
|
|
46
|
+
// Prepare computed
|
|
47
|
+
const isCreating = computed(() => $route.params.record === 'new')
|
|
48
|
+
const collectionRecord = computed(() => Collection.repo.where('slug', $route.params.collection).first())
|
|
49
|
+
const componentOptions = computed(() => {
|
|
50
|
+
const options = JSON.parse(JSON.stringify(collectionRecord.value?.editor || {}))
|
|
51
|
+
if (isCreating.value) {
|
|
52
|
+
options.componentOptions.layout.length = 1
|
|
53
|
+
options.componentOptions.layout[0].sections.length = 1
|
|
54
|
+
}
|
|
55
|
+
return options.componentOptions
|
|
56
|
+
})
|
|
57
|
+
const record = computed(() => {
|
|
58
|
+
// atm. we use onRetrieve directly in page, it should be better to have this
|
|
59
|
+
// handled directly in `pinia-orm` lib
|
|
60
|
+
if (!isCreating.value) {
|
|
61
|
+
return Record.onRetrieve(
|
|
62
|
+
Record.repo.where('_id', $route.params.record).first()
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
return {}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// const status = computed(() => {
|
|
69
|
+
// const { record: { status } } = this
|
|
70
|
+
// return status
|
|
71
|
+
// }
|
|
72
|
+
|
|
73
|
+
// const alertSort = computed(() => {
|
|
74
|
+
// const { status, $auth } = this
|
|
75
|
+
// switch (status) {
|
|
76
|
+
// case 'waiting':
|
|
77
|
+
// return $auth.hasScope('validate:events') || $auth.hasScope('manage:all') ? '' : 'warn'
|
|
78
|
+
// case 'ready':
|
|
79
|
+
// return $auth.hasScope('publish:events') || $auth.hasScope('manage:all') ? '' : 'info'
|
|
80
|
+
// case 'published':
|
|
81
|
+
// case 'scheduled':
|
|
82
|
+
// return 'success'
|
|
83
|
+
// case 'draft':
|
|
84
|
+
// default:
|
|
85
|
+
// return ''
|
|
86
|
+
// }
|
|
87
|
+
// }
|
|
88
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="w-full">
|
|
3
|
+
<default-table
|
|
4
|
+
v-if="!$route.params.event"
|
|
5
|
+
:headers="headers"
|
|
6
|
+
:filters="filters"
|
|
7
|
+
:items="records"
|
|
8
|
+
:actions="abilities"
|
|
9
|
+
:pagination-size="limit"
|
|
10
|
+
display-search-bar
|
|
11
|
+
paginated-content
|
|
12
|
+
@fetch-more="_fetchMoreRecords"
|
|
13
|
+
@update="_updateRecord"
|
|
14
|
+
@remove="_removeRecord"
|
|
15
|
+
/>
|
|
16
|
+
</section>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
import defaultTable from '~/components/data/lists/data-lists-table'
|
|
21
|
+
|
|
22
|
+
// Prepare composable
|
|
23
|
+
const { Collection, Record } = useModels()
|
|
24
|
+
const $route = useRoute()
|
|
25
|
+
const $router = useRouter()
|
|
26
|
+
|
|
27
|
+
// Prepare page meta
|
|
28
|
+
definePageMeta({
|
|
29
|
+
permissions: [`read:records`], // TODO handle collections
|
|
30
|
+
preFetch: ['Record']
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Prepare reactive data
|
|
34
|
+
const limit = ref(25)
|
|
35
|
+
|
|
36
|
+
// Prepare computed
|
|
37
|
+
const collectionRecord = computed(() => Collection.repo.where('slug', $route.params.collection).first())
|
|
38
|
+
const records = computed(() => Record.repo.where('relatedCollection', $route.params.collection).get().map(Record.onRetrieve))
|
|
39
|
+
const headers = computed(() => collectionRecord.value.display?.headers || [])
|
|
40
|
+
const filters = computed(() => [])
|
|
41
|
+
const abilities = computed(() => ['update', 'remove'])
|
|
42
|
+
|
|
43
|
+
// Prepare methods
|
|
44
|
+
const _fetchMoreRecords = async (skip = 0) => {
|
|
45
|
+
await Record.list($route.params.collection, undefined, { skip, limit: limit.value })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const _updateRecord = (item) => {
|
|
49
|
+
$router.push(`${$route.path}/${item._id}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const _removeRecord = async (item) => {
|
|
53
|
+
await Record.remove(item._id, $route.params.collection)
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mx-auto max-w-4xl space-y-6 p-8">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<h1 class="text-xl font-semibold">
|
|
5
|
+
Laboratoire éditeur
|
|
6
|
+
</h1>
|
|
7
|
+
<UBadge :color="roundTripOk ? 'success' : 'error'" variant="subtle">
|
|
8
|
+
{{ roundTripOk ? 'structure conservée' : 'STRUCTURE ALTÉRÉE' }}
|
|
9
|
+
</UBadge>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- The real production input: same menus, same handlers. -->
|
|
13
|
+
<forms-inputs-input-editor
|
|
14
|
+
label="Contenu"
|
|
15
|
+
:value="doc"
|
|
16
|
+
@change="doc = $event"
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
<details class="text-xs">
|
|
20
|
+
<summary class="cursor-pointer font-medium">JSON courant (sortie de l'éditeur)</summary>
|
|
21
|
+
<pre class="mt-2 overflow-x-auto rounded bg-neutral-100 p-3 dark:bg-neutral-900">{{ JSON.stringify(doc, null, 2) }}</pre>
|
|
22
|
+
</details>
|
|
23
|
+
<details class="text-xs">
|
|
24
|
+
<summary class="cursor-pointer font-medium">JSON d'origine (échantillon des bases clients)</summary>
|
|
25
|
+
<pre class="mt-2 overflow-x-auto rounded bg-neutral-100 p-3 dark:bg-neutral-900">{{ JSON.stringify(SAMPLE, null, 2) }}</pre>
|
|
26
|
+
</details>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
// Editor lab: the assembled editor against a sample shaped like real client
|
|
32
|
+
// content, with a live conservation badge. Dev only.
|
|
33
|
+
definePageMeta({ auth: false })
|
|
34
|
+
|
|
35
|
+
if (!import.meta.dev) {
|
|
36
|
+
await navigateTo('/', { replace: true })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Sample mirroring stored client structures. Any unported node must raise
|
|
40
|
+
// onContentError — never a silent drop.
|
|
41
|
+
const SAMPLE = {
|
|
42
|
+
type: 'doc',
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'heading',
|
|
46
|
+
attrs: { level: 2, textAlign: 'left', uid: 'c3d13996-0170-4b13-9a17-2b5eb84400c4' },
|
|
47
|
+
content: [{ type: 'text', text: 'Titre de section' }]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'paragraph',
|
|
51
|
+
attrs: { textAlign: 'left', uid: '0d5b74a5-c4a5-4485-9d38-2c69e2d1c264' },
|
|
52
|
+
content: [
|
|
53
|
+
{ type: 'text', text: 'Du texte ' },
|
|
54
|
+
{ type: 'text', marks: [{ type: 'bold' }], text: 'gras' },
|
|
55
|
+
{ type: 'text', text: ', de l’' },
|
|
56
|
+
{ type: 'text', marks: [{ type: 'italic' }], text: 'italique' },
|
|
57
|
+
{ type: 'text', text: ', un ' },
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
marks: [{ type: 'link', attrs: { href: 'https://exemple.fr', target: '_blank', rel: 'noopener noreferrer nofollow', class: 'underline text-primary' } }],
|
|
61
|
+
text: 'lien'
|
|
62
|
+
},
|
|
63
|
+
{ type: 'text', text: ' et une ' },
|
|
64
|
+
{
|
|
65
|
+
type: 'text',
|
|
66
|
+
marks: [{ type: 'textStyle' }],
|
|
67
|
+
text: 'portion neutre'
|
|
68
|
+
},
|
|
69
|
+
{ type: 'text', text: '.' }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{ type: 'paragraph', attrs: { textAlign: 'left', uid: '7c9e8b1a-2f3d-4e5a-8b6c-1d2e3f4a5b6c' }, content: [{ type: 'hardBreak' }] },
|
|
73
|
+
{
|
|
74
|
+
type: 'bulletList',
|
|
75
|
+
attrs: { uid: '58e3399e-92e5-4a6a-b25a-039a4694e9e6' },
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'listItem',
|
|
79
|
+
attrs: { uid: 'f9dc0f8f-64f2-40bd-8827-d64ba9214d29' },
|
|
80
|
+
content: [{ type: 'paragraph', attrs: { textAlign: 'left', uid: 'a1b2c3d4-0000-4000-8000-000000000001' }, content: [{ type: 'text', text: 'Premier item' }] }]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'callout',
|
|
86
|
+
attrs: { icon: 'iconoir:megaphone', uid: 'a1b2c3d4-0000-4000-8000-000000000002' },
|
|
87
|
+
content: [{ type: 'text', text: 'Un encart d’information.' }]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'file',
|
|
91
|
+
attrs: {
|
|
92
|
+
id: '64a1f2e3b4c5d6e7f8a9b0c1',
|
|
93
|
+
src: 'https://bucket.exemple.fr/medias/document.pdf',
|
|
94
|
+
title: 'document.pdf',
|
|
95
|
+
alt: null,
|
|
96
|
+
uid: 'a1b2c3d4-0000-4000-8000-000000000004'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'image',
|
|
101
|
+
attrs: {
|
|
102
|
+
id: '64a1f2e3b4c5d6e7f8a9b0c2',
|
|
103
|
+
src: 'https://bucket.exemple.fr/medias/photo.jpg',
|
|
104
|
+
title: 'photo.jpg',
|
|
105
|
+
alt: 'Une photo',
|
|
106
|
+
uid: 'a1b2c3d4-0000-4000-8000-000000000005'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'embed',
|
|
111
|
+
attrs: {
|
|
112
|
+
service: 'youtube',
|
|
113
|
+
src: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
|
|
114
|
+
title: null,
|
|
115
|
+
icon: null,
|
|
116
|
+
placeholder: null,
|
|
117
|
+
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
|
118
|
+
allowfullscreen: true,
|
|
119
|
+
frameborder: '0',
|
|
120
|
+
height: 315
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'details',
|
|
125
|
+
attrs: { toggle: true },
|
|
126
|
+
content: [
|
|
127
|
+
{ type: 'detailsSummary', content: [{ type: 'text', text: 'Titre de l’accordéon' }] },
|
|
128
|
+
{
|
|
129
|
+
type: 'detailsContent',
|
|
130
|
+
content: [{ type: 'paragraph', attrs: { textAlign: 'left', uid: 'a1b2c3d4-0000-4000-8000-000000000006' }, content: [{ type: 'text', text: 'Contenu replié.' }] }]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{ type: 'horizontalRule' },
|
|
135
|
+
{
|
|
136
|
+
type: 'blockquote',
|
|
137
|
+
attrs: { uid: 'a1b2c3d4-0000-4000-8000-000000000007' },
|
|
138
|
+
content: [{ type: 'paragraph', attrs: { textAlign: 'left', uid: 'a1b2c3d4-0000-4000-8000-000000000008' }, content: [{ type: 'text', text: 'Une citation.' }] }]
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const doc = ref(structuredClone(SAMPLE))
|
|
144
|
+
|
|
145
|
+
// Green: load → output altered nothing. Red is a regression.
|
|
146
|
+
const roundTripOk = computed(
|
|
147
|
+
() => JSON.stringify(doc.value) === JSON.stringify(SAMPLE)
|
|
148
|
+
)
|
|
149
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-12 text-center text-sm text-gray-500">
|
|
3
|
+
Connexion en cours…
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
// Callback for externally issued sessions (OIDC and the like). The
|
|
9
|
+
// token arrives in the URL FRAGMENT so it never reaches logs or headers,
|
|
10
|
+
// and grants nothing extra: it was issued by this very instance. Absent or
|
|
11
|
+
// invalid, back to login without a technical error.
|
|
12
|
+
definePageMeta({ auth: false })
|
|
13
|
+
|
|
14
|
+
const session = useSession()
|
|
15
|
+
|
|
16
|
+
onMounted(async () => {
|
|
17
|
+
const token = new URLSearchParams(window.location.hash.slice(1)).get('token')
|
|
18
|
+
window.history.replaceState(null, '', '/auth/callback')
|
|
19
|
+
if (!token) {
|
|
20
|
+
return navigateTo('/', { replace: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
session.setToken(token)
|
|
24
|
+
const user = await session.refresh()
|
|
25
|
+
if (user) {
|
|
26
|
+
await navigateTo('/overview', { replace: true })
|
|
27
|
+
} else {
|
|
28
|
+
await navigateTo('/', { replace: true })
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-12">
|
|
3
|
+
<form class="space-y-6" form @submit.prevent="_signIn">
|
|
4
|
+
<input-text
|
|
5
|
+
place-holder="Adresse email"
|
|
6
|
+
name="email"
|
|
7
|
+
type="email"
|
|
8
|
+
autocomplete="email"
|
|
9
|
+
:value="emailAddress"
|
|
10
|
+
:read-only="isLoading"
|
|
11
|
+
required
|
|
12
|
+
@change="emailAddress = $event"
|
|
13
|
+
/>
|
|
14
|
+
<input-text
|
|
15
|
+
place-holder="Mot de passe"
|
|
16
|
+
name="password"
|
|
17
|
+
type="password"
|
|
18
|
+
autocomplete="current-password"
|
|
19
|
+
:value="password"
|
|
20
|
+
:read-only="isLoading"
|
|
21
|
+
required
|
|
22
|
+
@change="password = $event"
|
|
23
|
+
/>
|
|
24
|
+
<button-primary type="submit" label="S'identifier" :loading="isLoading" class="w-full" />
|
|
25
|
+
</form>
|
|
26
|
+
<!-- Le lien de réinitialisation était commenté depuis le legacy, qui
|
|
27
|
+
n'envoyait AUCUN email : le seul recours était le formulaire de
|
|
28
|
+
support. L'événement `passwordResetRequested` étant désormais relayé
|
|
29
|
+
vers un fournisseur d'emails, le parcours autonome reprend sa place —
|
|
30
|
+
et Fillout redevient ce qu'il doit être, un second recours. -->
|
|
31
|
+
<div class="flex mt-8 flex-col items-center justify-center">
|
|
32
|
+
<button-tertiary
|
|
33
|
+
label="Vous avez oublié votre mot de passe ?"
|
|
34
|
+
@click="$router.push({ path: '/password/recover' })"
|
|
35
|
+
/>
|
|
36
|
+
<button-tertiary class="h-10" label="Je n'arrive pas à m'identifier" @click="_openForm()" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script setup>
|
|
42
|
+
import { useNotificationsStore } from '~/store/layout/notifications'
|
|
43
|
+
import InputText from '~/components/forms/inputs/input-text'
|
|
44
|
+
import ButtonPrimary from '~/components/elements/buttons/primary'
|
|
45
|
+
import ButtonTertiary from '~/components/elements/buttons/tertiary'
|
|
46
|
+
|
|
47
|
+
// Prepare composables
|
|
48
|
+
const notificationsStore = useNotificationsStore()
|
|
49
|
+
const session = useSession()
|
|
50
|
+
|
|
51
|
+
// Prepare reactive data
|
|
52
|
+
const isLoading = ref(false)
|
|
53
|
+
const emailAddress = ref('')
|
|
54
|
+
const password = ref('')
|
|
55
|
+
|
|
56
|
+
// Prepare emitters
|
|
57
|
+
defineEmits(['enter'])
|
|
58
|
+
|
|
59
|
+
// Prepare methods
|
|
60
|
+
const _openForm = () => {
|
|
61
|
+
// https://forms.fillout.com/t/vjkLG9ZXsLus
|
|
62
|
+
const formUrl = 'https://forms.fillout.com/t/vjkLG9ZXsLus'
|
|
63
|
+
const formWindow = window.open(formUrl, '_blank')
|
|
64
|
+
if (formWindow) {
|
|
65
|
+
// Browser has allowed it to be opened
|
|
66
|
+
formWindow.focus()
|
|
67
|
+
} else {
|
|
68
|
+
// Browser has blocked it
|
|
69
|
+
alert('Please allow popups for this website')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const _signIn = async () => {
|
|
73
|
+
isLoading.value = true
|
|
74
|
+
try {
|
|
75
|
+
await session.login(emailAddress.value, password.value)
|
|
76
|
+
notificationsStore.add({ icon: 'iconoir:password-pass', type: 'success', title: 'Authentification réussie.' })
|
|
77
|
+
await navigateTo('/overview')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log(err)
|
|
80
|
+
notificationsStore.add({ icon: 'iconoir:password-error', type: 'warn', title: 'Identifiants incorrects.' })
|
|
81
|
+
} finally {
|
|
82
|
+
isLoading.value = false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
</script>
|