@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,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>
|