thatcher 1.0.44 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -30
- package/package.json +1 -1
- package/src/adapters/google-auth.js +0 -7
- package/src/adapters/google-drive.js +1 -3
- package/src/adapters/google-gmail.js +1 -0
- package/src/app/api/[entity]/[[...path]]/route.js +1 -1
- package/src/app/api/audit/logs/route.js +1 -1
- package/src/app/api/audit/permissions/route.js +0 -1
- package/src/app/api/audit/route.js +2 -4
- package/src/app/api/auth/google/callback/route.js +1 -1
- package/src/app/api/auth/google/route.js +1 -1
- package/src/app/api/auth/logout/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/route.js +0 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/app/api/health/route.js +1 -1
- package/src/cli.js +9 -4
- package/src/config/spec-helpers.js +0 -1
- package/src/engine.server.js +1 -1
- package/src/lib/alert-manager.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/business-rules-engine.js +3 -3
- package/src/lib/busybase/adapter.js +2 -2
- package/src/lib/busybase/store.js +1 -1
- package/src/lib/config-generator-engine.js +0 -2
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +2 -2
- package/src/lib/csrf-protection.js +0 -1
- package/src/lib/debug-registry.js +1 -1
- package/src/lib/errors/recovery.js +0 -8
- package/src/lib/errors/wrap.js +1 -1
- package/src/lib/events-engine.js +1 -1
- package/src/lib/field-iterator.js +2 -2
- package/src/lib/field-registry.js +2 -1
- package/src/lib/hot-reload/index.js +2 -1
- package/src/lib/minifier.js +1 -1
- package/src/lib/next-shim.js +2 -2
- package/src/lib/perf.js +0 -2
- package/src/lib/request-trace.js +1 -4
- package/src/lib/route-resolver.js +1 -1
- package/src/lib/stage-pipeline.js +0 -1
- package/src/lib/static-server.js +1 -1
- package/src/lib/tracing.js +0 -3
- package/src/lib/validation/security-validators.js +2 -2
- package/src/lib/validation-middleware.js +1 -1
- package/src/lib/workflow-engine.js +1 -1
- package/src/lib/xstate-workflow-engine.js +1 -1
- package/src/server/server.js +5 -9
- package/src/services/collaborator-role.service.js +1 -3
- package/src/services/notification-engine.js +3 -3
- package/src/services/permission.service.js +1 -1
- package/src/ui/advanced-widgets.js +0 -3
- package/src/ui/auth-pages.js +1 -1
- package/src/ui/common-handlers.js +2 -2
- package/src/ui/component-engine.js +2 -2
- package/src/ui/dashboard-renderer.js +1 -1
- package/src/ui/dialog-engine.js +11 -9
- package/src/ui/engagement-cards.js +0 -1
- package/src/ui/engagement-dialogs.js +0 -2
- package/src/ui/entity-renderer.js +2 -3
- package/src/ui/event-delegation.js +1 -1
- package/src/ui/file-dialogs.js +0 -2
- package/src/ui/job-management-renderer.js +0 -1
- package/src/ui/layout.js +4 -4
- package/src/ui/page-handler-admin.js +2 -2
- package/src/ui/page-handler-helpers.js +3 -3
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +1 -1
- package/src/ui/permissions-ui.js +0 -12
- package/src/ui/picker-dialogs.js +0 -1
- package/src/ui/review/comparison.js +1 -1
- package/src/ui/review/detail-renderer.js +1 -1
- package/src/ui/review/mwr.js +1 -1
- package/src/ui/review/widgets.js +0 -1
- package/src/ui/rfi-detail-renderer.js +1 -1
- package/src/ui/settings/home.js +1 -1
- package/src/ui/settings/review.js +0 -1
- package/src/ui/settings/teams.js +1 -1
- package/src/ui/spacing-system.js +1 -1
|
@@ -27,7 +27,7 @@ export function resolveSpecificParams(routeFile, pathParts) {
|
|
|
27
27
|
for (let i = 0; i < routeSegments.length && i < urlSegments.length; i++) {
|
|
28
28
|
const seg = routeSegments[i];
|
|
29
29
|
if (seg.startsWith('[') && seg.endsWith(']')) {
|
|
30
|
-
const paramName = seg.replace(/^\[\.\.\./, '').replace(/[
|
|
30
|
+
const paramName = seg.replace(/^\[\.\.\./, '').replace(/[[\]]/g, '');
|
|
31
31
|
result[paramName] = urlSegments[i];
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -21,7 +21,6 @@ export function createStagePipeline(options = {}) {
|
|
|
21
21
|
stages.forEach((stage, idx) => {
|
|
22
22
|
const isComplete = idx < currentStageIndex;
|
|
23
23
|
const isCurrent = idx === currentStageIndex;
|
|
24
|
-
const isUpcoming = idx > currentStageIndex;
|
|
25
24
|
|
|
26
25
|
const stageItem = document.createElement('div');
|
|
27
26
|
stageItem.className = `stage-item stage-${isComplete ? 'complete' : isCurrent ? 'current' : 'upcoming'}`;
|
package/src/lib/static-server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const ROOT = path.join(__dirname, '../..');
|
|
7
7
|
|
|
8
|
-
export function serveStatic(pathname, req, res, compress, getCacheHeaders,
|
|
8
|
+
export function serveStatic(pathname, req, res, compress, getCacheHeaders, _loadModule) {
|
|
9
9
|
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
10
10
|
|
|
11
11
|
if (pathname === '/favicon.ico') {
|
package/src/lib/tracing.js
CHANGED
|
@@ -12,7 +12,7 @@ const XSS_PATTERNS = [
|
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
const SQL_INJECTION_PATTERNS = [
|
|
15
|
-
/('|(
|
|
15
|
+
/('|(--)|(;)|(\|\|)|(\/\*)|(\*\/)|xp_)/gi,
|
|
16
16
|
/(union|select|insert|update|delete|drop|create|alter|exec|execute)\s/gi,
|
|
17
17
|
/0x[0-9a-f]+/gi,
|
|
18
18
|
/char\(/gi,
|
|
@@ -20,7 +20,7 @@ const SQL_INJECTION_PATTERNS = [
|
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
const PATH_TRAVERSAL_PATTERNS = [
|
|
23
|
-
/\.\.[
|
|
23
|
+
/\.\.[\\/]/g,
|
|
24
24
|
/\.\.%/g,
|
|
25
25
|
/%2e%2e/gi,
|
|
26
26
|
/\.\.\./g
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { validateEntity, validateUpdate, hasErrors } from '@/lib/validation/entity-validators';
|
|
11
11
|
|
|
12
12
|
export async function validateRequest(request, options = {}) {
|
|
13
|
-
const { requireCSRF = true, rateLimit = { max: 100, window: 60000 }
|
|
13
|
+
const { requireCSRF = true, rateLimit = { max: 100, window: 60000 } } = options;
|
|
14
14
|
|
|
15
15
|
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
|
16
16
|
const sessionId = request.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] || ip;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// does NOT currently supersede this file; if anything the reverse holds (this file
|
|
19
19
|
// is live, that one is dormant/debug-only). Re-check import sites before treating
|
|
20
20
|
// either as the "old" one.
|
|
21
|
-
import { get
|
|
21
|
+
import { get } from './busybase/store.js';
|
|
22
22
|
import { getConfigEngineSync } from './config-generator-engine.js';
|
|
23
23
|
import { executeHook } from './hook-engine.js';
|
|
24
24
|
import { AppError } from './errors/index.js';
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// src/lib/events-engine.js and re-exported by src/index.js's Thatcher class).
|
|
15
15
|
// This file does NOT supersede workflow-engine.js; treat it as a dormant/
|
|
16
16
|
// debug-only alternate implementation until a real call site adopts it.
|
|
17
|
-
import { createMachine, createActor, assign
|
|
17
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
18
18
|
import { getConfigEngineSync } from './config-generator-engine.js';
|
|
19
19
|
import { hookEngine } from './hook-engine.js';
|
|
20
20
|
import { AppError } from './errors/index.js';
|
package/src/server/server.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createLogger } from '../lib/logger.js';
|
|
6
|
-
import * as thatcherLib from '../index.js';
|
|
7
6
|
|
|
8
7
|
const log = createLogger('[Server]');
|
|
9
8
|
const pageLog = createLogger('[Page]');
|
|
@@ -13,8 +12,7 @@ const apiLog = createLogger('[API]');
|
|
|
13
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
13
|
|
|
15
14
|
export function createServer(options) {
|
|
16
|
-
const { thatcher,
|
|
17
|
-
const PORT = port || 3000;
|
|
15
|
+
const { thatcher, configEngine } = options;
|
|
18
16
|
const SERVER_START = Date.now();
|
|
19
17
|
|
|
20
18
|
let systemInitialized = false;
|
|
@@ -50,7 +48,6 @@ export function createServer(options) {
|
|
|
50
48
|
};
|
|
51
49
|
|
|
52
50
|
const server = http.createServer(async (req, res) => {
|
|
53
|
-
const t0 = Date.now();
|
|
54
51
|
globalThis.__debug__.activeRequests.count++;
|
|
55
52
|
|
|
56
53
|
try {
|
|
@@ -98,7 +95,7 @@ export function createServer(options) {
|
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
// Static file serving (simplified)
|
|
101
|
-
if (serveStaticFile(pathname, req, res
|
|
98
|
+
if (serveStaticFile(pathname, req, res)) {
|
|
102
99
|
return;
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -135,7 +132,7 @@ export function createServer(options) {
|
|
|
135
132
|
return server;
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
async function serveStaticFile(pathname, req, res
|
|
135
|
+
async function serveStaticFile(pathname, req, res) {
|
|
139
136
|
if (pathname === '/' || pathname === '/index.html') {
|
|
140
137
|
// Could serve SPA
|
|
141
138
|
return false;
|
|
@@ -220,9 +217,8 @@ async function handleGenericCrud(req, res, entity, id, action, thatcher, configE
|
|
|
220
217
|
const { getConfigEngineSync } = await import('../lib/config-generator-engine.js');
|
|
221
218
|
configEngine = getConfigEngineSync();
|
|
222
219
|
}
|
|
223
|
-
let spec;
|
|
224
220
|
try {
|
|
225
|
-
|
|
221
|
+
configEngine.generateEntitySpec(entity);
|
|
226
222
|
} catch (e) {
|
|
227
223
|
res.writeHead(404);
|
|
228
224
|
res.end(JSON.stringify({ error: `Entity '${entity}' not found` }));
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// Extracted from moonlanding/src/services/collaborator-role.service.js
|
|
2
2
|
|
|
3
3
|
import { list, get, update, create } from '../lib/busybase/store.js';
|
|
4
|
-
import { AppError } from '../lib/errors/index.js';
|
|
5
|
-
import { HTTP } from '../config/constants.js';
|
|
6
4
|
|
|
7
5
|
const COLLABORATOR_ROLE_PERMISSIONS = {
|
|
8
6
|
viewer: ['view', 'view_highlights', 'view_pdfs'],
|
|
@@ -78,7 +76,7 @@ export function getCollaboratorRolePermissions(roleType) {
|
|
|
78
76
|
return COLLABORATOR_ROLE_PERMISSIONS[roleType] || [];
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
export function canAssignRole(userRole,
|
|
79
|
+
export function canAssignRole(userRole, _targetRoleType) {
|
|
82
80
|
return ['partner', 'manager'].includes(userRole);
|
|
83
81
|
}
|
|
84
82
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendEmail } from './email-sender.js';
|
|
2
2
|
import { getConfigEngineSync } from '../lib/config-generator-engine.js';
|
|
3
3
|
import { executeHook } from '../lib/hook-engine.js';
|
|
4
4
|
import { createLogger } from '../lib/logger.js';
|
|
@@ -65,7 +65,7 @@ export async function sendNotification(type, userId, context = {}, options = {})
|
|
|
65
65
|
return notification;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export async function markNotificationRead(notificationId,
|
|
68
|
+
export async function markNotificationRead(notificationId, _userId) {
|
|
69
69
|
const { update } = await import('../lib/busybase/store.js');
|
|
70
70
|
return update('notification', notificationId, {
|
|
71
71
|
read_at: Math.floor(Date.now() / 1000),
|
|
@@ -125,7 +125,7 @@ async function resolveRecipients(recipients, context) {
|
|
|
125
125
|
return [];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function templateNameForType(
|
|
128
|
+
function templateNameForType(_type) {
|
|
129
129
|
// events-engine.js passes descriptive template names (e.g.
|
|
130
130
|
// 'engagement_info_gathering'); the built-in getTemplates() set in
|
|
131
131
|
// email-sender.js is generic ('notification', 'invitation'), so an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Adapted from moonlanding/src/services/permission.service.js
|
|
2
2
|
|
|
3
|
-
import { getCollaboratorRole
|
|
3
|
+
import { getCollaboratorRole } from '../services/collaborator-role.service.js';
|
|
4
4
|
import { PermissionError } from '../lib/errors/index.js';
|
|
5
5
|
import { getConfigEngineSync } from '../lib/config-generator-engine.js';
|
|
6
6
|
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { h } from '@/ui/webjsx.js'
|
|
2
|
-
import { TOAST_SCRIPT } from '@/ui/render-helpers.js'
|
|
3
|
-
|
|
4
1
|
export function dataGridAdvanced(config) {
|
|
5
2
|
const { columns = [], data = [], groupBy = null, sortable = true, filterable = true, expandable = false, detailRenderer = null } = config
|
|
6
3
|
const gridId = 'dg-' + Math.random().toString(36).slice(2, 8)
|
package/src/ui/auth-pages.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateHtml } from '@/ui/layout.js'
|
|
2
2
|
import { nav as navFn } from '@/ui/layout.js'
|
|
3
|
-
import { AUTH_CSS, GOOGLE_SVG, WARN_SVG,
|
|
3
|
+
import { AUTH_CSS, GOOGLE_SVG, WARN_SVG, authLogoHtml } from '@/ui/auth-styles.js'
|
|
4
4
|
import { esc } from '@/ui/render-helpers.js'
|
|
5
5
|
|
|
6
6
|
export function renderLogin(error = null, hasGoogleAuth = false) {
|
|
@@ -122,8 +122,8 @@ export const commonHandlers = {
|
|
|
122
122
|
|
|
123
123
|
notify: {
|
|
124
124
|
show(message, type = 'info') {
|
|
125
|
-
if (typeof showToast === 'function') {
|
|
126
|
-
showToast(message, type);
|
|
125
|
+
if (typeof window.showToast === 'function') {
|
|
126
|
+
window.showToast(message, type);
|
|
127
127
|
}
|
|
128
128
|
},
|
|
129
129
|
success(message) { this.show(message, 'success'); },
|
|
@@ -32,7 +32,7 @@ export function renderComponent(componentId, props = {}) {
|
|
|
32
32
|
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
33
33
|
if (value && typeof value === 'object') {
|
|
34
34
|
if (Array.isArray(value)) {
|
|
35
|
-
html = html.replace(regex, value.map(v => renderItem(v
|
|
35
|
+
html = html.replace(regex, value.map(v => renderItem(v)).join(''));
|
|
36
36
|
} else {
|
|
37
37
|
html = html.replace(regex, esc(JSON.stringify(value)));
|
|
38
38
|
}
|
|
@@ -49,7 +49,7 @@ export function renderComponent(componentId, props = {}) {
|
|
|
49
49
|
return `<${element}${cssClass}>${html}</${element}>`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function renderItem(item
|
|
52
|
+
function renderItem(item) {
|
|
53
53
|
if (typeof item === 'string') return esc(item);
|
|
54
54
|
if (typeof item === 'object' && item.name && item.color) {
|
|
55
55
|
const bg = safeColor(item.color);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js'
|
|
2
2
|
import { getQuickActions } from '@/ui/permissions-ui.js'
|
|
3
3
|
import { esc, stagePill, statusPill, TABLE_SCRIPT, emptyRow } from '@/ui/render-helpers.js'
|
|
4
|
-
import { SPACING
|
|
4
|
+
import { SPACING } from '@/ui/spacing-system.js'
|
|
5
5
|
|
|
6
6
|
export function renderDashboard(user, stats = {}) {
|
|
7
7
|
const isClerk = user?.role === 'clerk';
|
package/src/ui/dialog-engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getConfigEngineSync } from '@/lib/config-generator-engine';
|
|
2
2
|
import { aria, role } from '@/lib/accessibility';
|
|
3
3
|
|
|
4
|
-
export function renderDialog(dialogId,
|
|
4
|
+
export function renderDialog(dialogId, _context = {}) {
|
|
5
5
|
const config = getConfigEngineSync().getConfig();
|
|
6
6
|
const dialogConfig = config.dialogs?.[dialogId];
|
|
7
7
|
|
|
@@ -10,15 +10,15 @@ export function renderDialog(dialogId, context = {}) {
|
|
|
10
10
|
return '';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const { title, fields = [], actions = [], width = '640px'
|
|
14
|
-
const dialogHtml = generateDialogHtml(dialogId, title, fields, actions, width
|
|
13
|
+
const { title, fields = [], actions = [], width = '640px' } = dialogConfig;
|
|
14
|
+
const dialogHtml = generateDialogHtml(dialogId, title, fields, actions, width);
|
|
15
15
|
|
|
16
16
|
return dialogHtml;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function generateDialogHtml(dialogId, title, fields, actions, width
|
|
19
|
+
function generateDialogHtml(dialogId, title, fields, actions, width) {
|
|
20
20
|
const titleId = `${dialogId}-title`;
|
|
21
|
-
const fieldHtml = fields.map(f => generateFieldHtml(f, dialogId
|
|
21
|
+
const fieldHtml = fields.map(f => generateFieldHtml(f, dialogId)).join('');
|
|
22
22
|
const actionButtons = (actions || []).map(a => {
|
|
23
23
|
if (a.type === 'cancel') {
|
|
24
24
|
return `<button type="button" class="btn btn-ghost btn-sm" ${aria.label('Cancel dialog')} data-action="closeDialog" data-args='["${dialogId}"]'>${a.label || 'Cancel'}</button>`;
|
|
@@ -66,8 +66,8 @@ function generateDialogHtml(dialogId, title, fields, actions, width, context, on
|
|
|
66
66
|
`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function generateFieldHtml(field, dialogId
|
|
70
|
-
const { name, label, type, required, placeholder, options
|
|
69
|
+
function generateFieldHtml(field, dialogId) {
|
|
70
|
+
const { name, label, type, required, placeholder, options } = field;
|
|
71
71
|
const fieldId = `${dialogId}-${name}`;
|
|
72
72
|
const requiredAttr = required ? 'required' : '';
|
|
73
73
|
const ariaRequired = required ? aria.required() : '';
|
|
@@ -92,21 +92,23 @@ function generateFieldHtml(field, dialogId, context) {
|
|
|
92
92
|
case 'date':
|
|
93
93
|
inputHtml = `<input type="date" id="${fieldId}" name="${name}" class="input input-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}/>`;
|
|
94
94
|
break;
|
|
95
|
-
case 'select':
|
|
95
|
+
case 'select': {
|
|
96
96
|
const optionsHtml = (options || []).map(opt =>
|
|
97
97
|
`<option value="${opt.value}">${escapeHtml(opt.label || opt.value)}</option>`
|
|
98
98
|
).join('');
|
|
99
99
|
inputHtml = `<select id="${fieldId}" name="${name}" class="select select-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}><option value="">Select...</option>${optionsHtml}</select>`;
|
|
100
100
|
break;
|
|
101
|
+
}
|
|
101
102
|
case 'checkbox':
|
|
102
103
|
inputHtml = `<label class="flex items-center gap-2"><input type="checkbox" id="${fieldId}" name="${name}" class="checkbox" ${requiredAttr} ${ariaRequired} ${ariaDesc}/><span class="text-sm">${label}</span></label>`;
|
|
103
104
|
break;
|
|
104
|
-
case 'multi-select':
|
|
105
|
+
case 'multi-select': {
|
|
105
106
|
const multiOptHtml = (options || []).map(opt =>
|
|
106
107
|
`<label class="flex items-center gap-2"><input type="checkbox" name="${name}" value="${opt.value}" class="checkbox"/><span class="text-sm">${escapeHtml(opt.label || opt.value)}</span></label>`
|
|
107
108
|
).join('');
|
|
108
109
|
inputHtml = `<div class="flex flex-col gap-2" ${role.list}>${multiOptHtml}</div>`;
|
|
109
110
|
break;
|
|
111
|
+
}
|
|
110
112
|
case 'file':
|
|
111
113
|
inputHtml = `<input type="file" id="${fieldId}" name="${name}" class="file-input file-input-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}/>`;
|
|
112
114
|
break;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { statusLabel } from '@/ui/renderer.js';
|
|
2
1
|
import { createDialog } from '@/ui/dialog-factory.js';
|
|
3
|
-
import { esc } from '@/ui/render-helpers.js';
|
|
4
2
|
|
|
5
3
|
export function engagementFileSearchDialog(engagementId) {
|
|
6
4
|
const body = `<div class="modal-form-group"><label for="efs-query" class="sr-only">Search files</label><input type="text" id="efs-query" class="input input-bordered w-full" placeholder="Search files..." aria-label="Search engagement files" oninput="efsSearch()"/></div><div id="efs-results" class="flex flex-col gap-2" style="max-height:400px;overflow:auto"></div>`;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { page, confirmDialog, dataTable } from '@/ui/layout.js'
|
|
1
|
+
import { page, dataTable } from '@/ui/layout.js'
|
|
3
2
|
import { fmtVal, TOAST_SCRIPT, esc } from '@/ui/render-helpers.js'
|
|
4
3
|
import { canCreate, canEdit, canDelete } from '@/ui/permissions-ui.js'
|
|
5
4
|
|
|
@@ -7,7 +6,7 @@ export function renderEntityList(entityName, items, spec, user, options = {}) {
|
|
|
7
6
|
const label = spec?.labelPlural || spec?.label || entityName
|
|
8
7
|
const fields = spec?.fields || {}
|
|
9
8
|
const { groupBy = null } = options
|
|
10
|
-
let listFields = Object.entries(fields).filter(([
|
|
9
|
+
let listFields = Object.entries(fields).filter(([, f]) => f.list).slice(0, 5)
|
|
11
10
|
if (!listFields.length) listFields = Object.entries(fields).filter(([k]) => !['created_by', 'updated_by'].includes(k)).slice(0, 6)
|
|
12
11
|
if (!listFields.length && items.length > 0) listFields = Object.keys(items[0]).filter(k => !['created_by', 'updated_by'].includes(k)).slice(0, 5).map(k => [k, { label: k }])
|
|
13
12
|
const headers = listFields.map(([k, f]) => `<th>${esc(f?.label || k)}</th>`).join('') + '<th>Actions</th>'
|
|
@@ -260,7 +260,7 @@ const eventDelegation = {
|
|
|
260
260
|
|
|
261
261
|
navigate(path) { window.location = path; },
|
|
262
262
|
|
|
263
|
-
toggle(elementId,
|
|
263
|
+
toggle(elementId, _attr = 'checked') {
|
|
264
264
|
const el = document.getElementById(elementId);
|
|
265
265
|
if (el?.type === 'checkbox') el.checked = !el.checked;
|
|
266
266
|
else el.classList.toggle('active');
|
package/src/ui/file-dialogs.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { statusLabel } from '@/ui/renderer.js';
|
|
2
|
-
|
|
3
1
|
// Inline HTML-escape for client-script innerHTML sinks (file names are DB-sourced).
|
|
4
2
|
const FD_ESC = `function fdEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}`;
|
|
5
3
|
|
package/src/ui/layout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
/* eslint-disable no-useless-escape -- this file embeds raw client <script> text inside a template literal; ESLint parses embedded regex escapes like \d as literal template text and misflags them */
|
|
2
|
+
import { isPartner, isClerk } from '@/ui/permissions-ui.js'
|
|
3
|
+
import { AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
|
|
4
4
|
import { FETCH_JSON_SCRIPT } from '@/ui/fetch-json.js'
|
|
5
5
|
import { aria, role, skipLink, liveRegion } from '@/lib/accessibility'
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ function withAppName(title) {
|
|
|
16
16
|
return base === appName ? base : `${base} | ${appName}`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function generateHtml(title, bodyContent, scripts = []
|
|
19
|
+
export function generateHtml(title, bodyContent, scripts = []) {
|
|
20
20
|
title = withAppName(title)
|
|
21
21
|
const scriptTags = scripts.map(s =>
|
|
22
22
|
typeof s === 'string' ? `<script>${s.replace(/<\/script/gi, '<\\/script').replace(/<!--/g, '<\\!--')}</script>` : `<script type="module" src="${s.src}"></script>`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { list, get } from '@/lib/busybase/store.js';
|
|
2
|
-
import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml
|
|
2
|
+
import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml } from '@/ui/renderer.js';
|
|
3
3
|
import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers } from '@/ui/settings/home.js';
|
|
4
|
-
import {
|
|
4
|
+
import { renderSettingsRfiSections } from '@/ui/settings/teams.js';
|
|
5
5
|
import { renderSettingsTemplates, renderSettingsChecklists, renderSettingsEntityTypes, renderSettingsEngagementTypes, renderSettingsTemplateManage } from '@/ui/settings/templates.js';
|
|
6
6
|
import { renderSettingsNotifications, renderSettingsIntegrations, renderSettingsRecreation, renderSettingsReviewSettings, renderSettingsFileReview, renderSettingsMwrPermissions } from '@/ui/settings/review.js';
|
|
7
7
|
import { renderChecklistsManagement } from '@/ui/checklist-renderer.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { list,
|
|
1
|
+
import { list, count } from '@/lib/busybase/store.js';
|
|
2
2
|
import { createLogger } from '@/lib/logger.js';
|
|
3
3
|
|
|
4
4
|
const log = createLogger('[PageHandlerHelpers]');
|
|
@@ -19,7 +19,7 @@ export function resolveEnumOptions(spec) {
|
|
|
19
19
|
|
|
20
20
|
export async function getRefOptions(spec) {
|
|
21
21
|
const refOptions = {};
|
|
22
|
-
for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([
|
|
22
|
+
for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([, f]) => f.type === 'ref' && f.ref)) {
|
|
23
23
|
try { refOptions[fieldKey] = (await list(field.ref, {})).map(r => ({ value: r.id, label: r.name || r.title || r.label || r.email || r.id })); }
|
|
24
24
|
catch { refOptions[fieldKey] = []; }
|
|
25
25
|
}
|
|
@@ -31,7 +31,7 @@ export async function getRefOptions(spec) {
|
|
|
31
31
|
// `_display` key exists (falling back to the raw id) without issuing any query.
|
|
32
32
|
export function resolveRefFields(items, spec) {
|
|
33
33
|
try {
|
|
34
|
-
const refFields = Object.entries(spec.fields || {}).filter(([
|
|
34
|
+
const refFields = Object.entries(spec.fields || {}).filter(([, f]) => f.type === 'ref' && f.ref);
|
|
35
35
|
if (!refFields.length) return items;
|
|
36
36
|
return items.map(item => {
|
|
37
37
|
const resolved = { ...item };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { list, get } from '@/lib/busybase/store.js';
|
|
2
|
-
import { canView, canList, isClerk
|
|
2
|
+
import { canView, canList, isClerk } from '@/ui/permissions-ui.js';
|
|
3
3
|
import { renderAccessDenied } from '@/ui/renderer.js';
|
|
4
4
|
import { renderRfiList } from '@/ui/rfi-list-renderer.js';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
package/src/ui/page-handler.js
CHANGED
|
@@ -2,7 +2,7 @@ import { getUser, setCurrentRequest } from '@/engine.server.js';
|
|
|
2
2
|
import { hasGoogleAuth } from '@/config/env.js';
|
|
3
3
|
import { getSpec } from '@/config/spec-helpers.js';
|
|
4
4
|
import { list, get } from '@/lib/busybase/store.js';
|
|
5
|
-
import { renderLogin, renderDashboard,
|
|
5
|
+
import { renderLogin, renderDashboard, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
|
|
6
6
|
import { renderClientDashboard, renderClientList } from '@/ui/client-renderer.js';
|
|
7
7
|
import { canList, canView, canCreate, canEdit, isPartner, isClerk, isClientUser, canClientAccessEntity } from '@/ui/permissions-ui.js';
|
|
8
8
|
import { renderEngagementGrid } from '@/ui/engagement-grid-renderer.js';
|
package/src/ui/permissions-ui.js
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import { getConfigEngineSync } from '@/lib/config-generator-engine.js';
|
|
2
2
|
|
|
3
|
-
function getRoleHierarchy() {
|
|
4
|
-
const config = getConfigEngineSync();
|
|
5
|
-
const roles = config.getRoles();
|
|
6
|
-
const hierarchy = {};
|
|
7
|
-
let level = 0;
|
|
8
|
-
for (const [roleName] of Object.entries(roles)) {
|
|
9
|
-
hierarchy[roleName] = level++;
|
|
10
|
-
}
|
|
11
|
-
return hierarchy;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
3
|
function getEntityPermissions() {
|
|
15
4
|
const config = getConfigEngineSync();
|
|
16
5
|
const entityNames = config.getAllEntities();
|
|
@@ -40,7 +29,6 @@ function getEntityPermissions() {
|
|
|
40
29
|
|
|
41
30
|
export function canAccess(user, entity, action) {
|
|
42
31
|
if (!user?.role) return false;
|
|
43
|
-
const config = getConfigEngineSync();
|
|
44
32
|
const allPermissions = getEntityPermissions();
|
|
45
33
|
const permissions = allPermissions[entity];
|
|
46
34
|
|
package/src/ui/picker-dialogs.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { h } from '@/ui/webjsx.js'
|
|
2
1
|
import { STAGE_COLORS, TOAST_SCRIPT } from '@/ui/render-helpers.js'
|
|
3
2
|
|
|
4
3
|
const DIALOG_COLORS = ['#B0B0B0','#44BBA4','#FF4141','#7F7EFF','#3b82f6','#f59e0b','#ec4899','#8b5cf6','#ef4444','#22c55e','#06b6d4','#f97316','#84cc16','#e11d48','#14b8a6','#6366f1']
|
|
@@ -2,7 +2,7 @@ import { statusLabel } from '@/ui/renderer.js';
|
|
|
2
2
|
import { page } from '@/ui/layout.js';
|
|
3
3
|
import { emptyRow, esc } from '../render-helpers.js';
|
|
4
4
|
|
|
5
|
-
function reviewSummaryPanel(review, highlights,
|
|
5
|
+
function reviewSummaryPanel(review, highlights, _side) {
|
|
6
6
|
const total = highlights.length;
|
|
7
7
|
const resolved = highlights.filter(h => h.status === 'resolved').length;
|
|
8
8
|
const sts = review.status ? statusLabel(review.status) : '';
|
|
@@ -2,7 +2,7 @@ import { page } from '@/ui/layout.js';
|
|
|
2
2
|
import { reviewZoneNav } from '@/ui/review/zone-nav.js';
|
|
3
3
|
export { reviewZoneNav };
|
|
4
4
|
import { canEdit } from '@/ui/permissions-ui.js';
|
|
5
|
-
import { esc, statusBadge,
|
|
5
|
+
import { esc, statusBadge, icon } from '@/ui/render-helpers.js';
|
|
6
6
|
import { reviewDetailScript } from '@/ui/review/detail-script.js';
|
|
7
7
|
import { highlightRow, collaboratorRow, addCollaboratorDialog } from '@/ui/review/detail-panels.js';
|
|
8
8
|
import { tenderPanelHtml, tenderDialog, linksPanelHtml, linkDialog, sectionsPanelHtml, compareDialogHtml } from '@/ui/review-detail-panels-extra.js';
|
package/src/ui/review/mwr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js';
|
|
2
2
|
import { reviewZoneNav } from '@/ui/review/zone-nav.js';
|
|
3
|
-
import { SPACING, renderCard, renderButton,
|
|
3
|
+
import { SPACING, renderCard, renderButton, renderEmptyState, renderStatsRow, renderPageHeader } from '@/ui/spacing-system.js';
|
|
4
4
|
import { icon, esc } from '@/ui/format-helpers.js';
|
|
5
5
|
import { TOAST_SCRIPT } from '@/ui/render-helpers.js';
|
|
6
6
|
|
package/src/ui/review/widgets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js';
|
|
2
2
|
import { canEdit, isPartner, isManager, isClientUser } from '@/ui/permissions-ui.js';
|
|
3
|
-
import { esc, statusBadge, TOAST_SCRIPT
|
|
3
|
+
import { esc, statusBadge, TOAST_SCRIPT } from '@/ui/render-helpers.js';
|
|
4
4
|
import { SPACING } from '@/ui/spacing-system.js';
|
|
5
5
|
|
|
6
6
|
const TOAST = TOAST_SCRIPT;
|
package/src/ui/settings/home.js
CHANGED
|
@@ -22,7 +22,7 @@ const SETTINGS_CARDS = [
|
|
|
22
22
|
{ key: 'permissions', icon: 'lock', title: 'Permissions', desc: 'MWR permissions and access control', href: '/admin/settings/permissions' },
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
export function renderSettingsHome(user,
|
|
25
|
+
export function renderSettingsHome(user, _config = {}, counts = {}) {
|
|
26
26
|
const cards = SETTINGS_CARDS.map(c => {
|
|
27
27
|
const cnt = c.countKey && counts[c.countKey] !== undefined ? counts[c.countKey] : null;
|
|
28
28
|
const badge = cnt !== null ? `<span class="badge badge-flat-primary text-xs">${cnt.toLocaleString('en-ZA')} items</span>` : '';
|
|
@@ -181,7 +181,6 @@ export function renderSettingsFileReview(user, config = {}, frSettings = {}) {
|
|
|
181
181
|
const fr = config.fileReview || {};
|
|
182
182
|
const reviewFlags = frSettings.review_flags || [];
|
|
183
183
|
const tenderFlags = frSettings.tender_flags || [];
|
|
184
|
-
const flagManagers = frSettings.flags_managers || [];
|
|
185
184
|
const tempAccess = frSettings.temp_review_access_period || 0;
|
|
186
185
|
const toggles = [
|
|
187
186
|
{ id: 'auto_pdf_cache', label: 'Auto-cache PDFs', desc: 'Cache PDF files for faster loading', checked: fr.auto_pdf_cache !== false },
|
package/src/ui/settings/teams.js
CHANGED
|
@@ -12,7 +12,7 @@ function parseMembers(t) {
|
|
|
12
12
|
} catch { return []; }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function renderSettingsTeams(user, teams = [],
|
|
15
|
+
export function renderSettingsTeams(user, teams = [], _allUsers = []) {
|
|
16
16
|
const editDialog = `<div id="team-dialog" class="modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="team-dialog-title">
|
|
17
17
|
<div class="modal-overlay" data-dialog-close="team-dialog"></div>
|
|
18
18
|
<div class="modal-content rounded-box max-w-md p-6">
|
package/src/ui/spacing-system.js
CHANGED
|
@@ -93,7 +93,7 @@ export function renderButton(label, opts = {}) {
|
|
|
93
93
|
return `<button type="button"${onclickAttr} class="${btnClass}" ${disabled ? 'disabled' : ''} style="${extraStyle}">${label}</button>`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export function renderProgress(value, max = 100,
|
|
96
|
+
export function renderProgress(value, max = 100, _variant = 'success', marginTop = SPACING.md) {
|
|
97
97
|
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
98
98
|
return `<div style="margin-top:${typeof marginTop === 'string' ? marginTop : SPACING.md}">
|
|
99
99
|
<div style="display:flex;justify-content:space-between;margin-bottom:${SPACING.sm};align-items:center">
|