Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 71 additions & 76 deletions public/js/admin-app-form.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=970a91b0';
import { clr as rc, el, inp as inpById, q as qSel, qa, qi, tgt } from '/js/utils.js?v=ada0c382';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=04e7796e';
import { state } from '/js/admin-state.js?v=7d68e98e';
import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c8c550c';
import { state } from '/js/admin-state.js?v=5a5d655f';
import {
isDockBlocked,
DOCK_MAX,
Expand All @@ -10,20 +10,21 @@ import {
failureIsMissingApiPath,
nextActiveIndex,
sameIconName,
} from '/js/admin-logic.js?v=74cb4272';
} from '/js/admin-logic.js?v=69e57d35';
import { t } from '/js/i18n.js?v=e644a5c5';
import {
toast,
ag,
ap,
PE_SVG,
CHEV_SVG,
initInlineEdit,
reveal,
setTogDisabled,
wireChecklist,
} from '/js/admin-shared.js?v=0f36d0dc';
swapContent,
} from '/js/admin-shared.js?v=ca64cc9c';
import { createListbox } from '/js/listbox.js?v=3e267705';
import { MAX_LABELS } from '/js/badge-logic.js?v=b3c8b6c2';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=d162431d';
import { renderColorControl, BADGE_SWATCHES, BADGE_DEFAULT } from '/js/admin-color-control.js?v=cfb3b3e9';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=10f3cdb1';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';

Expand All @@ -37,13 +38,6 @@ export function buildFolderForm(body, item) {
}
});

const opts = apps.length
? apps.map(
a =>
html`<li role="option" data-val="${a.id}" aria-selected="${children.includes(a.id) ? 'true' : 'false'}">${a.label || a.id}</li>`,
)
: html`<li class="row-dd-empty" aria-disabled="true">${t('folder.noApps')}</li>`;

setHtml(
body,
html`
Expand All @@ -54,44 +48,35 @@ export function buildFolderForm(body, item) {
<input id="f-fname" type="text" value="${item?.label || ''}" class="d-none">
<button class="pe" type="button">${raw(PE_SVG)}</button>
</div>
<div class="row">
<div class="row" id="folder-apps-row">
<span class="rl">${t('folder.addApps')}</span>
<div class="row-dd" id="folder-apps-dd">
<button class="row-dd-btn" id="folder-apps-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
<span id="folder-apps-label">${t('folder.selectApps')}</span>
${raw(CHEV_SVG)}
</button>
<ul class="row-dd-list checklist" id="folder-apps-list" role="listbox" aria-multiselectable="true" aria-label="${t('folder.appsInFolder')}" hidden>${opts}</ul>
</div>
</div>
</div>
<p class="grp-tip">${t('folder.tip')}</p>`,
);

initInlineEdit('ie-fname', 'f-fname', { placeholder: t('folder.namePh') });
_wireFolderApps();
}

function _wireFolderApps() {
const dd = el('folder-apps-dd');
const btn = inpById('folder-apps-btn');
const list = el('folder-apps-list');
const label = el('folder-apps-label');
if (!dd || !btn || !list || !label) return;
const sync = () => {
const sel = qa('li[aria-selected="true"]', list);
label.textContent =
_wireFolderApps(apps, children);
}

function _wireFolderApps(apps, children) {
const row = el('folder-apps-row');
if (!row) return;
const box = createListbox({
label: t('folder.appsInFolder'),
multiple: true,
options: apps.map(a => ({ value: a.id, label: a.label || a.id })),
value: children,
emptyText: t('folder.noApps'),
summary: sel =>
sel.length === 0
? t('folder.selectApps')
: sel.length === 1
? sel[0].textContent
: t('widgetCfg.selectedCount', { count: sel.length });
};
wireChecklist(dd, btn, list, li => {
li.setAttribute('aria-selected', li.getAttribute('aria-selected') === 'true' ? 'false' : 'true');
sync();
? sel[0].label
: t('widgetCfg.selectedCount', { count: sel.length }),
});
sync();
state._folderApps = box;
row.appendChild(box.el);
}

export function buildAppForm(body, item) {
Expand Down Expand Up @@ -152,7 +137,7 @@ export function buildAppForm(body, item) {
<p class="grp-hdr">${t('app.badge')}</p>
<div class="grp">
<div class="row"><span class="rl">${t('app.healthCheck')}</span>${tog('hc-en', hc.enabled, t('app.healthCheck'))}</div>
<div id="hc-sub" ${hc.enabled ? '' : 'hidden'}>
<div id="hc-sub" class="reveal${hc.enabled ? ' open' : ''}"><div class="reveal-in">
<div class="row"><span class="rl">${t('app.type')}</span><div class="segr">
<label class="segr-opt"><input type="radio" name="hc-type" id="hc-type-con" ${isPing ? '' : 'checked'}><span class="segr-dot"></span><span>${t('app.container')}</span></label>
<label class="segr-opt"><input type="radio" name="hc-type" id="hc-type-ping" ${isPing ? 'checked' : ''}><span class="segr-dot"></span><span>${t('app.ping')}</span></label>
Expand All @@ -162,34 +147,34 @@ export function buildAppForm(body, item) {
${ier('ie-hc-ping', t('app.pingUrl'), 'hc-ping', hc.pingUrl, t('app.pingUrlPh'), 'url')}
<div class="row"><span class="rl"></span><span id="hc-ping-status" class="row-status"></span><button type="button" class="row-btn" id="hc-ping-test">${t('app.test')}</button></div>
</div>
</div>
</div></div>
</div>
${globalHealthOn ? '' : html`<p class="grp-tip" id="hc-off-tip">${t('app.healthGlobalOff')}</p>`}

<div class="grp">
<div class="row"><span class="rl">${t('app.fixedLabel')}</span>${tog('static-en', hasStatic, t('app.fixedLabel'))}</div>
<div id="static-sub" ${hasStatic ? '' : 'hidden'}>
<div id="static-sub" class="reveal${hasStatic ? ' open' : ''}"><div class="reveal-in">
${ier('ie-static-label', t('app.labelText'), 'f-static-label', staticBadge.label, t('app.labelPh'))}
<div id="static-color-slot"></div>
</div>
</div></div>
</div>

<div class="grp">
<div class="row"><span class="rl">${t('app.liveActivity')}</span>${tog('act-en', act.enabled, t('app.liveActivity'))}</div>
<div id="act-sub" ${act.enabled ? '' : 'hidden'}>
<div id="act-sub" class="reveal${act.enabled ? ' open' : ''}"><div class="reveal-in">
${ier('ie-burl', t('app.apiUrl'), 'f-burl', act.url, t('app.apiUrlPh'), 'url')}
<div class="row"><span class="rl"></span><span id="bst" class="row-status"></span><button type="button" class="row-btn" id="bfetch">${t('app.fetch')}</button></div>
<div id="auth-row-wrap">
<div class="row"><span class="rl">${t('app.authentication')}</span>${tog('auth-en', !!(act.params || act.headers), t('app.authentication'))}</div>
<div id="auth-sub" ${act.params?.length || act.headers?.length ? '' : 'hidden'}>
<div id="auth-sub" class="reveal${act.params?.length || act.headers?.length ? ' open' : ''}"><div class="reveal-in">
<div class="row kv-hdr"><span class="rl">${t('app.addToUrl')} <span class="rl-sub">(query params)</span></span></div>
<div id="bpar-rows" class="kv-rows"></div>
<div class="row kv-hdr"><span class="rl">${t('app.addToHeader')}</span></div>
<div id="bhdr-rows" class="kv-rows"></div>
</div>
</div></div>
</div>
<div id="poll-row"><div class="row"><span class="rl">${t('app.poll')}</span><div class="poll-inline">${pollBefore}<input id="f-bint" type="number" min="10" max="3600" value="${act.interval || 30}" aria-label="${t('app.poll')}">${pollAfter}</div></div></div>
</div>
</div></div>
</div>
<div id="act-labels-wrap" class="bprow-hidden">
<div id="act-labels"></div>
Expand Down Expand Up @@ -252,16 +237,20 @@ export function buildAppForm(body, item) {
const hcEn = el('hc-en');
const showHide = (id, on) => {
const node = el(id);
if (node) node.hidden = !on;
if (!node) return;
if (node.classList.contains('reveal')) reveal(node, on);
else node.hidden = !on;
};
hcEn?.addEventListener('change', e => {
showHide('hc-sub', tgt(e).checked);
});
document.querySelectorAll('input[name="hc-type"]').forEach(r =>
r.addEventListener('change', () => {
const ping = inpById('hc-type-ping')?.checked;
showHide('hc-con-row', !ping);
showHide('hc-ping-row', ping);
swapContent(el('hc-con-row')?.parentElement, () => {
showHide('hc-con-row', !ping);
showHide('hc-ping-row', ping);
});
}),
);
el('hc-ping-test')?.addEventListener('click', testPing);
Expand Down Expand Up @@ -351,19 +340,19 @@ function actValueOptions() {
return out;
}

function _valueSelect(idx, path) {
function _valueSelectOptions() {
const opts = actValueOptions();
const groups = [
{ name: t('app.values'), rows: opts.filter(o => !o.computed) },
{ name: t('app.computedFromArray'), rows: opts.filter(o => o.computed) },
].filter(g => g.rows.length);
const optionOf = o =>
html`<option value="${o.path}" ${o.path === path ? 'selected' : ''}>${o.value == null ? o.label : `${o.label} — ${o.value}`}</option>`;
const body =
groups.length > 1
? groups.map(g => html`<optgroup label="${g.name}">${g.rows.map(optionOf)}</optgroup>`)
: opts.map(optionOf);
return html`<div class="row"><span class="rl">${t('app.value')}</span><div class="sel-wrap"><select class="row-sel" id="albl-path-${idx}" aria-label="${t('app.value')}">${body}</select>${raw(CHEV_SVG)}</div></div>`;
const entryOf = o => ({ value: o.path, label: o.value == null ? o.label : `${o.label} \u2014 ${o.value}` });
if (groups.length < 2) return opts.map(entryOf);
return groups.flatMap(g => [{ group: g.name }, ...g.rows.map(entryOf)]);
}

function _valueSelect(idx) {
return html`<div class="row" id="albl-path-row-${idx}"><span class="rl">${t('app.value')}</span></div>`;
}

function renderActLabels(host) {
Expand Down Expand Up @@ -405,7 +394,7 @@ function renderActLabels(host) {
card.className = 'grp';
setHtml(
card,
html`${_valueSelect(i, path)}
html`${_valueSelect(i)}
${_ieRow(`ie-albl-name-${i}`, _optRow(t('app.labelText')), `albl-name-${i}`, l.name, t('app.labelPh'))}
<div id="albl-col-slot-${i}"></div>
${_ieRow(`ie-albl-unit-${i}`, _optRow(t('app.unit')), `albl-unit-${i}`, l.unit, t('app.unitPh'))}
Expand All @@ -421,20 +410,26 @@ function renderActLabels(host) {
swatchColors: BADGE_SWATCHES,
label: t('common.color'),
});
const sel = inpById(`albl-path-${i}`);
if (sel) {
sel.onchange = () => {
captureActLabels();
const next = sel.value;
if (state.spaths.includes(next)) {
sel.value = state.spaths[i];
toast(t('app.labelValueTaken'), 'err');
return;
}
repathActLabel(state.spaths[i], next);
state.spaths[i] = next;
syncActMode();
};
const pathRow = el(`albl-path-row-${i}`);
if (pathRow) {
const box = createListbox({
id: `albl-path-${i}`,
label: t('app.value'),
options: _valueSelectOptions(),
value: path,
onChange: next => {
captureActLabels();
if (state.spaths.includes(String(next))) {
box.setValue(state.spaths[i]);
toast(t('app.labelValueTaken'), 'err');
return;
}
repathActLabel(state.spaths[i], String(next));
state.spaths[i] = String(next);
syncActMode();
},
});
pathRow.appendChild(box.el);
}
});
wireActLabelDrag(host);
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ag, ap } from '/js/admin-shared.js?v=0f36d0dc';
import { ag, ap } from '/js/admin-shared.js?v=ca64cc9c';
import { t } from '/js/i18n.js?v=e644a5c5';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { el, inp as inpById, qa } from '/js/utils.js?v=970a91b0';
import { el, inp as inpById, qa } from '/js/utils.js?v=ada0c382';

export async function checkAuth(onLogin) {
try {
Expand Down
17 changes: 11 additions & 6 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=0f36d0dc';
import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=ca64cc9c';
import { t } from '/js/i18n.js?v=e644a5c5';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=970a91b0';
import { qa, q } from '/js/utils.js?v=ada0c382';

const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'];
export const BADGE_DEFAULT = '#1e6ef4';
Expand Down Expand Up @@ -134,15 +134,17 @@ export function renderColorControl(
${swatchColors.map(swatch)}`;
const wrap = document.createElement('div');
const slider = (label, cls, id, max, val, lo, hi) => html`
<div class="row hsb-row cc-tune"><span class="rl">${label}</span><div class="hsb-track"><span class="hsb-ico">${raw(lo)}</span><input type="range" class="${cls}" id="${id}" min="0" max="${max}" value="${val}" aria-label="${label}"><span class="hsb-ico">${raw(hi)}</span></div></div>`;
<div class="row hsb-row"><span class="rl">${label}</span><div class="hsb-track"><span class="hsb-ico">${raw(lo)}</span><input type="range" class="${cls}" id="${id}" min="0" max="${max}" value="${val}" aria-label="${label}"><span class="hsb-ico">${raw(hi)}</span></div></div>`;
setHtml(
wrap,
html`
<div class="row cc-row"><span class="rl">${label}</span><div class="cc-sw">${swatches}</div></div>
<div class="row-wrap reveal cc-tune"><div class="reveal-in">
${slider('Hue', 'hsb-range hsb-hue', `${idPrefix}-h`, 360, init.h, _ccIco.hueLo, _ccIco.hueHi)}
${slider('Saturation', 'hsb-range', `${idPrefix}-s`, 100, init.s, _ccIco.satLo, _ccIco.satHi)}
${slider('Brightness', 'hsb-range', `${idPrefix}-v`, 100, init.v, _ccIco.brLo, _ccIco.brHi)}
<div class="row ie-row cc-tune" id="${idPrefix}-code-row"><span class="rl">${t('appearance.colorCode')}</span><span class="rv is-ph">#rrggbb or any CSS color</span><input id="${idPrefix}-hex" type="text" class="d-none"><button class="pe" type="button">${raw(PE_SVG)}</button></div>`,
<div class="row ie-row" id="${idPrefix}-code-row"><span class="rl">${t('appearance.colorCode')}</span><span class="rv is-ph">#rrggbb or any CSS color</span><input id="${idPrefix}-hex" type="text" class="d-none"><button class="pe" type="button">${raw(PE_SVG)}</button></div>
</div></div>`,
);
/* The markup carries no style attribute, so the page keeps style-src without
'unsafe-inline'. A swatch paints itself from its own value. */
Expand All @@ -157,13 +159,15 @@ export function renderColorControl(
sEl = qLocal(`#${idPrefix}-s`),
vEl = qLocal(`#${idPrefix}-v`);
const codeRv = qLocal(`#${idPrefix}-code-row .rv`);
const tune = rows.filter(r => r.classList.contains('cc-tune'));
const tune = rows.find(r => r.classList.contains('cc-tune'));
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.id = `${idPrefix}-val`;
container.appendChild(hidden);
let mode = isSem(value) ? value : 'color';
let showTune = false;
/* The first paint restores stored state, so it must not animate. */
let _painted = false;
/* The value as it was stored. The sliders are integers, so reading a colour
back out of them shifts it, and an untouched control would save a colour
nobody picked. Cleared the moment the user changes anything. */
Expand Down Expand Up @@ -200,13 +204,14 @@ export function renderColorControl(
});
const rb = q('.cc-rainbow', container);
if (rb) rb.classList.toggle('on', mode === 'color' && showTune);
tune.forEach(r => r.classList.toggle('d-none', !showTune));
reveal(tune, showTune, !_painted);
if (!codeRv.closest('.editing')) {
codeRv.textContent =
mode === 'color' ? hex : t(mode === 'dark' ? 'appearance.displayDark' : 'appearance.displayLight');
codeRv.classList.remove('is-ph');
}
hidden.value = mode === 'color' ? (pristine ?? hex) : mode;
_painted = true;
}
const commit = () => {
pristine = null;
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
it is near, and which classes say so. */

import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=7d68e98e';
import { state } from '/js/admin-state.js?v=5a5d655f';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { qa } from '/js/utils.js?v=970a91b0';
import { qa } from '/js/utils.js?v=ada0c382';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
12 changes: 6 additions & 6 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
The filter and the collapsed folders are in admin-state.js, because the page
reads and writes them too. */

import { collapsedFolders, filter, state } from '/js/admin-state.js?v=7d68e98e';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=5a5d655f';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { reorderItems } from '/js/admin-logic.js?v=74cb4272';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=3ffdf185';
import { paintIcon } from '/js/admin-shared.js?v=0f36d0dc';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=970a91b0';
import { reorderItems } from '/js/admin-logic.js?v=69e57d35';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=e514303e';
import { paintIcon } from '/js/admin-shared.js?v=ca64cc9c';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=ada0c382';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { t } from '/js/i18n.js?v=e644a5c5';
import { sizeLabel } from '/js/admin-widget-form.js?v=0cb044d0';
import { sizeLabel } from '/js/admin-widget-form.js?v=20f4cf98';
import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947';

/** @type {{ openModal: (idx: number|null) => void,
Expand Down
14 changes: 14 additions & 0 deletions public/js/admin-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,17 @@ export function failureIsMissingApiPath(url, advice) {
if (advice && advice.sessionExpired) return false;
return isBareHostUrl(url);
}

/** Compares what `read` returns now with what it returned at the last reset.

@param {() => string} read
@returns {{ dirty: () => boolean, reset: () => void }} */
export function createDirtyTracker(read) {
let base = read();
return {
dirty: () => read() !== base,
reset: () => {
base = read();
},
};
}
Loading