refac: migration crash, real-database tests, site deleted during create, ruels out of the repo, lists
This commit is contained in:
1 parent
680b9a1308
commit
9d284e65a9
62 files changed
+1858
-987
No files matched your search
@@ -8,7 +8,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<dc-import name="LocalizationShell" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" src-pane="{{ srcPane }}" ed-banner="{{ edBanner }}" ed-body-el="{{ edBodyEl }}" ed-header="{{ edHeader }}" ed-mob-bar="{{ edMobBar }}" hint-size="100%,100%"></dc-import>
|
||||
<dc-import name="AppFrame" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" src-pane="{{ srcPane }}" ed-banner="{{ edBanner }}" ed-body-el="{{ edBodyEl }}" ed-header="{{ edHeader }}" ed-mob-bar="{{ edMobBar }}" hint-size="100%,100%"></dc-import>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1440,"height":900},"s":{"editor":null,"tsType":"object"},"mobile":{"editor":"boolean","default":false,"tsType":"boolean"}}">
|
||||
class Component extends DCLogic {
|
||||
@@ -24,7 +24,7 @@ class Component extends DCLogic {
|
||||
const mono = "font-family:'IBM Plex Mono',monospace;font-size:12px"
|
||||
const Btn = (l, k = 'secondary', o = {}) => {
|
||||
const hh = m ? 44 : (o.sm ? 28 : 32)
|
||||
const st = { primary: 'background:var(--ui-primary);color:var(--ui-bg)', secondary: 'background:var(--ui-bg);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented)', ghost: 'color:var(--ui-text-toned)', danger: 'background:var(--ui-error);color:var(--ui-bg)' }[k]
|
||||
const st = { primary: 'background:var(--ui-primary);color:var(--ui-bg)', secondary: 'background:var(--ui-bg);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented)', ghost: 'color:var(--ui-text-toned)', danger: 'background:var(--ui-error);color:var(--ui-bg)', dangerOutline: 'background:var(--ui-bg);color:var(--ui-error);border:1px solid var(--ui-error)' }[k]
|
||||
return A('span', { role: 'button', 'aria-label': o.al || (o.ic && !l ? o.ic : undefined), 'aria-disabled': !!o.dis || undefined, 'aria-busy': o.busy || undefined }, 'display:inline-flex;align-items:center;justify-content:center;gap:6px;height:' + hh + 'px;padding:0 ' + (o.ic && !l ? 0 : 12) + 'px;' + (o.ic && !l ? 'width:' + hh + 'px;' : '') + 'font-weight:500;white-space:nowrap;flex-shrink:0;' + st + ';' + (o.dis ? 'opacity:.4;' : '') + (o.w ? 'width:' + o.w + ';' : ''), o.busy && Ic('loader-circle', 14), o.ic && Ic(o.ic, 16), l)
|
||||
}
|
||||
const TN = { neutral: 'background:var(--ui-bg-elevated);color:var(--ui-text-toned)', success: 'background:var(--rc-success-soft);color:var(--ui-success)', info: 'background:var(--rc-info-soft);color:var(--ui-info)', warning: 'background:var(--rc-warning-soft);color:var(--ui-warning)', error: 'background:var(--rc-error-soft);color:var(--ui-error)' }
|
||||
@@ -66,12 +66,13 @@ class Component extends DCLogic {
|
||||
m && D('display:flex;justify-content:center;padding:8px 0 0;flex-shrink:0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
|
||||
D('padding:10px 12px 6px;display:flex;flex-direction:column;gap:6px;flex-shrink:0', S('font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Versions you can open'), o.find && Fd('', 'Find a language', { val: false })),
|
||||
A('div', { role: 'menu', 'aria-label': 'Versions of this article' }, 'display:flex;flex-direction:column;overflow-y:auto;min-height:0', ...rows.map(LRow)),
|
||||
D('padding:10px 12px;border-top:1px solid var(--ui-border);display:flex;gap:8px;flex-shrink:0;' + MU + ';font-size:12px;line-height:18px', Ic('info', 14), 'Switching opens that version. The interface language stays the same.'))
|
||||
D('padding:10px 12px;border-top:1px solid var(--ui-border);display:flex;gap:8px;flex-shrink:0;' + MU + ';font-size:12px;line-height:18px', Ic('info', 14), o.note || 'Switching opens that version. The interface language stays the same.'))
|
||||
const MENUS = {
|
||||
all: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Arabic', c: 'AR', st: 'draft', fr: 1 }],
|
||||
two: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }],
|
||||
many: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'English (United Kingdom)', c: 'EN-GB', st: 'draft' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Italian', c: 'IT', st: 'draft' }, { n: 'Japanese', c: 'JA', st: 'pub' }, { n: 'Portuguese (Brazil)', c: 'PT-BR', add: 1 }, { n: 'Spanish', c: 'ES', add: 1 }, { n: 'Chinese (Simplified)', c: 'ZH-HANS', add: 1 }, { n: 'Arabic', c: 'AR', add: 1 }, { n: 'Dutch', c: 'NL', add: 1 }, { n: 'Polish', c: 'PL', add: 1 }],
|
||||
ro: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
|
||||
dis: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'Spanish', c: 'ES', st: 'draft', dis: 1 }],
|
||||
p4: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Arabic', c: 'AR', st: 'draft', fr: 1, tag: 'Read only', tagIc: 'lock' }],
|
||||
p5: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
|
||||
noAdd: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
|
||||
@@ -118,7 +119,7 @@ class Component extends DCLogic {
|
||||
if (cap.d) it.push(cap.part ? { l: 'Delete article', ic: 'trash-2', dis: 1, why: 'You can\'t delete this article.' } : { l: 'Delete article', ic: 'trash-2', danger: 1 })
|
||||
return it
|
||||
}
|
||||
const OvMenu = items => D('position:absolute;z-index:45;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:' + (nr ? 150 : 140) + 'px;width:320px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column',
|
||||
const OvMenu = (items, top0) => D('position:absolute;z-index:45;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:' + (top0 ?? (nr ? 150 : 140)) + 'px;width:320px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column',
|
||||
m && D('display:flex;justify-content:center;padding:8px 0 0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
|
||||
A('div', { role: 'menu', 'aria-label': 'More actions' }, 'display:flex;flex-direction:column;padding:4px 0', ...items.map(i => A('div', { role: 'menuitem', 'aria-disabled': i.dis || undefined, tabIndex: -1 }, 'flex-shrink:0;display:flex;align-items:center;gap:10px;min-height:' + (m ? 52 : 40) + 'px;padding:8px 12px;' + (i.dis ? 'opacity:.6;' : '') + (i.danger ? 'color:var(--ui-error)' : HL), Ic(i.ic, 16), D('display:flex;flex-direction:column;gap:2px', S('font-weight:500', i.l), i.why && S(MU + ';font-size:12px;font-weight:400;line-height:16px', i.why))))))
|
||||
const Hdr = (L, o) => {
|
||||
@@ -158,7 +159,7 @@ class Component extends DCLogic {
|
||||
const SOC = (site, code) => { const q = (FX.SOCIAL[site] || {})[code] || ['none']; return q[0] === 'derived' ? 'Derived: ' + q[1] : q[0] === 'set' ? 'Set: ' + q[1] : 'Not set' }
|
||||
const SiteRows = (siteKey, ro) => { const si = FX.SITES[siteKey], all = si.langs.map(k => [k, 1]).concat(si.disabled.map(k => [k, 0])); return all.map(([k, en]) => { const c = FX.count(k, si.stories), def = k === si.def, soc = SOC(siteKey, k)
|
||||
return D(ROW + ';flex-wrap:wrap;opacity:' + (en ? 1 : .85), D('flex:1 1 160px;min-width:0;display:flex;flex-direction:column', S('font-weight:500;' + HL + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', LNm[k] || k), S(mono + ';' + MU, k)),
|
||||
def ? Bd('Default', 'info', 'star') : !en ? Bd('Disabled', 'neutral', 'eye-off') : null, !m && S(MU + ';width:170px', FX.pl(c[0], 'version') + ' · ' + c[1] + ' published'),
|
||||
def ? Bd('Default', 'info', 'star') : !en ? Bd('Disabled', 'neutral', 'eye-off') : null, S((m ? 'flex:1 1 100%;font-size:12px;' : 'width:170px;') + MU, FX.pl(c[0], 'version') + ' · ' + c[1] + ' published'),
|
||||
D('display:flex;align-items:center;gap:6px;' + (m ? 'flex:1 1 100%' : 'width:270px;white-space:nowrap'), S('font-size:12px;white-space:nowrap;' + MU, 'Social locale'), S(mono + ';' + (soc === 'Not set' ? MU : HL), soc), !ro && Btn('Edit', 'ghost', { sm: true, al: 'Edit social locale for ' + (LNm[k] || k) })),
|
||||
!ro && !en ? Btn('Re-enable', 'secondary', { sm: true }) : null, !ro && Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Actions for ' + (LNm[k] || k) })) }) }
|
||||
const slPage = () => {
|
||||
@@ -168,7 +169,7 @@ class Component extends DCLogic {
|
||||
const banner = x === 'disableDefault' ? Alert('warning', 'triangle-alert', 'English is the default', 'Choose another default first, then disable English.') : x === 'disableLast' ? Alert('warning', 'triangle-alert', 'At least one language must stay enabled', 'Journal needs a language for new articles.') : x === 'defaultFail' ? Alert('error', 'circle-x', 'Couldn\'t change the default language', 'Nothing changed. English is still the default. Try again.', Btn('Try again', 'secondary', { sm: true })) : x === 'removeFail' ? Alert('error', 'circle-x', 'Couldn\'t remove Italian', 'Nothing changed. Italian is still listed. Try again.', Btn('Try again', 'secondary', { sm: true })) : x === 'stale' ? Alert('error', 'circle-x', 'Languages changed. Reload.', 'Another owner changed the languages for Journal while this page was open. Reload to see the current list. Your change wasn\'t applied.', Btn('Reload', 'secondary', { sm: true })) : null
|
||||
return Page(T, SUB, !ro && Btn('Add language', 'primary', { ic: 'plus' }),
|
||||
ro && Alert('neutral', 'lock', 'Only owners can change languages', 'You can see which languages are enabled, their social locale and how many versions each has. Ask an owner to change them.'), banner,
|
||||
single ? Card(S('font-weight:600;' + HL, 'Archive uses one language'), S(TO, 'New articles are written in Portuguese (Brazil). Add a language to write translations and publish them separately.'), D('display:flex;gap:10px;align-items:center', Bd('Default', 'info', 'star'), S('font-weight:500;' + HL, 'Portuguese (Brazil)'), S(mono + ';' + MU, 'pt-BR'), S(MU + ';font-size:12px', 'Social locale'), S(mono + ';' + HL, SOC('archive', 'pt-BR') === 'Not set' ? 'Derived: pt_BR' : '')))
|
||||
single ? Card(S('font-weight:600;' + HL, 'Archive uses one language'), S(TO, 'New articles are written in Portuguese (Brazil). Add a language to write translations and publish them separately.'), D('display:flex;gap:10px;align-items:center;flex-wrap:wrap', Bd('Default', 'info', 'star'), S('font-weight:500;' + HL, 'Portuguese (Brazil)'), S(mono + ';' + MU, 'pt-BR'), D('display:flex;gap:6px;align-items:center;' + (m ? 'flex:1 1 100%' : ''), S(MU + ';font-size:12px', 'Social locale'), S(mono + ';' + HL, 'Derived: pt_BR'))))
|
||||
: D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', 'Language'), ...SiteRows(skey, ro)),
|
||||
single && Alert('info', 'info', 'Regional language kept: pt-BR', 'Setup kept the full site locale, so the default content language is Portuguese (Brazil), not plain Portuguese. The site locale still formats dates and numbers. Social locale: Derived: pt_BR, taken from the language code.'),
|
||||
!single && !ro && S(MU + ';font-size:12px;text-wrap:pretty', 'Disabling a language keeps its versions readable and editable, and published ones stay online. Drafts can be published again after you re-enable it. Social locale: Derived comes from the language code, Set is chosen by an owner, Not set leaves og:locale out.'))
|
||||
@@ -199,14 +200,14 @@ class Component extends DCLogic {
|
||||
}
|
||||
// ---- public URL pattern
|
||||
const puPage = () => {
|
||||
const bad = { unknown: ['https://journal.example.com/{language}/{slug}', 'Unknown placeholder {language}. Use {lang} and {slug}.'], scheme: ['ftp://journal.example.com/{lang}/{slug}', 'Start with https:// or http://.'], encoded: ['https://journal.example.com/my pages/{lang}/{slug}', 'Remove spaces and characters that need encoding. Use letters, digits, - and _.'], noSlug: ['https://journal.example.com/{lang}', 'Add {slug} so each article has its own address.'] }[x]
|
||||
const bad = { unknown: ['https://journal.example.com/{language}/{slug}', 'Unknown placeholder {language}. Use {lang} and {slug}.'], scheme: ['ftp://journal.example.com/{lang}/{slug}', 'Start with https:// or http://.'], encoded: ['https://journal.example.com/my pages/{lang}/{slug}', 'Remove spaces and characters that need encoding. Use letters, digits, - and _.'], invalidHost: ['https://journal..example.com/{lang}/{slug}', 'Enter a valid host name, such as journal.example.com.'], noSlug: ['https://journal.example.com/{lang}', 'Add {slug} so each article has its own address.'] }[x]
|
||||
const ro = x === 'viewOnly', unset = x === 'unset', val = bad ? bad[0] : unset ? '' : x === 'editing' ? 'https://journal.example.com/{lang}/articles/{slug}' : 'https://journal.example.com/{lang}/{slug}'
|
||||
const prev = bad ? null : unset ? null : x === 'editing' ? 'https://journal.example.com/fr/articles/comment-nous-chiffrons-une-renovation' : 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation'
|
||||
const prev = bad ? null : unset ? null : x === 'editing' ? 'https://journal.example.com/en/articles/how-we-price-a-bathroom-renovation' : 'https://journal.example.com/en/how-we-price-a-bathroom-renovation'
|
||||
return Page('Public URL', 'Where Journal articles appear on your website. Used for canonical and alternate links.', null,
|
||||
ro && Alert('neutral', 'lock', 'Only owners can change this', 'You can read the pattern. Ask an owner to change it.'),
|
||||
unset && Alert('info', 'info', 'Public URL not configured', 'Page, canonical and alternate addresses stay hidden until you add a pattern. Links by article ID still work.'),
|
||||
Card(Fd('Public URL pattern', val || 'https://example.com/{lang}/{slug}', { mono: true, val: !!val, ro, focus: !!bad || x === 'editing', err: bad && bad[1], help: bad ? null : 'Placeholders: {lang} is the language code, such as fr or pt-BR. {slug} is the article address in that language.' }),
|
||||
prev && D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, 'Preview with "Comment nous chiffrons une rénovation de salle de bain" in French'), S(mono + ';' + HL + ';overflow-wrap:anywhere', prev)),
|
||||
prev && D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, 'Preview with "How we price a bathroom renovation" in English, the default language'), S(mono + ';' + HL + ';overflow-wrap:anywhere', prev), S('font-size:12px;' + MU, 'The preview uses the most recently updated article that has a version in the default language.')),
|
||||
!ro && D('display:flex;gap:8px', Btn('Save', 'primary', { dis: !!bad || unset }), Btn('Cancel'))))
|
||||
}
|
||||
// ---- setup
|
||||
@@ -227,29 +228,42 @@ class Component extends DCLogic {
|
||||
}
|
||||
// ---- article list
|
||||
const LN = LNm
|
||||
const Pager = (V) => D('display:flex;align-items:center;gap:12px;flex-wrap:wrap;' + (m ? 'justify-content:space-between' : ''), S(MU + ';font-size:12px;' + (m ? 'flex:1 1 100%' : 'flex:1'), 'Showing ' + V.from + '–' + V.to + ' of ' + V.unit),
|
||||
D('display:flex;align-items:center;gap:8px;' + MU + ';font-size:12px', 'Rows', D('', Fd('', String(V.size), { sel: true }))),
|
||||
D('display:flex;align-items:center;gap:6px', Btn('', 'secondary', { ic: 'chevron-left', al: 'Previous page', dis: V.page < 2, sm: !m }), S(HL + ';font-weight:500;padding:0 6px', 'Page ' + V.page + ' of ' + V.pages), Btn('', 'secondary', { ic: 'chevron-right', al: 'Next page', dis: V.page >= V.pages, sm: !m })))
|
||||
const Chip = l => D('white-space:nowrap;flex-shrink:0;display:inline-flex;align-items:center;gap:6px;height:' + (m ? 36 : 24) + 'px;padding:0 8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);' + HL + ';font-size:12px', l, A('span', { role: 'button', 'aria-label': 'Remove filter ' + l }, 'display:inline-flex', Ic('x', 14)))
|
||||
const ChipC = (c, k) => A('span', { 'aria-label': (LN[c.k] || c.k) + ': ' + (c.st === 'pub' ? 'published' : c.st === 'draft' ? 'draft' : 'no version') + (c.nr ? ', needs review' : '') + (c.off ? ', language disabled' : '') }, 'white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;' + (c.st ? 'border:1px solid var(--ui-border-accented);' + HL : 'border:1px dashed var(--ui-border-accented);' + MU) + (c.off ? ';opacity:.75' : ''), c.k.toUpperCase(), c.st === 'pub' ? Ic('circle-check', 12) : c.st === 'draft' ? Ic('pencil', 12) : Ic('plus', 12), c.nr && Ic('refresh-cw', 12))
|
||||
const Cov = (cs) => { const Lc = m || nr ? 4 : 5, vis = cs.slice(0, Lc), more = cs.slice(Lc); return D('display:flex;gap:6px;flex-wrap:' + (m ? 'wrap' : 'nowrap'), ...vis.map(ChipC), more.length ? A('span', { role: 'button', 'aria-label': more.length + ' more languages: ' + more.map(c => LN[c.k] || c.k).join(', ') }, 'display:inline-flex;align-items:center;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;background:var(--ui-bg-elevated);' + HL, '+' + more.length) : null) }
|
||||
const Pager = (V) => {
|
||||
const pb = m ? 44 : 32
|
||||
const edge = (n, al, dis) => A('span', { role: 'button', 'aria-label': al, 'aria-disabled': dis || undefined }, 'width:' + pb + 'px;height:' + pb + 'px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:' + (dis ? 0.4 : 1), Ic(n))
|
||||
const first = V.page === 1, last = V.page === V.pages
|
||||
const set = new Set([1, V.pages, V.page - 1, V.page, V.page + 1].filter(n => n >= 1 && n <= V.pages))
|
||||
const arr = V.pages <= 7 ? Array.from({ length: V.pages }, (_, i) => i + 1) : [...set].sort((p, q) => p - q)
|
||||
const nums = []
|
||||
arr.forEach((n, i) => { if (i && n - arr[i - 1] > 1) nums.push(S('min-width:' + pb + 'px;height:' + pb + 'px;display:flex;align-items:center;justify-content:center;' + MU, '…')); const cur = n === V.page; nums.push(A('span', { role: 'button', 'aria-label': 'Page ' + n, 'aria-current': cur ? 'page' : undefined }, 'min-width:' + pb + 'px;height:' + pb + 'px;padding:0 6px;display:flex;align-items:center;justify-content:center;border:1px solid ' + (cur ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (cur ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (cur ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';font-weight:' + (cur ? 600 : 400), String(n))) })
|
||||
const summary = A('span', { 'aria-live': 'polite' }, (m ? 'flex:0 1 auto;' : 'flex:1;') + MU, 'Showing ' + V.from + '–' + V.to + ' of ' + V.total)
|
||||
const size = A('span', { role: 'combobox', 'aria-label': 'Rows per page', 'aria-expanded': false }, 'height:' + pb + 'px;padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);flex-shrink:0;' + HL, V.size + ' per page', Ic('chevron-down', 14))
|
||||
if (!m) return D('flex-shrink:0;display:flex;align-items:center;gap:16px;padding:12px 0 0', summary, size, V.pages > 1 && A('nav', { 'aria-label': 'Pagination' }, 'display:flex;align-items:center;gap:4px;justify-content:flex-end', edge('chevrons-left', 'First page', first), edge('chevron-left', 'Previous page', first), ...nums, edge('chevron-right', 'Next page', last), edge('chevrons-right', 'Last page', last)))
|
||||
const mb = (lab, ico, al, dis, right) => A('span', { role: 'button', 'aria-label': al, 'aria-disabled': dis || undefined }, 'flex:1;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;opacity:' + (dis ? 0.4 : 1), !right && Ic(ico), lab, right && Ic(ico))
|
||||
return D('flex-shrink:0;display:flex;flex-direction:column;gap:12px;padding:12px 0 16px', D('display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:space-between', summary, size), V.pages > 1 && A('nav', { 'aria-label': 'Pagination' }, 'display:flex;align-items:center;gap:8px', mb('Previous', 'chevron-left', 'Previous page', first), S('flex:0 0 auto;padding:0 4px;white-space:nowrap;font-weight:500;' + HL, 'Page ' + V.page + ' of ' + V.pages), mb('Next', 'chevron-right', 'Next page', last, true)))
|
||||
}
|
||||
const Chip = l => D('white-space:nowrap;width:max-content;flex-shrink:0;display:inline-flex;align-items:center;gap:6px;height:' + (m ? 36 : 24) + 'px;padding:0 8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);' + HL + ';font-size:12px', l, A('span', { role: 'button', 'aria-label': 'Remove filter ' + l }, 'display:inline-flex', Ic('x', 14)))
|
||||
const ChipC = (c, k) => A('span', { 'aria-label': (LN[c.k] || c.k) + ': ' + (c.st === 'pub' ? 'published' : c.st === 'draft' ? 'draft' : 'no version') + (c.nr ? ', needs review' : '') + (c.off ? ', language disabled' : '') }, 'white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;' + (c.st ? 'border:1px solid var(--ui-border-accented);' + HL : 'border:1px dashed var(--ui-border-accented);' + MU) + (c.off ? ';opacity:.75' : ''), c.k.toUpperCase(), c.st === 'pub' ? Ic('circle-check', 12) : c.st === 'draft' ? Ic('pencil', 12) : Ic('plus', 12), !!c.nr && Ic('refresh-cw', 12))
|
||||
const Cov = (cs) => { const Lc = m || nr ? 4 : 5, vis = cs.slice(0, Lc), more = cs.slice(Lc); return D('display:flex;gap:6px;align-items:center;flex-wrap:' + (m ? 'wrap' : 'nowrap'), ...vis.map(ChipC), more.length ? (m ? (x0 => D('display:inline-flex;align-items:center;height:44px;margin:-11px 0', x0)) : (x0 => x0))(A('span', { role: 'button', 'aria-label': more.length + ' more languages: ' + more.map(c => LN[c.k] || c.k).join(', ') }, 'display:inline-flex;align-items:center;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;background:var(--ui-bg-elevated);' + HL, '+' + more.length)) : null) }
|
||||
const covW = m ? '' : (nr ? 'width:330px;' : 'width:430px;')
|
||||
const RowMenuBtn = () => Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Row actions' })
|
||||
const liPage = () => {
|
||||
const C = { page2: { page: 2 }, fr: { lang: 'fr' }, frMissing: { lang: 'fr', tab: 'missing' }, emptyMissing: { site: 'press', lang: 'fr', tab: 'missing' }, noResults: { q: 'zinc' }, frLoading: { lang: 'fr', load: 1 }, listFail: { fail: 1 }, limited: { lim: 1 }, filters: { lang: 'fr', status: 'draft' }, tabsScroll: { lang: 'fr', tab: 'missing', shift: 1 }, long: { site: 'atelier', status: 'draft', seo: 1 }, author: { author: 'Marta Alves' }, review: { tab: 'review' }, frReview: { lang: 'fr', tab: 'review' }, langFilter: { menu: 1 }, rowMenu: { row: 1 }, ...(x === 'home' ? {} : {}) }[x] || {}
|
||||
const site = FX.SITES[C.site || 'journal'], lim = !!C.lim
|
||||
const C = { page2: { page: 2 }, fr: { lang: 'fr' }, frMissing: { lang: 'fr', tab: 'missing' }, emptyMissing: { site: 'press', lang: 'fr', tab: 'missing' }, noResults: { q: 'zinc' }, frLoading: { lang: 'fr', load: 1 }, listFail: { fail: 1 }, limited: { lim: 1 }, filters: { lang: 'fr', status: 'draft' }, tabsScroll: { lang: 'fr', tab: 'missing', shift: 1 }, long: { site: 'atelier', status: 'draft', seo: 1 }, author: { author: 'Marta Alves' }, review: { tab: 'review' }, frReview: { lang: 'fr', tab: 'review' }, langFilter: { menu: 1 }, rowMenu: { row: 1 }, menuLang: { lang: 'fr' }, apiCopy: { lang: 'fr' }, single: { site: 'archive' }, singleNoResults: { site: 'archive', q: 'zinc' }, singleLoading: { site: 'archive', load: 1 }, singleFail: { site: 'archive', fail: 1 }, singleEmpty: { site: 'gallery' }, singleMenu: { site: 'archive' }, singleDraft: { site: 'archive', status: 'draft' }, ...(x === 'home' ? {} : {}) }[x] || {}
|
||||
const site = FX.SITES[C.site || 'journal'], lim = !!C.lim, single = site.langs.length <= 1
|
||||
const V = FX.view({ site, lang: C.lang, tab: C.tab, status: C.status, author: C.author, seo: C.seo, q: C.q, page: C.page, read: lim ? ['en'] : null, create: lim ? [] : null, scope: lim ? (s0 => ['Camille Roche', 'Luis Ferrer'].includes(s0.by)) : null })
|
||||
const tabs = (C.load || C.fail) ? V.tabs.map(q => [q[0], '–', q[2]]) : V.tabs, tabOn = Math.max(0, V.tabs.findIndex(q => q[2] === V.tab))
|
||||
const act = [C.status && 'Status: Draft', C.author && 'Author: ' + C.author, C.seo && 'SEO: Needs attention'].filter(Boolean)
|
||||
const chips = act.length ? ['Site: ' + site.name].concat(C.lang ? ['Language: ' + LN[C.lang]] : [], act) : []
|
||||
const nF = chips.length
|
||||
const filt = () => D('display:flex;gap:8px;flex-wrap:wrap;align-items:center', D('flex:1;min-width:' + (m ? '0' : '200px'), Fd('', C.q ? C.q : C.lang ? 'Search ' + LN[C.lang] + ' titles' : 'Search titles in every language', { val: !!C.q })),
|
||||
m ? Btn('Filters' + (nF ? ' · ' + nF : ''), 'secondary', { ic: 'sliders-horizontal', al: nF ? 'Filters, ' + nF + ' active' : 'Filters' }) : [Fd('', site.name, { sel: true }), Fd('', C.lang ? LN[C.lang] : 'All languages', { sel: true }), Fd('', C.status ? 'Draft' : 'Any status', { sel: true }), Fd('', C.author || 'Any author', { sel: true, focus: x === 'author' }), Fd('', C.seo ? 'Needs attention' : 'Any SEO state', { sel: true })].map(e => D('flex:0 0 auto;width:' + (nr ? 128 : 150) + 'px', e)))
|
||||
m ? Btn('Filters' + (nF ? ' · ' + nF : ''), 'secondary', { ic: 'sliders-horizontal', al: nF ? 'Filters, ' + nF + ' active' : 'Filters' }) : [Fd('', site.name, { sel: true }), single ? null : Fd('', C.lang ? LN[C.lang] : 'All languages', { sel: true }), Fd('', C.status ? 'Draft' : 'Any status', { sel: true }), Fd('', C.author || 'Any author', { sel: true, focus: x === 'author' }), Fd('', C.seo ? 'Needs attention' : 'Any SEO state', { sel: true })].filter(Boolean).map(e => D('flex:0 0 auto;width:' + (nr ? 128 : 150) + 'px', e)))
|
||||
const chipRow = chips.length ? D('display:flex;gap:6px;flex-wrap:wrap;align-items:center', ...chips.map(Chip), Btn('Clear all', 'ghost', { sm: !m })) : null
|
||||
const shift = C.shift ? -96 : 0
|
||||
const tabRow = D('position:relative;border-bottom:1px solid var(--ui-border);display:flex;align-items:center;gap:4px', m && Btn('', 'ghost', { ic: 'chevron-left', sm: true, al: 'Scroll tabs left', dis: !shift }),
|
||||
A('div', { role: 'tablist', 'aria-label': 'Article views' }, 'flex:1;min-width:0;overflow:hidden;display:flex;position:relative', D('display:flex;gap:' + (m ? 16 : 24) + 'px;flex-shrink:0;transform:translateX(' + (m ? shift : 0) + 'px)', ...tabs.map((q, i) => A('div', { role: 'tab', 'aria-selected': i === tabOn }, 'padding:' + (m ? '12px 0' : '8px 0') + ';white-space:nowrap;font-weight:' + (i === tabOn ? 600 : 500) + ';color:' + (i === tabOn ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';border-bottom:2px solid ' + (i === tabOn ? 'var(--ui-primary)' : 'transparent'), q[0] + ' ', S(MU + ';font-weight:400', q[1])))), m && D('position:absolute;right:0;top:0;bottom:0;width:28px;background:linear-gradient(90deg,transparent,var(--ui-bg))'), m && shift && D('position:absolute;left:0;top:0;bottom:0;width:28px;background:linear-gradient(270deg,transparent,var(--ui-bg))')), m && Btn('', 'ghost', { ic: 'chevron-right', sm: true, al: 'Scroll tabs right' }))
|
||||
A('div', { role: 'tablist', 'aria-label': 'Article views' }, 'flex:1;min-width:0;overflow:hidden;display:flex;position:relative', D('display:flex;gap:' + (m ? 16 : 24) + 'px;flex-shrink:0;transform:translateX(' + (m ? shift : 0) + 'px)', ...tabs.map((q, i) => A('div', { role: 'tab', 'aria-selected': i === tabOn }, 'padding:' + (m ? '12px 0' : '8px 0') + ';white-space:nowrap;font-weight:' + (i === tabOn ? 600 : 500) + ';color:' + (i === tabOn ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';border-bottom:2px solid ' + (i === tabOn ? 'var(--ui-primary)' : 'transparent'), q[0] + ' ', S(MU + ';font-weight:400', q[1])))), m && D('position:absolute;right:0;top:0;bottom:0;width:28px;background:linear-gradient(90deg,transparent,var(--ui-bg))'), m && !!shift && D('position:absolute;left:0;top:0;bottom:0;width:28px;background:linear-gradient(270deg,transparent,var(--ui-bg))')), m && Btn('', 'ghost', { ic: 'chevron-right', sm: true, al: 'Scroll tabs right' }))
|
||||
const card = 'background:var(--ui-bg);border:1px solid var(--ui-border)', hd = !m && D('display:flex;gap:12px;padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', S('flex:1', 'Article'), S(covW + 'flex-shrink:0', 'Languages'), S('width:110px', 'Author'), S('width:80px', 'Updated'), S('width:28px'))
|
||||
const hdS = !m && D('display:flex;gap:12px;padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', S('flex:1', 'Article'), S('width:110px', 'Status'), S('width:110px', 'Author'), S('width:80px', 'Updated'), S('width:28px'))
|
||||
const SingleRow = (r) => D(ROW + ';' + (m ? 'flex-wrap:wrap;gap:6px 12px' : ''), D('flex:1 1 ' + (m ? '70%' : '0') + ';min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: r.rep }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', FX.tt(r.s, r.rep)), m && S(MU + ';font-size:12px', r.s.by + ' · ' + r.s.upd)), D((m ? 'order:3' : 'width:110px'), Pub(r.s.v[r.rep][0])), !m && [S(MU + ';width:110px', r.s.by), S(MU + ';width:80px', r.s.upd)], D(m ? 'order:1' : '', RowMenuBtn()))
|
||||
const creat = lim ? [] : site.langs
|
||||
const StoryRow = (r) => { const s0 = r.s, cs = FX.chips(s0, site, creat), t = FX.tt(s0, r.rep)
|
||||
return D(ROW + ';' + (m ? 'flex-wrap:wrap;gap:6px 12px' : ''), D('flex:1 1 ' + (m ? '70%' : '0') + ';min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: r.rep }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', t), r.rep !== site.def && S(MU + ';font-size:12px', 'Shown in ' + (LN[r.rep] || r.rep) + (site.disabled.includes(r.rep) ? ' · language disabled' : ''))), m && D('order:2;flex:1 1 100%', Cov(cs)), !m && D(covW + 'flex-shrink:0', Cov(cs)), m ? S('order:3;' + MU + ';font-size:12px;flex:1', s0.by + ' · ' + s0.upd) : [S(MU + ';width:110px', s0.by), S(MU + ';width:80px', s0.upd)], D(m ? 'order:1' : '', RowMenuBtn())) }
|
||||
@@ -259,11 +273,12 @@ class Component extends DCLogic {
|
||||
if (C.fail) body = Alert('error', 'circle-x', 'Couldn\'t load articles', 'Your filters and page are kept. Try again.', Btn('Try again', 'secondary', { sm: true }))
|
||||
else if (C.load) body = Sk(5)
|
||||
else if (!V.total && C.lang && V.tab === 'missing') body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('circle-check', 28), S('font-weight:600;' + HL, 'Every article you can see has a ' + LN[C.lang] + ' version'), S(TO + ';max-width:360px', 'Articles without a ' + LN[C.lang] + ' version will appear here.'))
|
||||
else if (!V.total && single && !C.q) body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('file-text', 28), S('font-weight:600;' + HL, 'No articles yet'), S(TO + ';max-width:360px', 'Write your first one. It stays a draft until you publish it.'), Btn('New article', 'primary', { ic: 'plus' }))
|
||||
else if (!V.total) body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('search', 28), S('font-weight:600;' + HL, 'No articles match "' + (C.q || '') + '"'), S(TO, 'Search covers titles in every language you can read.'), Btn('Clear search', 'secondary', { sm: true }))
|
||||
else body = D(card, C.lang && V.tab === 'missing' ? null : (C.lang ? null : hd), ...V.rows.map(C.lang ? (V.tab === 'missing' ? MissRow : VerRow) : StoryRow))
|
||||
else body = D(card + ';min-height:0;overflow:auto;flex:0 1 auto', C.lang && V.tab === 'missing' ? null : (C.lang ? null : single ? hdS : hd), ...V.rows.map(C.lang ? (V.tab === 'missing' ? MissRow : VerRow) : single ? SingleRow : StoryRow))
|
||||
const unit = C.fail || C.load ? '' : V.unit
|
||||
return D('flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:' + (m ? 12 : 16) + 'px;padding:' + (m ? '16px' : '24px') + ';position:relative', D('display:flex;align-items:baseline;gap:12px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;' + HL, 'Articles'), A('span', { 'aria-live': 'polite' }, 'flex:1;' + MU + ';font-weight:500', unit), Btn(m ? '' : 'New article', 'primary', { ic: 'plus', al: 'New article' })),
|
||||
lim && Alert('neutral', 'lock', 'Showing only what you can read', 'Languages, counts and missing-translation results reflect the versions you have permission to see. You can\'t create translations on this site.'), filt(), chipRow, tabRow, body, (V.total && !C.fail && !C.load) ? Pager(V) : null)
|
||||
lim && Alert('neutral', 'lock', 'Showing only what you can read', 'Languages, counts and missing-translation results reflect the versions you have permission to see. You can\'t create translations on this site.'), filt(), chipRow, single ? null : tabRow, body, (V.total && !C.fail && !C.load) ? Pager(V) : null)
|
||||
}
|
||||
const sheet = (o) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'center') + ';justify-content:center', A('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Filters' }, 'width:100%;max-width:' + (m ? '100%' : '420px') + ';max-height:92%;background:var(--ui-bg);display:flex;flex-direction:column', D('display:flex;justify-content:center;padding:8px 0 0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
|
||||
D('padding:16px 16px 0;display:flex;align-items:center', S('flex:1;font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, 'Filters'), Btn('', 'ghost', { ic: 'x', al: 'Close filters' })),
|
||||
@@ -276,6 +291,10 @@ class Component extends DCLogic {
|
||||
if (x === 'author') return [D('position:absolute;z-index:40;left:' + (nr ? 500 : 1018) + 'px;top:170px;width:240px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)', A('div', { role: 'listbox', 'aria-label': 'Author' }, 'display:flex;flex-direction:column;padding:4px 0', ...[['Any author', J.stories.length]].concat(FX.AUTHORS).map((a, i) => A('div', { role: 'option', 'aria-selected': a[0] === 'Marta Alves' }, 'display:flex;align-items:center;gap:8px;min-height:36px;padding:6px 12px;' + (a[0] === 'Marta Alves' ? 'background:var(--rc-sand);color:var(--rc-on-sand);font-weight:600' : HL), S('width:16px', a[0] === 'Marta Alves' && Ic('check')), S('flex:1', a[0]), S(mono + ';' + (a[0] === 'Marta Alves' ? '' : MU), a[1])))))]
|
||||
const langRows = (grp) => (grp ? J.disabled : J.langs).map(k => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (grp ? MU : HL), S('width:16px'), S('flex:1', LN[k]), S(mono + ';' + MU, FX.count(k, J.stories)[0] + ' versions')))
|
||||
if (x === 'langFilter') return [D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'left:' + (nr ? 330 : 792) + 'px;top:170px;width:280px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column;max-height:70%', A('div', { role: 'listbox', 'aria-label': 'Language' }, 'display:flex;flex-direction:column;overflow-y:auto;padding:4px 0', D('padding:4px 12px;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Enabled'), D('display:flex;align-items:center;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + HL, S('width:16px'), 'All languages'), ...langRows(0), D('padding:8px 12px 4px;border-top:1px solid var(--ui-border);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Disabled'), ...langRows(1), D('padding:6px 12px 8px;font-size:12px;' + MU, 'Disabled languages keep their versions. They can\'t get new ones.')))]
|
||||
if (x === 'singleMenu') return [OvMenu([{ l: 'Open', ic: 'file-pen' }, { l: 'Unpublish', ic: 'eye-off' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete article', ic: 'trash-2', danger: 1 }], 272)]
|
||||
if (x === 'menuLang') return [OvMenu([{ l: 'Open French', ic: 'file-pen' }, { l: 'Open another language…', ic: 'languages' }, { l: 'Publish French', ic: 'globe' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete French version', ic: 'trash-2', danger: 1 }], 272)]
|
||||
if (x === 'apiCopy') return [Toast('success', 'circle-check', 'API URL copied', '/v1/article/14?lang=fr')]
|
||||
if (x === 'deleteStoryDone') return [Toast('success', 'circle-check', 'Article deleted', 'The article and all of its translations were deleted.')]
|
||||
if (x === 'rowMenu') return [OvMenu([{ l: 'Open English', ic: 'file-pen' }, { l: 'Open another language…', ic: 'languages' }, { l: 'Add translation…', ic: 'plus' }, { l: 'Unpublish English', ic: 'eye-off' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete English version', ic: 'trash-2', danger: 1 }])]
|
||||
return []
|
||||
}
|
||||
@@ -283,9 +302,9 @@ class Component extends DCLogic {
|
||||
const naOv = () => {
|
||||
const multi = x !== 'single'
|
||||
return Dlg({ title: 'New article', desc: x === 'translation' ? 'Create a translation of an article you can read.' : 'Give the article a title. You can write the rest in the editor.' }, [
|
||||
x === 'translation' ? [Fd('Article', 'Roof inspection checklist', { sel: true, help: 'No French version yet. Copies from English · published' }), Fd('Language', 'French', { sel: true }), Fd('Title', 'Liste de contrôle pour l\'inspection de la toiture', { focus: true, lang: 'fr' })] :
|
||||
x === 'translation' ? [D('display:flex;flex-direction:column;gap:8px', Radio('New article', 'Starts an article in this language.', false), Radio('Translation of an existing article', 'Uses the same rules as Add translation.', true)), Fd('Article', 'Roof inspection checklist', { sel: true, help: 'No French version yet.' }), Fd('Language', 'French', { sel: true, help: 'Only enabled languages this article doesn\'t have yet.' }), Fd('Source version', 'English · published', { sel: true, help: 'You choose which version to copy from. It is also the source tracked for review.' }), Fd('Title in French', 'Liste de contrôle pour l\'inspection de la toiture', { focus: true, lang: 'fr' }), D('display:flex;flex-direction:column;gap:8px', S('font-weight:500;' + HL, 'Starting content'), Radio('Start empty', 'Only the title. Freshness shows No source.', false), Radio('Copy from English', 'Copies body, excerpt, search and social text, image alternative text and authors. The version starts as Needs review.', true))] :
|
||||
[Fd('Title', x === 'sitechange' ? 'Spring newsletter' : 'Title', { val: x === 'sitechange', focus: x !== 'sitechange', help: '0 / 200' }), Fd('Site', x === 'sitechange' ? 'Notes' : x === 'single' ? 'Archive' : 'Journal', { sel: true }),
|
||||
multi && Fd('Language', 'English', { sel: true, help: x === 'sitechange' ? 'Notes only offers English, so the language was reset from French.' : 'Default for Journal.' }),
|
||||
multi && Fd('Language', 'English', { sel: true, help: x === 'sitechange' ? 'Notes only offers English, so the language was reset from French.' : 'Default language for Journal.' }),
|
||||
multi && D('display:flex;flex-direction:column;gap:8px', Radio('New article', 'Starts an article in this language.', true), Radio('Translation of an existing article', 'Uses the same rules as Add translation.', false))]
|
||||
], [Btn('Cancel'), Btn(x === 'translation' ? 'Create French draft' : 'Create article', 'primary')])
|
||||
}
|
||||
@@ -295,15 +314,15 @@ class Component extends DCLogic {
|
||||
const atOv = () => {
|
||||
const empty = x === 'empty', long = x === 'long', dupCase = x === 'dup' || x === 'dupHidden', st = dupCase ? S1 : S5, ft = dupCase ? FRT : FRT5
|
||||
const dis = ['gone', 'unreadable', 'langOff', 'dup', 'dupHidden', 'saving'].includes(x)
|
||||
const f = [Fd('Language', 'French', { sel: true, ro: x === 'langOff', err: x === 'langOff' ? 'French was disabled on this site.' : null }), Fd('Source version', x === 'unreadable' ? 'Choose a version' : 'English · published', { sel: true, val: x !== 'unreadable' && x !== 'gone', help: 'You choose which version to copy from. It is also the source tracked for review. Access is checked again when you create the draft.' }), Fd('Title in French', long ? ft + ' avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes' : ft, { focus: x === 'form', lang: 'fr', help: 'Copying text is a starting point: it stays in English until you translate it.' }),
|
||||
const f = [Fd('Language', 'French', { sel: true, ro: x === 'langOff', err: x === 'langOff' ? 'French was disabled on this site.' : null }), Fd('Source version', empty ? 'No source' : (x === 'unreadable' || x === 'gone') ? 'Choose a version' : 'English · published', { sel: true, val: !empty && x !== 'unreadable' && x !== 'gone', err: (x === 'unreadable' || x === 'gone') ? 'Choose a version you can read.' : null, help: (x === 'unreadable' || x === 'gone') ? null : empty ? 'Optional. Freshness shows No source until you pick a tracked source in the editor.' : 'You choose which version to copy from. It is also the source tracked for review. Access is checked again when you create the draft.' }), Fd('Title in French', long ? ft + ' avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes' : ft, { focus: x === 'form', lang: 'fr', help: 'Copying text is a starting point: it stays in English until you translate it.' }),
|
||||
D('display:flex;flex-direction:column;gap:8px', S('font-weight:500;' + HL, 'Starting content'), Radio('Start empty', 'Only the title. Freshness shows No source until you pick a tracked source.', empty), Radio('Copy from English', 'Copies body, excerpt, search and social text, image alternative text and authors. Images are reused, files aren\'t duplicated. The version starts as Needs review.', !empty))]
|
||||
if (long) f.push(Alert('neutral', 'info', 'Authors', 'Marta Alves, Camille Roche, Luis Ferrer, Ana Pereira, Jonas Weber and 4 more will be copied. You can change them in the editor.'), Alert('neutral', 'info', 'Images', 'Cover image and 7 inline images are reused. Alternative text is copied and stays in English until you translate it.'))
|
||||
const T = 'Add a translation'
|
||||
if (x === 'dup') return Dlg({ title: T }, [Alert('error', 'circle-x', 'French already exists for this article', 'Open it to continue. Your entries are kept here.', Btn('Open French version', 'secondary', { sm: true })), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'dupHidden') return Dlg({ title: T }, [Alert('error', 'circle-x', 'French can\'t be created for this article', 'Try a different language, or ask an owner. Your entries are kept.'), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'fail') return Dlg({ title: T }, [Alert('error', 'circle-x', 'Couldn\'t create the translation', 'Your entries are kept. Try again.'), ...f], [Btn('Cancel'), Btn('Try again', 'primary')])
|
||||
if (x === 'gone') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source was deleted', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'unreadable') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'You can no longer read the version you chose. Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'gone') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'unreadable') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'langOff') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'French was disabled', 'French was disabled on this site while you were filling this in. Ask an owner to re-enable it, or choose another language. Your entries are kept.'), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'saving') return Dlg({ title: T }, f, [Btn('Cancel', 'secondary', { dis: true }), Btn('Creating…', 'primary', { busy: true })])
|
||||
return Dlg({ title: T, desc: 'Creates a French draft of "' + st.title + '". It isn\'t published.', top: long ? 24 : 60 }, f, [Btn('Cancel'), Btn('Create French draft', 'primary')])
|
||||
@@ -315,14 +334,16 @@ class Component extends DCLogic {
|
||||
unpublish: () => Dlg({ title: 'Unpublish French?', desc: 'The French version goes back to draft and stops being public. English stays online.' }, [], [Btn('Cancel'), Btn('Unpublish French', 'primary')]),
|
||||
unpublishDirty: () => Dlg({ title: 'Unpublish French?', desc: 'The saved French version goes offline and becomes a draft. English stays online.' }, [Alert('neutral', 'info', 'Your edits stay in the editor', 'Your unsaved changes are kept here. Nothing is saved when you unpublish. Save them yourself when you are ready.')], [Btn('Cancel'), Btn('Unpublish French', 'primary')]),
|
||||
delete: () => Dlg({ title: 'Delete the French version?', desc: '"' + FRT + '" will be deleted. Other versions stay.' }, [], [Btn('Cancel'), Btn('Delete French version', 'danger')]),
|
||||
deleteSource: () => Dlg({ title: 'Delete the English version?', desc: 'English is the tracked source for versions you can read.' }, [Alert('warning', 'triangle-alert', 'Review tracking will pause', 'Versions that track English stay as they are. Their review history is kept, and they ask you to choose a new source.')], [Btn('Cancel'), Btn('Delete English version', 'danger')]),
|
||||
deleteStoryOk: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'This deletes the article and every version you can read. It can\'t be undone.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
|
||||
deleteSource: () => Dlg({ title: 'Delete the English version?', desc: 'English is the tracked source for versions you can read.' }, [Alert('warning', 'triangle-alert', 'Review tracking will pause', 'French, German and Arabic track English. They stay as they are and keep their review history. Each one asks you to choose a new source.')], [Btn('Cancel'), Btn('Delete English version', 'danger')]),
|
||||
deleteStoryOk: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'The article and all of its translations will be deleted. This can\'t be undone.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
|
||||
deleteLast: () => Dlg({ title: 'Delete the only version?', desc: 'English is the only version of this article. Deleting it deletes the article.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
|
||||
saveAndPublish: () => Dlg({ title: 'Save and publish French?', desc: 'Your unsaved changes to the French version are saved first, then only the French version goes online. English and German aren\'t affected.' }, [], [Btn('Cancel'), Btn('Save and publish French', 'primary')]),
|
||||
saveFailPublish: () => [Toast('error', 'circle-x', 'Couldn\'t save French', 'Nothing was published. Your changes are kept. Try again.')],
|
||||
deleteStory: () => Dlg({ title: 'You can\'t delete this article', desc: 'Deleting an article needs delete permission on every version. You can still delete the version you are editing.' }, [], [Btn('Close'), Btn('Delete French version', 'danger')]),
|
||||
fail: () => [Toast('error', 'circle-x', 'Couldn\'t publish French', 'Nothing changed. Try again.')],
|
||||
done: () => [Toast('success', 'circle-check', 'French published', 'Other versions weren\'t changed.')]
|
||||
done: () => [Toast('success', 'circle-check', 'French published', 'Other versions weren\'t changed.')],
|
||||
deleteDone: () => [Toast('success', 'circle-check', 'French version deleted', '"' + FRT + '" was deleted. English and German weren\'t changed.')],
|
||||
deleteStoryFail: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'The article and all of its translations will be deleted. This can\'t be undone.' }, [Alert('error', 'circle-x', 'Couldn\'t delete the article', 'Nothing was deleted. Every version is still there. Try again in a moment.')], [Btn('Cancel'), Btn('Try again', 'danger')])
|
||||
}[x]?.())
|
||||
// ---- freshness and review (release 2)
|
||||
const frOv = () => ({
|
||||
@@ -334,8 +355,8 @@ class Component extends DCLogic {
|
||||
fail: () => [Toast('error', 'circle-x', 'Couldn\'t mark as reviewed', 'Nothing was recorded. Try again.')]
|
||||
}[x]?.())
|
||||
// ---- conflicts, moves, permissions
|
||||
const CfOpt = (t, d, b, k = 'secondary', top) => D((top ? '' : 'border-top:1px solid var(--ui-border);padding-top:14px;') + 'display:flex;' + (m ? 'flex-direction:column;gap:8px' : 'align-items:center;gap:16px'), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:600;' + HL, t), S(MU + ';font-size:13px;line-height:19px;text-wrap:pretty', d)), D((m ? '' : 'width:200px;flex-shrink:0;') + 'display:flex', Btn(b, k, { w: '100%' })))
|
||||
const Cf = (o) => Dlg({ title: o.title || 'Someone else saved this version', desc: o.desc || 'Someone else saved this version at ' + (o.t || '14:32') + '. Your changes aren\'t saved. They stay here until you choose.', w: 560, top: 60 }, [o.alert, CfOpt('Keep my changes', 'Nothing is sent. Your changes stay in the editor.', 'Keep my changes', 'primary', 1), ...(o.rc ? [CfOpt('Reload theirs', 'This discards your unsaved changes.', 'Reload and discard my changes')] : [CfOpt('Download my version', 'Saves your title and body as a text file. Nothing is sent.', 'Download my version'), CfOpt('Reload theirs', 'Shows their version. You confirm first.', 'Reload theirs'), CfOpt('Save over theirs', 'Replaces the title, body and search fields of this version only. Other languages are untouched.', 'Save over theirs')])], [])
|
||||
const CfOpt = (t, d, b, k = 'secondary', top, ic) => D((top ? '' : 'border-top:1px solid var(--ui-border);padding-top:14px;') + 'display:flex;' + (m ? 'flex-direction:column;gap:8px' : 'align-items:center;gap:16px'), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', D('display:flex;align-items:center;gap:6px', ic && S('color:var(--ui-error)', Ic(ic)), S('font-weight:600;' + HL, t)), S(MU + ';font-size:13px;line-height:19px;text-wrap:pretty', d)), D((m ? '' : 'width:200px;flex-shrink:0;') + 'display:flex', Btn(b, k, { w: '100%', ic })))
|
||||
const Cf = (o) => Dlg({ title: o.title || 'Someone else saved this version', desc: o.desc || 'Someone else saved this version at ' + (o.t || '14:32') + '. Your changes aren\'t saved. They stay here until you choose.', w: 560, top: 60 }, [o.alert, CfOpt('Keep my changes', 'Nothing is sent. Your changes stay in the editor.', 'Keep my changes', 'primary', 1), ...(o.rc ? [CfOpt('Reload theirs', 'This discards your unsaved changes.', 'Reload and discard my changes')] : [CfOpt('Download my version', 'Saves your title and body as a text file. Nothing is sent.', 'Download my version'), CfOpt('Reload theirs', 'Shows their version. You confirm first.', 'Reload theirs'), CfOpt('Save over theirs', 'Replaces the title, body and search fields of this version only. Other languages are untouched.', 'Save over theirs', 'dangerOutline', 0, 'triangle-alert')])], [])
|
||||
const VList = [['English', 'Published'], ['French', 'Draft'], ['German', 'Published'], ['Arabic', 'Draft · Read only']]
|
||||
const moveNote = (hidden) => D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;' + HL, 'Moves all language versions of this article.'), ...VList.filter(r => !(hidden && r[0] === 'Arabic')).map(r => S(TO, r[0] + ' · ' + r[1])), hidden && S(MU + ';font-size:12px', 'Versions you can\'t read aren\'t listed or counted.'))
|
||||
const mvPanel = (hidden) => D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:215px;width:340px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:16px;display:flex;flex-direction:column;gap:12px', S('font-weight:600;' + HL, 'Site'), D('display:flex;flex-direction:column;gap:0;border:1px solid var(--ui-border)', ...['Journal', 'Atelier', 'Archive', 'Notes'].map(k => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (k === 'Journal' ? 'background:var(--rc-sand);color:var(--rc-on-sand);font-weight:600' : HL), S('width:16px', k === 'Journal' && Ic('check')), k))), moveNote(hidden), D('display:flex;gap:8px', Btn('Cancel'), D('flex:1;display:flex', Btn('Move article…', 'primary', { w: '100%' }))))
|
||||
@@ -345,7 +366,8 @@ class Component extends DCLogic {
|
||||
moveConfirm: () => Dlg({ title: 'Move to Atelier?', ...base }, [moveNote(false), Alert('neutral', 'info', 'Addresses and links', 'Each version keeps its address. Links from the old site to it will stop working. RenewCMS does not create redirects.')], [Btn('Cancel'), Btn('Move article', 'primary')]),
|
||||
moveBusy: () => Dlg({ title: 'Moving to Atelier…', desc: 'All 4 versions move together. Keep this page open.' }, [Alert('info', 'loader-circle', 'Moving', 'This takes a few seconds.')], [Btn('Cancel', 'secondary', { dis: true }), Btn('Moving…', 'primary', { busy: true })]),
|
||||
move: () => Dlg({ title: 'Move to Notes?', ...base }, [Alert('warning', 'triangle-alert', 'Notes doesn\'t enable every language in this article', 'Notes enables English only. Enable French, German and Arabic on Notes first. Languages disabled on Journal count too. Nothing was moved.')], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
|
||||
moveSlug: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'An address is already used on Atelier', 'The French address "comment-nous-chiffrons-une-renovation" is already used by another French article on Atelier. Change it before moving. Nothing was moved.', Btn('Edit slug', 'secondary', { sm: true }))], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
|
||||
moveSlug: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'An address is already used on Atelier', 'An address must be unique within its language on Atelier. Change the address below, then move again. Nothing was moved.'), D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;' + HL, 'Conflicting version'), D('display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--ui-border)', S('font-weight:500;' + HL, 'French'), S(mono + ';' + TO + ';flex:1 1 160px;min-width:0;overflow-wrap:anywhere', 'comment-nous-chiffrons-une-renovation'), Btn('Edit slug', 'secondary', { sm: true })), S(MU + ';font-size:12px;line-height:18px', 'Only versions you can read are named.'))], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
|
||||
moveFail: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'Couldn\'t move the article', 'Nothing was moved. All 4 versions are still on Journal. Try again.')], [Btn('Cancel'), Btn('Try again', 'primary')]),
|
||||
movePerm: () => Dlg({ title: 'You can\'t move this article' }, [Alert('error', 'circle-x', 'Moving needs edit permission on every version', 'Ask an owner to move it. Nothing was changed.')], [Btn('Close')]),
|
||||
moveDone: () => [Toast('success', 'circle-check', 'Moved to Atelier', 'All 4 versions moved together.')]
|
||||
}[kind]
|
||||
@@ -358,7 +380,7 @@ class Component extends DCLogic {
|
||||
conflict: () => Cf({}),
|
||||
conflictRestored: () => Cf({ alert: Alert('info', 'info', 'Restored unsaved changes from 14:18', 'Your session ended. These changes were kept in this browser.') }),
|
||||
reloadConfirm: () => Cf({ title: 'Reload theirs?', desc: 'This discards your unsaved changes.', rc: 1 }),
|
||||
conflict2: () => Cf({ t: '14:41', alert: Alert('error', 'circle-x', 'Couldn\'t replace theirs', 'Someone else saved this version again at 14:41, before your save went through. Nothing was overwritten. Your changes are still here.') }),
|
||||
conflict2: () => Cf({ t: '14:41', alert: Alert('error', 'circle-x', 'Their version changed again', 'Someone else saved this version again at 14:41, before your save went through. Nothing was overwritten. Your changes are still here.') }),
|
||||
download: () => [Cf({}), Toast('success', 'circle-check', 'Download started', 'Your French title and body are saved as a text file. Your changes are still in the editor.')],
|
||||
restored: () => [] }
|
||||
return dl[x]?.()
|
||||
@@ -368,44 +390,56 @@ class Component extends DCLogic {
|
||||
const mk2 = (t, d, ...c) => Card(S('font-weight:600;' + HL, t), d && S(TO, d), ...c)
|
||||
const RowS = (a, b, c) => D(ROW + ';padding-left:0;padding-right:0', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S(HL + ';font-weight:500', a), b && S(MU + ';font-size:12px', b)), c)
|
||||
if (x === 'home' || x === 'homeFr') { const fr = x === 'homeFr', V = FX.view({ site: J, read: fr ? ['fr'] : null }); return Page('Home', fr ? 'You can read French versions only.' : 'Welcome back, Marta.', null, mk2('Recently updated', null, ...V.rows.slice(0, 5).map(r => RowS(FX.tt(r.s, r.rep), r.s.by + ' · ' + r.s.upd, r.rep !== J.def ? S(mono + ';border:1px solid var(--ui-border-accented);padding:0 6px;' + HL, r.rep.toUpperCase()) : null))), S(MU + ';font-size:12px', 'Each article appears once, with its default-language title. A language code shows only when the title isn\'t in the default language.')) }
|
||||
if (x === 'activity') return Page('Activity', 'Recent changes in Journal and Archive.', null, mk2('Today and this week', null, ...[['Camille Roche', 'Published French version of "Guide de l\'enduit à la chaux"', '2 h ago'], ['Luis Ferrer', 'Published French version of "Liste d\'hiver pour les fenêtres en bois"', 'Yesterday'], ['Marta Alves', 'Published German version of "So kalkulieren wir eine Badsanierung"', '3 days ago'], ['Luis Ferrer', 'Created German version of "Checkliste für die Dachprüfung"', '14 Oct'], ['Camille Roche', 'Published "Guia de cal e areia"', '14 Oct']].map(r => RowS(r[1], r[0] + ' · ' + r[2]))), S(MU + ';font-size:12px', 'The last event is on Archive, which uses one language, so it names none.'))
|
||||
if (x === 'media') return Page('Media', 'Image details', null, mk2('bathroom-plan.jpg', null, D('height:160px;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;' + MU + ';font:12px \'IBM Plex Mono\',monospace', 'image preview'), S('font-weight:500;' + HL, 'Used in 2 articles · 4 versions · 1 file'), S(MU + ';font-size:12px', 'A shared image is one file. It is counted once per article, and the versions that use it are listed.'), RowS('How we price a bathroom renovation', 'English, French, German'), RowS('A guide to lime plaster', 'English')))
|
||||
if (x === 'menu') return Page('Article menu', 'The same menu names the language on multilingual sites.', null, D('display:flex;gap:16px;flex-wrap:wrap', ...[['Journal (several languages)', ['Publish French', 'Delete French version']], ['Archive (one language)', ['Publish', 'Delete']]].map(g => D('flex:1 1 220px;display:flex;flex-direction:column;gap:6px', S('font-size:12px;' + MU, g[0]), D('border:1px solid var(--ui-border);background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:4px 0', ...g[1].map((l, i) => D('display:flex;align-items:center;gap:10px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (i ? 'color:var(--ui-error)' : HL), Ic(i ? 'trash-2' : 'globe'), S('font-weight:500', l))))))))
|
||||
if (x === 'apiCopy') return Page('Details', 'French version', null, mk2('Public API URL', 'Each language version has its own address. The URL carries the language.', D('display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--ui-bg-elevated);' + mono, S('flex:1', '/v1/article/14?lang=fr'), Ic('copy', 14))))
|
||||
if (x === 'seoPreview') return Page('Search preview', 'French version', null, mk2('How it can appear in search', null, D('display:flex;flex-direction:column;gap:2px;padding:12px;background:var(--ui-bg-elevated)', S(mono + ';' + MU, 'journal.example.com › fr › comment-nous-chiffrons-une-renovation'), S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, FRT + ' | Journal'), S(TO, 'La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts.')), S(MU + ';font-size:12px', 'Shows the localized address and canonical. Description: Fallback: French excerpt.')))
|
||||
if (x === 'siteFallback') return Page('Search and sharing', 'Site-level text is used as a fallback only in its own language.', null, ...[['French article', 'Description', 'Fallback: French site description', 'Used: "Rénovation et conseils pour la maison" (site text in French)', 1], ['Arabic article', 'Description', 'No fallback', 'Empty. English site default not used.', 0]].map(r => Card(S('font-weight:600;' + HL, r[0]), Fd(r[1], r[4] ? 'Empty' : 'Empty', { val: false, help: r[2] + '. ' + r[3] }), r[4] ? Bd('Uses French site text', 'info', 'languages') : Bd('Tag left out', 'neutral', 'minus'))))
|
||||
if (x === 'activity') return Page('Activity', 'Recent changes in Journal and Archive.', null, mk2('Today and this week', null, ...[['Marta Alves', 'Added French to Journal', 'Just now'], ['Marta Alves', 'Made French the default language of Journal', '20 min ago'], ['Marta Alves', 'Changed the public URL pattern of Journal', '1 h ago'], ['Camille Roche', 'Published French version of "Guide de l\'enduit à la chaux"', '2 h ago'], ['Luis Ferrer', 'Published French version of "Liste d\'hiver pour les fenêtres en bois"', 'Yesterday'], ['Marta Alves', 'Published German version of "So kalkulieren wir eine Badsanierung"', '3 days ago'], ['Luis Ferrer', 'Created German version of "Checkliste für die Dachprüfung"', '14 Oct'], ['Camille Roche', 'Published "Guia de cal e areia"', '14 Oct']].map(r => RowS(r[1], r[0] + ' · ' + r[2]))), S(MU + ';font-size:12px', 'Site settings events name the site and the language. The last event is on Archive, which uses one language, so it names none.'))
|
||||
if (x === 'media' || x === 'mediaElsewhere') return Page('Media', 'Images and files for every site.', Btn('Upload', 'primary', { ic: 'upload' }), D('display:grid;grid-template-columns:repeat(auto-fill,minmax(' + (m ? 140 : 180) + 'px,1fr));gap:12px', ...['bathroom-plan.jpg', 'window-frame.jpg', 'lime-plaster.jpg', 'roof-detail.jpg', 'floorboards.jpg', 'tile-samples.jpg'].map((n, i) => D('display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--ui-bg);border:1px solid ' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? 'var(--ui-primary)' : 'var(--ui-border)') + ';' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? 'background:var(--rc-sand);color:var(--rc-on-sand)' : ''), D('height:' + (m ? 80 : 110) + 'px;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;font:12px \'IBM Plex Mono\',monospace;' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? '' : MU), 'image'), S('font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? '' : HL), n)))))
|
||||
return null
|
||||
}
|
||||
const PanelShell = (title, sub, body, o = {}) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;justify-content:' + (m ? 'center' : 'flex-end') + ';align-items:' + (m ? 'flex-end' : 'stretch'),
|
||||
A('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': title }, 'width:' + (m ? '100%' : 'min(' + (o.w || 960) + 'px,100%)') + ';' + (m ? 'height:92%;border-top:1px solid var(--ui-border)' : 'height:100%;border-left:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column',
|
||||
m && D('display:flex;justify-content:center;padding:8px 0 0;flex-shrink:0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
|
||||
D('padding:' + (m ? '12px 16px' : '16px 24px') + ';border-bottom:1px solid var(--ui-border);display:flex;gap:12px;align-items:flex-start;flex-shrink:0', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, title), sub && S(TO + ';text-wrap:pretty', sub)), Btn('', 'ghost', { ic: 'x', sm: true, al: 'Close' })),
|
||||
D('flex:1;min-height:0;overflow-y:auto;padding:' + (m ? '16px' : '16px 24px') + ';display:flex;flex-direction:column;gap:16px', ...body)))
|
||||
const seoPanel = () => {
|
||||
const unset = x === 'unset', slugD = x === 'editSlug'
|
||||
const rows = [['Page title', FRT + ' | Journal', 'Fallback: French title', 1], ['Description', 'Empty', 'No fallback. Empty. English site default not used. The tag is left out.', 0], ['Image alternative text', 'Un plan de salle de bain avec les mesures', 'Written per language.', 1]]
|
||||
let body
|
||||
if (x === 'preview') body = [Card(S('font-weight:600;' + HL, 'How it can appear in search'), D('display:flex;flex-direction:column;gap:2px;padding:12px;background:var(--ui-bg-elevated)', S(mono + ';' + MU + ';overflow-wrap:anywhere', 'journal.example.com › fr › comment-nous-chiffrons-une-renovation'), S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, FRT + ' | Journal'), S(TO, 'La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts.')), S(MU + ';font-size:12px', 'Shows the localized address and canonical. Description: Fallback: French excerpt.'))]
|
||||
else if (x === 'siteFallback') body = [S(TO, 'Site-level text is used as a fallback only in its own language.'), ...[['French version', 'Description', 'Fallback: French site description', 'Used: "Rénovation et conseils pour la maison" (site text in French)', 1], ['Arabic version', 'Description', 'No fallback', 'Empty. English site default not used.', 0]].map(r => Card(S('font-weight:600;' + HL, r[0]), Fd(r[1], 'Empty', { val: false, help: r[2] + '. ' + r[3] }), r[4] ? Bd('Uses French site text', 'info', 'languages') : Bd('Tag left out', 'neutral', 'minus')))]
|
||||
else body = [unset ? Alert('info', 'info', 'Public URL not configured', 'Add a public URL pattern for Journal to show page and canonical addresses. API links by article ID still work.', Btn('Open Public URL settings', 'secondary', { sm: true })) : Card(D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('Canonical', 'neutral', 'link'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation')), S(MU + ';font-size:12px', 'Built from the Public URL pattern https://journal.example.com/{lang}/{slug}. Each language points to itself unless you set a canonical override.')),
|
||||
!unset && Card(S('font-weight:600;' + HL, 'Alternate languages'), S(TO, 'Published versions in enabled languages only.'), D('display:flex;gap:10px;align-items:center', S(mono, 'EN'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/en/how-we-price-a-bathroom-renovation'), Bd('Published', 'success', 'circle-check')), D('display:flex;gap:10px;align-items:center', S(mono, 'DE'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/de/so-kalkulieren-wir-eine-badsanierung'), Bd('Published', 'success', 'circle-check')), S(MU + ';font-size:12px', 'Drafts and the disabled Spanish language are left out.')),
|
||||
Card(S('font-weight:600;' + HL, 'Social locale'), D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('og:locale', 'neutral', 'share-2'), x === 'noSocial' ? S(MU, 'Social locale not set') : S(mono + ';' + HL, 'Set: fr_FR')), S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', x === 'noSocial' ? 'French has no region in its code and no social locale is set for it, so og:locale and og:locale:alternate are left out. An owner can set one in Journal languages.' : 'Set by an owner for French in Journal languages. A region is never guessed from the language.')),
|
||||
Card(Fd('Address (slug)', 'comment-nous-chiffrons-une-renovation', { mono: true, ro: true, lock: true, help: 'Unique within Journal and French. Editing the title never changes the address.' }), D('display:flex;flex-direction:column;gap:6px;align-items:flex-start', Btn('Edit slug', 'secondary', { ic: 'pencil' }), S(MU + ';font-size:12px;line-height:18px', 'Editing the title never changes the slug. Only Edit slug does.'), S(HL + ';font-size:12px;line-height:18px', 'Links to the old address will stop working. RenewCMS does not create redirects.'))),
|
||||
...rows.map(r => Fd(r[0], r[1], { help: r[2], val: r[3] === 1 }))].filter(Boolean)
|
||||
const p = PanelShell('Search and sharing · French', 'Applies only to the French version. Page language: fr.', body)
|
||||
return slugD ? [p, Dlg({ z: 60, title: 'Edit slug for the French version', desc: 'The address changes for French only.' }, [Fd('Address (slug)', 'comment-nous-calculons-une-renovation', { mono: true, focus: true }), Alert('warning', 'triangle-alert', 'This version is published', 'Links to the old address will stop working. RenewCMS does not create redirects.'), S(MU + ';font-size:12px', 'Editing the title never changes the slug.')], [Btn('Cancel'), Btn('Change address', 'primary')])] : [p]
|
||||
}
|
||||
const mediaPanel = () => {
|
||||
const el = x === 'mediaElsewhere'
|
||||
const UR = (t, b2, link) => D('border-top:1px solid var(--ui-border);padding:10px 0;display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 40) + 'px', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:500;text-decoration:underline;text-underline-offset:3px;' + HL, t), b2 && S(MU + ';font-size:12px', b2)), link && S(mono + ';' + MU, link), Ic('arrow-up-right', 14))
|
||||
const SH = t => S('font:600 14px/22px \'IBM Plex Sans\',sans-serif;' + HL, t)
|
||||
const body = [D('height:160px;flex-shrink:0;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;' + MU + ';font:12px \'IBM Plex Mono\',monospace', 'image preview'), D('display:flex', Bd(el ? 'Used in 1' : 'Used in 2', 'neutral'))]
|
||||
if (el) body.push(D('display:flex;flex-direction:column;gap:6px', SH('Articles'), S(MU, 'No article shows it.')), D('display:flex;flex-direction:column;gap:6px', SH('Used elsewhere'), D('border-bottom:1px solid var(--ui-border)', UR('Author avatar', null, '/authors'))))
|
||||
else body.push(S('font-weight:500;' + HL, 'Used in 2 articles · 4 versions · 1 file'), S(MU + ';font-size:12px', 'A shared image is one file. It is counted once per article, and the versions that use it are listed.'), D('display:flex;flex-direction:column', UR('How we price a bathroom renovation', 'English, French, German'), UR('A guide to lime plaster', 'English')))
|
||||
return PanelShell(el ? 'window-frame.jpg' : 'bathroom-plan.jpg', 'Image details', body, { w: 420 })
|
||||
}
|
||||
const apiPage = () => Page('Integration · Languages', 'Proposed. Not available in the current API.', null, Alert('warning', 'triangle-alert', 'Proposed capability', 'These requests don\'t exist yet. Existing article-ID requests are unchanged.'),
|
||||
Card(S('font-weight:600;' + HL, '1. Exact lookup: site + language + slug'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/lang/{lang}/article/{slug}'), S(TO, 'Matches the published version with that slug in that language, or returns article_not_found. It never falls back, because a slug belongs to one language.'), S(mono + ';' + MU + ';overflow-wrap:anywhere', '{ "story_id": "st_01", "lang": "fr", "slug": "…", "alternates": [{ "lang": "en", "slug": "…" }] }')),
|
||||
Card(S('font-weight:600;' + HL, '2. Resolve by article group, with fallback'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/story/{story_id}?lang=fr&fallback=default'), S(TO, 'Use story_id from any response or from alternates[]. French published: lang "fr". French missing or unpublished with fallback=default: the published default, with "lang": "en", "requested_lang": "fr", "fallback": true. Without fallback=default, a missing French is language_not_available. No other language is tried, and drafts are never returned.')),
|
||||
Card(S('font-weight:600;' + HL, '3. Errors'), S(mono + ';' + HL, 'article_not_found · language_not_available · language_disabled'), S(TO, 'article_not_found: no published version has that slug in that language. language_not_available: the requested language and the allowed fallback are both unavailable. language_disabled: publishing in a disabled language.')),
|
||||
Card(S('font-weight:600;' + HL, 'Existing, unchanged'), S(mono + ';' + HL, 'GET /v1/article/{id}'), S(mono + ';' + HL, 'PATCH /v1/article/{id}/publish'), S(mono + ';' + HL, 'Editor: /article/{id}/edit'), S(TO, 'Each article ID is one language version. Article-ID links keep working.')))
|
||||
// ---- compose
|
||||
const apiToast = x === 'apiCopy' ? Toast('success', 'circle-check', 'API URL copied', '/v1/article/14?lang=fr') : null
|
||||
if (v === 'sl') { Object.assign(shell, setS, x === 'single' ? { crumbs: ['Settings', 'Sites', 'Archive', 'Languages'], mTitle: 'Archive · Languages' } : x === 'social' || x === 'socialEdit' || x === 'socialInvalid' ? { crumbs: ['Settings', 'Sites', 'Atelier', 'Languages'], mTitle: 'Atelier · Languages' } : {}); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', slPage()); ov = [].concat(slOv() || []) }
|
||||
if (v === 'sl') { Object.assign(shell, setS, x === 'single' ? { crumbs: ['Settings', 'Sites', 'Archive', 'Languages'], mTitle: 'Archive · Languages' } : x === 'social' || x === 'socialEdit' || x === 'socialInvalid' ? { crumbs: ['Settings', 'Sites', 'Atelier', 'Languages'], mTitle: 'Atelier · Languages' } : {}); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', slPage()); ov = [].concat(slOv() || []) }
|
||||
else if (v === 'pu') { Object.assign(shell, setS, { crumbs: ['Settings', 'Sites', 'Journal', 'Public URL'], mTitle: 'Journal · Public URL' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', puPage()); if (x === 'saved') ov = [Toast('success', 'circle-check', 'Public URL saved', 'Journal pages now use this pattern.')] }
|
||||
else if (v === 'su') { Object.assign(shell, setS, { crumbs: ['Settings', 'Sites', 'Journal', 'Languages', 'Setup'], mTitle: 'Set article languages' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column', suBody()) }
|
||||
else if (v === 'li') { Object.assign(shell, liS); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', liPage()); ov = listOv() }
|
||||
else if (v === 'na') { Object.assign(shell, liS); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', liPage()); ov = [naOv()] }
|
||||
else if (v === 'sf') { Object.assign(shell, { nav: x === 'home' || x === 'homeFr' ? 'home' : x === 'activity' ? 'activity' : x === 'media' ? 'media' : 'articles', crumbs: [x === 'home' || x === 'homeFr' ? 'Home' : x === 'activity' ? 'Activity' : x === 'media' ? 'Media' : 'Articles'], mTitle: 'Journal' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', sfPage()); if (apiToast) ov = [apiToast] }
|
||||
else if (v === 'seo' || v === 'api') {
|
||||
Object.assign(shell, edS)
|
||||
let mainEl
|
||||
if (v === 'seo') {
|
||||
const unset = x === 'unset', slugD = x === 'editSlug'
|
||||
const rows = [['Page title', FRT + ' | Journal', 'Fallback: French title', 1], ['Description', 'Empty', 'No fallback. Empty. English site default not used. The tag is left out.', 0], ['Image alternative text', 'Un plan de salle de bain avec les mesures', 'Written per language.', 1]]
|
||||
mainEl = Page('Search and sharing · French', 'Applies only to the French version. Page language: fr.', null,
|
||||
unset ? Alert('info', 'info', 'Public URL not configured', 'Add a public URL pattern for Journal to show page and canonical addresses. API links by article ID still work.', Btn('Open Public URL settings', 'secondary', { sm: true })) : Card(D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('Canonical', 'neutral', 'link'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation')), S(MU + ';font-size:12px', 'Built from the Public URL pattern https://journal.example.com/{lang}/{slug}. Each language points to itself unless you set a canonical override.')),
|
||||
!unset && Card(S('font-weight:600;' + HL, 'Alternate languages'), S(TO, 'Published versions in enabled languages only.'), D('display:flex;gap:10px;align-items:center', S(mono, 'EN'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/en/how-we-price-a-bathroom-renovation'), Bd('Published', 'success', 'circle-check')), D('display:flex;gap:10px;align-items:center', S(mono, 'DE'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/de/so-kalkulieren-wir-eine-badsanierung'), Bd('Published', 'success', 'circle-check')), S(MU + ';font-size:12px', 'Drafts and the disabled Spanish language are left out.')),
|
||||
Card(S('font-weight:600;' + HL, 'Social locale'), D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('og:locale', 'neutral', 'share-2'), x === 'noSocial' ? S(MU, 'Social locale not set') : S(mono + ';' + HL, 'Set: fr_FR')), S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', x === 'noSocial' ? 'French has no region in its code and no social locale is set for it, so og:locale and og:locale:alternate are left out. An owner can set one in Journal languages.' : 'Set by an owner for French in Journal languages. A region is never guessed from the language.')),
|
||||
Card(Fd('Address (slug)', 'comment-nous-chiffrons-une-renovation', { mono: true, ro: true, lock: true, help: 'Unique within Journal and French. Editing the title never changes the address.' }), D('display:flex;flex-direction:column;gap:6px;align-items:flex-start', Btn('Edit slug', 'secondary', { ic: 'pencil' }), S(MU + ';font-size:12px;line-height:18px', 'Editing the title never changes the slug. Only Edit slug does.'), S(HL + ';font-size:12px;line-height:18px', 'Links to the old address will stop working. RenewCMS does not create redirects.'))),
|
||||
...rows.map(r => Fd(r[0], r[1], { help: r[2], val: r[3] === 1 })))
|
||||
if (slugD) ov = [Dlg({ title: 'Edit slug for the French version', desc: 'The address changes for French only.' }, [Fd('Address (slug)', 'comment-nous-calculons-une-renovation', { mono: true, focus: true }), Alert('warning', 'triangle-alert', 'This version is published', 'Links to the old address will stop working. RenewCMS does not create redirects.'), S(MU + ';font-size:12px', 'Editing the title never changes the slug.')], [Btn('Cancel'), Btn('Change address', 'primary')])]
|
||||
} else mainEl = Page('Integration · Languages', 'Proposed. Not available in the current API.', null, Alert('warning', 'triangle-alert', 'Proposed capability', 'These requests don\'t exist yet. Existing article-ID requests are unchanged.'),
|
||||
Card(S('font-weight:600;' + HL, '1. Exact lookup: site + language + slug'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/lang/{lang}/article/{slug}'), S(TO, 'Matches the published version with that slug in that language, or returns article_not_found. It never falls back, because a slug belongs to one language.'), S(mono + ';' + MU + ';overflow-wrap:anywhere', '{ "story_id": "st_01", "lang": "fr", "slug": "…", "alternates": [{ "lang": "en", "slug": "…" }] }')),
|
||||
Card(S('font-weight:600;' + HL, '2. Resolve by article group, with fallback'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/story/{story_id}?lang=fr&fallback=default'), S(TO, 'Use story_id from any response or from alternates[]. French published: lang "fr". French missing or unpublished with fallback=default: the published default, with "lang": "en", "requested_lang": "fr", "fallback": true. Without fallback=default, a missing French is language_not_available. No other language is tried, and drafts are never returned.')),
|
||||
Card(S('font-weight:600;' + HL, '3. Errors'), S(mono + ';' + HL, 'article_not_found · language_not_available · language_disabled'), S(TO, 'article_not_found: no published version has that slug in that language. language_not_available: the requested language and the allowed fallback are both unavailable. language_disabled: publishing in a disabled language.')),
|
||||
Card(S('font-weight:600;' + HL, 'Existing, unchanged'), S(mono + ';' + HL, 'GET /v1/article/{id}'), S(mono + ';' + HL, 'PATCH /v1/article/{id}/publish'), S(mono + ';' + HL, 'Editor: /article/{id}/edit'), S(TO, 'Each article ID is one language version. Article-ID links keep working.')))
|
||||
main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', mainEl)
|
||||
else if (v === 'sf') { Object.assign(shell, { nav: x === 'home' || x === 'homeFr' ? 'home' : x === 'activity' ? 'activity' : x === 'media' || x === 'mediaElsewhere' ? 'media' : 'articles', crumbs: [x === 'home' || x === 'homeFr' ? 'Home' : x === 'activity' ? 'Activity' : x === 'media' || x === 'mediaElsewhere' ? 'Media' : 'Articles'], mTitle: 'Journal' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', sfPage()); if (x === 'media' || x === 'mediaElsewhere') ov = [mediaPanel()] }
|
||||
else if (v === 'api') {
|
||||
Object.assign(shell, { nav: 'integration', crumbs: ['Integration'], mTitle: 'Integration', mLead: 'menu' })
|
||||
main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', apiPage())
|
||||
} else {
|
||||
Object.assign(shell, edS)
|
||||
const rtl = x === 'rtl', en = x === 'navFail' || (v === 'ed' && ['cleanOnline', 'onlineDirty', 'unpublishDirty'].includes(x))
|
||||
const rtl = x === 'rtl', en = x === 'navFail' || (v === 'pd' && x === 'deleteDone') || (v === 'ed' && ['cleanOnline', 'onlineDirty', 'unpublishDirty'].includes(x))
|
||||
let L = rtl ? AR : FR
|
||||
const o = { cap: CAPS.full, pub: 'draft' }
|
||||
const bn = []
|
||||
@@ -413,7 +447,7 @@ class Component extends DCLogic {
|
||||
const pop = (rows, q = {}) => ovl.push(LangPop(rows, { left: 248, top: 176, ...q }))
|
||||
const lifeOnline = 'This version is online. Saving updates the live page right away.'
|
||||
if (v === 'ed') {
|
||||
if (x === 'menu') { o.open = 1; pop(MENUS.all) } else if (x === 'two') { o.open = 1; pop(MENUS.two) } else if (x === 'many') { o.open = 1; pop(MENUS.many, { find: 1, max: 520 }) }
|
||||
if (x === 'menu') { o.open = 1; pop(MENUS.all) } else if (x === 'two') { o.open = 1; pop(MENUS.two) } else if (x === 'many') { o.open = 1; pop(MENUS.many, { find: 1, max: 520 }) } else if (x === 'menuDisabled') { o.open = 1; pop(MENUS.dis, { note: 'Spanish is disabled on this site. Its version stays readable and editable. It can\'t be published until an owner re-enables Spanish.' }) }
|
||||
else if (x === 'dirty') { o.dirty = 1; o.cap = CAPS.p2 }
|
||||
else if (x === 'dirtyPublish') o.dirty = 1
|
||||
else if (x === 'cleanOnline') { o.pub = 'pub'; bn.push(Alert('neutral', 'info', lifeOnline)) }
|
||||
@@ -431,7 +465,7 @@ class Component extends DCLogic {
|
||||
}
|
||||
else if (v === 'pm') {
|
||||
if (x === 'restored') { o.dirty = 1; bn.push(Alert('info', 'info', 'Restored unsaved changes from 14:18', 'You logged in again. Your French title and body were kept in this browser.')) }
|
||||
else if (x === 'conflict' || x === 'conflict2' || x === 'download' || x === 'conflictRestored' || x === 'reloadConfirm') o.dirty = 1
|
||||
else if (x === 'session' || x === 'conflict' || x === 'conflict2' || x === 'download' || x === 'conflictRestored' || x === 'reloadConfirm') o.dirty = 1
|
||||
else if (x.startsWith('move') && x !== 'moveSelect' && x !== 'moveSelectHidden') { o.cap = { ...CAPS.full } }
|
||||
}
|
||||
else if (v === 'src') {
|
||||
@@ -447,8 +481,9 @@ class Component extends DCLogic {
|
||||
else if (x === 'inaccessible') { o.fresh = 'Source unavailable'; bn.push(Alert('warning', 'lock', 'The source isn\'t available to you right now', 'It may be restricted. Review history is kept. Mark as reviewed is off until you can read it again.', Btn('Choose a new source', 'ghost', { sm: true }))) }
|
||||
else { if (x === 'pub') o.pub = 'pub'; bn.push(D('display:flex;flex-direction:column;gap:8px', D('display:flex;gap:8px;align-items:center', Tracked('English')), Alert('warning', 'triangle-alert', 'English changed after French was reviewed', 'Changed fields: Title, Body. English was saved 14 Oct 2026, 09:12. French was last reviewed 12 Oct 2026.' + (x === 'pub' ? ' French is still published. Reviewing doesn\'t change publication.' : ''), buttons))) }
|
||||
ovl = ovl.concat(frOv() || [])
|
||||
} else if (v === 'pd') { o.pub = ['unpublish', 'unpublishDirty', 'done', 'deleteSource', 'deleteLast', 'deleteStoryOk', 'deleteStory'].includes(x) ? 'pub' : 'draft'; if (['saveAndPublish', 'saveFailPublish', 'unpublishDirty'].includes(x)) o.dirty = 1; if (x === 'saveFailPublish') o.saveFail = 1; if (x === 'publishDisabled') { o.pubDis = 1; bn.push(Alert('warning', 'triangle-alert', 'French is disabled on this site. Re-enable it to publish.', 'You can still edit and save this version. Ask an owner to re-enable French.')) } ovl = ovl.concat(pdOv() || []) }
|
||||
} else if (v === 'pd') { o.pub = ['unpublish', 'unpublishDirty', 'done', 'deleteSource', 'deleteLast', 'deleteStoryOk', 'deleteStoryFail', 'deleteDone', 'deleteStory'].includes(x) ? 'pub' : 'draft'; if (['saveAndPublish', 'saveFailPublish', 'unpublishDirty'].includes(x)) o.dirty = 1; if (x === 'saveFailPublish') { o.saveFail = 1; bn.push(Alert('error', 'circle-x', 'Couldn\'t save French', 'Your changes are kept here. Nothing was published. Try saving again.')) } if (x === 'done') { o.saved = 1; bn.push(Alert('neutral', 'info', lifeOnline)) } if (x === 'publishDisabled') { o.pubDis = 1; bn.push(Alert('warning', 'triangle-alert', 'French is disabled on this site. Re-enable it to publish.', 'You can still edit and save this version. Ask an owner to re-enable French.')) } ovl = ovl.concat(pdOv() || []) }
|
||||
else if (v === 'at') { o.pub = 'pub'; L = LS(x === 'dup' || x === 'dupHidden' ? S1 : S5); ovl = ovl.concat(atOv()) }
|
||||
else if (v === 'seo') ovl = ovl.concat(seoPanel())
|
||||
if (v === 'pm') ovl = ovl.concat(pmOv() || [])
|
||||
if (v === 'perm') {
|
||||
const cp = { ro: CAPS.ro, roMenu: CAPS.ro, p2: CAPS.p2, p3: CAPS.p3, p4: CAPS.full, p5: CAPS.full, p6: CAPS.p6, noAdd: CAPS.full, withAdd: CAPS.full }[x] || CAPS.full
|
||||
|
||||
Reference in new issue
Block a user