mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
* feat(widgets): bulk import for the directory board (JSON / CSV / TSV / text)
Adds an "Import from JSON / CSV" button to the directory-board editor. Paste JSON
(the { company, tenantsByFloor, advertisements, backgroundImages } shape plus
categories[]/floors[]/flat-array/bare-floor-map variants), a CSV/TSV/pipe/semicolon
table (with or without a header — vacant/yes/1 => available, quoted fields), or a
sectioned "room name" text list, and it auto-fills title, footer, floors->categories,
rooms/names/details/availability, and background-image URLs. "Replace / append" toggle.
Tolerant key matching (room/suite/unit/id, name/tenant/company, details/subtitle, …);
warns on things it can't use (bare-filename background images, headerless columns).
parseDirectoryImport is pure and was unit-tested in node across every format.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(widgets): directory board — logo replaces title, and images load on the player
Two on-screen bugs on the directory board:
1. A logo did not remove the title text — both rendered, stacking the wordmark over
the name. renderDirectoryBoard (and the directory-search header) now gate the title
h1 behind !logoSrc, so a logo replaces the title. New render test guards it.
2. Logo + background images did not show on the player (NS_ERROR_DOM_CORP_FAILED,
0 bytes). The player embeds widgets in a sandbox="allow-scripts" (opaque-origin)
iframe, so /api/content image requests are cross-origin, and the helmet default
Cross-Origin-Resource-Policy: same-origin blocks them. Set CORP: cross-origin (+
ACAO:*) on the content file + thumbnail routes, matching the existing /uploads/content
static route. Content already serves publicly, so no new exposure. Verified in a real
sandboxed iframe: same-origin blocks, cross-origin loads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
44 lines
2.3 KiB
JavaScript
44 lines
2.3 KiB
JavaScript
'use strict';
|
|
|
|
// Guards the directory-board header behaviour: a logo REPLACES the title text
|
|
// (showing both stacked the wordmark over the name). Renders the public widget
|
|
// endpoint and inspects the emitted board script. Mirrors widget-render-xss.test.js.
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const Database = require('better-sqlite3');
|
|
|
|
process.env.JWT_SECRET = 'test-secret-dir-board';
|
|
|
|
const db = new Database(':memory:');
|
|
db.exec(`CREATE TABLE widgets (id TEXT PRIMARY KEY, widget_type TEXT, config TEXT, workspace_id TEXT);`);
|
|
const dbModulePath = require.resolve('../db/database');
|
|
require.cache[dbModulePath] = { id: dbModulePath, filename: dbModulePath, loaded: true, exports: { db } };
|
|
|
|
const express = require('express');
|
|
const widgetsRouter = require('../routes/widgets');
|
|
const app = express();
|
|
app.use('/api/widgets', widgetsRouter);
|
|
const server = app.listen(0);
|
|
let base;
|
|
test.before(async () => { await new Promise(r => server.listening ? r() : server.once('listening', r)); base = `http://127.0.0.1:${server.address().port}`; });
|
|
test.after(() => { server.close(); db.close(); });
|
|
|
|
const seed = (id, config) => db.prepare('INSERT INTO widgets (id, widget_type, config, workspace_id) VALUES (?,?,?,?)').run(id, 'directory-board', JSON.stringify(config), 'ws1');
|
|
const render = async (id) => (await fetch(`${base}/api/widgets/${id}/render`)).text();
|
|
|
|
test('directory board: title text is gated behind !logoSrc (logo replaces title)', async () => {
|
|
seed('b1', { title: 'LINNcinnati', logo_url: '/api/content/abc/file', categories: [] });
|
|
const html = await render('b1');
|
|
// The title h1 must only be appended when there is no logo.
|
|
assert.match(html, /if \(cfg\.title && !logoSrc\)/, 'title render must be guarded by !logoSrc');
|
|
assert.doesNotMatch(html, /if \(cfg\.title\) \{\s*\n\s*var h1/, 'title must not be rendered unconditionally');
|
|
});
|
|
|
|
test('directory board: still embeds title + logo config for the client', async () => {
|
|
seed('b2', { title: 'Lincoln Warehouse', logo_url: '/api/content/xyz/file', categories: [] });
|
|
const html = await render('b2');
|
|
assert.match(html, /Lincoln Warehouse/, 'title present in embedded config');
|
|
assert.match(html, /\/api\/content\/xyz\/file/, 'logo url present in embedded config');
|
|
});
|