@studiotitta/studio-editori 0.1.5 → 0.1.7
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/maarittely.d.ts +7 -4
- package/dist/selain/Muokkaustila.js +11 -0
- package/dist/selain/Paneeli.js +79 -4
- package/dist/selain/polku.d.ts +23 -1
- package/dist/selain/polku.js +52 -11
- package/dist/tarkistus.d.ts +8 -0
- package/dist/tarkistus.js +37 -6
- package/package.json +1 -1
package/dist/maarittely.d.ts
CHANGED
|
@@ -118,9 +118,9 @@ export interface TekstilistaKentta<K extends boolean = boolean> extends KentanPe
|
|
|
118
118
|
* Editorissa se näkyy omana lohkonaan, jotta sivun kentät eivät sekoitu
|
|
119
119
|
* hakukoneteksteihin.
|
|
120
120
|
*/
|
|
121
|
-
export interface RyhmaKentta extends KentanPerus {
|
|
121
|
+
export interface RyhmaKentta<R extends Record<string, Kentta> = Record<string, Kentta>> extends KentanPerus {
|
|
122
122
|
tyyppi: "ryhma";
|
|
123
|
-
kentat:
|
|
123
|
+
kentat: R;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* Lista samanlaisia kohteita, esimerkiksi kurssit.
|
|
@@ -167,7 +167,7 @@ export declare function kielivalinta(asetukset?: Omit<KielivalintaKentta, "tyypp
|
|
|
167
167
|
export declare function tekstilista<K extends boolean = false>(asetukset?: Omit<TekstilistaKentta<K>, "tyyppi" | "kaannettava"> & {
|
|
168
168
|
kaannettava?: K;
|
|
169
169
|
}): TekstilistaKentta<K>;
|
|
170
|
-
export declare function ryhma(asetukset: Omit<RyhmaKentta
|
|
170
|
+
export declare function ryhma<R extends Record<string, Kentta>>(asetukset: Omit<RyhmaKentta<R>, "tyyppi">): RyhmaKentta<R>;
|
|
171
171
|
export declare function kohdelista(asetukset: Omit<KohdelistaKentta, "tyyppi">): KohdelistaKentta;
|
|
172
172
|
export interface SivunMaarittely {
|
|
173
173
|
/** Nimi, jonka muokkaaja näkee editorissa. Oletuksena sivun avain. */
|
|
@@ -209,7 +209,10 @@ export type KentanArvo<K extends Kentta> = K extends {
|
|
|
209
209
|
tyyppi: "kohdelista";
|
|
210
210
|
} ? Record<string, unknown>[] : K extends {
|
|
211
211
|
tyyppi: "ryhma";
|
|
212
|
-
|
|
212
|
+
kentat: infer R extends Record<string, Kentta>;
|
|
213
|
+
} ? {
|
|
214
|
+
[A in keyof R]: KentanArvo<R[A]>;
|
|
215
|
+
} : string;
|
|
213
216
|
/** Kentän arvo sellaisena kuin se on tiedostossa. */
|
|
214
217
|
type TallennettuArvo<K extends Kentta, L extends string> = K extends {
|
|
215
218
|
kaannettava: true;
|
|
@@ -464,6 +464,13 @@ export function Muokkaustila() {
|
|
|
464
464
|
}
|
|
465
465
|
continue;
|
|
466
466
|
}
|
|
467
|
+
/* Sivun oma ryhmä: kentät ovat ryhmän sisällä, eivät sivun juuressa. */
|
|
468
|
+
if (kenttä.tyyppi === "ryhma") {
|
|
469
|
+
for (const [ryhmanKentta, ryhmanMaaritys] of Object.entries(kenttä.kentat)) {
|
|
470
|
+
tutki({ sivu: sivunAvain, kentta: avain, alikentta: ryhmanKentta }, ryhmanMaaritys);
|
|
471
|
+
}
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
467
474
|
tutki({ sivu: sivunAvain, kentta: avain }, kenttä);
|
|
468
475
|
}
|
|
469
476
|
return loydot;
|
|
@@ -677,6 +684,10 @@ function kentanMaaritys(sisalto, polku) {
|
|
|
677
684
|
return undefined;
|
|
678
685
|
if (polku.alikentta === undefined)
|
|
679
686
|
return kentta;
|
|
687
|
+
/* Ilman tunnusta alikenttä on sivun oman ryhmän kenttä. */
|
|
688
|
+
if (polku.tunnus === undefined) {
|
|
689
|
+
return kentta.tyyppi === "ryhma" ? kentta.kentat[polku.alikentta] : undefined;
|
|
690
|
+
}
|
|
680
691
|
if (kentta.tyyppi !== "kohdelista")
|
|
681
692
|
return undefined;
|
|
682
693
|
const [ensimmainen, ryhmanKentta] = polku.alikentta.split(".");
|
package/dist/selain/Paneeli.js
CHANGED
|
@@ -11,8 +11,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
siis kovakoodata mitään sivustokohtaista: kun sivusto lisää kentän, se
|
|
12
12
|
ilmestyy paneeliin oikeana säätimenä.
|
|
13
13
|
========================================================================== */
|
|
14
|
-
import { useState } from "react";
|
|
15
|
-
import { asetaArvo, lueArvo, vapaaTunnus } from "./polku.js";
|
|
14
|
+
import { useEffect, useRef, useState } from "react";
|
|
15
|
+
import { asetaArvo, lueArvo, vapaaTunnus, virheenAlikentta, } from "./polku.js";
|
|
16
16
|
function onOlio(arvo) {
|
|
17
17
|
return typeof arvo === "object" && arvo !== null && !Array.isArray(arvo);
|
|
18
18
|
}
|
|
@@ -125,6 +125,36 @@ function Vapaana({ polku, maaritys, arvo, tiedot, }) {
|
|
|
125
125
|
function uusi(tunnus) {
|
|
126
126
|
return /^uusi(-\d+)?$/.test(tunnus);
|
|
127
127
|
}
|
|
128
|
+
/** Tyhjä arvo: puuttuva, pelkkää välilyöntiä tai tyhjä lista. */
|
|
129
|
+
function tyhja(arvo) {
|
|
130
|
+
if (arvo === undefined || arvo === null)
|
|
131
|
+
return true;
|
|
132
|
+
if (typeof arvo === "string")
|
|
133
|
+
return arvo.trim() === "";
|
|
134
|
+
if (Array.isArray(arvo))
|
|
135
|
+
return arvo.length === 0;
|
|
136
|
+
/* Käännettävä kenttä on olio kielittäin: tyhjä, jos yksikään ei ole täytetty. */
|
|
137
|
+
if (onOlio(arvo))
|
|
138
|
+
return Object.values(arvo).every((osa) => tyhja(osa));
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Onko kohde vielä kesken, eli puuttuuko siltä pakollisia tietoja?
|
|
143
|
+
*
|
|
144
|
+
* Piilotetun kohteen saa tallentaa keskeneräisenä, joten tämä on aivan
|
|
145
|
+
* tavallinen tila eikä virhe. Merkintä listassa kertoo sen avaamatta
|
|
146
|
+
* jokaista kohdetta, ja samasta tiedosta tiedetään, ettei kohteella ole
|
|
147
|
+
* vielä omaa sivua.
|
|
148
|
+
*/
|
|
149
|
+
function kesken(maaritys, kohde) {
|
|
150
|
+
return Object.entries(maaritys.kentat).some(([alikentta, kenttä]) => {
|
|
151
|
+
if (kenttä.tyyppi === "ryhma") {
|
|
152
|
+
const ryhma = onOlio(kohde[alikentta]) ? kohde[alikentta] : {};
|
|
153
|
+
return Object.entries(kenttä.kentat).some(([ryhmanKentta, sisempi]) => sisempi.pakollinen === true && tyhja(ryhma[ryhmanKentta]));
|
|
154
|
+
}
|
|
155
|
+
return kenttä.pakollinen === true && tyhja(kohde[alikentta]);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
128
158
|
function kohteenOtsikko(maaritys, kohde, kieli) {
|
|
129
159
|
const arvo = kohde[maaritys.otsikko];
|
|
130
160
|
if (typeof arvo === "string")
|
|
@@ -151,6 +181,14 @@ function virheenKohde(tiedot, kohde) {
|
|
|
151
181
|
const maaritys = kentta ? tiedot.malli.kentat[kentta] : undefined;
|
|
152
182
|
if (!maaritys)
|
|
153
183
|
return { nimi: osat.at(-1) ?? kohde, sivulla: false };
|
|
184
|
+
if (maaritys.tyyppi === "ryhma") {
|
|
185
|
+
const ryhmanKentta = osat[2];
|
|
186
|
+
const sisempi = ryhmanKentta ? maaritys.kentat[ryhmanKentta] : undefined;
|
|
187
|
+
return {
|
|
188
|
+
nimi: sisempi?.nimi ?? ryhmanKentta ?? maaritys.nimi ?? kentta,
|
|
189
|
+
sivulla: tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${kentta}.${ryhmanKentta}`),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
154
192
|
if (maaritys.tyyppi !== "kohdelista") {
|
|
155
193
|
return {
|
|
156
194
|
nimi: maaritys.nimi ?? kentta,
|
|
@@ -183,13 +221,20 @@ export function Paneeli(tiedot) {
|
|
|
183
221
|
/* Kielivalinta on turha, jos sivustolla on vain yksi kieli. */
|
|
184
222
|
if (maaritys.tyyppi === "kielivalinta" && tiedot.kielet.length < 2)
|
|
185
223
|
return false;
|
|
224
|
+
/* Ryhmä, jonka jokainen kenttä muokataan jo sivulla, ei kuulu paneeliin:
|
|
225
|
+
tyhjä otsikko näyttäisi siltä, että jotain puuttuu. */
|
|
226
|
+
if (maaritys.tyyppi === "ryhma") {
|
|
227
|
+
return Object.keys(maaritys.kentat).some((ryhmanKentta) => !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}.${ryhmanKentta}`));
|
|
228
|
+
}
|
|
186
229
|
return !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}`);
|
|
187
230
|
};
|
|
188
231
|
const omat = kentat.filter(([avain, maaritys]) => maaritys.tyyppi !== "kohdelista" && nayta([avain, maaritys]));
|
|
189
232
|
return (_jsxs("aside", { className: "paneeli", "aria-label": "Sis\u00E4ll\u00F6n tiedot", children: [tiedot.virheet && tiedot.virheet.length > 0 ? (_jsxs("section", { className: "virheet", role: "alert", children: [_jsx("h2", { children: "Tallennus pys\u00E4htyi" }), _jsx("ul", { children: tiedot.virheet.slice(0, 6).map((virhe) => {
|
|
190
233
|
const tiedot_ = virheenKohde(tiedot, virhe.kohde);
|
|
191
234
|
return (_jsxs("li", { children: [_jsx("strong", { children: tiedot_.nimi }), " ", virhe.viesti, tiedot_.kohteenNimi ? (_jsx("button", { type: "button", className: "linkki", onClick: () => tiedot_.tunnus && tiedot.valitse(tiedot_.tunnus), children: tiedot_.kohteenNimi })) : null, tiedot_.sivulla ? _jsx("span", { className: "merkki", children: " Muokataan sivulla." }) : null] }, `${virhe.kohde}-${virhe.viesti}`));
|
|
192
|
-
}) })] })) : null, omat.length > 0 || tiedot.kaannettavia.sivu.length > 0 ? (_jsxs("section", { children: [_jsx("h2", { children: tiedot.malli.nimi }), omat.map(([avain, maaritys]) => (
|
|
235
|
+
}) })] })) : null, omat.length > 0 || tiedot.kaannettavia.sivu.length > 0 ? (_jsxs("section", { children: [_jsx("h2", { children: tiedot.malli.nimi }), omat.map(([avain, maaritys]) => maaritys.tyyppi === "ryhma" ? (_jsxs("div", { className: "ryhma", children: [_jsx("h3", { children: maaritys.nimi ?? avain }), maaritys.ohje ? _jsx("p", { className: "ohje", children: maaritys.ohje }) : null, Object.entries(maaritys.kentat)
|
|
236
|
+
.filter(([ryhmanKentta]) => !tiedot.sivullaNakyvat.has(`${tiedot.sivu}.${avain}.${ryhmanKentta}`))
|
|
237
|
+
.map(([ryhmanKentta, ryhmanMaaritys]) => (_jsx(Kentta_, { polku: { sivu: tiedot.sivu, kentta: avain, alikentta: ryhmanKentta }, maaritys: ryhmanMaaritys, tiedot: tiedot }, ryhmanKentta)))] }, avain)) : (_jsx(Kentta_, { polku: { sivu: tiedot.sivu, kentta: avain }, maaritys: maaritys, tiedot: tiedot }, avain))), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "sivu" }) : null] })) : null, lista ? _jsx(Kohteet, { avain: lista[0], maaritys: lista[1], tiedot: tiedot }) : null] }));
|
|
193
238
|
}
|
|
194
239
|
const KIELELLE = {
|
|
195
240
|
fi: "suomeksi",
|
|
@@ -227,6 +272,22 @@ function Kaannos({ tiedot, mihin }) {
|
|
|
227
272
|
setTila("valmis");
|
|
228
273
|
}, children: tila === "kaantaa" ? "Käännetään…" : `Käännä ${kielella}` })) : null] })) : null, viesti ? (_jsx("p", { className: "ohje", role: "status", children: viesti })) : null] }));
|
|
229
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Taitos kerran asetettaville kentille.
|
|
277
|
+
*
|
|
278
|
+
* Taitos aukeaa itsestään, kun tallennus pysähtyy sen sisällä olevaan
|
|
279
|
+
* kenttään: virhelistan rivi ei auta, jos kenttä on piilossa taitoksen alla.
|
|
280
|
+
* Auki mennyttä taitosta ei suljeta ohjelmallisesti — muokkaaja päättää itse,
|
|
281
|
+
* milloin se menee kiinni, eikä se saa napsahtaa kiinni kesken työn.
|
|
282
|
+
*/
|
|
283
|
+
function Lisatiedot({ avaa, children }) {
|
|
284
|
+
const taitos = useRef(null);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (avaa && taitos.current)
|
|
287
|
+
taitos.current.open = true;
|
|
288
|
+
}, [avaa]);
|
|
289
|
+
return (_jsxs("details", { className: "lisatiedot", ref: taitos, children: [_jsx("summary", { children: "Lis\u00E4tiedot" }), children] }));
|
|
290
|
+
}
|
|
230
291
|
function Kohteet({ avain, maaritys, tiedot, }) {
|
|
231
292
|
if (maaritys.tyyppi !== "kohdelista")
|
|
232
293
|
return null;
|
|
@@ -243,6 +304,10 @@ function Kohteet({ avain, maaritys, tiedot, }) {
|
|
|
243
304
|
for (const [alikentta, kenttä] of Object.entries(maaritys.kentat)) {
|
|
244
305
|
if (kenttä.tyyppi === "kytkin" && kenttä.piilottaa && kohde[alikentta] === true) {
|
|
245
306
|
tulos.push("piilossa");
|
|
307
|
+
/* Vain piilotettu kohde voi olla kesken: näkyvältä puuttuvat tiedot
|
|
308
|
+
ovat virhe, eivätkä ne pääse tallennuksesta läpi. */
|
|
309
|
+
if (kesken(maaritys, kohde))
|
|
310
|
+
tulos.push("kesken");
|
|
246
311
|
}
|
|
247
312
|
if (kenttä.tyyppi === "valinta") {
|
|
248
313
|
const valittu = kenttä.vaihtoehdot.find((vaihtoehto) => vaihtoehto.arvo === kohde[alikentta]);
|
|
@@ -269,6 +334,16 @@ function Kohteet({ avain, maaritys, tiedot, }) {
|
|
|
269
334
|
/* Usein muuttuvat kentät esiin, kerran asetettavat taitoksen alle. */
|
|
270
335
|
const ensisijaiset = alikentat.filter(([, kenttä]) => kenttä.lisatieto !== true);
|
|
271
336
|
const lisatiedot = alikentat.filter(([, kenttä]) => kenttä.lisatieto === true);
|
|
337
|
+
/* Osuuko jokin virhe valitun kohteen lisätietokenttään? Jos osuu, taitos
|
|
338
|
+
avataan: virhelistan rivi ei auta, jos kenttä on piilossa sen alla. */
|
|
339
|
+
const virheLisatiedoissa = (tiedot.virheet ?? []).some((virhe) => {
|
|
340
|
+
const alikentta = virheenAlikentta(virhe.kohde, {
|
|
341
|
+
sivu: tiedot.sivu,
|
|
342
|
+
kentta: avain,
|
|
343
|
+
tunnus: valitunTunnus ?? "",
|
|
344
|
+
});
|
|
345
|
+
return alikentta !== undefined && maaritys.kentat[alikentta]?.lisatieto === true;
|
|
346
|
+
});
|
|
272
347
|
const kentta = ([alikentta, kenttä]) => kenttä.tyyppi === "ryhma" ? (_jsxs("div", { className: "ryhma", children: [_jsx("h3", { children: kenttä.nimi ?? alikentta }), kenttä.ohje ? _jsx("p", { className: "ohje", children: kenttä.ohje }) : null, Object.entries(kenttä.kentat).map(([ryhmanKentta, ryhmanMaaritys]) => (_jsx(Kentta_, { polku: {
|
|
273
348
|
sivu: tiedot.sivu,
|
|
274
349
|
kentta: avain,
|
|
@@ -279,7 +354,7 @@ function Kohteet({ avain, maaritys, tiedot, }) {
|
|
|
279
354
|
const tunnus = String(kohde[maaritys.tunnus] ?? "");
|
|
280
355
|
const omatMerkinnat = merkinnat(kohde);
|
|
281
356
|
return (_jsx("li", { children: _jsxs("button", { type: "button", className: tunnus === valitunTunnus ? "valittu" : undefined, "aria-current": tunnus === valitunTunnus ? "true" : undefined, onClick: () => tiedot.valitse(tunnus), children: [_jsx("span", { children: nimi(kohde) }), omatMerkinnat.length > 0 ? (_jsx("span", { className: "merkki", children: omatMerkinnat.join(" · ") })) : null] }) }, tunnus));
|
|
282
|
-
}) }), _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", onClick: () => tiedot.lisaaKohde(avain, maaritys), children: ["+ Lis\u00E4\u00E4 ", yksikko] }) }), valittu ? null : (_jsxs("p", { className: "ohje", children: ["Valitse ", yksikko, ", niin n\u00E4et sen tiedot."] }))] }), valittu && valitunTunnus ? (_jsxs("section", { children: [_jsx("h2", { children: nimi(valittu) }), maaritys.osoite ? (uusi(valitunTunnus) ? (_jsx("p", { className: "ohje", children: "Oma sivu syntyy, kun tallennat." })) : (_jsx("p", { className: "ohje", children: _jsx("a", { className: "linkki", href: maaritys.osoite.replace("{tunnus}", valitunTunnus), children: "Avaa sivu" }) }))) : null, ensisijaiset.map(kentta), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "kohde" }) : null, lisatiedot.length > 0 ? (
|
|
357
|
+
}) }), _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", onClick: () => tiedot.lisaaKohde(avain, maaritys), children: ["+ Lis\u00E4\u00E4 ", yksikko] }) }), valittu ? null : (_jsxs("p", { className: "ohje", children: ["Valitse ", yksikko, ", niin n\u00E4et sen tiedot."] }))] }), valittu && valitunTunnus ? (_jsxs("section", { children: [_jsx("h2", { children: nimi(valittu) }), maaritys.osoite ? (uusi(valitunTunnus) ? (_jsx("p", { className: "ohje", children: "Oma sivu syntyy, kun tallennat." })) : kesken(maaritys, valittu) ? (_jsx("p", { className: "ohje", children: "Oma sivu syntyy, kun pakolliset tiedot on t\u00E4ytetty." })) : (_jsx("p", { className: "ohje", children: _jsx("a", { className: "linkki", href: maaritys.osoite.replace("{tunnus}", valitunTunnus), children: "Avaa sivu" }) }))) : null, ensisijaiset.map(kentta), tiedot.kielet.length > 1 ? _jsx(Kaannos, { tiedot: tiedot, mihin: "kohde" }) : null, lisatiedot.length > 0 ? (_jsx(Lisatiedot, { avaa: virheLisatiedoissa, children: lisatiedot.map(kentta) })) : null, _jsx("div", { className: "napit", children: _jsxs("button", { type: "button", className: "vaarallinen", onClick: () => tiedot.poistaKohde(avain, maaritys.tunnus, valitunTunnus), children: ["Poista ", yksikko] }) })] })) : null] }));
|
|
283
358
|
}
|
|
284
359
|
/**
|
|
285
360
|
* Kuvakenttä: nykyinen kuva, vaihto ja vaihtoehtoinen teksti.
|
package/dist/selain/polku.d.ts
CHANGED
|
@@ -3,7 +3,13 @@ export interface Kentanpolku {
|
|
|
3
3
|
kentta: string;
|
|
4
4
|
/** Kohteen tunnus, jos kenttä on listan sisällä. */
|
|
5
5
|
tunnus?: string;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Kentän nimi ryhmän sisällä.
|
|
8
|
+
*
|
|
9
|
+
* Ilman tunnusta tämä on sivun oman ryhmän kenttä ("hero.otsikko" →
|
|
10
|
+
* kentta "hero", alikenttä "otsikko"). Tunnuksen kanssa se on kohteen
|
|
11
|
+
* kenttä, joka voi olla myös kohteen ryhmässä ("meta.title").
|
|
12
|
+
*/
|
|
7
13
|
alikentta?: string;
|
|
8
14
|
}
|
|
9
15
|
export declare function lueKentanpolku(polku: string): Kentanpolku | null;
|
|
@@ -17,5 +23,21 @@ export declare function kirjoitaKentanpolku(polku: Kentanpolku): string;
|
|
|
17
23
|
export declare function asetaArvo(sivunData: Record<string, unknown>, polku: Kentanpolku, arvo: unknown, kieli?: string, tunnusKentta?: string): Record<string, unknown>;
|
|
18
24
|
/** Lukee yhden arvon sivun sisällöstä. */
|
|
19
25
|
export declare function lueArvo(sivunData: Record<string, unknown>, polku: Kentanpolku, tunnusKentta?: string): unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Mihin kohteen kenttään virheen polku osoittaa?
|
|
28
|
+
*
|
|
29
|
+
* Palvelin kertoo virheen polkuna ("kurssit.lista.testi.location"), mutta
|
|
30
|
+
* paneelin pitää tietää kaksi asiaa: koskeeko virhe juuri avattua kohdetta,
|
|
31
|
+
* ja mitä kenttää. Palauttaa kentän nimen tai undefined, jos virhe tulee
|
|
32
|
+
* muualta — toisesta kohteesta, toisesta kentästä tai sivun omista kentistä.
|
|
33
|
+
*
|
|
34
|
+
* Ryhmän sisällä olevasta kentästä ("...meta.title") palautuu ryhmän nimi,
|
|
35
|
+
* koska paneelissa ryhmä on yksi lohko.
|
|
36
|
+
*/
|
|
37
|
+
export declare function virheenAlikentta(kohde: string, polku: {
|
|
38
|
+
sivu: string;
|
|
39
|
+
kentta: string;
|
|
40
|
+
tunnus: string;
|
|
41
|
+
}): string | undefined;
|
|
20
42
|
/** Tunnus, joka ei ole vielä käytössä. Uusi kohde saa sen pohjaksi. */
|
|
21
43
|
export declare function vapaaTunnus(kaytossa: readonly string[], ehdotus: string): string;
|
package/dist/selain/polku.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
Sivulla oleva merkintä kertoo, mitä kohtaa sisällöstä elementti näyttää:
|
|
5
5
|
|
|
6
6
|
kurssit.otsikko sivun oma kenttä
|
|
7
|
+
etusivu.hero.otsikko kenttä sivun ryhmässä
|
|
7
8
|
kurssit.lista[joogaretriitti].nimi yhden kohteen kenttä listassa
|
|
8
9
|
|
|
9
10
|
Hakasulkeissa on kohteen tunnus, ei järjestysnumero. Näin muokkaus osuu
|
|
@@ -13,23 +14,31 @@
|
|
|
13
14
|
Tämä tiedosto on tarkoituksella pelkkää logiikkaa ilman selainta, jotta
|
|
14
15
|
sen voi testata suoraan.
|
|
15
16
|
========================================================================== */
|
|
16
|
-
/*
|
|
17
|
-
|
|
17
|
+
/* Kolme muotoa: sivun kenttä, sivun ryhmän kenttä ja kohteen kenttä
|
|
18
|
+
listassa. Kohteen kenttä voi olla vielä kohteen oman ryhmän sisällä:
|
|
19
|
+
"kurssit.lista[tunnus].meta.title". */
|
|
20
|
+
const MUOTO = /^([a-zA-Z][a-zA-Z0-9]*)\.([a-zA-Z][a-zA-Z0-9]*)(?:\[([^\]]+)\]\.([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)?)|\.([a-zA-Z][a-zA-Z0-9]*))?$/;
|
|
18
21
|
export function lueKentanpolku(polku) {
|
|
19
22
|
const osuma = MUOTO.exec(polku.trim());
|
|
20
23
|
if (!osuma)
|
|
21
24
|
return null;
|
|
22
|
-
const [, sivu, kentta, tunnus,
|
|
25
|
+
const [, sivu, kentta, tunnus, kohteenKentta, ryhmanKentta] = osuma;
|
|
23
26
|
if (!sivu || !kentta)
|
|
24
27
|
return null;
|
|
25
|
-
if (tunnus !== undefined
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
if (tunnus !== undefined) {
|
|
29
|
+
if (!kohteenKentta)
|
|
30
|
+
return null;
|
|
31
|
+
return { sivu, kentta, tunnus, alikentta: kohteenKentta };
|
|
32
|
+
}
|
|
33
|
+
return ryhmanKentta === undefined ? { sivu, kentta } : { sivu, kentta, alikentta: ryhmanKentta };
|
|
28
34
|
}
|
|
29
35
|
export function kirjoitaKentanpolku(polku) {
|
|
30
|
-
|
|
36
|
+
if (polku.tunnus !== undefined) {
|
|
37
|
+
return `${polku.sivu}.${polku.kentta}[${polku.tunnus}].${polku.alikentta}`;
|
|
38
|
+
}
|
|
39
|
+
return polku.alikentta === undefined
|
|
31
40
|
? `${polku.sivu}.${polku.kentta}`
|
|
32
|
-
: `${polku.sivu}.${polku.kentta}
|
|
41
|
+
: `${polku.sivu}.${polku.kentta}.${polku.alikentta}`;
|
|
33
42
|
}
|
|
34
43
|
/* ---- Arvon asettaminen --------------------------------------------------- */
|
|
35
44
|
function onOlio(arvo) {
|
|
@@ -49,7 +58,17 @@ export function asetaArvo(sivunData, polku, arvo, kieli, tunnusKentta = "tunnus"
|
|
|
49
58
|
return { ...pohja, [kieli]: arvo };
|
|
50
59
|
};
|
|
51
60
|
if (polku.tunnus === undefined) {
|
|
52
|
-
|
|
61
|
+
if (polku.alikentta === undefined) {
|
|
62
|
+
return { ...sivunData, [polku.kentta]: yhdista(sivunData[polku.kentta]) };
|
|
63
|
+
}
|
|
64
|
+
/* Sivun ryhmä: muut ryhmän kentät säilyvät ennallaan. */
|
|
65
|
+
const ryhma = onOlio(sivunData[polku.kentta])
|
|
66
|
+
? sivunData[polku.kentta]
|
|
67
|
+
: {};
|
|
68
|
+
return {
|
|
69
|
+
...sivunData,
|
|
70
|
+
[polku.kentta]: { ...ryhma, [polku.alikentta]: yhdista(ryhma[polku.alikentta]) },
|
|
71
|
+
};
|
|
53
72
|
}
|
|
54
73
|
const lista = Array.isArray(sivunData[polku.kentta]) ? [...sivunData[polku.kentta]] : [];
|
|
55
74
|
const sija = lista.findIndex((kohde) => onOlio(kohde) && kohde[tunnusKentta] === polku.tunnus);
|
|
@@ -73,8 +92,12 @@ export function asetaArvo(sivunData, polku, arvo, kieli, tunnusKentta = "tunnus"
|
|
|
73
92
|
}
|
|
74
93
|
/** Lukee yhden arvon sivun sisällöstä. */
|
|
75
94
|
export function lueArvo(sivunData, polku, tunnusKentta = "tunnus") {
|
|
76
|
-
if (polku.tunnus === undefined)
|
|
77
|
-
|
|
95
|
+
if (polku.tunnus === undefined) {
|
|
96
|
+
const arvo = sivunData[polku.kentta];
|
|
97
|
+
if (polku.alikentta === undefined)
|
|
98
|
+
return arvo;
|
|
99
|
+
return onOlio(arvo) ? arvo[polku.alikentta] : undefined;
|
|
100
|
+
}
|
|
78
101
|
const lista = sivunData[polku.kentta];
|
|
79
102
|
if (!Array.isArray(lista))
|
|
80
103
|
return undefined;
|
|
@@ -87,6 +110,24 @@ export function lueArvo(sivunData, polku, tunnusKentta = "tunnus") {
|
|
|
87
110
|
return arvo;
|
|
88
111
|
return onOlio(arvo) ? arvo[ryhmanKentta] : undefined;
|
|
89
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Mihin kohteen kenttään virheen polku osoittaa?
|
|
115
|
+
*
|
|
116
|
+
* Palvelin kertoo virheen polkuna ("kurssit.lista.testi.location"), mutta
|
|
117
|
+
* paneelin pitää tietää kaksi asiaa: koskeeko virhe juuri avattua kohdetta,
|
|
118
|
+
* ja mitä kenttää. Palauttaa kentän nimen tai undefined, jos virhe tulee
|
|
119
|
+
* muualta — toisesta kohteesta, toisesta kentästä tai sivun omista kentistä.
|
|
120
|
+
*
|
|
121
|
+
* Ryhmän sisällä olevasta kentästä ("...meta.title") palautuu ryhmän nimi,
|
|
122
|
+
* koska paneelissa ryhmä on yksi lohko.
|
|
123
|
+
*/
|
|
124
|
+
export function virheenAlikentta(kohde, polku) {
|
|
125
|
+
const osat = kohde.split(".");
|
|
126
|
+
if (osat[0] !== polku.sivu || osat[1] !== polku.kentta || osat[2] !== polku.tunnus) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return osat[3];
|
|
130
|
+
}
|
|
90
131
|
/** Tunnus, joka ei ole vielä käytössä. Uusi kohde saa sen pohjaksi. */
|
|
91
132
|
export function vapaaTunnus(kaytossa, ehdotus) {
|
|
92
133
|
const siisti = ehdotus
|
package/dist/tarkistus.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface Huomio {
|
|
|
5
5
|
/** Kieli, jos huomio koskee yhtä kieliversiota. */
|
|
6
6
|
kieli?: string;
|
|
7
7
|
viesti: string;
|
|
8
|
+
/**
|
|
9
|
+
* "puuttuu" = pakollinen kenttä on tyhjä, ei siis väärässä muodossa.
|
|
10
|
+
*
|
|
11
|
+
* Ero on tarpeen, koska piilotetussa kohteessa tyhjä kenttä on varoitus
|
|
12
|
+
* eikä virhe: keskeneräisen luonnoksen pitää voida tallentaa. Väärä muoto
|
|
13
|
+
* on virhe silloinkin — sitä ei kirjoiteta tiedostoon missään tilanteessa.
|
|
14
|
+
*/
|
|
15
|
+
syy?: "puuttuu";
|
|
8
16
|
}
|
|
9
17
|
export interface Tarkistustulos {
|
|
10
18
|
virheet: Huomio[];
|
package/dist/tarkistus.js
CHANGED
|
@@ -33,7 +33,7 @@ function tarkistaTeksti(teksti, maaritys, kohde, kieli, tulos) {
|
|
|
33
33
|
tulos.virheet.push(huomio("Tähän kenttään ei voi lisätä rivinvaihtoa."));
|
|
34
34
|
}
|
|
35
35
|
if (maaritys.pakollinen && teksti.trim() === "") {
|
|
36
|
-
tulos.virheet.push(huomio("Kenttä ei voi olla tyhjä."));
|
|
36
|
+
tulos.virheet.push({ ...huomio("Kenttä ei voi olla tyhjä."), syy: "puuttuu" });
|
|
37
37
|
}
|
|
38
38
|
const suositus = "suositusPituus" in maaritys ? maaritys.suositusPituus : undefined;
|
|
39
39
|
if (suositus !== undefined) {
|
|
@@ -53,7 +53,7 @@ function tarkistaTekstilista(arvo, maaritys, kohde, kieli, tulos) {
|
|
|
53
53
|
tulos.virheet.push(huomio(`Listassa voi olla enintään ${maaritys.enintaan} kohtaa.`));
|
|
54
54
|
}
|
|
55
55
|
if (maaritys.pakollinen && arvo.length === 0) {
|
|
56
|
-
tulos.virheet.push(huomio("Kenttä ei voi olla tyhjä."));
|
|
56
|
+
tulos.virheet.push({ ...huomio("Kenttä ei voi olla tyhjä."), syy: "puuttuu" });
|
|
57
57
|
}
|
|
58
58
|
arvo.forEach((kohta, sija) => {
|
|
59
59
|
if (typeof kohta !== "string") {
|
|
@@ -120,8 +120,9 @@ function tarkistaKentta(kielet, maaritys, arvo, kohde, sisarukset, tulos) {
|
|
|
120
120
|
/* null on sama kuin tyhjä: sisältötiedostoissa sillä merkitään "ei vielä
|
|
121
121
|
tiedossa" (esimerkiksi maksulinkki, jota ei ole avattu). */
|
|
122
122
|
if (arvo === undefined || arvo === null) {
|
|
123
|
-
if (maaritys.pakollinen)
|
|
124
|
-
tulos.virheet.push({ kohde, viesti: "Kenttä ei voi olla tyhjä." });
|
|
123
|
+
if (maaritys.pakollinen) {
|
|
124
|
+
tulos.virheet.push({ kohde, viesti: "Kenttä ei voi olla tyhjä.", syy: "puuttuu" });
|
|
125
|
+
}
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
127
128
|
if (onKaannettava(maaritys)) {
|
|
@@ -145,7 +146,7 @@ function tarkistaKentta(kielet, maaritys, arvo, kohde, sisarukset, tulos) {
|
|
|
145
146
|
varoituksena, jotta se ei myöskään unohdu.
|
|
146
147
|
*/
|
|
147
148
|
if (maaritys.pakollinen && kieli === oletuskieli) {
|
|
148
|
-
tulos.virheet.push({ kohde, kieli, viesti: "Kenttä ei voi olla tyhjä." });
|
|
149
|
+
tulos.virheet.push({ kohde, kieli, viesti: "Kenttä ei voi olla tyhjä.", syy: "puuttuu" });
|
|
149
150
|
}
|
|
150
151
|
else if (maaritys.pakollinen || Object.values(arvo).some((muu) => !onTyhja(muu))) {
|
|
151
152
|
tulos.varoitukset.push({ kohde, kieli, viesti: "Käännös puuttuu." });
|
|
@@ -205,7 +206,7 @@ function tarkistaKentta(kielet, maaritys, arvo, kohde, sisarukset, tulos) {
|
|
|
205
206
|
tulos.virheet.push({ kohde, viesti: "Sama kieli on valittu kahdesti." });
|
|
206
207
|
}
|
|
207
208
|
if (maaritys.pakollinen && arvo.length === 0) {
|
|
208
|
-
tulos.virheet.push({ kohde, viesti: "Valitse vähintään yksi kieli." });
|
|
209
|
+
tulos.virheet.push({ kohde, viesti: "Valitse vähintään yksi kieli.", syy: "puuttuu" });
|
|
209
210
|
}
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
@@ -239,12 +240,42 @@ function tarkistaKentta(kielet, maaritys, arvo, kohde, sisarukset, tulos) {
|
|
|
239
240
|
}
|
|
240
241
|
tunnukset.add(tunnus);
|
|
241
242
|
}
|
|
243
|
+
/*
|
|
244
|
+
Piilotettu kohde on luonnos, ja luonnoksen pitää voida olla kesken:
|
|
245
|
+
muokkaaja lisää kurssin nimen ja hinnan nyt, kuvan ja ajankohdan
|
|
246
|
+
ensi viikolla. Tyhjä pakollinen kenttä on siksi varoitus, ei virhe.
|
|
247
|
+
Kun piilotus otetaan pois, samat kentät vaaditaan taas — portti on
|
|
248
|
+
julkaisussa, ei tallennuksessa.
|
|
249
|
+
*/
|
|
250
|
+
if (onPiilotettu(maaritys, kohta)) {
|
|
251
|
+
const omat = { virheet: [], varoitukset: [] };
|
|
252
|
+
tarkistaJoukko(kielet, maaritys.kentat, kohta, `${kohde}.${nimi}`, omat);
|
|
253
|
+
for (const virhe of omat.virheet) {
|
|
254
|
+
if (virhe.syy === "puuttuu") {
|
|
255
|
+
tulos.varoitukset.push({ ...virhe, viesti: "Täytä tämä ennen kuin julkaiset." });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
tulos.virheet.push(virhe);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
tulos.varoitukset.push(...omat.varoitukset);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
242
264
|
tarkistaJoukko(kielet, maaritys.kentat, kohta, `${kohde}.${nimi}`, tulos);
|
|
243
265
|
});
|
|
244
266
|
return;
|
|
245
267
|
}
|
|
246
268
|
}
|
|
247
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Onko kohde piilotettu sivustolta?
|
|
272
|
+
*
|
|
273
|
+
* Piilotus on kytkin, jonka mallissa on `piilottaa: true`. Sama tieto
|
|
274
|
+
* näkyy paneelissa merkintänä "piilossa".
|
|
275
|
+
*/
|
|
276
|
+
function onPiilotettu(maaritys, kohta) {
|
|
277
|
+
return Object.entries(maaritys.kentat).some(([avain, kenttä]) => kenttä.tyyppi === "kytkin" && kenttä.piilottaa === true && kohta[avain] === true);
|
|
278
|
+
}
|
|
248
279
|
function tarkistaJoukko(kielet, kentat, data, polku, tulos) {
|
|
249
280
|
for (const avain of Object.keys(data)) {
|
|
250
281
|
if (!Object.hasOwn(kentat, avain)) {
|
package/package.json
CHANGED