@studiotitta/studio-editori 0.1.6 → 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.
@@ -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")
@@ -242,6 +272,22 @@ function Kaannos({ tiedot, mihin }) {
242
272
  setTila("valmis");
243
273
  }, children: tila === "kaantaa" ? "Käännetään…" : `Käännä ${kielella}` })) : null] })) : null, viesti ? (_jsx("p", { className: "ohje", role: "status", children: viesti })) : null] }));
244
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
+ }
245
291
  function Kohteet({ avain, maaritys, tiedot, }) {
246
292
  if (maaritys.tyyppi !== "kohdelista")
247
293
  return null;
@@ -258,6 +304,10 @@ function Kohteet({ avain, maaritys, tiedot, }) {
258
304
  for (const [alikentta, kenttä] of Object.entries(maaritys.kentat)) {
259
305
  if (kenttä.tyyppi === "kytkin" && kenttä.piilottaa && kohde[alikentta] === true) {
260
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");
261
311
  }
262
312
  if (kenttä.tyyppi === "valinta") {
263
313
  const valittu = kenttä.vaihtoehdot.find((vaihtoehto) => vaihtoehto.arvo === kohde[alikentta]);
@@ -284,6 +334,16 @@ function Kohteet({ avain, maaritys, tiedot, }) {
284
334
  /* Usein muuttuvat kentät esiin, kerran asetettavat taitoksen alle. */
285
335
  const ensisijaiset = alikentat.filter(([, kenttä]) => kenttä.lisatieto !== true);
286
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
+ });
287
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: {
288
348
  sivu: tiedot.sivu,
289
349
  kentta: avain,
@@ -294,7 +354,7 @@ function Kohteet({ avain, maaritys, tiedot, }) {
294
354
  const tunnus = String(kohde[maaritys.tunnus] ?? "");
295
355
  const omatMerkinnat = merkinnat(kohde);
296
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));
297
- }) }), _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 ? (_jsxs("details", { className: "lisatiedot", children: [_jsx("summary", { children: "Lis\u00E4tiedot" }), 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] }));
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] }));
298
358
  }
299
359
  /**
300
360
  * Kuvakenttä: nykyinen kuva, vaihto ja vaihtoehtoinen teksti.
@@ -23,5 +23,21 @@ export declare function kirjoitaKentanpolku(polku: Kentanpolku): string;
23
23
  export declare function asetaArvo(sivunData: Record<string, unknown>, polku: Kentanpolku, arvo: unknown, kieli?: string, tunnusKentta?: string): Record<string, unknown>;
24
24
  /** Lukee yhden arvon sivun sisällöstä. */
25
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;
26
42
  /** Tunnus, joka ei ole vielä käytössä. Uusi kohde saa sen pohjaksi. */
27
43
  export declare function vapaaTunnus(kaytossa: readonly string[], ehdotus: string): string;
@@ -110,6 +110,24 @@ export function lueArvo(sivunData, polku, tunnusKentta = "tunnus") {
110
110
  return arvo;
111
111
  return onOlio(arvo) ? arvo[ryhmanKentta] : undefined;
112
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
+ }
113
131
  /** Tunnus, joka ei ole vielä käytössä. Uusi kohde saa sen pohjaksi. */
114
132
  export function vapaaTunnus(kaytossa, ehdotus) {
115
133
  const siisti = ehdotus
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiotitta/studio-editori",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Studio Titan sisältöeditori asiakassivustoille: tekstit ja kuvat muokataan suoraan sivulla",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",