@tekkare/romulus 0.1.231 → 0.1.233

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.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Les projets Virtual Lab : un dossier partage ou l'utilisateur range ce qu'il
3
+ * croise dans les apps OIP (un essai, une publication, un avis HAS...).
4
+ *
5
+ * L'API ne garde d'un element que sa paire (resourceId, resourceType). Pour
6
+ * l'afficher, une app relit le document dans Elasticsearch, et c'est tout ce
7
+ * qu'elle a a decrire : une « famille » (`RmProjectKind`) dit dans quel index
8
+ * chercher, comment titrer le document et ou mene son lien. Le reste (les
9
+ * lignes, la recherche, le tri, les groupes de projets, le resume d'un projet)
10
+ * vit ici, pour que toutes les apps affichent n'importe quel projet de la
11
+ * meme facon, y compris les familles rangees depuis une autre app des qu'elles
12
+ * en connaissent la description.
13
+ *
14
+ * Les composants RmProject* ne font aucun appel : ils recoivent ces objets et
15
+ * rendent des intentions (ajouter, ouvrir, retirer). Les types sont ici et pas
16
+ * dans les `.vue` : le paquet n'expose que `utils/*`, et une app qui construit
17
+ * ces objets doit pouvoir les typer.
18
+ */
19
+ export type RmProjectLang = 'fr' | 'en';
20
+ /** Un projet, tel que les listes l'affichent. */
21
+ export interface RmProject {
22
+ id: string;
23
+ title: string;
24
+ description?: string;
25
+ createdAt?: string;
26
+ /** Proprietaire : nom affiche, a defaut l'email. */
27
+ ownerName?: string;
28
+ /** Le lecteur en est le proprietaire. */
29
+ mine?: boolean;
30
+ /** Ouvert a l'espace de travail ou a d'autres personnes. */
31
+ shared?: boolean;
32
+ /** Ressources presentes, toutes familles confondues. Absent : non compte. */
33
+ resourcesCount?: number;
34
+ pinned?: boolean;
35
+ /** Lien vers la page du projet. */
36
+ to?: string;
37
+ }
38
+ export interface RmProjectGroup {
39
+ key: 'pinned' | 'mine' | 'shared';
40
+ label: string;
41
+ projects: RmProject[];
42
+ }
43
+ /**
44
+ * Une famille de ressources : ce qu'une app sait relire et afficher.
45
+ *
46
+ * `S` est la forme du `_source` Elasticsearch. L'identifiant range est
47
+ * toujours l'`_id` du document : c'est par lui que l'API verifie un ajout.
48
+ */
49
+ export interface RmProjectKind<S = any> {
50
+ /** Valeur de `resourceType` cote API. */
51
+ type: string;
52
+ index: string;
53
+ label: Record<RmProjectLang, string>;
54
+ /** Rubrique de l'app d'ou vient la famille (Essais, Publications...). */
55
+ section?: Record<RmProjectLang, string>;
56
+ /** Icone Phosphor. */
57
+ icon: string;
58
+ /** Champs a relire. Absent : le document entier. */
59
+ source?: string[];
60
+ title: (source: S, lang: RmProjectLang) => string;
61
+ subtitle?: (source: S, lang: RmProjectLang) => string;
62
+ /** Chemin de la fiche dans l'app, sans prefixe de langue. */
63
+ path?: (source: S, id: string) => string;
64
+ /** Titre lisible dans l'identifiant seul, quand le document a disparu. */
65
+ titleFromId?: (id: string) => string;
66
+ }
67
+ /** Un element du projet, tel que l'API le rend. */
68
+ export interface RmProjectItem {
69
+ resourceId: string;
70
+ resourceType: string;
71
+ createdAt?: string;
72
+ /** Commentaires et citations dans les notes. */
73
+ commentsCount?: number;
74
+ }
75
+ /** Une ligne de ressource, prete a afficher. */
76
+ export interface RmProjectResource {
77
+ id: string;
78
+ type: string;
79
+ /** Vide quand le document n'a pas ete retrouve. */
80
+ title: string;
81
+ subtitle?: string;
82
+ path?: string;
83
+ /** Libelle de la famille. */
84
+ family: string;
85
+ section?: string;
86
+ icon: string;
87
+ createdAt?: string;
88
+ comments?: number;
89
+ /** La famille est decrite par l'app. Faux : element range ailleurs. */
90
+ known: boolean;
91
+ /** Le document relu, pour un affichage propre a l'app. */
92
+ source?: unknown;
93
+ }
94
+ /** Une fiche refusee parce qu'archivee, nommee pour la question a poser. */
95
+ export interface RmProjectArchivedEntry {
96
+ key: string;
97
+ label: string;
98
+ }
99
+ /** Une rangee du cadrage, deduite par l'app de ce que le projet range. */
100
+ export interface RmProjectScopeRow {
101
+ key: string;
102
+ label: string;
103
+ icon: string;
104
+ values: string[];
105
+ /** Texte quand la rangee est vide. */
106
+ none?: string;
107
+ }
108
+ /** Une facon d'avancer : un onglet du projet. */
109
+ export interface RmProjectStep {
110
+ tab: string;
111
+ icon: string;
112
+ title: string;
113
+ text: string;
114
+ }
115
+ /**
116
+ * Une entree du fil d'une ressource : un commentaire, ou une note du projet
117
+ * qui la cite (avec les passages qui la citent).
118
+ */
119
+ export interface RmProjectCommentEntry {
120
+ id: string;
121
+ author?: string;
122
+ date?: string;
123
+ text?: string;
124
+ /** Presente quand l'entree est une note qui cite la ressource. */
125
+ note?: {
126
+ id: string;
127
+ title?: string;
128
+ icon?: string;
129
+ private?: boolean;
130
+ excerpts?: string[];
131
+ };
132
+ }
133
+ export type RmProjectSort = 'date-desc' | 'date-asc' | 'title' | 'family';
134
+ /** Les documents relus, par famille puis par identifiant. */
135
+ export type RmProjectDocuments = Record<string, Record<string, unknown>>;
136
+ /** « 1 ressource », « 3 ressources ». */
137
+ export declare function rmProjectCount(count: number, lang?: RmProjectLang): string;
138
+ /**
139
+ * La ligne qui situe un projet : qui le tient, s'il est partage, combien il
140
+ * contient. Sans compte, elle s'en passe plutot que d'afficher zero.
141
+ */
142
+ export declare function rmProjectSummary(project: RmProject, lang?: RmProjectLang): string;
143
+ /**
144
+ * Les epingles d'abord, quel que soit leur proprietaire, puis les miens, puis
145
+ * ceux qu'on m'a partages. Un groupe vide n'est pas rendu ; l'ordre recu tient
146
+ * dans chaque groupe.
147
+ */
148
+ export declare function rmProjectGroups(list: RmProject[], lang?: RmProjectLang): RmProjectGroup[];
149
+ /**
150
+ * La requete qui relit les documents d'une famille. Un appel par famille : la
151
+ * requete porte sur l'index du type, c'est le seul lien entre un element range
152
+ * et sa donnee.
153
+ */
154
+ export declare function rmProjectQuery(kind: RmProjectKind, ids: string[]): Record<string, unknown>;
155
+ /** Les hits d'une reponse, ranges par `_id`. */
156
+ export declare function rmProjectIndexHits(hits: Array<{
157
+ _id: string;
158
+ _source?: any;
159
+ }>): Record<string, unknown>;
160
+ /**
161
+ * Relit les documents de tous les elements, une requete par famille, en
162
+ * parallele. `search` est l'appel Elasticsearch de l'app (son proxy). Une
163
+ * famille en echec reste vide : ses lignes sortent sans titre, la page tient.
164
+ */
165
+ export declare function rmProjectHydrate(items: RmProjectItem[], kinds: RmProjectKind[], search: (index: string, body: Record<string, unknown>) => Promise<{
166
+ hits?: {
167
+ hits?: Array<{
168
+ _id: string;
169
+ _source?: any;
170
+ }>;
171
+ };
172
+ } | null | undefined>): Promise<RmProjectDocuments>;
173
+ /**
174
+ * Les lignes du projet, dans l'ordre des familles de l'app. Un element dont
175
+ * la famille n'est pas decrite sort quand meme (`known: false`), sous son
176
+ * type : mieux vaut le montrer que le faire disparaitre du projet. A l'app de
177
+ * le filtrer si elle ne veut que les siennes.
178
+ */
179
+ export declare function rmProjectRows(items: RmProjectItem[], documents: RmProjectDocuments, kinds: RmProjectKind[], lang?: RmProjectLang): RmProjectResource[];
180
+ /** Les familles presentes, avec leur compte, dans l'ordre des lignes. */
181
+ export declare function rmProjectFamilies(rows: RmProjectResource[]): Array<{
182
+ type: string;
183
+ label: string;
184
+ icon: string;
185
+ count: number;
186
+ }>;
187
+ export interface RmProjectFilter {
188
+ search?: string;
189
+ /** Famille retenue, `all` ou absent pour toutes. */
190
+ family?: string;
191
+ onlyCommented?: boolean;
192
+ sort?: RmProjectSort;
193
+ }
194
+ /**
195
+ * Filtre et trie en memoire : un projet tient quelques centaines d'elements,
196
+ * deja relus. La recherche porte sur tout ce que la ligne affiche et sur
197
+ * l'identifiant (un element sans document n'a plus que lui) ; chaque mot doit
198
+ * se trouver quelque part, casse et accents ignores.
199
+ */
200
+ export declare function rmProjectFilterRows(rows: RmProjectResource[], filter: RmProjectFilter, lang?: RmProjectLang): RmProjectResource[];
@@ -0,0 +1,123 @@
1
+ const WORDS = {
2
+ fr: {
3
+ pinned: "\xC9pingl\xE9s",
4
+ mine: "Mes projets",
5
+ shared: "Partag\xE9s avec moi",
6
+ me: "Moi",
7
+ isShared: "Partag\xE9",
8
+ personal: "Personnel",
9
+ resource: "ressource",
10
+ resources: "ressources"
11
+ },
12
+ en: {
13
+ pinned: "Pinned",
14
+ mine: "My projects",
15
+ shared: "Shared with me",
16
+ me: "Me",
17
+ isShared: "Shared",
18
+ personal: "Personal",
19
+ resource: "resource",
20
+ resources: "resources"
21
+ }
22
+ };
23
+ export function rmProjectCount(count, lang = "fr") {
24
+ const T = WORDS[lang] ?? WORDS.fr;
25
+ return `${count} ${count > 1 ? T.resources : T.resource}`;
26
+ }
27
+ export function rmProjectSummary(project, lang = "fr") {
28
+ const T = WORDS[lang] ?? WORDS.fr;
29
+ const owner = project.mine ? T.me : project.ownerName || "";
30
+ const kind = project.shared ? T.isShared : T.personal;
31
+ const size = project.resourcesCount == null ? "" : rmProjectCount(project.resourcesCount, lang);
32
+ return [owner, kind, size].filter(Boolean).join(" \xB7 ");
33
+ }
34
+ export function rmProjectGroups(list, lang = "fr") {
35
+ const T = WORDS[lang] ?? WORDS.fr;
36
+ const groups = [
37
+ { key: "pinned", label: T.pinned, projects: list.filter((p) => p.pinned) },
38
+ { key: "mine", label: T.mine, projects: list.filter((p) => !p.pinned && p.mine) },
39
+ { key: "shared", label: T.shared, projects: list.filter((p) => !p.pinned && !p.mine) }
40
+ ];
41
+ return groups.filter((group) => group.projects.length);
42
+ }
43
+ function fold(value) {
44
+ return String(value ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
45
+ }
46
+ export function rmProjectQuery(kind, ids) {
47
+ return {
48
+ size: ids.length,
49
+ ...kind.source && { _source: kind.source },
50
+ query: { ids: { values: ids } }
51
+ };
52
+ }
53
+ export function rmProjectIndexHits(hits) {
54
+ return Object.fromEntries(hits.map((hit) => [String(hit._id), hit._source ?? {}]));
55
+ }
56
+ export async function rmProjectHydrate(items, kinds, search) {
57
+ const byType = /* @__PURE__ */ new Map();
58
+ for (const item of items) {
59
+ if (!kinds.some((kind) => kind.type === item.resourceType)) continue;
60
+ byType.set(item.resourceType, [...byType.get(item.resourceType) ?? [], item.resourceId]);
61
+ }
62
+ const out = {};
63
+ await Promise.all(
64
+ [...byType].map(async ([type, ids]) => {
65
+ const kind = kinds.find((k) => k.type === type);
66
+ out[type] = {};
67
+ try {
68
+ const response = await search(kind.index, rmProjectQuery(kind, [...new Set(ids)]));
69
+ out[type] = rmProjectIndexHits(response?.hits?.hits ?? []);
70
+ } catch {
71
+ }
72
+ })
73
+ );
74
+ return out;
75
+ }
76
+ export function rmProjectRows(items, documents, kinds, lang = "fr") {
77
+ const rank = (type) => {
78
+ const index = kinds.findIndex((kind) => kind.type === type);
79
+ return index < 0 ? kinds.length : index;
80
+ };
81
+ return [...items].sort((a, b) => rank(a.resourceType) - rank(b.resourceType)).map((item) => {
82
+ const kind = kinds.find((k) => k.type === item.resourceType);
83
+ const source = documents[item.resourceType]?.[item.resourceId];
84
+ return {
85
+ id: item.resourceId,
86
+ type: item.resourceType,
87
+ title: kind && source ? kind.title(source, lang) : kind?.titleFromId?.(item.resourceId) ?? "",
88
+ subtitle: kind && source ? kind.subtitle?.(source, lang) ?? "" : "",
89
+ path: kind && source ? kind.path?.(source, item.resourceId) ?? "" : "",
90
+ family: kind ? kind.label[lang] : item.resourceType,
91
+ section: kind?.section?.[lang],
92
+ icon: kind?.icon ?? "Question",
93
+ createdAt: item.createdAt,
94
+ comments: item.commentsCount ?? 0,
95
+ known: !!kind,
96
+ source
97
+ };
98
+ });
99
+ }
100
+ export function rmProjectFamilies(rows) {
101
+ const out = [];
102
+ for (const row of rows) {
103
+ const found = out.find((family) => family.type === row.type);
104
+ if (found) found.count++;
105
+ else out.push({ type: row.type, label: row.family, icon: row.icon, count: 1 });
106
+ }
107
+ return out;
108
+ }
109
+ export function rmProjectFilterRows(rows, filter, lang = "fr") {
110
+ const words = fold(filter.search).split(/\s+/).filter(Boolean);
111
+ const family = filter.family && filter.family !== "all" ? filter.family : "";
112
+ const order = rmProjectFamilies(rows).map((f) => f.type);
113
+ const list = rows.filter((row) => !family || row.type === family).filter((row) => !filter.onlyCommented || (row.comments ?? 0) > 0).filter((row) => !words.length || words.every((word) => fold([row.title, row.subtitle, row.family, row.section, row.id].join(" ")).includes(word)));
114
+ const byDate = (a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""));
115
+ const byTitle = (a, b) => (a.title || a.id).localeCompare(b.title || b.id, lang);
116
+ const sort = filter.sort ?? "date-desc";
117
+ return [...list].sort((a, b) => {
118
+ if (sort === "date-asc") return byDate(a, b);
119
+ if (sort === "title") return byTitle(a, b);
120
+ if (sort === "family") return order.indexOf(a.type) - order.indexOf(b.type) || byTitle(a, b);
121
+ return byDate(b, a);
122
+ });
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.231",
3
+ "version": "0.1.233",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",