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
7 changes: 7 additions & 0 deletions api/src/routes/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ const ICON_CACHE_TTL = 24 * 60 * 60 * 1000;
on('GET', '/api/icons/search', async (req, res) => {
const q = (new URL(req.url, 'http://x').searchParams.get('q') || '').trim();
if (!q) return json(res, 200, { results: [] });
if (IS_DEMO) {
return json(res, 403, {
error: 'Outbound requests are disabled in demo mode',
kind: KIND.BLOCKED,
code: 'blocked.demo',
});
}
try {
json(res, 200, { results: await searchIcons(q) });
} catch (e) {
Expand Down
15 changes: 15 additions & 0 deletions api/test/demo-routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,18 @@ test('no demo write reached the disk', () => {
assert.equal(fs.existsSync(process.env.CONFIG_PATH), false);
assert.deepEqual(fs.readdirSync(process.env.ICONS_PATH), []);
});

test('icon search answers the demo refusal instead of failing on the blocked catalogues', async () => {
const r = await new Promise((resolve, reject) => {
http
.get(`${base}/api/icons/search?q=plex`, res => {
let raw = '';
res.on('data', c => (raw += c));
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(raw) }));
})
.on('error', reject);
});
assert.equal(r.status, 403);
assert.equal(r.body.code, 'blocked.demo');
assert.equal(r.body.kind, 'blocked');
});
2 changes: 1 addition & 1 deletion ui/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,6 @@ <h1 class="sec-title" id="ev-title">General</h1>
</div></div>
</div>

<script type="module" src="/js/admin.js?v=cbe9f554"></script>
<script type="module" src="/js/admin.js?v=4b6830bd"></script>
</body>
</html>
1 change: 1 addition & 0 deletions ui/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"iconNoMatch": "Kein Symbol für „{q}“ gefunden",
"iconTryUpload": "Lade ein eigenes hoch oder lasse das Feld leer, um den ersten Buchstaben zu verwenden.",
"iconSearchFailed": "Symbolkataloge sind nicht erreichbar",
"iconSearchDemo": "In der Live-Demo ist die Symbolsuche deaktiviert.",
"iconFrom": "von {source}",
"iconResults": "Symbolvorschläge",
"iconResultCount_one": "{count} Symbol gefunden",
Expand Down
1 change: 1 addition & 0 deletions ui/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"iconNoMatch": "No icon found for “{q}”",
"iconTryUpload": "Upload your own, or leave it empty to use the first letter.",
"iconSearchFailed": "Icon catalogues could not be reached",
"iconSearchDemo": "Icon search is turned off in the live demo.",
"iconFrom": "from {source}",
"iconResults": "Icon suggestions",
"iconResultCount_one": "{count} icon found",
Expand Down
1 change: 1 addition & 0 deletions ui/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
"iconNoMatch": "No se encontró ningún icono para «{q}»",
"iconTryUpload": "Sube el tuyo o déjalo vacío para usar la primera letra.",
"iconSearchFailed": "No se pudo acceder a los catálogos de iconos",
"iconSearchDemo": "La búsqueda de iconos está desactivada en la demo en vivo.",
"iconFrom": "de {source}",
"iconResults": "Sugerencias de iconos",
"iconResultCount_one": "{count} icono encontrado",
Expand Down
1 change: 1 addition & 0 deletions ui/i18n/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"iconNoMatch": "آیکونی برای «{q}» پیدا نشد",
"iconTryUpload": "آیکون خود را بارگذاری کنید، یا خالی بگذارید تا حرف اول به کار برود.",
"iconSearchFailed": "دسترسی به فهرست‌های آیکون ممکن نشد",
"iconSearchDemo": "جستجوی آیکون در نسخه نمایشی آنلاین غیرفعال است.",
"iconFrom": "از {source}",
"iconResults": "پیشنهادهای آیکون",
"iconResultCount_one": "{count} آیکون پیدا شد",
Expand Down
1 change: 1 addition & 0 deletions ui/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
"iconNoMatch": "Aucune icône trouvée pour « {q} »",
"iconTryUpload": "Téléversez la vôtre, ou laissez vide pour utiliser la première lettre.",
"iconSearchFailed": "Les catalogues d’icônes sont inaccessibles",
"iconSearchDemo": "La recherche d’icônes est désactivée dans la démo en ligne.",
"iconFrom": "de {source}",
"iconResults": "Suggestions d’icônes",
"iconResultCount_one": "{count} icône trouvée",
Expand Down
1 change: 1 addition & 0 deletions ui/i18n/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
"iconNoMatch": "未找到“{q}”的图标",
"iconTryUpload": "可上传自定义图标,或留空以使用首字母。",
"iconSearchFailed": "无法访问图标目录",
"iconSearchDemo": "在线演示中已关闭图标搜索。",
"iconFrom": "来自 {source}",
"iconResults": "图标建议",
"iconResultCount_other": "找到 {count} 个图标",
Expand Down
8 changes: 5 additions & 3 deletions ui/js/admin-app-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,12 +674,14 @@ function wireIcon() {
const d = await apiGet(`/api/icons/search?q=${encodeURIComponent(q)}`);
if (mine !== run) return;
showIPRes(d.results || [], v);
} catch {
} catch (e) {
if (mine !== run) return;
/* No catalogue to confirm against, so the typed name is all that is left. */
updPrev();
ipMessage(t('app.iconSearchFailed'), t('app.iconTryUpload'));
ipSay(t('app.iconSearchFailed'));
const demo = /** @type {{ code?: string }} */ (e)?.code === 'blocked.demo';
const why = t(demo ? 'app.iconSearchDemo' : 'app.iconSearchFailed');
ipMessage(why, demo ? '' : t('app.iconTryUpload'));
ipSay(why);
}
};
inp.oninput = () => {
Expand Down
2 changes: 1 addition & 1 deletion ui/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=4853370b';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=622c5ce8';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=5246ff39';
import { recoveryShown } from '/js/config-recovery.js?v=85138697';
import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=61d89772';
Expand Down
7 changes: 5 additions & 2 deletions ui/test/icon-picker.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ test('the count and the empty result are announced', () => {
assert.match(form, /role="status" aria-live="polite"/, 'nothing is announced');
assert.match(form, /ipSay\(t\('app\.iconResultCount'/);
assert.match(form, /ipSay\(t\('app\.iconNoMatch'/);
assert.match(form, /ipSay\(t\('app\.iconSearchFailed'/);
assert.match(form, /const why = t\(demo \? 'app\.iconSearchDemo' : 'app\.iconSearchFailed'\)/);
assert.match(form, /ipSay\(why\)/, 'a failed search is not announced');
assert.match(form, /aria-describedby="ip-status"/, 'the input is not tied to the status line');
});

Expand Down Expand Up @@ -172,6 +173,7 @@ test('every string the picker shows is translated', () => {
'iconNoMatch',
'iconTryUpload',
'iconSearchFailed',
'iconSearchDemo',
'iconFrom',
'iconResults',
'iconVariant',
Expand Down Expand Up @@ -246,7 +248,8 @@ test('the tile follows the result that matches what was typed', () => {
to try, or the field would go quiet whenever the catalogues are down. */
test('a search that fails still previews the typed name', () => {
const search = form.slice(form.indexOf('const search = async'), form.indexOf('inp.oninput'));
const fail = search.slice(search.indexOf('} catch {'));
const fail = search.slice(search.indexOf('} catch (e) {'));
assert.ok(fail.length > 1, 'the failure branch is gone');
assert.match(fail, /updPrev\(\)/);
});

Expand Down
Loading