@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,189 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative block w-full">
|
|
3
|
+
<!-- <div class="bg-gray-200 text-gray-500 text-sm rounded-md w-full p-4">
|
|
4
|
+
/!\ Navigation should be based on organization.collections array only<br>
|
|
5
|
+
Collections that does not belong to organization can't be updated or deleted only enabled / disabled<br>
|
|
6
|
+
Let user duplicate an existing collection for this organization only
|
|
7
|
+
</div> -->
|
|
8
|
+
<button-secondary class="my-1" label="Ajouter une collection" @click="_openModalForm('add')" />
|
|
9
|
+
<list-table
|
|
10
|
+
:headers="TABLE_HEADERS"
|
|
11
|
+
:items="collections"
|
|
12
|
+
:actions="['update', 'remove']"
|
|
13
|
+
@update="_openModalForm('update', $event)"
|
|
14
|
+
@remove="_handleCollection('remove', $event)"
|
|
15
|
+
@usage="_updateCollectionUsage($event)"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import listTable from '~/components/data/lists/data-lists-table'
|
|
22
|
+
import buttonSecondary from '~/components/elements/buttons/secondary'
|
|
23
|
+
import { useModalStore } from '~/store/layout/modal'
|
|
24
|
+
import { useWorkspaceStore } from '~/store/layout/workspace'
|
|
25
|
+
|
|
26
|
+
const { Organization, Collection } = useModels()
|
|
27
|
+
const { open } = useModalStore()
|
|
28
|
+
const $workspaceStore = useWorkspaceStore()
|
|
29
|
+
|
|
30
|
+
// prepare constants
|
|
31
|
+
const TABLE_HEADERS = [
|
|
32
|
+
{
|
|
33
|
+
title: 'Utilisation',
|
|
34
|
+
property: 'usage',
|
|
35
|
+
component: 'data-toggle'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Nom',
|
|
39
|
+
property: 'name',
|
|
40
|
+
component: 'data-text'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'Organisation',
|
|
44
|
+
property: 'organization',
|
|
45
|
+
component: 'data-badge'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'Dernière modification',
|
|
49
|
+
nestedValue: false,
|
|
50
|
+
alignRight: true,
|
|
51
|
+
component: 'data-last-update',
|
|
52
|
+
componentOptions: {
|
|
53
|
+
inline: true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: '',
|
|
58
|
+
component: 'actions'
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
// Prepare dynamic data
|
|
63
|
+
const availableCollections = ref([])
|
|
64
|
+
|
|
65
|
+
// Prepare computed
|
|
66
|
+
const organizationId = computed(() => $workspaceStore.workspaceId)
|
|
67
|
+
const organizationPath = computed(() => $workspaceStore.workspacePath)
|
|
68
|
+
const organizationCollections = computed(() => $workspaceStore.workspaceCollections)
|
|
69
|
+
const collections = computed(() => {
|
|
70
|
+
const collectionRecords = Collection.repo.all()
|
|
71
|
+
return collectionRecords.map(({ _id, ...rest }) => ({ _id, usage: organizationCollections.value.includes(_id), ...rest }))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Prepare methods
|
|
75
|
+
const _updateCollectionUsage = async ({ _id, usage }) => {
|
|
76
|
+
if (usage && !organizationCollections.value.includes(_id)) {
|
|
77
|
+
await Organization.update(organizationId.value, { collections: [...organizationCollections.value, _id] })
|
|
78
|
+
}
|
|
79
|
+
if (!usage && organizationCollections.value.includes(_id)) {
|
|
80
|
+
await Organization.update(organizationId.value, { collections: [...organizationCollections.value.filter(check => check !== _id)] })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const _handleCollection = async (action, data = {}) => {
|
|
85
|
+
const { _id } = data
|
|
86
|
+
switch (action) {
|
|
87
|
+
case 'add':
|
|
88
|
+
await Collection.create(data)
|
|
89
|
+
break
|
|
90
|
+
case 'update':
|
|
91
|
+
await Collection.update(_id, data)
|
|
92
|
+
break
|
|
93
|
+
case 'remove':
|
|
94
|
+
await Collection.remove(_id)
|
|
95
|
+
break
|
|
96
|
+
default:
|
|
97
|
+
throw new Error('E_UNHANDLED')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const _openModalForm = (action, record) => {
|
|
102
|
+
console.log(action, record)
|
|
103
|
+
open({
|
|
104
|
+
onSubmit: async (data) => _handleCollection(action, data),
|
|
105
|
+
details: {
|
|
106
|
+
title: `${action === 'add'? 'Créer' : 'Modifier'} une collection`,
|
|
107
|
+
subTitle: 'Entrez un nom pour votre collection afin de l\'identifier facilement.',
|
|
108
|
+
acceptButton: `${action === 'add' ? 'Créer' : 'Modifier'}`,
|
|
109
|
+
closeButton: 'Annuler'
|
|
110
|
+
},
|
|
111
|
+
attributes: [
|
|
112
|
+
{
|
|
113
|
+
property: 'name',
|
|
114
|
+
componentOptions: {
|
|
115
|
+
label: 'Titre de la collection',
|
|
116
|
+
name: 'name'
|
|
117
|
+
},
|
|
118
|
+
component: 'input-text'
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
property: 'slug',
|
|
122
|
+
componentOptions: {
|
|
123
|
+
label: 'Slug de la collection',
|
|
124
|
+
name: 'slug'
|
|
125
|
+
},
|
|
126
|
+
component: 'input-text'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
property: 'description',
|
|
130
|
+
componentOptions: {
|
|
131
|
+
label: 'Description de la collection',
|
|
132
|
+
name: 'description'
|
|
133
|
+
},
|
|
134
|
+
component: 'input-text-area'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
property: 'attributes',
|
|
138
|
+
componentOptions: {
|
|
139
|
+
label: 'Attribut(s) de la collection',
|
|
140
|
+
name: 'attributes'
|
|
141
|
+
},
|
|
142
|
+
component: 'input-json'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
property: 'editor',
|
|
146
|
+
componentOptions: {
|
|
147
|
+
label: 'Editeur de la collection',
|
|
148
|
+
name: 'editor'
|
|
149
|
+
},
|
|
150
|
+
component: 'input-json'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
property: 'headings',
|
|
154
|
+
componentOptions: {
|
|
155
|
+
label: 'Titres de la collection',
|
|
156
|
+
name: 'headings'
|
|
157
|
+
},
|
|
158
|
+
component: 'input-json'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
property: 'display',
|
|
162
|
+
componentOptions: {
|
|
163
|
+
label: 'Affichage de la collection',
|
|
164
|
+
name: 'display'
|
|
165
|
+
},
|
|
166
|
+
component: 'input-json'
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
record
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Prepare lifecyle hooks
|
|
174
|
+
onMounted(async () => {
|
|
175
|
+
if (!collections.value.length) {
|
|
176
|
+
await Collection.list()
|
|
177
|
+
}
|
|
178
|
+
if (!availableCollections.value.length) {
|
|
179
|
+
const parents = organizationPath.value.split('/')
|
|
180
|
+
if (parents.length) {
|
|
181
|
+
parents.pop()
|
|
182
|
+
const results = await Promise.all(
|
|
183
|
+
parents.map(parent => Collection.list(undefined, parent))
|
|
184
|
+
)
|
|
185
|
+
availableCollections.value = results.flat()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
</script>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<div class="flex items-center w-36 text-left shadow-xs text-sm" @click="_toggle">
|
|
4
|
+
<button
|
|
5
|
+
class="focus:outline-none border border-black text-black block truncate cursor-default px-4 py-2 w-full rounded-l-md"
|
|
6
|
+
>
|
|
7
|
+
{{ actionLabel }}
|
|
8
|
+
</button>
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
aria-haspopup="listbox"
|
|
12
|
+
aria-expanded="true"
|
|
13
|
+
aria-labelledby="listbox-label"
|
|
14
|
+
class="flex items-center p-2 rounded-r-md cursor-pointer bg-black focus:outline-none border border-black"
|
|
15
|
+
>
|
|
16
|
+
<svg class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
17
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
18
|
+
</svg>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<transition
|
|
22
|
+
leave-active-class="transition ease-in duration-100"
|
|
23
|
+
leave-from-class="opacity-100"
|
|
24
|
+
leave-to-class="opacity-0"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
v-if="display"
|
|
28
|
+
ref="publicationStatusHandler"
|
|
29
|
+
class="absolute flex flex-col justify-center right-0 mt-1 p-4 space-y-2 w-64 rounded-md bg-white shadow-lg z-50"
|
|
30
|
+
>
|
|
31
|
+
<!-- <alert sort="info" :description="actionAlert" /> -->
|
|
32
|
+
<!-- <ul tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-item-3" class="text-base overflow-auto focus:outline-none sm:text-sm">
|
|
33
|
+
<li
|
|
34
|
+
v-for="({ label, option }, i) in list"
|
|
35
|
+
:id="`listbox-item-${i}`"
|
|
36
|
+
:key="option"
|
|
37
|
+
class="cursor-pointer select-none relative py-2 pl-4 pr-4 text-gray-900 hover:bg-gray-100"
|
|
38
|
+
role="option"
|
|
39
|
+
@click="_updateStatus(option)"
|
|
40
|
+
>
|
|
41
|
+
<span class="font-normal block truncate">
|
|
42
|
+
{{ label }}
|
|
43
|
+
</span>
|
|
44
|
+
</li>
|
|
45
|
+
</ul> -->
|
|
46
|
+
<!-- si publish dans les permissions -->
|
|
47
|
+
<tertiary-button
|
|
48
|
+
v-if="status === 'draft'"
|
|
49
|
+
:label="'Valider la ressource'"
|
|
50
|
+
@click="_updateStatus('ready')"
|
|
51
|
+
/>
|
|
52
|
+
<tertiary-button
|
|
53
|
+
v-if="['draft', 'waiting'].includes(status)"
|
|
54
|
+
:label="'Publier la ressource'"
|
|
55
|
+
@click="_updateStatus('published')"
|
|
56
|
+
/>
|
|
57
|
+
<primary-button
|
|
58
|
+
:label="`${actionLabel} la ressource`"
|
|
59
|
+
class="mt-1"
|
|
60
|
+
@click="_updateStatus(actionTo)"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</transition>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup>
|
|
68
|
+
import { onClickOutside } from '@vueuse/core'
|
|
69
|
+
// import alert from '~/components/template/content-section/Alert'
|
|
70
|
+
import primaryButton from '~/components/elements/buttons/primary'
|
|
71
|
+
import tertiaryButton from '~/components/elements/buttons/tertiary'
|
|
72
|
+
|
|
73
|
+
// Prepare props
|
|
74
|
+
const $props = defineProps({
|
|
75
|
+
collection: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: ''
|
|
78
|
+
},
|
|
79
|
+
model: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: ''
|
|
82
|
+
},
|
|
83
|
+
record: {
|
|
84
|
+
type: Object,
|
|
85
|
+
default: () => ({})
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Prepare emitters
|
|
90
|
+
const $emitters = defineEmits(['select', 'loaded', 'loading'])
|
|
91
|
+
|
|
92
|
+
// Prepare composables
|
|
93
|
+
const { Record } = useModels()
|
|
94
|
+
|
|
95
|
+
// Prepare constants
|
|
96
|
+
const STATUSES_LIST = [
|
|
97
|
+
{ label: 'Brouillon', option: 'draft' },
|
|
98
|
+
{ label: 'En cours de valiation', option: 'waiting' },
|
|
99
|
+
{ label: 'Prêt', option: 'ready' },
|
|
100
|
+
// { label: '', option: 'scheduled' },
|
|
101
|
+
{ label: 'Publié', option: 'published' }
|
|
102
|
+
]
|
|
103
|
+
const STATUSES_ACTIONS = [
|
|
104
|
+
{
|
|
105
|
+
label: 'Soumettre',
|
|
106
|
+
alert: 'Une fois soumise, cette ressource devra être validée avant publication.',
|
|
107
|
+
from: 'draft',
|
|
108
|
+
to: 'waiting'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: 'Valider',
|
|
112
|
+
// ability: this.canValidate,
|
|
113
|
+
alert: 'Une fois validée, cette ressource devra être publiée afin de la rendre disponible sur votre site web.',
|
|
114
|
+
from: 'waiting',
|
|
115
|
+
to: 'ready'
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: 'Publier',
|
|
119
|
+
// ability: this.canPublish,
|
|
120
|
+
alert: 'Une fois publiée, cette ressource apparaîtré sur votre site web.',
|
|
121
|
+
from: 'ready',
|
|
122
|
+
to: 'published' // scheduled
|
|
123
|
+
},
|
|
124
|
+
// {
|
|
125
|
+
// label: '',
|
|
126
|
+
// alert: '',
|
|
127
|
+
// from: 'scheduled',
|
|
128
|
+
// to: ''
|
|
129
|
+
// },
|
|
130
|
+
{
|
|
131
|
+
label: 'Dé-publier',
|
|
132
|
+
// ability: this.canPublish,
|
|
133
|
+
alert: 'Attention, cette resource actuellement en ligné, une fois dé-publiée, cette ressource n\'apparaîtra plus sur votre site web.',
|
|
134
|
+
from: 'published',
|
|
135
|
+
to: 'ready'
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
// Prepare data
|
|
140
|
+
const display = ref(false)
|
|
141
|
+
const publicationStatusHandler = ref(null)
|
|
142
|
+
|
|
143
|
+
// Prepare computed
|
|
144
|
+
const status = computed(() => $props.record?.status)
|
|
145
|
+
const actionLabel = computed(() => STATUSES_ACTIONS.find(({ from }) => from === status.value)?.label)
|
|
146
|
+
const actionAlert = computed(() => STATUSES_ACTIONS.find(({ from }) => from === status.value)?.alert)
|
|
147
|
+
const actionTo = computed(() => STATUSES_ACTIONS.find(({ from }) => from === status.value)?.to)
|
|
148
|
+
// const canValidate = computed(() => {
|
|
149
|
+
// const { $auth, $props: { model } } = this
|
|
150
|
+
// return $auth.hasScope('manage:all') || $auth.hasScope(`validate:${model.toLowerCase()}s`)
|
|
151
|
+
// })
|
|
152
|
+
// const canPublish = computed(() => {
|
|
153
|
+
// const { $auth, $props: { model } } = this
|
|
154
|
+
// return $auth.hasScope('manage:all') || $auth.hasScope(`publish:${model.toLowerCase()}s`)
|
|
155
|
+
// })
|
|
156
|
+
|
|
157
|
+
// Prepare methods
|
|
158
|
+
const _select = (option) => {
|
|
159
|
+
let val = option
|
|
160
|
+
if ($props.object) {
|
|
161
|
+
const { [$props.itemValue]: objectVal } = $props.items.find(({ [$props.itemKey]: val }) => val === option)
|
|
162
|
+
val = objectVal
|
|
163
|
+
}
|
|
164
|
+
_close()
|
|
165
|
+
$emitters('select', val)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const _getLabel = (value) => {
|
|
169
|
+
const { label = '' } = STATUSES_ACTIONS.find(({ from }) => from === value) || {}
|
|
170
|
+
return label
|
|
171
|
+
}
|
|
172
|
+
const _updateStatus = async (status) => {
|
|
173
|
+
$emitters('loading')
|
|
174
|
+
const { _id } = $props.record
|
|
175
|
+
try {
|
|
176
|
+
await Record.update(_id, { status }, $props.collection)
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.log(err)
|
|
179
|
+
} finally {
|
|
180
|
+
// await this.$api[model].read(collection, _id)
|
|
181
|
+
$emitters('loaded')
|
|
182
|
+
_close()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const _toggle = () => {
|
|
186
|
+
display.value = !display.value
|
|
187
|
+
}
|
|
188
|
+
const _close = () => {
|
|
189
|
+
display.value = false
|
|
190
|
+
}
|
|
191
|
+
// Prepare directives
|
|
192
|
+
onClickOutside(publicationStatusHandler, _close)
|
|
193
|
+
</script>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative block w-full">
|
|
3
|
+
<button-secondary class="my-1" label="Ajouter une thématique" @click="_openModalForm('add')" />
|
|
4
|
+
<!-- <list-table
|
|
5
|
+
:headers="TABLES_HEADERS"
|
|
6
|
+
:items="items"
|
|
7
|
+
:actions="['update', 'remove']"
|
|
8
|
+
@update="_openModalForm('update', $event)"
|
|
9
|
+
@remove="_handleRecord('remove', $event)"
|
|
10
|
+
/> -->
|
|
11
|
+
<!-- TODO: replace by data-list-table? -->
|
|
12
|
+
<data-list :items="items" :density="1" :density-children="1" :group-by="'directParent'" :item-value="'_id'" class="space-y-8">
|
|
13
|
+
<template #item="{ item }">
|
|
14
|
+
<data-list-row
|
|
15
|
+
:item="item"
|
|
16
|
+
:headers="ROW_DEFINITION"
|
|
17
|
+
@click="_openModalForm('update', item)"
|
|
18
|
+
class="mb-2 py-1 border-b border-b-gray-200 rounded-t-md"
|
|
19
|
+
/>
|
|
20
|
+
</template>
|
|
21
|
+
<template #item-child="{ item: itemChild, isStart, isLast }">
|
|
22
|
+
<data-list-row
|
|
23
|
+
:item="itemChild"
|
|
24
|
+
:headers="ROW_DEFINITION"
|
|
25
|
+
@click="_openModalForm('update', itemChild)"
|
|
26
|
+
:class="{ 'rounded-t-md': isStart, 'rounded-b-md': isLast }"
|
|
27
|
+
class="py-0.5"
|
|
28
|
+
/>
|
|
29
|
+
</template>
|
|
30
|
+
</data-list>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup>
|
|
35
|
+
import dataList from '~/components/data/lists/data-list'
|
|
36
|
+
import dataListRow from '~/components/data/display/row'
|
|
37
|
+
import buttonSecondary from '~/components/elements/buttons/secondary'
|
|
38
|
+
import { useModalStore } from '~/store/layout/modal'
|
|
39
|
+
|
|
40
|
+
const { Collection, Record } = useModels()
|
|
41
|
+
const { open } = useModalStore()
|
|
42
|
+
|
|
43
|
+
// Prepare constants
|
|
44
|
+
const ROW_DEFINITION = [
|
|
45
|
+
{
|
|
46
|
+
property: '_id',
|
|
47
|
+
alignleft: true,
|
|
48
|
+
component: 'data-label'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
component: 'data-spacer',
|
|
52
|
+
grow: true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
nestedValue: false,
|
|
56
|
+
alignRight: true,
|
|
57
|
+
component: 'data-last-update',
|
|
58
|
+
componentOptions: {
|
|
59
|
+
inline: true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: '',
|
|
64
|
+
component: 'actions',
|
|
65
|
+
componentOptions: {
|
|
66
|
+
options: [
|
|
67
|
+
{ label: 'Modifier', action: 'update' },
|
|
68
|
+
{ label: 'Supprimer', action: 'remove' }
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
// Prepare props
|
|
75
|
+
const $props = defineProps({
|
|
76
|
+
collection: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: ''
|
|
79
|
+
},
|
|
80
|
+
alwaysPublished: {
|
|
81
|
+
type: Boolean,
|
|
82
|
+
default: false
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Prepare computed
|
|
87
|
+
const items = computed(() => {
|
|
88
|
+
const items = Record.repo.where('relatedCollection', $props.collection).get()
|
|
89
|
+
// atm. we use .map(onRetrieve) directly in page & components, it should be better to have this
|
|
90
|
+
// handled directly in `pinia-orm` lib
|
|
91
|
+
return items.map(Record.onRetrieve)
|
|
92
|
+
})
|
|
93
|
+
const collectionAttributes = computed(() => {
|
|
94
|
+
const collectionRecords = Collection.repo.all()
|
|
95
|
+
return collectionRecords.find(({ slug }) => slug === $props.collection)?.editor?.componentOptions?.layout?.[0]?.sections?.[0]?.componentOptions?.attributes || []
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Prepare methods
|
|
99
|
+
const _handleRecord = async (action, data = {}) => {
|
|
100
|
+
const { _id } = data
|
|
101
|
+
if ($props.alwaysPublished) {
|
|
102
|
+
data.status = 'published'
|
|
103
|
+
}
|
|
104
|
+
switch (action) {
|
|
105
|
+
case 'add':
|
|
106
|
+
await Record.create(data, $props.collection)
|
|
107
|
+
break
|
|
108
|
+
case 'update':
|
|
109
|
+
await Record.update(_id, data, $props.collection)
|
|
110
|
+
break
|
|
111
|
+
case 'remove':
|
|
112
|
+
await Record.remove(_id, $props.collection)
|
|
113
|
+
break
|
|
114
|
+
default:
|
|
115
|
+
throw new Error('E_UNHANDLED')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const _openModalForm = (action, record) => {
|
|
119
|
+
open({
|
|
120
|
+
onSubmit: async (data) => _handleRecord(action, data),
|
|
121
|
+
details: {
|
|
122
|
+
title: `${action === 'add' ? 'Créer' : 'Modifier'} une thématique`,
|
|
123
|
+
subTitle: 'Entrez un nom pour votre thématique afin de l\'identifier facilement.',
|
|
124
|
+
acceptButton: `${action === 'add' ? 'Créer' : 'Modifier'}`,
|
|
125
|
+
closeButton: 'Annuler'
|
|
126
|
+
},
|
|
127
|
+
attributes: collectionAttributes.value,
|
|
128
|
+
record
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Prepare lifecyle hooks
|
|
133
|
+
onMounted(async () => {
|
|
134
|
+
if (!items.value.length) {
|
|
135
|
+
await Record.list($props.collection)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
</script>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative block w-full">
|
|
3
|
+
<label
|
|
4
|
+
v-if="label"
|
|
5
|
+
id="listbox-label"
|
|
6
|
+
class="block text-2xs font-light tracking-wider text-gray-500 uppercase"
|
|
7
|
+
>
|
|
8
|
+
{{ label }}
|
|
9
|
+
</label>
|
|
10
|
+
<div class="grid grid-cols-5 space-x-2 mb-2" :class="{ 'mt-1': label }">
|
|
11
|
+
<div class="inline-flex col-span-4 space-x-1">
|
|
12
|
+
<select-enum
|
|
13
|
+
:items="organizations"
|
|
14
|
+
:value="currentOrganization"
|
|
15
|
+
place-holder="Organisation"
|
|
16
|
+
class="w-full"
|
|
17
|
+
@change="currentOrganization = $event"
|
|
18
|
+
/>
|
|
19
|
+
<select-enum
|
|
20
|
+
:items="roles"
|
|
21
|
+
:value="currentRole"
|
|
22
|
+
:item-key="'name'"
|
|
23
|
+
:read-only="!currentOrganization"
|
|
24
|
+
place-holder="Role"
|
|
25
|
+
class="w-full"
|
|
26
|
+
@change="currentRole = $event"
|
|
27
|
+
/>
|
|
28
|
+
<select-enum
|
|
29
|
+
:items="collections"
|
|
30
|
+
:value="currentCollections"
|
|
31
|
+
:read-only="!currentOrganization || !currentRole"
|
|
32
|
+
place-holder="Collection(s)"
|
|
33
|
+
class="w-full"
|
|
34
|
+
enable-multiple
|
|
35
|
+
@change="currentCollections = $event"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<button-secondary class="col-span-1" label="Ajouter un rôle" @click="_addRole" />
|
|
39
|
+
<!-- @click="_openModalForm('add')" -->
|
|
40
|
+
</div>
|
|
41
|
+
<list-table
|
|
42
|
+
:headers="TABLE_HEADERS"
|
|
43
|
+
:items="selection"
|
|
44
|
+
:actions="['remove']"
|
|
45
|
+
class="bg-gray-100 p-2"
|
|
46
|
+
@remove="_removeRole"
|
|
47
|
+
/>
|
|
48
|
+
<!-- @update="_openModalForm('update', $event)"
|
|
49
|
+
@remove="_handle('remove', $event)" -->
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
import { computedAsync } from '@vueuse/core'
|
|
55
|
+
import listTable from '~/components/data/lists/data-lists-table'
|
|
56
|
+
import selectEnum from '~/components/forms/selects/select-enum'
|
|
57
|
+
import buttonSecondary from '~/components/elements/buttons/secondary'
|
|
58
|
+
|
|
59
|
+
// Prepare composable
|
|
60
|
+
const { Organization, Role, Collection } = useModels()
|
|
61
|
+
|
|
62
|
+
// Prepare constants
|
|
63
|
+
const TABLE_HEADERS = [
|
|
64
|
+
{
|
|
65
|
+
title: 'Organisation',
|
|
66
|
+
nestedValue: false,
|
|
67
|
+
component: 'data-summary',
|
|
68
|
+
componentOptions: {
|
|
69
|
+
titleKey: 'organization',
|
|
70
|
+
descriptionKey: 'role',
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
property: 'collections',
|
|
75
|
+
component: 'data-text'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
component: 'data-spacer',
|
|
79
|
+
grow: true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: '',
|
|
83
|
+
component: 'actions',
|
|
84
|
+
componentOptions: {
|
|
85
|
+
options: [
|
|
86
|
+
{ label: 'Supprimer', action: 'remove' }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
// Prepare props
|
|
93
|
+
const $props = defineProps({
|
|
94
|
+
label: {
|
|
95
|
+
type: String,
|
|
96
|
+
default: ''
|
|
97
|
+
},
|
|
98
|
+
value: {
|
|
99
|
+
type: Array,
|
|
100
|
+
default: () => []
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Prepare $emitters
|
|
105
|
+
const $emitters = defineEmits(['keyup.enter', 'change'])
|
|
106
|
+
|
|
107
|
+
// Prepare data
|
|
108
|
+
const selection = ref([])
|
|
109
|
+
const currentOrganization = ref(null)
|
|
110
|
+
const currentRole = ref(null)
|
|
111
|
+
const currentCollections = ref([])
|
|
112
|
+
|
|
113
|
+
// Prepare computed
|
|
114
|
+
const organizations = computed(() => Organization.repo.all())
|
|
115
|
+
const roles = computed(() => Role.repo.all())
|
|
116
|
+
const collections = computedAsync(
|
|
117
|
+
async () => {
|
|
118
|
+
if (currentOrganization.value) {
|
|
119
|
+
const collections = await Collection.list(undefined, currentOrganization.value)
|
|
120
|
+
console.log(collections)
|
|
121
|
+
return [{ _id: '*', name: 'Toutes les collections (*)'}, ...collections]
|
|
122
|
+
}
|
|
123
|
+
return []
|
|
124
|
+
},
|
|
125
|
+
[],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
// Prepare methods
|
|
130
|
+
const _addRole = () => {
|
|
131
|
+
selection.value.push({
|
|
132
|
+
organization: currentOrganization.value,
|
|
133
|
+
role: currentRole.value,
|
|
134
|
+
collections: currentCollections.value
|
|
135
|
+
})
|
|
136
|
+
currentOrganization.value = null
|
|
137
|
+
currentRole.value = null
|
|
138
|
+
currentCollections.value = []
|
|
139
|
+
}
|
|
140
|
+
const _removeRole = (val) => {
|
|
141
|
+
const indexToRemove = selection.value.findIndex(
|
|
142
|
+
({ organization, collections, role }) =>organization === val.organization
|
|
143
|
+
&& role === val.role
|
|
144
|
+
&& val.collections.every((_id) => collections.includes(_id))
|
|
145
|
+
)
|
|
146
|
+
if (indexToRemove >= 0) {
|
|
147
|
+
selection.value.splice(indexToRemove, 1)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// prepare watchers
|
|
152
|
+
watch(
|
|
153
|
+
() => $props.value,
|
|
154
|
+
(newValue) => {
|
|
155
|
+
if (newValue) {
|
|
156
|
+
selection.value = newValue
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{ immediate: true }
|
|
160
|
+
)
|
|
161
|
+
watch(
|
|
162
|
+
selection,
|
|
163
|
+
(newValue) => $emitters('change', newValue),
|
|
164
|
+
{ immediate: true }
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// Prepare lifecycle hook
|
|
168
|
+
onMounted(async () => {
|
|
169
|
+
if (!roles.value.length) {
|
|
170
|
+
await Role.list()
|
|
171
|
+
}
|
|
172
|
+
if (!organizations.value.length) {
|
|
173
|
+
await Organization.list()
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
</script>
|