@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,274 @@
1
+ <template>
2
+ <div>
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
11
+ :class="{ 'mt-1':label }"
12
+ class="relative"
13
+ >
14
+ <input
15
+ v-model="selectionText"
16
+ id="combobox"
17
+ type="text"
18
+ role="combobox"
19
+ aria-controls="options"
20
+ aria-expanded="false"
21
+ :class="{ 'ring-gray-50 border-gray-900':display }"
22
+ :placeholder="placeHolder"
23
+ class="w-full bg-white rounded-md pl-3 pr-10 py-2 h-10 text-left cursor-pointer shadow-xs border border-gray-200 focus:outline-none focus:ring-gray-50 focus:border-black hover:border-black text-sm"
24
+ @keyup="_search"
25
+ @click="_toggle"
26
+ >
27
+ <span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
28
+ <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
29
+ <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
30
+ </svg>
31
+ </span>
32
+ <transition
33
+ leave-active-class="transition ease-in duration-100"
34
+ leave-from-class="opacity-100"
35
+ leave-to-class="opacity-0"
36
+ >
37
+ <div
38
+ v-if="display"
39
+ ref="selectRecord"
40
+ class="absolute mt-1 w-full rounded-md bg-white shadow-lg z-50"
41
+ >
42
+ <ul
43
+ abindex="-1"
44
+ role="listbox"
45
+ aria-labelledby="listbox-label"
46
+ aria-activedescendant="listbox-item-3"
47
+ class="max-h-60 rounded-md py-2 text-base ring-1 ring-black/5 overflow-auto focus:outline-none sm:text-sm"
48
+ >
49
+ <div v-if="groupByDirectParents">
50
+ <div v-for="({ title, children }, i) in options" :key="i" class="p-2 space-y-1">
51
+ <div class="border-b border-gray-200">
52
+ <p class="pb-4 pl-4 pr-4 text-gray-500 font-light uppercase text-xs">
53
+ {{ title }}
54
+ </p>
55
+ </div>
56
+ <li
57
+ v-for="({ title: childTitle }, j) in children"
58
+ :key="`${childTitle}-${j}`"
59
+ :class="{ 'bg-gray-100': _isSelected(childTitle) }"
60
+ class="cursor-pointer select-none relative rounded-md py-2 pl-4 pr-4 text-gray-900 hover:bg-gray-100"
61
+ @click="_select(childTitle)"
62
+ >
63
+ <span :class="{ 'font-semibold':_isSelected(childTitle) }" class="font-normal block truncate">
64
+ {{ childTitle }}
65
+ </span>
66
+ <span
67
+ v-show="_isSelected(childTitle)"
68
+ class="absolute text-gray-900 inset-y-0 right-0 flex items-center pr-4"
69
+ >
70
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
71
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
72
+ </svg>
73
+ </span>
74
+ </li>
75
+ </div>
76
+ </div>
77
+ <div v-else>
78
+ <li
79
+ v-for="({ title, relatedCollection }) in options"
80
+ :key="title"
81
+ :class="{ 'bg-gray-100': _isSelected(title) }"
82
+ class="cursor-pointer select-none relative rounded-md py-2 pl-4 pr-4 text-gray-900 hover:bg-gray-100"
83
+ @click="_select(title)"
84
+ >
85
+ <span :class="{ 'font-semibold':_isSelected(title) }" class="font-normal block truncate">
86
+ {{ title }}
87
+ </span>
88
+ <span class="font-normal text-gray-600 block truncate">
89
+ {{ relatedCollection }}
90
+ </span>
91
+ <span
92
+ v-show="_isSelected(title)"
93
+ class="absolute text-gray-900 inset-y-0 right-0 flex items-center pr-4"
94
+ >
95
+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
96
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
97
+ </svg>
98
+ </span>
99
+ </li>
100
+ </div>
101
+ </ul>
102
+ </div>
103
+ </transition>
104
+ </div>
105
+ </div>
106
+ </template>
107
+
108
+ <script setup>
109
+ import { onClickOutside } from '@vueuse/core'
110
+ import { useWorkspaceStore } from '~/store/layout/workspace'
111
+ import { useModalStore } from '~/store/layout/modal'
112
+
113
+ const { Record } = useModels()
114
+ const workspaceStore = useWorkspaceStore()
115
+
116
+ // Prepare emitters
117
+ const $emitters = defineEmits(['change'])
118
+
119
+ // Prepare props
120
+ const $props = defineProps({
121
+ filter: {
122
+ type: Function,
123
+ default: () => () => true
124
+ },
125
+ onlyDirectParents: {
126
+ type: Boolean,
127
+ default: false
128
+ },
129
+ groupByDirectParents: {
130
+ type: Boolean,
131
+ default: false
132
+ },
133
+ collections: {
134
+ type: Array,
135
+ default: () => []
136
+ },
137
+ label: {
138
+ type: String,
139
+ default: ''
140
+ },
141
+ enableMultiple: {
142
+ type: Boolean,
143
+ default: false
144
+ },
145
+ placeHolder: {
146
+ type: String,
147
+ default: 'Rechercher...'
148
+ },
149
+ value: {
150
+ type: [String, Array],
151
+ default: ''
152
+ }
153
+ })
154
+
155
+ // Prepare reactive data
156
+ const selectRecord = ref(null)
157
+ const display = ref(false)
158
+ const selection = ref('')
159
+ const search = ref('')
160
+
161
+ // Prepare computed
162
+ const items = computed(() => {
163
+ const items = Record.repo
164
+ .where((record) => $props.collections.includes(record.relatedCollection) && record.title.toLowerCase().includes(search.value))
165
+ .get()
166
+
167
+ // atm. we use onRetrieve directly in page, it should be better to have this
168
+ // handled directly in `pinia-orm` lib
169
+ return items.map(Record.onRetrieve)
170
+ })
171
+ const options = computed(() => {
172
+ if ($props.groupByDirectParents) {
173
+ return items.value
174
+ .filter($props.filter)
175
+ .filter(({ directParent }) => !directParent)
176
+ .map(({ _id, ...rest }) => {
177
+ const children = items.value
178
+ .filter($props.filter)
179
+ .filter(({ directParent }) => directParent === _id)
180
+
181
+ return { children, ...rest }
182
+ }, [])
183
+ }
184
+ if ($props.onlyDirectParents) {
185
+ return items.value.filter($props.filter).filter(({ directParent }) => !directParent)
186
+ }
187
+ return items.value.filter($props.filter)
188
+ })
189
+ const selectionText = computed(() => {
190
+ if (display.value || search.value) {
191
+ return ''
192
+ }
193
+ if (selection.value) {
194
+ if ($props.enableMultiple) {
195
+ return selection.value
196
+ .map(_id => items.value.find(({ _id: check }) => check === _id))
197
+ .map((item) => item?.title)
198
+ .join(', ')
199
+ }
200
+ const item = items.value.find(({ _id }) => selection.value === _id)
201
+ return item?.title
202
+ }
203
+ return ''
204
+ })
205
+
206
+ // Prepare watchers
207
+ watch(
208
+ () => $props.value,
209
+ (newValue) => {
210
+ if (!newValue || !newValue.length) {
211
+ selection.value = $props.enableMultiple ? [] : ''
212
+ }
213
+ if (newValue && newValue.length && newValue !== selection.value) {
214
+ if ($props.enableMultiple) {
215
+ selection.value = newValue.every(el => typeof el === 'object')
216
+ ? newValue.map(({ _id: value }) => value)
217
+ : [...newValue]
218
+ } else {
219
+ selection.value = typeof newValue === 'object'
220
+ ? newValue._id
221
+ : newValue
222
+ }
223
+ }
224
+ },
225
+ { immediate: true }
226
+ )
227
+
228
+ // Prepare methods
229
+ const _isSelected = (option) => {
230
+ const { _id } = items.value.find(({ title }) => title === option)
231
+ if ($props.enableMultiple) {
232
+ return selection.value.includes(_id)
233
+ }
234
+ return selection.value === _id
235
+ }
236
+
237
+ const _search = (str) => {
238
+ search.value = str?.target?.value
239
+ }
240
+
241
+ const _toggle = () => {
242
+ display.value = !display.value
243
+ search.value = ''
244
+ }
245
+
246
+ const _close = () => {
247
+ display.value = false
248
+ search.value = ''
249
+ }
250
+
251
+ const _select = (option) => {
252
+ const { _id } = items.value.find(({ title }) => title === option)
253
+ if ($props.enableMultiple) {
254
+ const index = selection.value.indexOf(_id)
255
+ index >= 0 ? selection.value.splice(index, 1) : selection.value.push(_id)
256
+ } else {
257
+ selection.value = _id
258
+ _close()
259
+ }
260
+ $emitters('change', selection.value)
261
+ }
262
+
263
+ // Prepare lifecycle hooks
264
+ onMounted(async () => {
265
+ await Promise.all(
266
+ $props.collections.map(
267
+ (collection) => Record.list(collection, workspaceStore.workspaceId, { limit: 500 })
268
+ )
269
+ )
270
+ })
271
+
272
+ // Prepare directives
273
+ onClickOutside(selectRecord, _close)
274
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div v-show="headingsStore.hasHeadings" class="border-b border-gray-200">
3
+ <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8 flex items-start justify-between">
4
+ <div>
5
+ <h3 class="text-3xl leading-10 font-medium text-gray-900">
6
+ {{ headingsStore.headings.title }}
7
+ </h3>
8
+ <p class="mt-2 pb-4 text-sm text-gray-500">
9
+ {{ headingsStore.headings.description }}
10
+ </p>
11
+ </div>
12
+ <button-primary
13
+ v-if="headingsStore.headings.mainAction"
14
+ :label="headingsStore.headings.mainAction"
15
+ @click="$router.replace(`${$route.fullPath}/new`)"
16
+ />
17
+ </div>
18
+ </div>
19
+
20
+ </template>
21
+
22
+ <script setup>
23
+ import { useHeadingsStore } from '~/store/layout/headings'
24
+ import ButtonPrimary from '~/components/elements/buttons/primary'
25
+
26
+ const $route = useRoute()
27
+ const $router = useRouter()
28
+ const headingsStore = useHeadingsStore()
29
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div v-if="!isCreating" class="border-b border-gray-200">
3
+ <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8 flex items-start justify-between">
4
+ <div class="flex flex-col items-start space-y-1">
5
+ <h3 class="text-3xl leading-10 font-medium text-gray-900">
6
+ {{ recordTitle }}
7
+ </h3>
8
+ <data-last-update v-if="record" :value="record" inline />
9
+ <data-display-badge-publish-status v-if="recordStatus" :status="recordStatus" />
10
+ </div>
11
+ <handler-publication-status :collection="collection" :model="'Record'" :record="record" />
12
+ </div>
13
+ </div>
14
+
15
+ </template>
16
+
17
+ <script setup>
18
+ import dataLastUpdate from '~/components/data/display/row/data-last-update'
19
+ import dataDisplayBadgePublishStatus from '~/components/data/display/row/data-display-badge-publish-status'
20
+ import handlerPublicationStatus from '~/components/forms/handlers/handler-publication-status'
21
+
22
+ const $route = useRoute()
23
+ const { Record } = useModels()
24
+
25
+ // Prepare computed
26
+ const isCreating = computed(() => $route.params.record === 'new')
27
+ // atm. we use onRetrieve directly in page, it should be better to have this
28
+ // handled directly in `pinia-orm` lib
29
+ const collection = computed(() => $route.params.collection)
30
+ const record = computed(() => Record.onRetrieve(Record.repo.where('_id', $route.params.record).first()))
31
+ const recordStatus = computed(() => record.value?.status)
32
+ const recordTitle = computed(() => record.value?.title || '')
33
+ </script>
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <div v-if="record?.deployment?.vercel?.hook" class="relative">
3
+ <button
4
+ id="options-menu"
5
+ class="inline-flex items-center justify-center border border-transparent rounded-md shadow-sm text-sm text-white hover:bg-white dark:hover:bg-gray-950 hover:text-black focus:outline-none hover:border-black disabled:bg-gray-200 disabled:border-gray-300 disabled:text-gray-400"
6
+ :class="finish ? 'bg-green-400' : 'bg-black'"
7
+ aria-haspopup="true"
8
+ aria-expanded="true"
9
+ @click.prevent="_toggle"
10
+ >
11
+ <icon v-if="finish" icon="iconoir:window-check" class="text-white hover:text-primary h-9 w-9 p-2" />
12
+ <icon v-else-if="deploying" icon="ei:spinner-2" class="animate-spin h-9 w-9 p-1" />
13
+ <icon
14
+ v-else
15
+ icon="iconoir:upload-data-window"
16
+ class="text-white hover:text-primary h-9 w-9 p-2"
17
+ />
18
+ </button>
19
+ <transition
20
+ enter-active-class="transition ease-out duration-100"
21
+ enter-class="transform opacity-0 scale-95"
22
+ enter-to-class="transform opacity-100 scale-100"
23
+ leave-active-class="transition ease-in duration-75"
24
+ leave-class="transform opacity-100 scale-100"
25
+ leave-to-class="transform opacity-0 scale-95"
26
+ >
27
+ <div
28
+ v-if="display"
29
+ ref="buttonPublish"
30
+ class="origin-top-right absolute right-0 p-2 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black/5 z-10"
31
+ >
32
+ <div class="py-1 w-44" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
33
+ <p class="text-xs text-gray-500 pb-2 space-y-2">
34
+ Domaine de publication:
35
+ <nuxt-link :to="record.deployment?.url" class="inline-flex items-center space-x-2 text-sm text-black my-1" target="_blank" external>
36
+ <span>{{ record.deployment?.url }}</span>
37
+ <icon
38
+ icon="lucide:external-link"
39
+ class="text-primary h-4 w-4"
40
+ />
41
+ </nuxt-link>
42
+ </p>
43
+ <button-primary
44
+ :disabled="publishing"
45
+ :loading="publishing"
46
+ label="confirmer"
47
+ class="w-full"
48
+ @click="_publish"
49
+ />
50
+ <div v-if="deploying || finish" class="p-2 border border-gray-300 rounded-md mt-2">
51
+ <p class="text-xs text-gray-400">
52
+ Patientez 2 à 5 minutes pour que votre site soit publié...
53
+ </p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </transition>
58
+ </div>
59
+ </template>
60
+
61
+ <script setup>
62
+ import { onClickOutside, useIntervalFn } from '@vueuse/core'
63
+ import { Icon } from '@iconify/vue'
64
+ import { useNotificationsStore } from '~/store/layout/notifications'
65
+ import { useWorkspaceStore } from '~/store/layout/workspace'
66
+ import buttonPrimary from '~/components/elements/buttons/primary'
67
+
68
+ // Prepare reactive data
69
+ const buttonPublish = ref(null)
70
+ const display = ref(false)
71
+ const loading = ref(true)
72
+ const publishing = ref(false)
73
+ const deploying = ref(false)
74
+ const finish = ref(false)
75
+
76
+ // Prepare composables
77
+ const { Organization } = useModels()
78
+ const { pause, resume, isActive } = useIntervalFn(
79
+ async() => {
80
+ loading.value = true
81
+ const deploymentState = await Organization.getDeployment()
82
+ deploying.value = deploymentState.state !== 'READY'
83
+ loading.value = false
84
+ },
85
+ 10000,
86
+ { immediate: true, immediateCallback: true }
87
+ )
88
+ const $workspaceStore = useWorkspaceStore()
89
+ const notificationsStore = useNotificationsStore()
90
+
91
+ // Prepare computed
92
+ const record = computed(() => $workspaceStore.workspace)
93
+
94
+ // Prepare methods
95
+ const _toggle = () => {
96
+ if (display.value) {
97
+ _close()
98
+ } else {
99
+ _open()
100
+ }
101
+ }
102
+
103
+ const _open = () => {
104
+ display.value = true
105
+ resume()
106
+ }
107
+
108
+ const _close = () => {
109
+ display.value = false
110
+ if (!deploying.value) {
111
+ pause()
112
+ }
113
+ }
114
+
115
+ const _publish = async () => {
116
+ publishing.value = true
117
+ try {
118
+ await Organization.createDeployment()
119
+ notificationsStore.add({ icon: 'iconoir:reload-window', type: 'default', title: 'Déploiement déclenché...' })
120
+ finish.value = true
121
+ setTimeout(() => { finish.value = false }, 8000)
122
+ } catch (err) {
123
+ const noHook = String(err?.message ?? '').includes('E_NO_DEPLOY_HOOK')
124
+ notificationsStore.add({
125
+ icon: 'iconoir:window-close',
126
+ type: 'warn',
127
+ title: noHook ? 'Aucun hook de déploiement configuré.' : 'Échec du déclenchement du déploiement.'
128
+ })
129
+ } finally {
130
+ publishing.value = false
131
+ }
132
+ }
133
+
134
+ // Prepare watch
135
+ watch(deploying, async (after, before) => {
136
+ if (before === true && after === false) {
137
+ notificationsStore.add({ icon: 'iconoir:window-check', type: 'success', title: 'Déploiement terminé' })
138
+ finish.value = true
139
+ setTimeout(() => { finish.value = false }, 6000)
140
+ } else if (before === false && after === true) {
141
+ notificationsStore.add({ icon: 'iconoir:reload-window', type: 'default', title: 'Déploiement en cours...' })
142
+ }
143
+ })
144
+
145
+ // Prepare directives
146
+ onClickOutside(buttonPublish, _close)
147
+ </script>
@@ -0,0 +1,132 @@
1
+ <template>
2
+ <header class="border-b border-gray-200 relative">
3
+ <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8">
4
+ <div class="py-5 flex items-center justify-between">
5
+ <!-- Single-tenant : identité de la collectivité, sans sélecteur. -->
6
+ <nuxt-link to="/overview" class="flex items-center space-x-3">
7
+ <avatar-circular :place-holder="workspaceStore.workspaceName || 'C'" :media="workspaceStore.workspaceLogo" size="sm" />
8
+ <span class="font-medium">{{ workspaceStore.workspaceName }}</span>
9
+ </nuxt-link>
10
+ <div class="flex flex-row items-center justify-center space-x-2">
11
+ <dropdown-publish v-if="displayPublishButton" class="h-10" />
12
+ <button-secondary class="h-10" icon="lucide:message-circle-question" @click="_openForm()" />
13
+ <button-secondary class="h-10" label="Se déconnecter" @click="_logout()" />
14
+ </div>
15
+ </div>
16
+ </div>
17
+ <div class="max-w-3xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8">
18
+ <div class="hidden sm:block">
19
+ <nav class="-mb-px flex space-x-8 text-gray-500 font-light whitespace-nowrap tracking-wide text-sm" aria-label="Tabs">
20
+ <nuxt-link
21
+ v-for="({ title, route }) in navigationItems"
22
+ :key="route"
23
+ :to="route"
24
+ active-class="text-gray-900 border-b-2 border-gray-900 font-normal"
25
+ class="hover:text-gray-900 hover:font-normal border-transparent py-4 px-1"
26
+ >
27
+ {{ title }}
28
+ </nuxt-link>
29
+ </nav>
30
+ </div>
31
+ <p v-if="demoEnv" class="absolute bg-black rounded-b-lg text-white px-4 text-xs py-0.5">
32
+ beta
33
+ </p>
34
+ </div>
35
+ </header>
36
+ </template>
37
+
38
+ <script setup>
39
+ import { useWorkspaceStore } from '~/store/layout/workspace'
40
+ import avatarCircular from '~/components/elements/avatars/avatar-circular'
41
+ import buttonSecondary from '~/components/elements/buttons/secondary'
42
+ import dropdownPublish from '~/components/layout/navigations/primary/dropdown-publish'
43
+ // import mobileButton from '~/components/template/navigation-section/mobile-section/Button'
44
+ // import mobileMenu from '~/components/template/navigation-section/mobile-section/Menu'
45
+ // import profileDropdown from '~/components/template/navigation-section/ProfileDropdown'
46
+
47
+ const session = useSession()
48
+ const { Collection } = useModels()
49
+ const $ability = useAbility()
50
+ const $router = useRouter()
51
+ const $route = useRoute()
52
+ const $config = useRuntimeConfig()
53
+
54
+ const workspaceStore = useWorkspaceStore()
55
+
56
+ // Prepare constants
57
+ // Single-tenant : les écrans Organisations et Rôles n'ont plus d'objet
58
+ // (une seule collectivité, rôles figés admin/rédacteur) — retirés de la nav.
59
+ const DEFAULT_NAV_ITEMS = [
60
+ {
61
+ title: 'Accueil',
62
+ route: '/overview',
63
+ permission: 'manage:all'
64
+ },
65
+ {
66
+ title: 'Membres',
67
+ route: '/management/members',
68
+ permission: 'manage:all'
69
+ }
70
+ ]
71
+ const WORKSPACE_NAV_ITEMS = [
72
+ {
73
+ title: 'Accueil',
74
+ route: '/overview',
75
+ permission: ''
76
+ },
77
+ {
78
+ title: 'Paramètres',
79
+ route: '/settings',
80
+ permission: 'admin:organization'
81
+ }
82
+ ]
83
+
84
+ // Prepare computed data
85
+ const displayPublishButton = computed(() => !!workspaceStore.workspaceId
86
+ && ($ability.can('manage:all')
87
+ || $ability.can('admin:organization')
88
+ || $ability.can('create:deployments'))
89
+ )
90
+ const navigationItems = computed(() => {
91
+ const items = []
92
+ // Display sub nav if necessary
93
+ if (workspaceStore.workspaceId) {
94
+ const [overview, ...subItemsRest] = WORKSPACE_NAV_ITEMS
95
+ const collections = Collection.repo.find(workspaceStore.workspaceCollections)
96
+ items.push(
97
+ // Single-tenant : les routes vivent à la racine, sans slug d'organisation.
98
+ ...[overview],
99
+ ...collections.map(({ name, slug }) => ({ title: name, route: `/${slug}` })),
100
+ ...subItemsRest
101
+ )
102
+ } else {
103
+ items.push(...DEFAULT_NAV_ITEMS)
104
+ }
105
+ // Filter on user permissions
106
+ return items.filter(({ permission }) => !permission || $ability.can(permission))
107
+ })
108
+ // L'image d'instance est COMMUNE à tous les clients : rien de spécifique n'y est
109
+ // cuit. L'hôte servi est donc la seule source fiable pour distinguer une démo.
110
+ const demoEnv = computed(() => {
111
+ const host = import.meta.client ? window.location.host : ''
112
+ return host.includes('.beta') || host.includes('localhost')
113
+ })
114
+
115
+ // Prepare methods
116
+ const _openForm = () => {
117
+ // https://forms.fillout.com/t/vjkLG9ZXsLus
118
+ const formUrl = 'https://forms.fillout.com/t/vjkLG9ZXsLus'
119
+ const formWindow = window.open(formUrl, '_blank')
120
+ if (formWindow) {
121
+ // Browser has allowed it to be opened
122
+ formWindow.focus()
123
+ } else {
124
+ // Browser has blocked it
125
+ alert('Please allow popups for this website')
126
+ }
127
+ }
128
+ const _logout = async () => {
129
+ await session.logout()
130
+ $router.replace('/')
131
+ }
132
+ </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <div class="fixed bottom-10 right-10 max-w-sm z-[55]">
3
+ <div v-if="notificationsStore.notifications.length" class="relative overflow-hidden space-y-2">
4
+ <notifications-default
5
+ v-for="notification of notificationsStore.notifications"
6
+ :key="notification.id"
7
+ v-bind="notification"
8
+ @close="notificationsStore.close(notification.id)"
9
+ />
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup>
15
+ import { useNotificationsStore } from '~/store/layout/notifications'
16
+ import notificationsDefault from '~/components/elements/notifications/default'
17
+
18
+ const notificationsStore = useNotificationsStore()
19
+ </script>