@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
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @open-commune/web
|
|
2
|
+
|
|
3
|
+
The [Open Commune](https://github.com/open-commune/open-commune) application, published as a **Nuxt layer**: the admin screens, the public API routes, and the tRPC handler that binds them to [`@open-commune/core`](https://www.npmjs.com/package/@open-commune/core).
|
|
4
|
+
|
|
5
|
+
It is shipped as sources, not as a bundle — a Nuxt layer is compiled by the application that extends it, and that is what lets you override any page, component or route by placing a file of the same name in your own project.
|
|
6
|
+
|
|
7
|
+
## Use it as it is
|
|
8
|
+
|
|
9
|
+
The published Docker image runs this layer unmodified. See the [repository](https://github.com/open-commune/open-commune) for self-hosting.
|
|
10
|
+
|
|
11
|
+
## Extend it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @open-commune/web
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// nuxt.config.ts
|
|
19
|
+
export default defineNuxtConfig({
|
|
20
|
+
extends: ['@open-commune/web'],
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Your project then holds only what it adds. Anything you place at the same path wins over the layer:
|
|
25
|
+
|
|
26
|
+
- `pages/…` — your own screens, alongside the ones the layer brings
|
|
27
|
+
- `server/api/…` — extra routes, served from the same origin
|
|
28
|
+
- `server/tasks/…` and `nitro.scheduledTasks` — scheduled work
|
|
29
|
+
- `components/…` — override a component by name
|
|
30
|
+
|
|
31
|
+
Run it with Bun: the database layer uses `bun:sqlite`, so `bun --bun nuxt dev`, not `nuxt dev`.
|
|
32
|
+
|
|
33
|
+
> **Known upstream issue.** Nitro runs a layer's `scheduledTasks` twice ([nitrojs/nitro#4155](https://github.com/nitrojs/nitro/issues/4155)). Declare your cron entries in the top application, not in a layer.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Environment is validated at startup by `@open-commune/core`; see `.env.example` in the repository for the full list.
|
|
38
|
+
|
|
39
|
+
## Licence
|
|
40
|
+
|
|
41
|
+
AGPL-3.0-or-later.
|
package/app.config.ts
ADDED
package/app.vue
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UApp :locale="fr">
|
|
3
|
+
<NuxtLayout>
|
|
4
|
+
<NuxtPage />
|
|
5
|
+
</NuxtLayout>
|
|
6
|
+
</UApp>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
// UApp provides the Nuxt UI overlay providers (tooltips, toasts) —
|
|
11
|
+
// components using them crash without it.
|
|
12
|
+
import { fr } from '@nuxt/ui/locale'
|
|
13
|
+
</script>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "@nuxt/ui";
|
|
3
|
+
|
|
4
|
+
/* Legacy text sizes still referenced by pre-refonte screens; drop once every
|
|
5
|
+
screen has moved to Nuxt UI components. */
|
|
6
|
+
@theme {
|
|
7
|
+
--text-7xs: 0.125rem;
|
|
8
|
+
--text-6xs: 0.25rem;
|
|
9
|
+
--text-5xs: 0.375rem;
|
|
10
|
+
--text-4xs: 0.5rem;
|
|
11
|
+
--text-3xs: 0.625rem;
|
|
12
|
+
--text-2xs: 0.75rem;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<avatar-circular
|
|
3
|
+
:place-holder="name"
|
|
4
|
+
:media="media"
|
|
5
|
+
:organization="organization"
|
|
6
|
+
size="sm"
|
|
7
|
+
/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup>
|
|
11
|
+
import avatarCircular from '~/components/elements/avatars/avatar-circular'
|
|
12
|
+
|
|
13
|
+
const $props = defineProps({
|
|
14
|
+
value: {
|
|
15
|
+
type: Object,
|
|
16
|
+
default: () => ({})
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const organization = computed(() => $props.value._id)
|
|
21
|
+
const name = computed(() => $props.value.name)
|
|
22
|
+
const media = computed(() => $props.value.logo)
|
|
23
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<badge
|
|
4
|
+
:title="badgeTitle"
|
|
5
|
+
:color="badgeColor"
|
|
6
|
+
:dot="false"
|
|
7
|
+
:border="true"
|
|
8
|
+
@click="$emitters('click')"
|
|
9
|
+
/>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
import badge from '~/components/elements/badges/badge'
|
|
15
|
+
|
|
16
|
+
// Prepare emitters
|
|
17
|
+
const $emitters = defineEmits(['click'])
|
|
18
|
+
|
|
19
|
+
// Prepare props
|
|
20
|
+
const $props = defineProps({
|
|
21
|
+
color: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: '#e5e7eb'
|
|
24
|
+
},
|
|
25
|
+
value: {
|
|
26
|
+
type: [String, Object],
|
|
27
|
+
default: ''
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Prepare computed
|
|
32
|
+
const badgeTitle = computed(() => typeof $props.value === 'object' ? $props.value.name : $props.value)
|
|
33
|
+
const badgeColor = computed(() => typeof $props.value === 'object' ? $props.value.color :$props.color)
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-5 w-5 rounded-full max-w-xs" :style="`background-color: ${value}`" @click="$emit('click')">
|
|
3
|
+
​
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
export default {
|
|
9
|
+
name: 'DataColor',
|
|
10
|
+
props: {
|
|
11
|
+
value: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: ''
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<badge :title="badgeTitle" :color="badgeColor" border dot />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import badge from '~/components/elements/badges/badge'
|
|
7
|
+
|
|
8
|
+
// Prepare constants
|
|
9
|
+
const PUBLICATION_STATUSES = [
|
|
10
|
+
{ label: 'Brouillon', color: '#e5e7eb', option: 'draft' },
|
|
11
|
+
{ label: 'A valider', color: '#facc15', option: 'waiting' },
|
|
12
|
+
{ label: 'Prêt', color: '#60a5fa', option: 'ready' },
|
|
13
|
+
{ label: 'Programmé', option: 'scheduled' },
|
|
14
|
+
{ label: 'Publié', color: '#34d399', option: 'published' }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
// Prepare props
|
|
18
|
+
const $props = defineProps({
|
|
19
|
+
status: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: ''
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Prepare computed
|
|
26
|
+
const correspondingStatus = computed(() => PUBLICATION_STATUSES.find(({ option }) => option === $props.status))
|
|
27
|
+
const badgeTitle = computed(() => correspondingStatus.value?.label)
|
|
28
|
+
const badgeColor = computed(() => correspondingStatus.value?.color)
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<badge
|
|
4
|
+
v-for="({ _id, title, color, icon }) in records"
|
|
5
|
+
:key="_id"
|
|
6
|
+
:title="title"
|
|
7
|
+
:color="color"
|
|
8
|
+
:icon="icon"
|
|
9
|
+
@click="$emitters('click', title)"
|
|
10
|
+
/>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import badge from '~/components/elements/badges/badge'
|
|
16
|
+
|
|
17
|
+
const { Record } = useModels()
|
|
18
|
+
|
|
19
|
+
// Prepare emitters
|
|
20
|
+
const $emitters = defineEmits(['click'])
|
|
21
|
+
|
|
22
|
+
// Prepare props
|
|
23
|
+
const $props = defineProps({
|
|
24
|
+
value: {
|
|
25
|
+
type: [String, Array],
|
|
26
|
+
default: () => []
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Prepare computed
|
|
31
|
+
const records = computed(() => {
|
|
32
|
+
if (!Array.isArray($props.value)) {
|
|
33
|
+
return [Record.onRetrieve(Record.repo.find($props.value))]
|
|
34
|
+
}
|
|
35
|
+
return Record.repo.find($props.value).map(Record.onRetrieve)
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-right space-y-1">
|
|
3
|
+
<div v-if="inline" class="inline-flex items-center">
|
|
4
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
5
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
6
|
+
</svg>
|
|
7
|
+
<span class="text-gray-500 text-sm">
|
|
8
|
+
Modifié {{ updatedAtFromNow }}, par
|
|
9
|
+
</span>
|
|
10
|
+
<avatar-with-text :user="updatedBy" prefix="@" no-avatar disabled />
|
|
11
|
+
</div>
|
|
12
|
+
<div v-else>
|
|
13
|
+
<avatar-with-text :user="updatedBy" disabled class="space-x-2" />
|
|
14
|
+
<div class="flex items-center justify-end space-x-1">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
17
|
+
</svg>
|
|
18
|
+
<span class="text-gray-500 text-xs">
|
|
19
|
+
{{ updatedAtFromNow }}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup>
|
|
27
|
+
import { DateTime } from 'luxon'
|
|
28
|
+
import avatarWithText from '~/components/elements/avatars/avatar-with-text'
|
|
29
|
+
|
|
30
|
+
// Prepare props
|
|
31
|
+
const $props = defineProps({
|
|
32
|
+
inline: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false
|
|
35
|
+
},
|
|
36
|
+
value: {
|
|
37
|
+
type: Object,
|
|
38
|
+
default: () => ({})
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Prepare computed
|
|
43
|
+
const updatedBy = computed(() => $props.value?.updatedBy || {})
|
|
44
|
+
const updatedAtFromNow = computed(() => DateTime.fromISO($props.value?.updatedAt).setLocale('fr').toRelative())
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col space-y-1 py-1 items-start justify-center text-sm cursor-pointer" @click="$emit('click')">
|
|
3
|
+
<data-display-badge-publish-status :status="status" class="sm:hidden" />
|
|
4
|
+
<div class="flex items-center space-x-2">
|
|
5
|
+
<!-- max-w borné (upgrade-admin-nuxt4) : truncate = nowrap, un titre long
|
|
6
|
+
dictait la largeur max-content de la colonne (table-auto). -->
|
|
7
|
+
<p class="font-normal xs:w-60 truncate sm:min-w-0 sm:max-w-xl">
|
|
8
|
+
{{ title }}
|
|
9
|
+
</p>
|
|
10
|
+
<data-display-badge-publish-status :status="status" class="hidden sm:inline-flex" />
|
|
11
|
+
</div>
|
|
12
|
+
<!-- Bornée (upgrade-admin-nuxt4) : une description longue étirait la
|
|
13
|
+
colonne au-delà de l'écran (table-auto suit la largeur du contenu). -->
|
|
14
|
+
<p class="hidden sm:block font-light text-gray-500 max-w-xl line-clamp-2 break-words">
|
|
15
|
+
{{ description }}
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import DataDisplayBadgePublishStatus from '~/components/data/display/row/data-display-badge-publish-status'
|
|
22
|
+
import badge from '~/components/elements/badges/badge'
|
|
23
|
+
|
|
24
|
+
// Prepare props
|
|
25
|
+
const $props = defineProps({
|
|
26
|
+
titleKey: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: 'title'
|
|
29
|
+
},
|
|
30
|
+
descriptionKey: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: 'description'
|
|
33
|
+
},
|
|
34
|
+
value: {
|
|
35
|
+
type: Object,
|
|
36
|
+
default: () => ({})
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Prepare computed
|
|
41
|
+
const title = computed(() => $props.value[$props.titleKey])
|
|
42
|
+
const description = computed(() => $props.value[$props.descriptionKey])
|
|
43
|
+
const status = computed(() => $props.value.status)
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="inline-flex">
|
|
3
|
+
<input-toggle :value="value" @change="$emit('change', $event)" />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import inputToggle from '~/components/forms/inputs/input-toggle'
|
|
9
|
+
|
|
10
|
+
defineProps({
|
|
11
|
+
value: {
|
|
12
|
+
type: Boolean,
|
|
13
|
+
default: false
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<li class="w-full inline-flex items-center justify-start hover:bg-gray-50 cursor-pointer" @click.self="_handleClick">
|
|
3
|
+
<div
|
|
4
|
+
v-for="({ property, component, componentOptions, nestedValue = true, alignRight = false, alignLeft = false, grow = false }) in headers"
|
|
5
|
+
:key="property"
|
|
6
|
+
:class="{
|
|
7
|
+
'flex-grow': grow,
|
|
8
|
+
'text-right': alignRight,
|
|
9
|
+
'text-left': alignLeft
|
|
10
|
+
}"
|
|
11
|
+
class="px-4 py-1"
|
|
12
|
+
>
|
|
13
|
+
<data-avatar
|
|
14
|
+
v-if="component === 'data-avatar'"
|
|
15
|
+
v-bind="componentOptions"
|
|
16
|
+
:value="nestedValue ? item[property] : item"
|
|
17
|
+
@click="_handleClick"
|
|
18
|
+
/>
|
|
19
|
+
<data-text
|
|
20
|
+
v-if="component === 'data-text'"
|
|
21
|
+
v-bind="componentOptions"
|
|
22
|
+
:value="nestedValue ? item[property] : item"
|
|
23
|
+
@click="_handleClick"
|
|
24
|
+
/>
|
|
25
|
+
<data-badge
|
|
26
|
+
v-if="component === 'data-badge'"
|
|
27
|
+
v-bind="componentOptions"
|
|
28
|
+
:value="nestedValue ? item[property] : item"
|
|
29
|
+
@click="_handleClick"
|
|
30
|
+
/>
|
|
31
|
+
<data-label
|
|
32
|
+
v-if="component === 'data-label'"
|
|
33
|
+
v-bind="componentOptions"
|
|
34
|
+
:value="nestedValue ? item[property] : item"
|
|
35
|
+
@click="_handleClick"
|
|
36
|
+
/>
|
|
37
|
+
<data-spacer
|
|
38
|
+
v-if="component === 'data-spacer'"
|
|
39
|
+
v-bind="componentOptions"
|
|
40
|
+
/>
|
|
41
|
+
<data-last-update
|
|
42
|
+
v-if="component === 'data-last-update'"
|
|
43
|
+
v-bind="componentOptions"
|
|
44
|
+
:value="nestedValue ? item[property] : item"
|
|
45
|
+
@click="_handleClick"
|
|
46
|
+
/>
|
|
47
|
+
<!-- <data-color v-if="component === 'data-color'" v-bind="componentOptions" :value="nestedValue ? item[property] : item" class="hidden lg:block" /> -->
|
|
48
|
+
<data-summary
|
|
49
|
+
v-if="component === 'data-summary'"
|
|
50
|
+
v-bind="componentOptions"
|
|
51
|
+
:value="nestedValue ? item[property] : item"
|
|
52
|
+
@click="_handleClick"
|
|
53
|
+
/>
|
|
54
|
+
<data-toggle
|
|
55
|
+
v-if="component === 'data-toggle'"
|
|
56
|
+
v-bind="componentOptions"
|
|
57
|
+
:value="nestedValue ? item[property] : item"
|
|
58
|
+
class="item-self-end"
|
|
59
|
+
@change="$emit(property, { ...item, [property]: $event })"
|
|
60
|
+
/>
|
|
61
|
+
<dropdown-dots
|
|
62
|
+
v-else-if="component === 'actions'"
|
|
63
|
+
v-bind="componentOptions"
|
|
64
|
+
@action="$emit($event, item)"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</li>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup>
|
|
71
|
+
import dataBadge from '~/components/data/display/row/data-badge'
|
|
72
|
+
// import dataColor from '~/components/data/display/row/data-color'
|
|
73
|
+
import dropdownDots from '~/components/elements/dropdowns/dots'
|
|
74
|
+
import dataLabel from '~/components/data/display/row/data-label'
|
|
75
|
+
import dataLastUpdate from '~/components/data/display/row/data-last-update'
|
|
76
|
+
import dataSummary from '~/components/data/display/row/data-summary'
|
|
77
|
+
import dataSpacer from '~/components/data/display/row/data-spacer'
|
|
78
|
+
import dataAvatar from '~/components/data/display/row/data-avatar'
|
|
79
|
+
import dataText from '~/components/data/display/row/data-text'
|
|
80
|
+
import dataToggle from '~/components/data/display/row/data-toggle'
|
|
81
|
+
|
|
82
|
+
// Prepare emit
|
|
83
|
+
const $router = useRouter()
|
|
84
|
+
const $emit = defineEmits(['click', 'update', 'remove', 'fetch-more', 'usage'])
|
|
85
|
+
|
|
86
|
+
// Prepare props
|
|
87
|
+
const $props = defineProps({
|
|
88
|
+
displayHeader: {
|
|
89
|
+
type: Boolean,
|
|
90
|
+
default: false
|
|
91
|
+
},
|
|
92
|
+
headers: {
|
|
93
|
+
type: Array,
|
|
94
|
+
default: () => []
|
|
95
|
+
},
|
|
96
|
+
item: {
|
|
97
|
+
type: Object,
|
|
98
|
+
default: () => ({})
|
|
99
|
+
},
|
|
100
|
+
to: {
|
|
101
|
+
type: String,
|
|
102
|
+
default: ''
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Prepare methods
|
|
107
|
+
const _handleClick = () => {
|
|
108
|
+
console.log('routing to', $props.to)
|
|
109
|
+
$props.to ? $router.replace($props.to) : $emit('click')
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ul :class="gridDensity" class="grid gap-2 w-full">
|
|
3
|
+
<li
|
|
4
|
+
v-for="({ children, ...item }, i) in groupedItems"
|
|
5
|
+
:key="`group-item-${item[itemValue]}`"
|
|
6
|
+
:class="{ 'col-span-full': children?.length }"
|
|
7
|
+
>
|
|
8
|
+
<!-- [list]item-by slot -->
|
|
9
|
+
<slot name="item" v-bind="{ item, isFirst: i === 0, isLast: i === groupedItems.length - 1 }">
|
|
10
|
+
{{ item }}
|
|
11
|
+
</slot>
|
|
12
|
+
|
|
13
|
+
<!-- [list]item-child slot -->
|
|
14
|
+
<ul v-if="children?.length" :class="gridDensityChidren" class="grid w-full">
|
|
15
|
+
<li v-for="(itemChild, j) in children" :key="`item-${itemChild[itemValue]}`">
|
|
16
|
+
<slot name="item-child" v-bind="{ item: itemChild, isFirst: j === 0, isLast: j === children.length - 1 }">
|
|
17
|
+
{{ itemChild }}
|
|
18
|
+
</slot>
|
|
19
|
+
</li>
|
|
20
|
+
</ul>
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup>
|
|
26
|
+
// Prepare constants
|
|
27
|
+
const GRID_PRESETS = {
|
|
28
|
+
1: 'grid-cols-1',
|
|
29
|
+
2: 'grid-cols-1 sm:grid-cols-2',
|
|
30
|
+
3: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
|
31
|
+
4: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
|
32
|
+
5: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Prepare props
|
|
36
|
+
const $props = defineProps({
|
|
37
|
+
items: {
|
|
38
|
+
type: Array,
|
|
39
|
+
default: () => []
|
|
40
|
+
},
|
|
41
|
+
itemKey: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: ''
|
|
44
|
+
},
|
|
45
|
+
itemValue: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: 'value'
|
|
48
|
+
},
|
|
49
|
+
groupBy: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: ''
|
|
52
|
+
},
|
|
53
|
+
density: {
|
|
54
|
+
type: Number,
|
|
55
|
+
default: 5,
|
|
56
|
+
enums: [1, 2, 3, 4, 5]
|
|
57
|
+
},
|
|
58
|
+
densityChildren: {
|
|
59
|
+
type: Number,
|
|
60
|
+
default: 5,
|
|
61
|
+
enums: [1, 2, 3, 4, 5]
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Prepare computed
|
|
66
|
+
const gridDensity = computed(() => GRID_PRESETS[$props.density])
|
|
67
|
+
const gridDensityChidren = computed(() => GRID_PRESETS[$props.densityChildren])
|
|
68
|
+
const groupedItems = computed(() => {
|
|
69
|
+
if ($props.groupBy) {
|
|
70
|
+
const _items = new Map()
|
|
71
|
+
for (const item of $props.items) {
|
|
72
|
+
const group = item[$props.groupBy]
|
|
73
|
+
if (group) {
|
|
74
|
+
const parent = $props.items.find(({ [$props.itemValue]: check }) => check === group)
|
|
75
|
+
if (!_items.has(parent)) {
|
|
76
|
+
_items.set(parent, [])
|
|
77
|
+
}
|
|
78
|
+
_items.get(parent).push(item)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Array.from(_items, ([parent, children]) => ({ ...parent, children }))
|
|
82
|
+
}
|
|
83
|
+
return $props.items
|
|
84
|
+
})
|
|
85
|
+
</script>
|