@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-12 text-center">
|
|
3
|
+
<h2 class="inline-flex mt-6 text-sm font-semibold">
|
|
4
|
+
Nouveau mot de passe
|
|
5
|
+
</h2>
|
|
6
|
+
<p class="mt-4 font-light text-sm text-gray-600">
|
|
7
|
+
Choisissez un mot de passe robuste (10 caractères minimum).
|
|
8
|
+
</p>
|
|
9
|
+
<form class="mt-12 space-y-6" @submit.prevent="_updatePassword">
|
|
10
|
+
<input-text
|
|
11
|
+
place-holder="Mot de passe"
|
|
12
|
+
name="password"
|
|
13
|
+
type="password"
|
|
14
|
+
autocomplete="new-password"
|
|
15
|
+
:value="password"
|
|
16
|
+
required
|
|
17
|
+
@change="password = $event"
|
|
18
|
+
/>
|
|
19
|
+
<input-text
|
|
20
|
+
place-holder="Confirmation"
|
|
21
|
+
name="password-confirmation"
|
|
22
|
+
type="password"
|
|
23
|
+
autocomplete="new-password"
|
|
24
|
+
:value="passwordConfirmation"
|
|
25
|
+
required
|
|
26
|
+
@change="passwordConfirmation = $event"
|
|
27
|
+
/>
|
|
28
|
+
<button-primary
|
|
29
|
+
type="submit"
|
|
30
|
+
:label="isLoading ? 'Envoie en cours' : 'Confirmer'"
|
|
31
|
+
:disabled="!password || password !== passwordConfirmation"
|
|
32
|
+
:loading="isLoading"
|
|
33
|
+
class="w-full"
|
|
34
|
+
/>
|
|
35
|
+
</form>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
import { useNotificationsStore } from '~/store/layout/notifications'
|
|
41
|
+
import InputText from '~/components/forms/inputs/input-text'
|
|
42
|
+
import ButtonPrimary from '~/components/elements/buttons/primary'
|
|
43
|
+
|
|
44
|
+
// Prepare composables
|
|
45
|
+
const notificationsStore = useNotificationsStore()
|
|
46
|
+
const $route = useRoute()
|
|
47
|
+
const $router = useRouter()
|
|
48
|
+
const { Entrance } = useModels()
|
|
49
|
+
|
|
50
|
+
// Prepare data
|
|
51
|
+
const password = ref('')
|
|
52
|
+
const passwordConfirmation = ref('')
|
|
53
|
+
const isLoading = ref(false)
|
|
54
|
+
|
|
55
|
+
// Prepare methods
|
|
56
|
+
// Cette page ne sert QUE la réinitialisation : le compte existe déjà, son nom
|
|
57
|
+
// est conservé côté serveur. L'invitation, elle, a son propre écran (/welcome)
|
|
58
|
+
// — c'est le seul parcours qui a un nom d'affichage à demander. Les deux
|
|
59
|
+
// partageaient cet écran, et un utilisateur venu du mail « mot de passe
|
|
60
|
+
// oublié » se voyait réclamer un nom sous un titre « Votre compte ».
|
|
61
|
+
const _updatePassword = async () => {
|
|
62
|
+
isLoading.value = true
|
|
63
|
+
try {
|
|
64
|
+
const { token } = $route.params
|
|
65
|
+
await Entrance.updatePassword({ token, password: password.value })
|
|
66
|
+
notificationsStore.add({ icon: 'iconoir:password-cursor', type: 'success', title: 'Mot de passe mis à jour, connectez-vous.' })
|
|
67
|
+
$router.push('/')
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(err)
|
|
70
|
+
notificationsStore.add({
|
|
71
|
+
icon: 'iconoir:password-error',
|
|
72
|
+
type: 'warn',
|
|
73
|
+
title: 'Lien expiré ou déjà utilisé, ou mot de passe trop court (10 caractères minimum).'
|
|
74
|
+
})
|
|
75
|
+
} finally {
|
|
76
|
+
isLoading.value = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-12 text-center">
|
|
3
|
+
<h2 class="inline-flex mt-6 text-sm font-semibold">
|
|
4
|
+
{{ title }}
|
|
5
|
+
</h2>
|
|
6
|
+
<p class="mt-4 font-light text-sm text-gray-600">
|
|
7
|
+
{{ message }}
|
|
8
|
+
</p>
|
|
9
|
+
<form v-if="!isSent" class="mt-12 space-y-6" @submit.prevent="_recoverPassword">
|
|
10
|
+
<input-text
|
|
11
|
+
place-holder="Adresse email"
|
|
12
|
+
name="email"
|
|
13
|
+
type="email"
|
|
14
|
+
autocomplete="email"
|
|
15
|
+
:value="emailAddress"
|
|
16
|
+
:read-only="isLoading"
|
|
17
|
+
required
|
|
18
|
+
@change="emailAddress = $event"
|
|
19
|
+
/>
|
|
20
|
+
<button-primary
|
|
21
|
+
type="submit"
|
|
22
|
+
:label="isLoading ? 'Envoie en cours' : 'Continuer'"
|
|
23
|
+
:disabled="!emailAddress"
|
|
24
|
+
:loading="isLoading"
|
|
25
|
+
class="w-full"
|
|
26
|
+
/>
|
|
27
|
+
</form>
|
|
28
|
+
<button-primary
|
|
29
|
+
v-if="isSent"
|
|
30
|
+
label="← Revenir à l'espace de connexion"
|
|
31
|
+
class="w-full mt-8"
|
|
32
|
+
@click="$router.push('/')"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { useNotificationsStore } from '~/store/layout/notifications'
|
|
39
|
+
import InputText from '~/components/forms/inputs/input-text'
|
|
40
|
+
import ButtonPrimary from '~/components/elements/buttons/primary'
|
|
41
|
+
|
|
42
|
+
// Prepare composables
|
|
43
|
+
const notificationsStore = useNotificationsStore()
|
|
44
|
+
const { Entrance } = useModels()
|
|
45
|
+
|
|
46
|
+
// Prepare data
|
|
47
|
+
const isLoading = ref(false)
|
|
48
|
+
const isSent = ref(false)
|
|
49
|
+
const emailAddress = ref('')
|
|
50
|
+
|
|
51
|
+
// Prepare computed
|
|
52
|
+
const title = computed(() => {
|
|
53
|
+
return isSent.value ? 'Le mail est parti' : 'Réinitialiser votre mot de passe'
|
|
54
|
+
})
|
|
55
|
+
const message = computed(() => {
|
|
56
|
+
return isSent.value ? 'N\'hésitez pas à vérifier vos courriers indesirables.' : 'Entrez votre adresse email et nous vous enverrons un lien pour mettre à jour votre mot de passe.'
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Prepare methods
|
|
60
|
+
const _recoverPassword = async () => {
|
|
61
|
+
isLoading.value = true
|
|
62
|
+
try {
|
|
63
|
+
await Entrance.recoverPassword(emailAddress.value)
|
|
64
|
+
notificationsStore.add({ icon: 'iconoir:mail-out', type: 'success', title: 'Email envoyé.' })
|
|
65
|
+
isSent.value = true
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.log(err) // eslint-disable-line
|
|
68
|
+
} finally {
|
|
69
|
+
setTimeout(() => { isLoading.value = false }, 2 * 1000)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-12 text-center">
|
|
3
|
+
<h2 class="inline-flex mt-6 text-sm font-semibold">
|
|
4
|
+
Bienvenue !
|
|
5
|
+
</h2>
|
|
6
|
+
<p class="mt-4 font-light text-sm text-gray-600">
|
|
7
|
+
Vous avez été invité·e à rejoindre l'espace d'administration.
|
|
8
|
+
Choisissez votre nom d'affichage et un mot de passe robuste (10 caractères minimum).
|
|
9
|
+
</p>
|
|
10
|
+
<form class="mt-12 space-y-6" @submit.prevent="_createAccount">
|
|
11
|
+
<input-text
|
|
12
|
+
place-holder="Votre nom"
|
|
13
|
+
name="name"
|
|
14
|
+
autocomplete="name"
|
|
15
|
+
:value="name"
|
|
16
|
+
required
|
|
17
|
+
@change="name = $event"
|
|
18
|
+
/>
|
|
19
|
+
<input-text
|
|
20
|
+
place-holder="Mot de passe"
|
|
21
|
+
name="password"
|
|
22
|
+
type="password"
|
|
23
|
+
autocomplete="new-password"
|
|
24
|
+
:value="password"
|
|
25
|
+
required
|
|
26
|
+
@change="password = $event"
|
|
27
|
+
/>
|
|
28
|
+
<input-text
|
|
29
|
+
place-holder="Confirmation"
|
|
30
|
+
name="password-confirmation"
|
|
31
|
+
type="password"
|
|
32
|
+
autocomplete="new-password"
|
|
33
|
+
:value="passwordConfirmation"
|
|
34
|
+
required
|
|
35
|
+
@change="passwordConfirmation = $event"
|
|
36
|
+
/>
|
|
37
|
+
<button-primary
|
|
38
|
+
type="submit"
|
|
39
|
+
:label="isLoading ? 'Envoie en cours' : 'Créer mon compte'"
|
|
40
|
+
:disabled="!name || !password || password !== passwordConfirmation"
|
|
41
|
+
:loading="isLoading"
|
|
42
|
+
class="w-full"
|
|
43
|
+
/>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup>
|
|
49
|
+
import { useNotificationsStore } from '~/store/layout/notifications'
|
|
50
|
+
import InputText from '~/components/forms/inputs/input-text'
|
|
51
|
+
import ButtonPrimary from '~/components/elements/buttons/primary'
|
|
52
|
+
|
|
53
|
+
// Flux invitation Commun : la validité du jeton est vérifiée au moment de
|
|
54
|
+
// l'acceptation (auth.acceptInvitation) — plus d'étape « confirmation d'email »
|
|
55
|
+
// préalable.
|
|
56
|
+
//
|
|
57
|
+
// L'écran porte désormais le formulaire au lieu de renvoyer sur
|
|
58
|
+
// /password/define : cette page-là ne sert plus qu'à la réinitialisation, où
|
|
59
|
+
// il n'y a pas de nom à demander.
|
|
60
|
+
const notificationsStore = useNotificationsStore()
|
|
61
|
+
const $route = useRoute()
|
|
62
|
+
const $router = useRouter()
|
|
63
|
+
const { Entrance } = useModels()
|
|
64
|
+
|
|
65
|
+
// Prepare data
|
|
66
|
+
const name = ref('')
|
|
67
|
+
const password = ref('')
|
|
68
|
+
const passwordConfirmation = ref('')
|
|
69
|
+
const isLoading = ref(false)
|
|
70
|
+
|
|
71
|
+
// Prepare methods
|
|
72
|
+
const _createAccount = async () => {
|
|
73
|
+
isLoading.value = true
|
|
74
|
+
try {
|
|
75
|
+
const { token } = $route.params
|
|
76
|
+
await Entrance.acceptInvitation({ token, name: name.value, password: password.value })
|
|
77
|
+
notificationsStore.add({ icon: 'iconoir:password-cursor', type: 'success', title: 'Compte activé, connectez-vous.' })
|
|
78
|
+
$router.push('/')
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.log(err)
|
|
81
|
+
notificationsStore.add({
|
|
82
|
+
icon: 'iconoir:password-error',
|
|
83
|
+
type: 'warn',
|
|
84
|
+
title: 'Invitation invalide ou mot de passe trop court (10 caractères minimum).'
|
|
85
|
+
})
|
|
86
|
+
} finally {
|
|
87
|
+
isLoading.value = false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
</script>
|
package/pages/index.vue
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="min-h-screen bg-white flex">
|
|
3
|
+
<div class="m-auto w-full max-w-lg lg:w-96">
|
|
4
|
+
<div class="flex flex-col items-center space-y-2 mb-4">
|
|
5
|
+
<img
|
|
6
|
+
src="/logo-black.svg"
|
|
7
|
+
alt="Logo"
|
|
8
|
+
class="dark:hidden h-16 w-auto"
|
|
9
|
+
>
|
|
10
|
+
<img
|
|
11
|
+
src="/logo-white.svg"
|
|
12
|
+
alt="Logo"
|
|
13
|
+
class="hidden h-24 w-auto"
|
|
14
|
+
>
|
|
15
|
+
<p v-if="demoEnv" class="text-sm text-gray-400 w-full border-t border-gray-100 text-center pt-2">
|
|
16
|
+
<span class="bg-black rounded-lg text-white px-4 text-xs py-0.5 h-5 mx-auto">
|
|
17
|
+
beta
|
|
18
|
+
</span>
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
<nuxt-page />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup>
|
|
27
|
+
const $config = useRuntimeConfig()
|
|
28
|
+
|
|
29
|
+
definePageMeta({
|
|
30
|
+
layout: 'entrance',
|
|
31
|
+
auth: {
|
|
32
|
+
unauthenticatedOnly: true,
|
|
33
|
+
navigateAuthenticatedTo: '/overview',
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Prepare computed
|
|
38
|
+
// L'image d'instance est COMMUNE à tous les clients : rien de spécifique n'y est
|
|
39
|
+
// cuit. L'hôte servi est donc la seule source fiable pour distinguer une démo.
|
|
40
|
+
const demoEnv = computed(() => {
|
|
41
|
+
const host = import.meta.client ? window.location.host : ''
|
|
42
|
+
return host.includes('.beta') || host.includes('localhost')
|
|
43
|
+
})
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative block w-full">
|
|
3
|
+
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
definePageMeta({
|
|
9
|
+
permissions: ['manage:all'],
|
|
10
|
+
headings: {
|
|
11
|
+
title: 'Bienvenue',
|
|
12
|
+
description: 'Cette section est réservée aux administrateurs.'
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="w-full">
|
|
3
|
+
<button-secondary label="Inviter un membre" class="mb-6" @click="_openModalForm('create')" />
|
|
4
|
+
<default-table
|
|
5
|
+
v-if="!$route.params.event"
|
|
6
|
+
:headers="TABLE_HEADERS"
|
|
7
|
+
:filters="filters"
|
|
8
|
+
:items="records"
|
|
9
|
+
:actions="abilities"
|
|
10
|
+
:pagination-size="limit"
|
|
11
|
+
display-search-bar
|
|
12
|
+
paginated-content
|
|
13
|
+
@fetch-more="_fetchMore"
|
|
14
|
+
@update="_openModalForm('update', $event)"
|
|
15
|
+
@remove="_handle('remove', $event)"
|
|
16
|
+
/>
|
|
17
|
+
</section>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import buttonSecondary from '~/components/elements/buttons/secondary'
|
|
22
|
+
import defaultTable from '~/components/data/lists/data-lists-table'
|
|
23
|
+
import { useModalStore } from '~/store/layout/modal'
|
|
24
|
+
import { useNotificationsStore } from '~/store/layout/notifications'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
definePageMeta({
|
|
28
|
+
permissions: ['manage:all'],
|
|
29
|
+
preFetch: ['User'],
|
|
30
|
+
headings: {
|
|
31
|
+
title: 'Membres',
|
|
32
|
+
description: 'Retrouvez & managez ici toutes les membres de la plateforme.'
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const { User } = useModels()
|
|
37
|
+
const $route = useRoute()
|
|
38
|
+
const $modalStore = useModalStore()
|
|
39
|
+
const notificationsStore = useNotificationsStore()
|
|
40
|
+
|
|
41
|
+
// Prepare constants
|
|
42
|
+
const TABLE_HEADERS = [
|
|
43
|
+
{
|
|
44
|
+
nestedValue: false,
|
|
45
|
+
componentOptions: {
|
|
46
|
+
titleKey: 'fullName',
|
|
47
|
+
descriptionKey: 'emailAddress',
|
|
48
|
+
},
|
|
49
|
+
component: 'data-summary'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: 'Rôle',
|
|
53
|
+
property: 'role',
|
|
54
|
+
component: 'data-text'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
component: "data-spacer",
|
|
58
|
+
grow: true
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
component: 'actions',
|
|
62
|
+
componentOptions: {
|
|
63
|
+
options: [{ label: 'Voir', action: 'read' }, { label: 'Modifier', action: 'update' }, { label: 'Supprimer', action: 'remove' }]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// Prepare reactive data
|
|
69
|
+
const limit = ref(25)
|
|
70
|
+
|
|
71
|
+
// Prepare computed
|
|
72
|
+
const records = computed(() => User.repo.all())
|
|
73
|
+
const filters = computed(() => [])
|
|
74
|
+
const abilities = computed(() => ['update', 'remove'])
|
|
75
|
+
|
|
76
|
+
// Prepare methods
|
|
77
|
+
const _fetchMore = async (skip = 0) => {
|
|
78
|
+
await User.list(undefined, undefined, { skip, limit: limit.value })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Prepare methods
|
|
82
|
+
const _handle = async (action, data = {}) => {
|
|
83
|
+
const { _id } = data
|
|
84
|
+
switch (action) {
|
|
85
|
+
case 'create': {
|
|
86
|
+
// Une INVITATION est émise : l'email part côté serveur, et le lien est
|
|
87
|
+
// copié au presse-papier comme second recours.
|
|
88
|
+
// L'origine vient du navigateur, jamais d'une URL cuite au build —
|
|
89
|
+
// l'image d'instance est commune à tous les clients.
|
|
90
|
+
const { token } = await User.create(data)
|
|
91
|
+
const link = `${window.location.origin}/welcome/${token}`
|
|
92
|
+
try {
|
|
93
|
+
await navigator.clipboard.writeText(link)
|
|
94
|
+
notificationsStore.add({
|
|
95
|
+
icon: 'iconoir:mail-out',
|
|
96
|
+
type: 'success',
|
|
97
|
+
title: 'Lien d\'invitation copié dans le presse-papier (valable 7 jours).'
|
|
98
|
+
})
|
|
99
|
+
} catch {
|
|
100
|
+
console.log('Lien d\'invitation :', link)
|
|
101
|
+
notificationsStore.add({
|
|
102
|
+
icon: 'iconoir:mail-out',
|
|
103
|
+
type: 'success',
|
|
104
|
+
title: 'Invitation créée — lien affiché dans la console (valable 7 jours).'
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
await User.list()
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
case 'update':
|
|
111
|
+
await User.update(_id, data)
|
|
112
|
+
break
|
|
113
|
+
case 'remove':
|
|
114
|
+
await User.remove(data._id)
|
|
115
|
+
break
|
|
116
|
+
default:
|
|
117
|
+
throw new Error('E_UNHANDLED')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const _openModalForm = (action, record) => {
|
|
122
|
+
$modalStore.open({
|
|
123
|
+
onSubmit: async (data) => _handle(action, data),
|
|
124
|
+
details: {
|
|
125
|
+
title: `${action === 'create' ? 'Inviter' : 'Modifier'} un membre`,
|
|
126
|
+
subTitle: action === 'create'
|
|
127
|
+
? 'Un lien d\'invitation sera généré pour ce nouveau membre.'
|
|
128
|
+
: 'Modifiez les informations du membre.',
|
|
129
|
+
acceptButton: `${action === 'create' ? 'Inviter' : 'Modifier'}`,
|
|
130
|
+
closeButton: 'Annuler'
|
|
131
|
+
},
|
|
132
|
+
attributes: action === 'create'
|
|
133
|
+
? [
|
|
134
|
+
{
|
|
135
|
+
property: 'emailAddress',
|
|
136
|
+
componentOptions: {
|
|
137
|
+
label: 'Adresse mail',
|
|
138
|
+
name: 'emailAddress'
|
|
139
|
+
},
|
|
140
|
+
component: 'input-text'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
property: 'role',
|
|
144
|
+
componentOptions: {
|
|
145
|
+
label: 'Rôle',
|
|
146
|
+
name: 'role',
|
|
147
|
+
items: ['admin', 'redacteur'],
|
|
148
|
+
object: false
|
|
149
|
+
},
|
|
150
|
+
component: 'select-enum'
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
: [
|
|
154
|
+
{
|
|
155
|
+
property: 'firstName',
|
|
156
|
+
componentOptions: {
|
|
157
|
+
label: 'Nom d\'affichage',
|
|
158
|
+
name: 'firstName'
|
|
159
|
+
},
|
|
160
|
+
component: 'input-text'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
property: 'role',
|
|
164
|
+
componentOptions: {
|
|
165
|
+
label: 'Rôle',
|
|
166
|
+
name: 'role',
|
|
167
|
+
items: ['admin', 'redacteur'],
|
|
168
|
+
object: false
|
|
169
|
+
},
|
|
170
|
+
component: 'select-enum'
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
record
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
</script>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative block w-full">
|
|
3
|
+
<!-- Instance single-tenant : une seule collectivité par déploiement. -->
|
|
4
|
+
<p class="mb-6 rounded-md bg-amber-50 border border-amber-200 px-4 py-3 text-sm text-amber-800">
|
|
5
|
+
Cette instance héberge une seule collectivité — la création d'organisations n'est plus disponible.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<data-list :items="sortedOrganizations" :density="1" :density-children="2" :group-by="'type'" :item-value="'type'" class="space-y-4 bg-gray-50 p-2 rounded-md">
|
|
9
|
+
<template #item="{ item }">
|
|
10
|
+
<h3 class="text-gray-400 text-sm font-medium mb-2 pl-2">
|
|
11
|
+
@ {{ item.type }}
|
|
12
|
+
</h3>
|
|
13
|
+
</template>
|
|
14
|
+
<template #item-child="{ item: itemChild }">
|
|
15
|
+
<data-list-row
|
|
16
|
+
:item="itemChild"
|
|
17
|
+
:to="`/${itemChild._id}`"
|
|
18
|
+
:headers="ROW_DEFINITION"
|
|
19
|
+
class="border border-gray-100 bg-white shadow-sm rounded-lg"
|
|
20
|
+
@click="_handleRowEvent"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
23
|
+
</data-list>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup>
|
|
28
|
+
import ButtonSecondary from '~/components/elements/buttons/secondary'
|
|
29
|
+
import dataList from '~/components/data/lists/data-list'
|
|
30
|
+
import dataListRow from '~/components/data/display/row'
|
|
31
|
+
import { useWorkspaceStore } from '~/store/layout/workspace'
|
|
32
|
+
import { useModalStore } from '~/store/layout/modal'
|
|
33
|
+
|
|
34
|
+
definePageMeta({
|
|
35
|
+
permissions: ['manage:all'],
|
|
36
|
+
preFetch: ['Organization'],
|
|
37
|
+
headings: {
|
|
38
|
+
title: 'Organisations',
|
|
39
|
+
description: 'Retrouvez & managez ici toutes les organisations.'
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const { Organization } = useModels()
|
|
44
|
+
const modalStore = useModalStore()
|
|
45
|
+
|
|
46
|
+
// Prepare constants
|
|
47
|
+
const ORGANIZATIONS_ORDER = ['customer', 'network', 'country', 'bu', 'root'];
|
|
48
|
+
const ROW_DEFINITION = [
|
|
49
|
+
{
|
|
50
|
+
component: 'data-avatar',
|
|
51
|
+
nestedValue: false
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
nestedValue: false,
|
|
55
|
+
componentOptions: {
|
|
56
|
+
titleKey: 'name',
|
|
57
|
+
descriptionKey: 'path',
|
|
58
|
+
},
|
|
59
|
+
component: 'data-summary'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
component: "data-spacer",
|
|
63
|
+
grow: true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
component: 'actions',
|
|
67
|
+
componentOptions: {
|
|
68
|
+
options: [{ label: 'Voir', action: 'read' }]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
// Prepare data
|
|
74
|
+
const { data: organizations } = await useAsyncData('overview', () => Organization.list())
|
|
75
|
+
|
|
76
|
+
// Prepare computed
|
|
77
|
+
const sortedOrganizations = computed(() => organizations.value.sort((a, b) => ORGANIZATIONS_ORDER.indexOf(a.type) - ORGANIZATIONS_ORDER.indexOf(b.type)))
|
|
78
|
+
|
|
79
|
+
// Prepare methods
|
|
80
|
+
const _handleRowEvent = async (eventName, value) => {
|
|
81
|
+
console.log(eventName, value)
|
|
82
|
+
}
|
|
83
|
+
// import { UPDATE_ORGANIZATION } from '~/store/actions'
|
|
84
|
+
// import buttonSecondary from '~/components/elements/buttons/secondary'
|
|
85
|
+
|
|
86
|
+
// export default {
|
|
87
|
+
// name: 'Organizations',
|
|
88
|
+
// meta: {
|
|
89
|
+
// permissions: ['entrance:self'],
|
|
90
|
+
// preFetch: ['Organization'],
|
|
91
|
+
// headings: {
|
|
92
|
+
// title: 'Organisations',
|
|
93
|
+
// description: 'Retrouvez & managez ici toutes les organisations.'
|
|
94
|
+
// }
|
|
95
|
+
// },
|
|
96
|
+
// components: { buttonSecondary },
|
|
97
|
+
// computed: {
|
|
98
|
+
// organizations () {
|
|
99
|
+
// return this.$api.Organization.all()
|
|
100
|
+
// }
|
|
101
|
+
// },
|
|
102
|
+
// beforeDestroy () {
|
|
103
|
+
// this.$nuxt.$off('modalCreateOrganization')
|
|
104
|
+
// },
|
|
105
|
+
// mounted () {
|
|
106
|
+
// this.$nuxt.$on('modalCreateOrganization', this.createOrganization)
|
|
107
|
+
// },
|
|
108
|
+
// methods: {
|
|
109
|
+
// async updateSelectedOrganization (organizationRecord) {
|
|
110
|
+
// try {
|
|
111
|
+
// await this.$store.dispatch(UPDATE_ORGANIZATION, organizationRecord)
|
|
112
|
+
// this.$router.replace(`${organizationRecord.slug}/`)
|
|
113
|
+
// } catch (err) {
|
|
114
|
+
// console.log(err) // eslint-disable-line
|
|
115
|
+
// }
|
|
116
|
+
// },
|
|
117
|
+
// openModalForm (record) {
|
|
118
|
+
// this.$nuxt.$emit('openModalForm', {
|
|
119
|
+
// submitEvent: 'modalCreateOrganization',
|
|
120
|
+
// details: {
|
|
121
|
+
// title: 'Créer une nouvelle organisation',
|
|
122
|
+
// subTitle: 'Renseignez cette nouvelle entité.'
|
|
123
|
+
// },
|
|
124
|
+
// attributes: [
|
|
125
|
+
// // directParent
|
|
126
|
+
// {
|
|
127
|
+
// property: 'directParent',
|
|
128
|
+
// componentOptions: {
|
|
129
|
+
// placeHolder: 'Parent de l\'organisation',
|
|
130
|
+
// name: 'directParent',
|
|
131
|
+
// model: 'Organization',
|
|
132
|
+
// object: true,
|
|
133
|
+
// itemKey: 'name',
|
|
134
|
+
// itemValue: 'slug'
|
|
135
|
+
// },
|
|
136
|
+
// component: 'select-model'
|
|
137
|
+
// },
|
|
138
|
+
// {
|
|
139
|
+
// property: 'type',
|
|
140
|
+
// componentOptions: {
|
|
141
|
+
// placeHolder: 'Type d\'organisation',
|
|
142
|
+
// name: 'type',
|
|
143
|
+
// items: [
|
|
144
|
+
// 'bu',
|
|
145
|
+
// 'country',
|
|
146
|
+
// 'network',
|
|
147
|
+
// 'customer'
|
|
148
|
+
// ]
|
|
149
|
+
// },
|
|
150
|
+
// component: 'select-enum'
|
|
151
|
+
// },
|
|
152
|
+
// {
|
|
153
|
+
// property: 'slug',
|
|
154
|
+
// componentOptions: {
|
|
155
|
+
// placeHolder: 'Identifiant de l\'organisation (slug)',
|
|
156
|
+
// name: 'slug'
|
|
157
|
+
// },
|
|
158
|
+
// component: 'input-text'
|
|
159
|
+
// },
|
|
160
|
+
// {
|
|
161
|
+
// property: 'name',
|
|
162
|
+
// componentOptions: {
|
|
163
|
+
// placeHolder: 'Nom de l\'organisation',
|
|
164
|
+
// name: 'name'
|
|
165
|
+
// },
|
|
166
|
+
// component: 'input-text'
|
|
167
|
+
// },
|
|
168
|
+
// {
|
|
169
|
+
// property: 'type',
|
|
170
|
+
// componentOptions: {
|
|
171
|
+
// placeHolder: 'Localisation',
|
|
172
|
+
// name: 'organization'
|
|
173
|
+
// },
|
|
174
|
+
// component: 'input-location'
|
|
175
|
+
// }
|
|
176
|
+
// ],
|
|
177
|
+
// record
|
|
178
|
+
// })
|
|
179
|
+
// },
|
|
180
|
+
// async createOrganization (data = {}) {
|
|
181
|
+
// this.$nuxt.$loading.start()
|
|
182
|
+
// try {
|
|
183
|
+
// await this.$api.Organization.create(data)
|
|
184
|
+
// } catch (err) {
|
|
185
|
+
// console.log(err)
|
|
186
|
+
// } finally {
|
|
187
|
+
// this.$nuxt.$loading.finish()
|
|
188
|
+
// }
|
|
189
|
+
// }
|
|
190
|
+
// }
|
|
191
|
+
// }
|
|
192
|
+
</script>
|