mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
feat(switcher): per-workspace device count in dropdown rows
/me's accessible_workspaces query gains a device_count field via a correlated subquery on workspaces.id - WHERE workspace_id = w.id strictly excludes the unclaimed pair-pool (workspace_id IS NULL fails equality). Added to both query branches (platform_admin LEFT JOIN and regular INNER JOIN); microseconds per row at current scale (~37 rows worst case), not optimizing. Frontend appends the count to the muted org-name line with a middle-dot separator: 'Acme Studios . 2 devices'. Singular/plural respected via the existing tn() helper convention; 'No devices' for empty workspaces. New formatResourceCount(n, keyBase, zeroKey) helper is generic so the same shape can wire users/playlists/schedules counts later without refactor. New i18n keys: switcher.devices_count_one, switcher.devices_count_other, switcher.no_devices. Added to en.js only; other locales fall back to en via the existing lookup chain (verified in i18n.js:19). API smoke verified: switcher-test sees Studio A=2, Field Crew=2; dw5304 (platform_admin) sees all 37 workspaces with their device counts varying 0-4; single-workspace zero-device user (geoff.case) sees 0.
This commit is contained in:
parent
42966da973
commit
ce332ead67
|
|
@ -1,5 +1,18 @@
|
|||
import { api } from '../api.js';
|
||||
import { showToast } from './toast.js';
|
||||
import { t, tn } from '../i18n.js';
|
||||
|
||||
// Reusable resource-count formatter. Returns localized "1 device" / "N devices"
|
||||
// / "No devices" based on n. Generic so the same shape can wire users /
|
||||
// playlists / schedules counts later without refactor - caller supplies the
|
||||
// i18n key bases.
|
||||
// keyBase: e.g. 'switcher.devices_count' (looks up _one / _other variants via tn)
|
||||
// zeroKey: e.g. 'switcher.no_devices' (direct lookup for n === 0)
|
||||
function formatResourceCount(n, keyBase, zeroKey) {
|
||||
if (n === undefined || n === null) return '';
|
||||
if (n === 0) return t(zeroKey);
|
||||
return tn(keyBase, n);
|
||||
}
|
||||
|
||||
// Render the workspace switcher inside #workspaceSwitcher based on the
|
||||
// /api/auth/me response. Three modes:
|
||||
|
|
@ -37,14 +50,21 @@ export function renderWorkspaceSwitcher(me) {
|
|||
</svg>
|
||||
</button>
|
||||
<div class="workspace-switcher-menu" role="listbox">
|
||||
${sorted.map(w => `
|
||||
${sorted.map(w => {
|
||||
const countStr = formatResourceCount(w.device_count, 'switcher.devices_count', 'switcher.no_devices');
|
||||
const orgName = w.organization_name || '';
|
||||
const subtitle = orgName && countStr ? esc(orgName) + ' · ' + esc(countStr)
|
||||
: orgName ? esc(orgName)
|
||||
: countStr ? esc(countStr)
|
||||
: '';
|
||||
return `
|
||||
<div class="workspace-switcher-item ${w.id === currentId ? 'current' : ''}" data-workspace-id="${esc(w.id)}" role="option">
|
||||
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" style="${w.id === currentId ? '' : 'visibility:hidden'}">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
<div class="ws-meta">
|
||||
<div class="ws-name">${esc(w.name)}</div>
|
||||
<div class="ws-org">${esc(w.organization_name || '')}</div>
|
||||
<div class="ws-org">${subtitle}</div>
|
||||
</div>
|
||||
${w.can_admin ? `
|
||||
<button class="workspace-switcher-pencil" type="button" data-rename-id="${esc(w.id)}" aria-label="Rename workspace" title="Rename">
|
||||
|
|
@ -54,7 +74,8 @@ export function renderWorkspaceSwitcher(me) {
|
|||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1105,4 +1105,10 @@ export default {
|
|||
'add_display.windows': 'Windows',
|
||||
'add_display.smart_tv_note': 'Smart TVs (LG/Samsung): open the built-in browser and navigate to <code style="background:var(--bg-input,#0f172a);padding:1px 4px;border-radius:3px">/player</code>',
|
||||
'add_display.pair_btn': 'Pair Display',
|
||||
|
||||
// Workspace switcher (Phase 3 MVP). devices_count is the only count exposed
|
||||
// today; matching pattern for users/playlists/etc. when those land later.
|
||||
'switcher.devices_count_one': '1 device',
|
||||
'switcher.devices_count_other': '{n} devices',
|
||||
'switcher.no_devices': 'No devices',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -305,11 +305,16 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
|||
// UI can render admin affordances (rename pencil etc.) only where the
|
||||
// caller has permission. The server still enforces permission on the
|
||||
// actual mutation routes regardless of this advisory flag.
|
||||
// device_count: correlated subquery on workspaces.id. Equality fails on NULL
|
||||
// so unclaimed pair-pool devices (workspace_id IS NULL) are correctly excluded.
|
||||
// Microseconds per row at current scale (~37 rows worst case for platform_admin);
|
||||
// not optimizing - revisit if the admin list grows past a few hundred workspaces.
|
||||
const isPlatformAdmin = req.user.role === 'platform_admin' || req.user.role === 'superadmin';
|
||||
const accessible = isPlatformAdmin
|
||||
? db.prepare(`
|
||||
SELECT w.id, w.name, w.organization_id, o.name AS organization_name,
|
||||
wm.role AS workspace_role, om.role AS org_role
|
||||
wm.role AS workspace_role, om.role AS org_role,
|
||||
(SELECT COUNT(*) FROM devices WHERE workspace_id = w.id) AS device_count
|
||||
FROM workspaces w
|
||||
JOIN organizations o ON o.id = w.organization_id
|
||||
LEFT JOIN workspace_members wm ON wm.workspace_id = w.id AND wm.user_id = ?
|
||||
|
|
@ -318,7 +323,8 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
|||
`).all(req.user.id, req.user.id)
|
||||
: db.prepare(`
|
||||
SELECT w.id, w.name, w.organization_id, o.name AS organization_name,
|
||||
wm.role AS workspace_role, om.role AS org_role
|
||||
wm.role AS workspace_role, om.role AS org_role,
|
||||
(SELECT COUNT(*) FROM devices WHERE workspace_id = w.id) AS device_count
|
||||
FROM workspace_members wm
|
||||
JOIN workspaces w ON w.id = wm.workspace_id
|
||||
JOIN organizations o ON o.id = w.organization_id
|
||||
|
|
|
|||
Loading…
Reference in a new issue