@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.
Files changed (132) hide show
  1. package/README.md +41 -0
  2. package/app.config.ts +10 -0
  3. package/app.vue +13 -0
  4. package/assets/css/main.css +13 -0
  5. package/components/data/display/row/data-avatar.vue +23 -0
  6. package/components/data/display/row/data-badge.vue +34 -0
  7. package/components/data/display/row/data-color.vue +17 -0
  8. package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
  9. package/components/data/display/row/data-label.vue +37 -0
  10. package/components/data/display/row/data-last-update.vue +45 -0
  11. package/components/data/display/row/data-spacer.vue +4 -0
  12. package/components/data/display/row/data-summary.vue +44 -0
  13. package/components/data/display/row/data-text.vue +14 -0
  14. package/components/data/display/row/data-toggle.vue +16 -0
  15. package/components/data/display/row.vue +111 -0
  16. package/components/data/lists/data-list.vue +85 -0
  17. package/components/data/lists/data-lists-table.vue +155 -0
  18. package/components/elements/avatars/avatar-circular.vue +68 -0
  19. package/components/elements/avatars/avatar-with-text.vue +47 -0
  20. package/components/elements/badges/badge.vue +85 -0
  21. package/components/elements/buttons/primary.vue +32 -0
  22. package/components/elements/buttons/secondary.vue +35 -0
  23. package/components/elements/buttons/tertiary.vue +25 -0
  24. package/components/elements/dropdowns/dots.vue +75 -0
  25. package/components/elements/media/image.vue +82 -0
  26. package/components/elements/notifications/default.vue +90 -0
  27. package/components/forms/handlers/handler-collections.vue +189 -0
  28. package/components/forms/handlers/handler-publication-status.vue +193 -0
  29. package/components/forms/handlers/handler-records.vue +138 -0
  30. package/components/forms/handlers/handler-roles.vue +176 -0
  31. package/components/forms/handlers/handler-schedules.vue +218 -0
  32. package/components/forms/handlers/handler-steps.vue +147 -0
  33. package/components/forms/handlers/handler-tokens.vue +79 -0
  34. package/components/forms/inputs/input-color-picker.vue +159 -0
  35. package/components/forms/inputs/input-editor.vue +363 -0
  36. package/components/forms/inputs/input-files-media.vue +150 -0
  37. package/components/forms/inputs/input-files.vue +98 -0
  38. package/components/forms/inputs/input-json.vue +65 -0
  39. package/components/forms/inputs/input-location.vue +179 -0
  40. package/components/forms/inputs/input-text-area.vue +65 -0
  41. package/components/forms/inputs/input-text.vue +122 -0
  42. package/components/forms/inputs/input-toggle.vue +125 -0
  43. package/components/forms/inputs/media-section/media-editor.vue +95 -0
  44. package/components/forms/layouts/form.vue +203 -0
  45. package/components/forms/layouts/layout.vue +47 -0
  46. package/components/forms/layouts/modal.vue +96 -0
  47. package/components/forms/layouts/navigation.vue +48 -0
  48. package/components/forms/layouts/section.vue +118 -0
  49. package/components/forms/selects/select-collection-enum.vue +195 -0
  50. package/components/forms/selects/select-date.vue +245 -0
  51. package/components/forms/selects/select-enum.vue +186 -0
  52. package/components/forms/selects/select-record.vue +274 -0
  53. package/components/layout/headings/headings-collection.vue +29 -0
  54. package/components/layout/headings/headings-record.vue +33 -0
  55. package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
  56. package/components/layout/navigations/primary.vue +132 -0
  57. package/components/layout/notifications/notifications-list.vue +19 -0
  58. package/composables/core/domains/use-definitions.ts +57 -0
  59. package/composables/core/domains/use-entries.ts +62 -0
  60. package/composables/core/domains/use-media.ts +82 -0
  61. package/composables/core/use-core.ts +12 -0
  62. package/composables/use-ability.js +38 -0
  63. package/composables/use-models.js +27 -0
  64. package/composables/use-session.ts +80 -0
  65. package/composables/use-timer.js +62 -0
  66. package/composables/use-trpc.js +17 -0
  67. package/editor/index.ts +44 -0
  68. package/editor/views/CalloutView.vue +22 -0
  69. package/editor/views/DetailsContentView.vue +11 -0
  70. package/editor/views/DetailsSummaryView.vue +11 -0
  71. package/editor/views/DetailsView.vue +72 -0
  72. package/editor/views/EmbedView.vue +53 -0
  73. package/editor/views/FileView.vue +79 -0
  74. package/editor/views/ImageView.vue +56 -0
  75. package/editor/views/UploadView.vue +71 -0
  76. package/layouts/default.vue +21 -0
  77. package/layouts/entrance.vue +10 -0
  78. package/layouts/record.vue +21 -0
  79. package/middleware/00.auth.global.js +16 -0
  80. package/middleware/01.instance.global.js +21 -0
  81. package/middleware/03.verify-permissions.global.js +11 -0
  82. package/middleware/04.prefetch-page.global.js +18 -0
  83. package/middleware/05.headings-page.global.js +25 -0
  84. package/models/_commun.js +243 -0
  85. package/models/_factory.js +34 -0
  86. package/models/collection.js +87 -0
  87. package/models/entrance.js +42 -0
  88. package/models/media.js +82 -0
  89. package/models/organization.js +92 -0
  90. package/models/record.js +165 -0
  91. package/models/role.js +66 -0
  92. package/models/token.js +56 -0
  93. package/models/user.js +97 -0
  94. package/nuxt.config.ts +151 -0
  95. package/package.json +71 -0
  96. package/pages/[collection]/[record]/index.vue +88 -0
  97. package/pages/[collection]/index.vue +55 -0
  98. package/pages/_lab/editor.vue +149 -0
  99. package/pages/auth/callback.vue +31 -0
  100. package/pages/index/index.vue +85 -0
  101. package/pages/index/password/define/[token].vue +79 -0
  102. package/pages/index/password/recover/index.vue +72 -0
  103. package/pages/index/welcome/[token].vue +90 -0
  104. package/pages/index.vue +44 -0
  105. package/pages/management/index.vue +15 -0
  106. package/pages/management/members.vue +176 -0
  107. package/pages/management/organizations.vue +192 -0
  108. package/pages/management/roles.vue +183 -0
  109. package/pages/overview.vue +15 -0
  110. package/pages/settings.vue +197 -0
  111. package/plugins/01.session.client.ts +5 -0
  112. package/plugins/02.vue-query.ts +25 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/logo-black.svg +1 -0
  115. package/public/logo-white.svg +1 -0
  116. package/server/api/trpc/[...trpc].ts +28 -0
  117. package/server/api/v1/content/deployment.get.ts +45 -0
  118. package/server/api/v1/content/records.get.ts +12 -0
  119. package/server/middleware/0.security.ts +19 -0
  120. package/server/middleware/1.cors.ts +31 -0
  121. package/server/middleware/2.session.ts +20 -0
  122. package/server/middleware/3.api-token.ts +20 -0
  123. package/server/plugins/core.ts +8 -0
  124. package/server/routes/_tasks/[name].post.ts +20 -0
  125. package/server/routes/health.get.ts +6 -0
  126. package/server/tasks/deploy.ts +26 -0
  127. package/server/types.d.ts +8 -0
  128. package/server/utils/core.ts +18 -0
  129. package/store/layout/headings.js +21 -0
  130. package/store/layout/modal.js +80 -0
  131. package/store/layout/notifications.js +52 -0
  132. 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>