@tekkare/romulus 0.1.248 → 0.1.250
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/dist/module.json +1 -1
- package/dist/runtime/components/Notes.vue +40 -2
- package/layer/app/assets/oip-frame.css +36 -0
- package/layer/app/components/panels/OipPanelNotes.vue +4 -1
- package/layer/app/components/panels/OipPanelStatistics.vue +17 -2
- package/layer/nuxt.config.ts +2 -0
- package/layer/server/api/oip/stats.get.ts +3 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -17,9 +17,16 @@
|
|
|
17
17
|
main : une note qu'on rallonge annoncerait sinon une duree fausse. La date,
|
|
18
18
|
elle, vient de l'appelant — c'est une date de revision editoriale, que rien
|
|
19
19
|
dans le contenu ne permet de deviner.
|
|
20
|
+
|
|
21
|
+
`openId` sert au lecteur qui arrive par un renvoi (le « i » d'un filtre) :
|
|
22
|
+
la note qu'il est venu chercher s'ouvre depliee et defile en vue, au lieu
|
|
23
|
+
d'une liste repliee ou il devrait la retrouver. La recherche et la pastille
|
|
24
|
+
sont remises a zero, sinon la note pourrait etre filtree hors de la liste.
|
|
25
|
+
Le depliage n'a lieu qu'une fois par identifiant : un rechargement des notes
|
|
26
|
+
ne rouvre pas une note que le lecteur vient de replier.
|
|
20
27
|
-->
|
|
21
28
|
<script setup lang="ts">
|
|
22
|
-
import { computed, ref } from 'vue'
|
|
29
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
23
30
|
|
|
24
31
|
export type RmNoteTone = 'primary' | 'info' | 'warning' | 'success' | 'neutral'
|
|
25
32
|
|
|
@@ -90,6 +97,11 @@ const props = withDefaults(defineProps<{
|
|
|
90
97
|
*/
|
|
91
98
|
editable?: boolean
|
|
92
99
|
lang?: 'fr' | 'en'
|
|
100
|
+
/**
|
|
101
|
+
* Note a ouvrir depliee et a montrer. Attendue meme si les notes arrivent
|
|
102
|
+
* apres (chargement distant) : elle s'ouvre des qu'elle est dans la liste.
|
|
103
|
+
*/
|
|
104
|
+
openId?: string | null
|
|
93
105
|
}>(), {
|
|
94
106
|
categories: () => [],
|
|
95
107
|
title: 'Notes',
|
|
@@ -97,6 +109,7 @@ const props = withDefaults(defineProps<{
|
|
|
97
109
|
badge: undefined,
|
|
98
110
|
editable: false,
|
|
99
111
|
lang: 'fr',
|
|
112
|
+
openId: null,
|
|
100
113
|
})
|
|
101
114
|
|
|
102
115
|
const emit = defineEmits<{ navigate: [to: string], edit: [id: string] }>()
|
|
@@ -136,6 +149,30 @@ const search = ref('')
|
|
|
136
149
|
const category = ref('all')
|
|
137
150
|
const open = ref<string[]>([])
|
|
138
151
|
|
|
152
|
+
const root = ref<HTMLElement | null>(null)
|
|
153
|
+
let revealed: string | null = null
|
|
154
|
+
|
|
155
|
+
watch(
|
|
156
|
+
() => [props.openId, props.notes] as const,
|
|
157
|
+
([id]) => {
|
|
158
|
+
if (!id) {
|
|
159
|
+
revealed = null
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if (id === revealed || !props.notes.some(note => note.id === id)) return
|
|
163
|
+
revealed = id
|
|
164
|
+
search.value = ''
|
|
165
|
+
category.value = 'all'
|
|
166
|
+
if (!open.value.includes(id)) open.value = [...open.value, id]
|
|
167
|
+
nextTick(() => {
|
|
168
|
+
root.value
|
|
169
|
+
?.querySelector(`[data-note-id="${CSS.escape(id)}"]`)
|
|
170
|
+
?.scrollIntoView({ block: 'start' })
|
|
171
|
+
})
|
|
172
|
+
},
|
|
173
|
+
{ immediate: true },
|
|
174
|
+
)
|
|
175
|
+
|
|
139
176
|
function toggle(id: string) {
|
|
140
177
|
open.value = open.value.includes(id)
|
|
141
178
|
? open.value.filter(item => item !== id)
|
|
@@ -237,7 +274,7 @@ function onLink(to: string) {
|
|
|
237
274
|
</script>
|
|
238
275
|
|
|
239
276
|
<template>
|
|
240
|
-
<div class="rm-notes">
|
|
277
|
+
<div ref="root" class="rm-notes">
|
|
241
278
|
<div class="rm-notes__head">
|
|
242
279
|
<h2 v-if="title" class="rm-notes__title">{{ title }}</h2>
|
|
243
280
|
<span v-if="badge" class="rm-notes__badge">
|
|
@@ -290,6 +327,7 @@ function onLink(to: string) {
|
|
|
290
327
|
<article
|
|
291
328
|
v-for="note in shown"
|
|
292
329
|
:key="note.id"
|
|
330
|
+
:data-note-id="note.id"
|
|
293
331
|
class="rm-notes__note"
|
|
294
332
|
:class="{ 'rm-notes__note--open': open.includes(note.id) }"
|
|
295
333
|
>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Regles du cadre OIP 7 communes a toutes les apps qui etendent le layer.
|
|
3
|
+
Non prefixees : elles visent la colonne `.main_container` que chaque app
|
|
4
|
+
pose dans son layout, sous le `.rm-shell` d'OipShell.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Le bandeau de page traverse tout le cadre, quel que soit le wrapper de la
|
|
8
|
+
page (`oip_grid`, `arg_page_layout`...) : meme calcul que la barre de
|
|
9
|
+
filtres (largeur publiee par OipShell, marges negatives). Son contenu reste
|
|
10
|
+
cale sur la colonne de contenu. */
|
|
11
|
+
.rm-shell .main_container .rm-page-header {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
width: var(--oip-main-w, 100%);
|
|
14
|
+
max-width: none;
|
|
15
|
+
margin-inline: calc((min(var(--oip-content-max, 100%), 100%) - var(--oip-main-w, 100%)) / 2);
|
|
16
|
+
padding-inline: max(32px, (var(--oip-main-w, 100%) - var(--oip-content-max, 100%)) / 2 + 32px);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* La barre calcule sa largeur elle-meme : le plafond de la colonne
|
|
20
|
+
(`.main_container > *`) la couperait quand elle en est enfant direct. */
|
|
21
|
+
.rm-shell .main_container #rm_filter_bar {
|
|
22
|
+
max-width: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* La barre de filtres colle au bandeau : le gap du wrapper de page saute
|
|
26
|
+
entre les deux (barre soeur du bandeau, ou premiere fille de son voisin),
|
|
27
|
+
l'espacement est rendu aux blocs suivants. Pas de `:has()` dans un
|
|
28
|
+
`:has()` : le selecteur serait invalide et la regle entiere ignoree. */
|
|
29
|
+
.rm-shell .main_container :has(> .rm-page-header + #rm_filter_bar),
|
|
30
|
+
.rm-shell .main_container :has(> .rm-page-header + * > #rm_filter_bar:first-child) {
|
|
31
|
+
row-gap: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.rm-shell .main_container .rm-page-header + :is(#rm_filter_bar, :has(> #rm_filter_bar:first-child)) ~ * {
|
|
35
|
+
margin-top: 32px;
|
|
36
|
+
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
badge="Tekkare"
|
|
30
30
|
:lang="locale === 'en' ? 'en' : 'fr'"
|
|
31
31
|
:editable="superAdmin"
|
|
32
|
+
:open-id="openId"
|
|
32
33
|
@navigate="go"
|
|
33
34
|
@edit="openEditor"
|
|
34
35
|
/>
|
|
@@ -60,8 +61,10 @@ const props = withDefaults(
|
|
|
60
61
|
showOrigin?: boolean;
|
|
61
62
|
/** Notes encore ecrites dans le code de l'app (cf. useOipNotes). */
|
|
62
63
|
localNotes?: Note[];
|
|
64
|
+
/** Note a ouvrir depliee : le « i » d'un filtre renvoie a sa note. */
|
|
65
|
+
openId?: string | null;
|
|
63
66
|
}>(),
|
|
64
|
-
{ sourceUuids: () => [], superAdmin: false, showOrigin: undefined, localNotes: () => [] },
|
|
67
|
+
{ openId: null, sourceUuids: () => [], superAdmin: false, showOrigin: undefined, localNotes: () => [] },
|
|
65
68
|
);
|
|
66
69
|
|
|
67
70
|
const { locale } = useI18n();
|
|
@@ -136,6 +136,15 @@
|
|
|
136
136
|
</tr>
|
|
137
137
|
</tbody>
|
|
138
138
|
</table>
|
|
139
|
+
<RmButton
|
|
140
|
+
v-if="pagesShown < viewedPages.length"
|
|
141
|
+
variant="secondary"
|
|
142
|
+
size="sm"
|
|
143
|
+
class="stats_more"
|
|
144
|
+
@click="pagesShown += PAGES_STEP"
|
|
145
|
+
>
|
|
146
|
+
{{ isFr ? "Charger plus" : "Load more" }}
|
|
147
|
+
</RmButton>
|
|
139
148
|
</section>
|
|
140
149
|
|
|
141
150
|
<p class="stats_note">
|
|
@@ -257,12 +266,18 @@ const screensHint = computed(() =>
|
|
|
257
266
|
const seen = (iso: string) =>
|
|
258
267
|
iso ? new Date(iso).toLocaleDateString(isFr.value ? "fr-FR" : "en-GB", { day: "2-digit", month: "short" }) : "";
|
|
259
268
|
|
|
260
|
-
|
|
269
|
+
// Toutes les pages vues sur la periode, dix par dix : les ecrans peu visites
|
|
270
|
+
// (une fiche, une page recente) doivent rester trouvables.
|
|
271
|
+
const PAGES_STEP = 10;
|
|
272
|
+
const pagesShown = ref(PAGES_STEP);
|
|
273
|
+
watch([period, internal], () => (pagesShown.value = PAGES_STEP));
|
|
274
|
+
|
|
275
|
+
const viewedPages = computed(() =>
|
|
261
276
|
(data.value?.topPages ?? [])
|
|
262
277
|
.filter((p) => p.pageviews[period.value] > 0)
|
|
263
278
|
.sort((a, b) => b.pageviews[period.value] - a.pageviews[period.value])
|
|
264
|
-
.slice(0, 10)
|
|
265
279
|
);
|
|
280
|
+
const topPages = computed(() => viewedPages.value.slice(0, pagesShown.value));
|
|
266
281
|
</script>
|
|
267
282
|
|
|
268
283
|
<style scoped>
|
package/layer/nuxt.config.ts
CHANGED
|
@@ -21,6 +21,8 @@ export default defineNuxtConfig({
|
|
|
21
21
|
future: {
|
|
22
22
|
compatibilityVersion: 4,
|
|
23
23
|
},
|
|
24
|
+
// Regles du cadre communes aux apps (bandeau de page, barre de filtres).
|
|
25
|
+
css: [fileURLToPath(new URL("./app/assets/oip-frame.css", import.meta.url))],
|
|
24
26
|
// Reglages serveur propres au layer. Les cles Directus (directus*Url,
|
|
25
27
|
// directus*Token) restent declarees par l'app, qui les avait deja.
|
|
26
28
|
runtimeConfig: {
|
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
* Reservee aux superadmins, comme les metadonnees.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Large : chaque fiche (une marque, une association) est une page, et le
|
|
31
|
+
// panneau doit pouvoir descendre jusqu'aux ecrans peu visites.
|
|
32
|
+
const TOP_PAGES = 300;
|
|
31
33
|
|
|
32
34
|
export default defineEventHandler(async (event): Promise<OipStats> => {
|
|
33
35
|
const { path, hosts, where, hogql } = await oipStatsContext(event);
|