thatcher 1.0.7 → 1.0.34
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 +4 -16
- package/package.json +12 -18
- package/src/adapters/google-auth.js +4 -43
- package/src/adapters/google-drive.js +12 -67
- package/src/app/api/audit/dashboard/route.js +14 -11
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +6 -3
- package/src/app/api/audit/permissions/route.js +10 -7
- package/src/app/api/audit/permissions/stats/route.js +8 -5
- package/src/app/api/audit/route.js +4 -4
- package/src/app/api/audit/stats/route.js +1 -1
- package/src/app/api/auth/google/callback/route.js +5 -2
- package/src/app/api/auth/google/route.js +4 -1
- package/src/app/api/auth/login/route.js +5 -2
- package/src/app/api/auth/mwr-bridge/route.js +11 -21
- package/src/app/api/auth/password-reset/route.js +15 -14
- package/src/app/api/cron/trigger/route.js +4 -1
- package/src/app/api/debug/[[...path]]/route.js +6 -5
- package/src/app/api/debug/sqlite/route.js +7 -12
- package/src/app/api/debug/sync/route.js +5 -5
- package/src/app/api/domains/route.js +4 -1
- package/src/app/api/email/allocate/batch/route.js +19 -29
- package/src/app/api/email/allocate/route.js +16 -17
- package/src/app/api/email/receive/route.js +31 -42
- package/src/app/api/email/send/route.js +28 -16
- package/src/app/api/email/unallocated/route.js +10 -13
- package/src/app/api/files/[id]/route.js +4 -1
- package/src/app/api/formula/[[...path]]/route.js +0 -4
- package/src/app/api/health/route.js +10 -9
- package/src/app/api/metrics/route.js +1 -1
- package/src/cli.js +4 -4
- package/src/config/config-loader.js +1 -21
- package/src/config/constants.js +0 -35
- package/src/config/env.js +0 -30
- package/src/config/spec-helpers.js +6 -95
- package/src/engine.js +23 -0
- package/src/engine.server.js +8 -78
- package/src/index.js +42 -67
- package/src/lib/alert-manager.js +7 -3
- package/src/lib/api-helpers.js +4 -3
- package/src/lib/api.js +10 -10
- package/src/lib/audit-logger-enhanced.js +77 -83
- package/src/lib/auth-middleware.js +1 -32
- package/src/lib/auth-route-helpers.js +5 -0
- package/src/lib/busybase-adapter.js +0 -5
- package/src/lib/busybase-audit-reads.js +142 -0
- package/src/lib/busybase-audit.js +41 -0
- package/src/lib/busybase-lucia-adapter.js +72 -0
- package/src/lib/busybase-store.js +338 -0
- package/src/lib/config-field-helpers.js +0 -17
- package/src/lib/config-generator-engine.js +19 -36
- package/src/lib/config-helpers.js +0 -21
- package/src/lib/crud-action-helpers.js +2 -2
- package/src/lib/crud-factory.js +1 -16
- package/src/lib/crud-handlers.js +17 -52
- package/src/lib/csrf-protection.js +5 -2
- package/src/lib/date-utils.js +3 -127
- package/src/lib/debug-registry.js +0 -5
- package/src/lib/domain-loader.js +4 -1
- package/src/lib/email-sender.js +25 -23
- package/src/lib/error-handler.js +5 -41
- package/src/lib/error-recovery.js +10 -7
- package/src/lib/error-resilience.js +7 -5
- package/src/lib/events-engine.js +9 -6
- package/src/lib/export-sink.js +0 -5
- package/src/lib/field-iterator.js +7 -76
- package/src/lib/field-registry.js +0 -22
- package/src/lib/generic-crud-handler.js +0 -16
- package/src/lib/hook-engine.js +5 -53
- package/src/lib/hot-reload/checkpoint.js +5 -2
- package/src/lib/hot-reload/promise-container.js +7 -3
- package/src/lib/hot-reload/route-wrapper.js +4 -1
- package/src/lib/hot-reload/supervisor.js +6 -3
- package/src/lib/hyperformula-service.js +0 -5
- package/src/lib/id-helpers.js +10 -0
- package/src/lib/index.js +4 -7
- package/src/lib/log-aggregator.js +0 -1
- package/src/lib/logger.js +0 -15
- package/src/lib/metrics-collector.js +2 -46
- package/src/lib/next-compat.js +15 -1
- package/src/lib/observability-bootstrap.js +0 -5
- package/src/lib/perf-monitor.js +4 -1
- package/src/lib/perf-profiler.js +0 -5
- package/src/lib/query-string-adapter.js +5 -47
- package/src/lib/realtime-server.js +4 -26
- package/src/lib/request-tracing.js +0 -5
- package/src/lib/resource-monitor.js +4 -1
- package/src/lib/response-formatter.js +0 -38
- package/src/lib/route-helpers.js +3 -1
- package/src/lib/safe-json.js +5 -1
- package/src/lib/server-bootstrap.js +10 -9
- package/src/lib/stage-pipeline.js +2 -2
- package/src/lib/state-transport-client.js +4 -1
- package/src/lib/static-server.js +2 -2
- package/src/lib/status-helpers.js +2 -51
- package/src/lib/tracing.js +0 -5
- package/src/lib/utils.js +6 -67
- package/src/lib/validate.js +5 -88
- package/src/lib/validators.js +0 -38
- package/src/lib/with-audit-logging.js +1 -1
- package/src/lib/workflow-engine.js +10 -77
- package/src/lib/xstate-workflow-engine.js +3 -9
- package/src/plugins/index.js +5 -3
- package/src/server/server.js +26 -27
- package/src/services/collaborator-role.service.js +21 -77
- package/src/services/email-sender.js +7 -34
- package/src/services/notification-engine.js +12 -33
- package/src/services/permission.service.js +10 -73
- package/src/ui/advanced-search-renderer.js +3 -2
- package/src/ui/advanced-widgets.js +1 -1
- package/src/ui/auth-pages.js +10 -9
- package/src/ui/auth-styles.js +1 -1
- package/src/ui/client.js +1 -1
- package/src/ui/component-engine.js +1 -1
- package/src/ui/dashboard-renderer.js +5 -5
- package/src/ui/dialog-engine.js +6 -9
- package/src/ui/dialog-factory.js +2 -2
- package/src/ui/engagement-cards.js +7 -7
- package/src/ui/engagement-dialogs.js +2 -3
- package/src/ui/engagement-grid-renderer.js +20 -19
- package/src/ui/entity-renderer.js +56 -42
- package/src/ui/file-dialogs.js +25 -9
- package/src/ui/flexup-report-renderer.js +21 -18
- package/src/ui/format-helpers.js +27 -28
- package/src/ui/highlight-threading-renderer.js +9 -9
- package/src/ui/job-management-renderer.js +18 -16
- package/src/ui/layout.js +51 -25
- package/src/ui/ml-console-renderer.js +15 -16
- package/src/ui/monitoring-dashboard-client.js +9 -8
- package/src/ui/notifications-renderer.js +11 -10
- package/src/ui/page-handler-admin.js +32 -23
- package/src/ui/page-handler-helpers.js +27 -23
- package/src/ui/page-handler-reviews.js +71 -35
- package/src/ui/page-handler-rfi.js +28 -14
- package/src/ui/page-handler.js +65 -48
- package/src/ui/picker-dialogs.js +26 -19
- package/src/ui/render-helpers.js +84 -46
- package/src/ui/review-comparison-renderer.js +8 -7
- package/src/ui/review-detail-panels.js +8 -9
- package/src/ui/review-detail-renderer.js +3 -3
- package/src/ui/review-detail-script.js +13 -12
- package/src/ui/review-mwr-renderer.js +6 -5
- package/src/ui/review-renderer.js +7 -6
- package/src/ui/review-widgets.js +3 -3
- package/src/ui/rfi-detail-renderer.js +37 -20
- package/src/ui/rfi-report-renderer.js +14 -9
- package/src/ui/settings-renderer-advanced.js +21 -18
- package/src/ui/settings-renderer-advanced2.js +20 -19
- package/src/ui/settings-renderer-teams.js +4 -4
- package/src/ui/settings-renderer.js +4 -4
- package/src/ui/spacing-system.js +5 -2
- package/src/ui/standalone-login.js +2 -2
- package/src/ui/styles.css +2 -1
- package/src/ui/styles2.css +75 -5
- package/src/ui/test-page.js +4 -4
- package/src/ui/widgets.js +20 -15
- package/src/lib/audit-logger.js +0 -193
- package/src/lib/database-core.js +0 -255
- package/src/lib/database-migrations.js +0 -350
- package/src/lib/db-backup.js +0 -97
- package/src/lib/error-boundary.js +0 -134
- package/src/lib/errors.js +0 -69
- package/src/lib/health-monitor.js +0 -134
- package/src/lib/index-optimizer.js +0 -96
- package/src/lib/query-engine-write.js +0 -221
- package/src/lib/query-engine.js +0 -446
- package/src/ui/collaboration-dialogs.js +0 -31
- package/src/ui/global-tags.js +0 -144
- package/src/ui/idle-logout.js +0 -156
- package/src/ui/letter-dialogs.js +0 -37
- package/src/ui/password-reset-page.js +0 -146
- package/src/ui/perf-renderer.js +0 -71
- package/src/ui/rfi-renderer.js +0 -194
- package/src/ui/validation-ui.js +0 -147
package/src/lib/query-engine.js
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Query Engine - Read operations
|
|
3
|
-
* Adapted from moonlanding/src/lib/query-engine.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getDatabase } from './database-core.js';
|
|
7
|
-
import { getSpec } from '../config/spec-helpers.js';
|
|
8
|
-
import { RECORD_STATUS } from '../config/constants.js';
|
|
9
|
-
|
|
10
|
-
const db = getDatabase();
|
|
11
|
-
const logger = createLogger('[QueryEngine]');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Execute a query with error handling
|
|
15
|
-
* @param {string} sql
|
|
16
|
-
* @param {Array} params
|
|
17
|
-
* @param {object} context
|
|
18
|
-
* @returns {Array}
|
|
19
|
-
*/
|
|
20
|
-
function execQuery(sql, params = [], context = {}) {
|
|
21
|
-
try {
|
|
22
|
-
return db.prepare(sql).all(...params);
|
|
23
|
-
} catch (e) {
|
|
24
|
-
logger.error(`${context.operation || 'Query'} ${context.entity || ''}`, { sql, error: e.message });
|
|
25
|
-
throw new Error(`Database query failed: ${e.message}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Execute a single-row query
|
|
31
|
-
* @param {string} sql
|
|
32
|
-
* @param {Array} params
|
|
33
|
-
* @param {object} context
|
|
34
|
-
* @returns {object|null}
|
|
35
|
-
*/
|
|
36
|
-
function execGet(sql, params = [], context = {}) {
|
|
37
|
-
try {
|
|
38
|
-
return db.prepare(sql).get(...params);
|
|
39
|
-
} catch (e) {
|
|
40
|
-
logger.error(`${context.operation || 'Get'} ${context.entity || ''}`, { sql, error: e.message });
|
|
41
|
-
throw new Error(`Database get failed: ${e.message}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Execute a write statement (INSERT/UPDATE/DELETE) with error handling
|
|
47
|
-
* @param {string} sql
|
|
48
|
-
* @param {Array} params
|
|
49
|
-
* @param {object} context
|
|
50
|
-
* @returns {object} better-sqlite3 RunResult
|
|
51
|
-
*/
|
|
52
|
-
function execRun(sql, params = [], context = {}) {
|
|
53
|
-
try {
|
|
54
|
-
return db.prepare(sql).run(...params);
|
|
55
|
-
} catch (e) {
|
|
56
|
-
logger.error(`${context.operation || 'Run'} ${context.entity || ''}`, { sql, error: e.message });
|
|
57
|
-
throw new Error(`Database run failed: ${e.message}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get table name for entity (users vs user)
|
|
63
|
-
* @param {object} spec
|
|
64
|
-
* @returns {string}
|
|
65
|
-
*/
|
|
66
|
-
function tableName(spec) {
|
|
67
|
-
return spec.name === 'user' ? 'users' : spec.name;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Build SELECT query with joins, where, sorting, pagination
|
|
72
|
-
* @param {object} spec - Entity specification
|
|
73
|
-
* @param {object} where - Where conditions
|
|
74
|
-
* @param {object} options - Query options
|
|
75
|
-
* @returns {{sql: string, params: Array}}
|
|
76
|
-
*/
|
|
77
|
-
function buildSpecQuery(spec, where = {}, options = {}) {
|
|
78
|
-
const tbl = tableName(spec);
|
|
79
|
-
const table = `"${tbl}"`;
|
|
80
|
-
const selects = [`${table}.*`];
|
|
81
|
-
const joins = [];
|
|
82
|
-
|
|
83
|
-
// Add computed fields
|
|
84
|
-
if (spec.computed) {
|
|
85
|
-
Object.entries(spec.computed).forEach(([k, c]) => {
|
|
86
|
-
selects.push(`${c.sql} as "${k}"`);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Add joins for ref fields with display
|
|
91
|
-
Object.entries(spec.fields || {}).forEach(([k, f]) => {
|
|
92
|
-
if (f.type === 'ref' && f.display) {
|
|
93
|
-
const refTbl = f.ref === 'user' ? 'users' : f.ref;
|
|
94
|
-
const alias = `"${refTbl}_${k}"`;
|
|
95
|
-
joins.push(`LEFT JOIN "${refTbl}" ${alias} ON ${table}."${k}" = ${alias}.id`);
|
|
96
|
-
|
|
97
|
-
const displayField = f.display.split('.')[1] || 'name';
|
|
98
|
-
selects.push(`${alias}."${displayField}" as "${k}_display"`);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const wc = [];
|
|
103
|
-
const p = [];
|
|
104
|
-
|
|
105
|
-
// Where conditions
|
|
106
|
-
Object.entries(where).forEach(([k, v]) => {
|
|
107
|
-
if (v !== undefined && v !== null) {
|
|
108
|
-
wc.push(`${table}."${k}" = ?`);
|
|
109
|
-
p.push(v);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Exclude soft-deleted by default
|
|
114
|
-
if (spec.fields?.status && !where.status && !options.includeDeleted) {
|
|
115
|
-
wc.push(`${table}."status" != '${RECORD_STATUS.DELETED}'`);
|
|
116
|
-
}
|
|
117
|
-
if (spec.fields?.archived && !where.archived && !options.includeArchived) {
|
|
118
|
-
wc.push(`${table}."archived" = 0`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
let sql = `SELECT ${selects.join(', ')} FROM ${table}`;
|
|
122
|
-
if (joins.length) {
|
|
123
|
-
sql += ' ' + joins.join(' ');
|
|
124
|
-
}
|
|
125
|
-
if (wc.length) {
|
|
126
|
-
sql += ` WHERE ` + wc.join(` AND `);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Sorting
|
|
130
|
-
const sort = options.sort || spec.list?.defaultSort;
|
|
131
|
-
if (sort && sort.field && spec.fields?.[sort.field]) {
|
|
132
|
-
const dir = (sort.dir || 'ASC').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
133
|
-
sql += ` ORDER BY ${table}."${sort.field}" ${dir}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Limit/offset
|
|
137
|
-
if (options.limit) {
|
|
138
|
-
sql += ` LIMIT ${parseInt(options.limit, 10)}`;
|
|
139
|
-
if (options.offset) {
|
|
140
|
-
sql += ` OFFSET ${parseInt(options.offset, 10)}`;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return { sql, params: p };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Get total count for entity (optionally with where)
|
|
149
|
-
* @param {string} entity
|
|
150
|
-
* @param {object} where
|
|
151
|
-
* @param {object} options
|
|
152
|
-
* @returns {number}
|
|
153
|
-
*/
|
|
154
|
-
export function count(entity, where = {}, options = {}) {
|
|
155
|
-
const spec = getSpec(entity);
|
|
156
|
-
const tbl = tableName(spec);
|
|
157
|
-
const table = `"${tbl}"`;
|
|
158
|
-
const wc = [];
|
|
159
|
-
const p = [];
|
|
160
|
-
|
|
161
|
-
Object.entries(where).forEach(([k, v]) => {
|
|
162
|
-
if (v !== undefined && v !== null) {
|
|
163
|
-
wc.push(`${table}."${k}" = ?`);
|
|
164
|
-
p.push(v);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (spec.fields?.status && !where.status && !options.includeDeleted) {
|
|
169
|
-
wc.push(`${table}."status" != '${RECORD_STATUS.DELETED}'`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const whereClause = wc.length ? `WHERE ${wc.join(' AND ')}` : '';
|
|
173
|
-
const sql = `SELECT COUNT(*) as cnt FROM ${table} ${whereClause}`;
|
|
174
|
-
|
|
175
|
-
const result = execGet(sql, p, { entity, operation: 'Count' });
|
|
176
|
-
return result?.cnt || 0;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* List all records (with optional where)
|
|
181
|
-
* @param {string} entity
|
|
182
|
-
* @param {object} where
|
|
183
|
-
* @param {object} options
|
|
184
|
-
* @returns {Array}
|
|
185
|
-
*/
|
|
186
|
-
export function list(entity, where = {}, options = {}) {
|
|
187
|
-
const spec = getSpec(entity);
|
|
188
|
-
const { sql, params } = buildSpecQuery(spec, where, options);
|
|
189
|
-
return execQuery(sql, params, { entity, operation: 'List' });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* List with pagination
|
|
194
|
-
* @param {string} entity
|
|
195
|
-
* @param {object} where
|
|
196
|
-
* @param {number} page
|
|
197
|
-
* @param {number|null} pageSize
|
|
198
|
-
* @returns {{items: Array, pagination: object}}
|
|
199
|
-
*/
|
|
200
|
-
export async function listWithPagination(entity, where = {}, page = 1, pageSize = null) {
|
|
201
|
-
const spec = getSpec(entity);
|
|
202
|
-
const tbl = tableName(spec);
|
|
203
|
-
|
|
204
|
-
const paginationCfg = getPaginationConfig(spec);
|
|
205
|
-
const defaultPageSize = spec.list?.pageSize || paginationCfg.default_page_size;
|
|
206
|
-
const finalPageSize = pageSize || defaultPageSize;
|
|
207
|
-
const finalPage = Math.max(1, page);
|
|
208
|
-
|
|
209
|
-
const offset = (finalPage - 1) * finalPageSize;
|
|
210
|
-
|
|
211
|
-
const { sql, params } = buildSpecQuery(spec, where, {
|
|
212
|
-
...options,
|
|
213
|
-
limit: finalPageSize,
|
|
214
|
-
offset,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const items = execQuery(sql, params, { entity, operation: 'ListWithPagination' });
|
|
218
|
-
|
|
219
|
-
const total = count(entity, where, options);
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
items,
|
|
223
|
-
pagination: {
|
|
224
|
-
page: finalPage,
|
|
225
|
-
pageSize: finalPageSize,
|
|
226
|
-
total,
|
|
227
|
-
totalPages: Math.ceil(total / finalPageSize),
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Get single record by ID
|
|
234
|
-
* @param {string} entity
|
|
235
|
-
* @param {string|number} id
|
|
236
|
-
* @returns {object|null}
|
|
237
|
-
*/
|
|
238
|
-
export function get(entity, id) {
|
|
239
|
-
const spec = getSpec(entity);
|
|
240
|
-
const tbl = tableName(spec);
|
|
241
|
-
const sql = `SELECT * FROM "${tbl}" WHERE id = ?`;
|
|
242
|
-
return execGet(sql, [id], { entity, operation: 'Get' });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Get single record by field value
|
|
247
|
-
* @param {string} entity
|
|
248
|
-
* @param {string} field
|
|
249
|
-
* @param {any} value
|
|
250
|
-
* @returns {object|null}
|
|
251
|
-
*/
|
|
252
|
-
export function getBy(entity, field, value) {
|
|
253
|
-
const spec = getSpec(entity);
|
|
254
|
-
const tbl = tableName(spec);
|
|
255
|
-
const sql = `SELECT * FROM "${tbl}" WHERE "${field}" = ? LIMIT 1`;
|
|
256
|
-
return execGet(sql, [value], { entity, operation: 'GetBy' });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Search using FTS
|
|
261
|
-
* @param {string} entity
|
|
262
|
-
* @param {string} query
|
|
263
|
-
* @param {object} where
|
|
264
|
-
* @param {object} options
|
|
265
|
-
* @returns {Array}
|
|
266
|
-
*/
|
|
267
|
-
export function search(entity, query, where = {}, options = {}) {
|
|
268
|
-
const spec = getSpec(entity);
|
|
269
|
-
const tbl = tableName(spec);
|
|
270
|
-
const ftsTable = `${tbl}_fts`;
|
|
271
|
-
|
|
272
|
-
// Check if FTS table exists
|
|
273
|
-
const ftsExists = db.prepare(`
|
|
274
|
-
SELECT name FROM sqlite_master WHERE type='table' AND name=?
|
|
275
|
-
`).get(ftsTable);
|
|
276
|
-
|
|
277
|
-
if (!ftsExists) {
|
|
278
|
-
// Fallback to simple LIKE search
|
|
279
|
-
const { sql, params } = buildSpecQuery(spec, where, options);
|
|
280
|
-
const baseSql = sql.replace('SELECT *', `SELECT *`);
|
|
281
|
-
const searchTerm = `%${query}%`;
|
|
282
|
-
const searchFields = Object.keys(spec.fields || {}).filter(
|
|
283
|
-
f => ['text', 'textarea', 'email'].includes(spec.fields[f].type)
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
if (searchFields.length > 0) {
|
|
287
|
-
const conditions = searchFields.map(f => `"${f}" LIKE ?`).join(' OR ');
|
|
288
|
-
const whereAnd = sql.includes('WHERE') ? ' AND ' : ' WHERE ';
|
|
289
|
-
const fullSql = baseSql + whereAnd + `(${conditions})`;
|
|
290
|
-
const searchParams = [...params, ...searchFields.map(() => searchTerm)];
|
|
291
|
-
return execQuery(fullSql, searchParams, { entity, operation: 'SearchFallback' });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Use FTS
|
|
298
|
-
const ftsResults = db.prepare(`
|
|
299
|
-
SELECT rowid as id FROM ${ftsTable}
|
|
300
|
-
WHERE ${ftsTable} MATCH ?
|
|
301
|
-
`).all(query);
|
|
302
|
-
|
|
303
|
-
if (!ftsResults.length) return [];
|
|
304
|
-
|
|
305
|
-
const ids = ftsResults.map(r => r.id);
|
|
306
|
-
const idPlaceholders = ids.map(() => '?').join(',');
|
|
307
|
-
const { sql, params } = buildSpecQuery(spec, { ...where, id: { $in: ids } }, options);
|
|
308
|
-
const finalSql = sql.replace('WHERE', `WHERE "${tbl}"."id" IN (${idPlaceholders}) AND `);
|
|
309
|
-
|
|
310
|
-
return execQuery(finalSql, [...ids, ...params], { entity, operation: 'Search' });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Search with pagination
|
|
315
|
-
* @param {string} entity
|
|
316
|
-
* @param {string} query
|
|
317
|
-
* @param {object} where
|
|
318
|
-
* @param {number} page
|
|
319
|
-
* @param {number} pageSize
|
|
320
|
-
* @returns {{items: Array, pagination: object}}
|
|
321
|
-
*/
|
|
322
|
-
export async function searchWithPagination(entity, query, where = {}, page = 1, pageSize = null) {
|
|
323
|
-
const spec = getSpec(entity);
|
|
324
|
-
const paginationCfg = getPaginationConfig(spec);
|
|
325
|
-
const defaultPageSize = spec.list?.pageSize || paginationCfg.default_page_size;
|
|
326
|
-
const finalPageSize = pageSize || defaultPageSize;
|
|
327
|
-
const finalPage = Math.max(1, page);
|
|
328
|
-
|
|
329
|
-
const items = search(entity, query, where, {
|
|
330
|
-
limit: finalPageSize,
|
|
331
|
-
offset: (finalPage - 1) * finalPageSize,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// For total count, we'd need a separate FTS count query - simplified here
|
|
335
|
-
const total = items.length; // Approximate
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
items,
|
|
339
|
-
pagination: {
|
|
340
|
-
page: finalPage,
|
|
341
|
-
pageSize: finalPageSize,
|
|
342
|
-
total,
|
|
343
|
-
totalPages: Math.ceil(total / finalPageSize),
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Get children of a parent entity
|
|
350
|
-
* @param {string} parentEntity
|
|
351
|
-
* @param {string|number} parentId
|
|
352
|
-
* @param {object} childDef
|
|
353
|
-
* @returns {Array}
|
|
354
|
-
*/
|
|
355
|
-
export function getChildren(parentEntity, parentId, childDef) {
|
|
356
|
-
const childSpec = getSpec(childDef.entity);
|
|
357
|
-
const childTbl = tableName(childSpec);
|
|
358
|
-
const fk = childDef.fk || `${parentEntity}_id`;
|
|
359
|
-
|
|
360
|
-
const sql = `SELECT * FROM "${childTbl}" WHERE "${fk}" = ? AND status != 'deleted'`;
|
|
361
|
-
return execQuery(sql, [parentId], { entity: childDef.entity, operation: 'GetChildren' });
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Batch-fetch children of a parent for several child definitions at once.
|
|
366
|
-
* Ported from moonlanding; uses thatcher's `list` + getChildren fk convention
|
|
367
|
-
* (`fk`, falling back to moon's `foreignKey`, then `${parentEntity}_id`).
|
|
368
|
-
* @param {string} parentEntity
|
|
369
|
-
* @param {string|number} parentId
|
|
370
|
-
* @param {object|Array} childSpecs - map of key->def, or array of entity names
|
|
371
|
-
* @returns {Promise<object>} map of key -> rows
|
|
372
|
-
*/
|
|
373
|
-
export function batchGetChildren(parentEntity, parentId, childSpecs) {
|
|
374
|
-
const childEntries = Array.isArray(childSpecs)
|
|
375
|
-
? childSpecs.map(s => [s, { entity: s }])
|
|
376
|
-
: Object.entries(childSpecs);
|
|
377
|
-
const queries = childEntries.map(async ([key, def]) => {
|
|
378
|
-
const entity = def.entity || def;
|
|
379
|
-
const foreignKey = def.fk || def.foreignKey || `${parentEntity}_id`;
|
|
380
|
-
const results = list(entity, { [foreignKey]: parentId });
|
|
381
|
-
return [key, results];
|
|
382
|
-
});
|
|
383
|
-
return Promise.all(queries).then(results => Object.fromEntries(results));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Get pagination config from system settings
|
|
388
|
-
* @param {object} spec - Entity spec
|
|
389
|
-
* @returns {{default_page_size: number, max_page_size: number}}
|
|
390
|
-
*/
|
|
391
|
-
function getPaginationConfig(spec) {
|
|
392
|
-
// Try to get from config engine, fallback to defaults
|
|
393
|
-
try {
|
|
394
|
-
// Will be provided by caller or fetched from global config
|
|
395
|
-
return {
|
|
396
|
-
default_page_size: spec.list?.pageSize || 50,
|
|
397
|
-
max_page_size: 500,
|
|
398
|
-
};
|
|
399
|
-
} catch {
|
|
400
|
-
return { default_page_size: 50, max_page_size: 500 };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Simple logger
|
|
406
|
-
*/
|
|
407
|
-
function createLogger(prefix) {
|
|
408
|
-
return {
|
|
409
|
-
error: (msg, meta = {}) => {
|
|
410
|
-
console.error(`${prefix} ${msg}`, meta);
|
|
411
|
-
},
|
|
412
|
-
warn: (msg, meta = {}) => {
|
|
413
|
-
console.warn(`${prefix} ${msg}`, meta);
|
|
414
|
-
},
|
|
415
|
-
info: (msg, meta = {}) => {
|
|
416
|
-
console.info(`${prefix} ${msg}`, meta);
|
|
417
|
-
},
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Run in transaction
|
|
423
|
-
* @param {Function} callback
|
|
424
|
-
* @returns {Promise<any>}
|
|
425
|
-
*/
|
|
426
|
-
export async function withTransaction(callback) {
|
|
427
|
-
const dbInstance = getDatabase();
|
|
428
|
-
try {
|
|
429
|
-
dbInstance.prepare('BEGIN').run();
|
|
430
|
-
const result = await callback();
|
|
431
|
-
dbInstance.prepare('COMMIT').run();
|
|
432
|
-
return result;
|
|
433
|
-
} catch (e) {
|
|
434
|
-
dbInstance.prepare('ROLLBACK').run();
|
|
435
|
-
throw e;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Re-export write operations so consumers can import all CRUD from a single
|
|
440
|
-
// module (ported from moonlanding). Several thatcher modules already import
|
|
441
|
-
// `create`/`update`/`remove` from './query-engine.js'; this wires them through
|
|
442
|
-
// to the existing write implementation without changing those callers.
|
|
443
|
-
export { create, update, remove } from './query-engine-write.js';
|
|
444
|
-
|
|
445
|
-
// Expose low-level executors for callers that need raw SQL access.
|
|
446
|
-
export { execQuery, execGet, execRun };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export function collaboratorManagementDialog(entityType, entityId) {
|
|
2
|
-
return `<div id="collab-mgmt-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="collab-mgmt-dialog-title" aria-hidden="true">
|
|
3
|
-
<div class="dialog-panel" style="max-width:640px">
|
|
4
|
-
<div class="dialog-header"><span class="dialog-title" id="collab-mgmt-dialog-title">Manage Collaborators</span><button class="dialog-close" data-dialog-close="collab-mgmt-dialog" aria-label="Close dialog">×</button></div>
|
|
5
|
-
<div class="dialog-body">
|
|
6
|
-
<div id="cmd-list" class="flex flex-col gap-2" style="max-height:300px;overflow:auto"><div class="text-gray-500 text-sm text-center py-4">Loading...</div></div>
|
|
7
|
-
<div class="inline-form" style="margin-top:1rem">
|
|
8
|
-
<div class="inline-form-row">
|
|
9
|
-
<div class="inline-form-field" style="flex:1"><label for="cmd-email">Email</label><input type="email" id="cmd-email" class="input input-bordered input-sm w-full" placeholder="collaborator@example.com"/></div>
|
|
10
|
-
<div class="inline-form-field"><label for="cmd-role">Role</label><select id="cmd-role" class="select select-bordered select-sm"><option value="viewer">Viewer</option><option value="commenter">Commenter</option><option value="reviewer" selected>Reviewer</option><option value="manager">Manager</option></select></div>
|
|
11
|
-
<div class="inline-form-field"><label> </label><button class="btn btn-primary btn-sm" data-action="cmdAdd">Add</button></div>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
<div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="collab-mgmt-dialog">Close</button></div>
|
|
16
|
-
</div></div>
|
|
17
|
-
<script>
|
|
18
|
-
window.openCollabMgmt=function(){document.getElementById('collab-mgmt-dialog').style.display='flex';cmdLoad()};
|
|
19
|
-
async function cmdLoad(){var c=document.getElementById('cmd-list');c.innerHTML='<div class="text-gray-500 text-sm text-center py-2">Loading...</div>';try{var res=await fetch('/api/collaborator?${entityType}_id=${entityId}');var d=await res.json();var items=d.data||d||[];c.innerHTML='';if(!items.length){c.innerHTML='<div class="text-gray-500 text-sm text-center py-2">No collaborators</div>';return}items.forEach(function(item){var div=document.createElement('div');div.className='flex items-center justify-between gap-2';div.style.padding='0.5rem 0';div.style.borderBottom='1px solid #f3f4f6';var expired=item.expires_at&&item.expires_at<=Math.floor(Date.now()/1000);var expiryText=item.expires_at?'Expires: '+new Date(item.expires_at*1000).toLocaleDateString():'Permanent';if(expired)expiryText='<span style="color:#ef4444">Expired</span>';div.innerHTML='<div><div class="text-sm font-medium">'+(item.email||item.name||'Unknown')+'</div><div class="text-xs text-gray-500">'+(item.role_type||item.access_type||'viewer')+' · '+expiryText+'</div></div><div class="flex gap-1"><button class="btn btn-xs btn-ghost" data-action="cmdExtend" data-args='["'+item.id+'"]'>Extend</button><button class="btn btn-xs btn-error btn-outline" data-action="cmdRevoke" data-args='["'+item.id+'"]'>Revoke</button></div>';c.appendChild(div)})}catch(e){c.innerHTML='<div class="text-red-500 text-sm text-center">Error loading</div>'}}
|
|
20
|
-
window.cmdAdd=async function(){var email=document.getElementById('cmd-email').value.trim();var role=document.getElementById('cmd-role').value;if(!email){showToast('Email required','error');return}try{var r=await fetch('/api/collaborator',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email,role_type:role,${entityType}_id:'${entityId}'})});if(r.ok){showToast('Collaborator added','success');document.getElementById('cmd-email').value='';cmdLoad()}else{var d=await r.json().catch(function(){return{}});showToast(d.error||'Failed','error')}}catch(e){showToast('Error','error')}};
|
|
21
|
-
window.cmdRevoke=async function(id){if(!confirm('Revoke access?'))return;try{var r=await fetch('/api/collaborator/'+id,{method:'DELETE'});if(r.ok){showToast('Revoked','success');cmdLoad()}else showToast('Failed','error')}catch(e){showToast('Error','error')}};
|
|
22
|
-
window.cmdExtend=async function(id){var days=prompt('Extend by how many days?','7');if(!days)return;var ts=Math.floor(Date.now()/1000)+Number(days)*86400;try{var r=await fetch('/api/collaborator/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({expires_at:ts})});if(r.ok){showToast('Extended','success');cmdLoad()}else showToast('Failed','error')}catch(e){showToast('Error','error')}};
|
|
23
|
-
</script>`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function collaboratorExpiryUI(entityType, entityId) {
|
|
27
|
-
return `<div class="card-clean" style="margin-bottom:1rem"><div class="card-clean-body"><div class="flex justify-between items-center"><h3 style="font-size:0.875rem;font-weight:600">Collaborator Access</h3><button class="btn btn-outline btn-sm" data-action="openCollabMgmt">Manage</button></div><div id="collab-expiry-list" class="mt-3 flex flex-col gap-1"></div></div></div>
|
|
28
|
-
<script>
|
|
29
|
-
(function(){fetch('/api/collaborator?${entityType}_id=${entityId}').then(function(r){return r.json()}).then(function(d){var items=d.data||d||[];var el=document.getElementById('collab-expiry-list');var now=Math.floor(Date.now()/1000);var expiring=items.filter(function(c){return c.expires_at&&c.expires_at>now&&c.expires_at-now<3*86400});var expired=items.filter(function(c){return c.expires_at&&c.expires_at<=now});if(!expiring.length&&!expired.length){el.innerHTML='<div class="text-xs text-gray-500">All access current</div>';return}var html='';expired.forEach(function(c){html+='<div class="text-xs" style="color:#ef4444">'+c.email+' - Expired</div>'});expiring.forEach(function(c){var days=Math.ceil((c.expires_at-now)/86400);html+='<div class="text-xs" style="color:#f59e0b">'+c.email+' - Expires in '+days+'d</div>'});el.innerHTML=html}).catch(function(){})})();
|
|
30
|
-
</script>`;
|
|
31
|
-
}
|
package/src/ui/global-tags.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
let tagsStore = {};
|
|
2
|
-
|
|
3
|
-
async function apiCall(endpoint, options = {}) {
|
|
4
|
-
try {
|
|
5
|
-
const res = await fetch(`/api/tag${endpoint}`, { headers: { 'Content-Type': 'application/json' }, ...options });
|
|
6
|
-
if (!res.ok) throw new Error('API error');
|
|
7
|
-
return await res.json();
|
|
8
|
-
} catch (e) {
|
|
9
|
-
throw e;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function createTag(name, color = '#808080') {
|
|
14
|
-
const data = await apiCall('', { method: 'POST', body: JSON.stringify({ name: name.trim(), color }) });
|
|
15
|
-
tagsStore[data.id] = { id: data.id, name: data.name, color: data.color, usage: 0 };
|
|
16
|
-
showToast('Tag created', 'success');
|
|
17
|
-
return data;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function updateTag(id, name, color) {
|
|
21
|
-
const data = await apiCall(`/${id}`, { method: 'PUT', body: JSON.stringify({ name: name.trim(), color }) });
|
|
22
|
-
tagsStore[id] = { ...tagsStore[id], name: data.name, color: data.color };
|
|
23
|
-
showToast('Tag updated', 'success');
|
|
24
|
-
return data;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function deleteTag(id) {
|
|
28
|
-
await apiCall(`/${id}`, { method: 'DELETE' });
|
|
29
|
-
delete tagsStore[id];
|
|
30
|
-
showToast('Tag deleted', 'success');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function listTags() {
|
|
34
|
-
try {
|
|
35
|
-
const data = await apiCall('');
|
|
36
|
-
const tags = Array.isArray(data) ? data : data.data || [];
|
|
37
|
-
tagsStore = tags.reduce((acc, tag) => { acc[tag.id] = tag; return acc; }, {});
|
|
38
|
-
return tags;
|
|
39
|
-
} catch (e) {
|
|
40
|
-
console.error('Error loading tags:', e);
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function esc(text) {
|
|
46
|
-
const m = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
47
|
-
return text.replace(/[&<>"']/g, c => m[c]);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function renderTagsList() {
|
|
51
|
-
const tags = Object.values(tagsStore).sort((a, b) => (b.usage || 0) - (a.usage || 0));
|
|
52
|
-
return `<div class="tags-list">${tags.map(t => `
|
|
53
|
-
<div class="tag-item" data-tag-id="${t.id}">
|
|
54
|
-
<div class="tag-dot" style="background-color: ${t.color}"></div>
|
|
55
|
-
<div class="tag-info"><span class="tag-name">${esc(t.name)}</span><span class="tag-usage">${t.usage || 0} items</span></div>
|
|
56
|
-
<div class="tag-actions">
|
|
57
|
-
<button class="btn btn-xs btn-ghost" data-action="gtsEditTag" data-args='["${t.id}"]'>Edit</button>
|
|
58
|
-
<button class="btn btn-xs btn-ghost text-error" data-action="gtsDeleteTag" data-args='["${t.id}"]'>Delete</button>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
`).join('') || '<p class="text-xs text-gray-500">No tags created</p>'}</div>`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function showDialog(mode = 'create', tagId = null) {
|
|
65
|
-
const tag = mode === 'edit' ? tagsStore[tagId] : null;
|
|
66
|
-
const html = `
|
|
67
|
-
<div class="dialog-overlay" id="tag-dialog-overlay" data-action="gtsCloseDialog" onkeydown="if(event.key==='Escape')gtsCloseDialog()" role="dialog" aria-modal="true" aria-labelledby="tag-dialog-heading" aria-hidden="true">
|
|
68
|
-
<div class="dialog-content" data-stop-propagation="true">
|
|
69
|
-
<div class="dialog-header"><h2 id="tag-dialog-heading">${mode === 'create' ? 'Create New Tag' : 'Edit Tag'}</h2><button class="btn btn-sm btn-ghost" data-action="gtsCloseDialog" aria-label="Close dialog">×</button></div>
|
|
70
|
-
<div class="dialog-body">
|
|
71
|
-
<div class="form-group"><label class="label" for="tag-name-input">Tag Name</label><input type="text" class="input input-bordered w-full" id="tag-name-input" placeholder="e.g., Priority" value="${tag ? esc(tag.name) : ''}" maxlength="50" /></div>
|
|
72
|
-
<div class="form-group"><label class="label" for="tag-color-input">Color</label><div class="flex gap-2"><input type="color" id="tag-color-input" value="${tag ? tag.color : '#808080'}" aria-label="Tag color" /><span id="color-preview" class="w-8 h-8 rounded border" style="background-color: ${tag ? tag.color : '#808080'}" aria-hidden="true"></span></div></div>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="dialog-footer"><button class="btn btn-ghost" data-action="gtsCloseDialog">Cancel</button><button class="btn btn-primary" data-action="gtsSaveTag" data-args='["${mode}","${tagId || ''}"]'>${mode === 'create' ? 'Create' : 'Update'}</button></div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>`;
|
|
77
|
-
const container = document.getElementById('tag-dialog-container') || (c => (document.body.appendChild(c), c))(Object.assign(document.createElement('div'), { id: 'tag-dialog-container' }));
|
|
78
|
-
container.innerHTML = html;
|
|
79
|
-
document.getElementById('tag-color-input').addEventListener('input', (e) => {
|
|
80
|
-
document.getElementById('color-preview').style.backgroundColor = e.target.value;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function showToast(msg, type = 'info') {
|
|
85
|
-
let c = document.getElementById('toast-container');
|
|
86
|
-
if (!c) {
|
|
87
|
-
c = document.createElement('div');
|
|
88
|
-
c.id = 'toast-container';
|
|
89
|
-
c.className = 'fixed bottom-4 right-4 z-50 flex flex-col gap-2';
|
|
90
|
-
document.body.appendChild(c);
|
|
91
|
-
}
|
|
92
|
-
const t = document.createElement('div');
|
|
93
|
-
t.className = `alert alert-${type === 'error' ? 'error' : type === 'success' ? 'success' : 'info'} shadow-lg max-w-sm`;
|
|
94
|
-
t.textContent = msg;
|
|
95
|
-
c.appendChild(t);
|
|
96
|
-
setTimeout(() => t.remove(), 3000);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
window.gtsOpenDialog = (mode = 'create', tagId = null) => showDialog(mode, tagId);
|
|
100
|
-
window.gtsEditTag = (tagId) => gtsOpenDialog('edit', tagId);
|
|
101
|
-
window.gtsDeleteTag = async (tagId) => {
|
|
102
|
-
if (confirm(`Delete "${esc(tagsStore[tagId].name)}"?`)) {
|
|
103
|
-
await deleteTag(tagId);
|
|
104
|
-
refreshTagsList();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
window.gtsSaveTag = async (mode, tagId) => {
|
|
108
|
-
const name = document.getElementById('tag-name-input').value.trim();
|
|
109
|
-
const color = document.getElementById('tag-color-input').value;
|
|
110
|
-
if (!name) { showToast('Tag name required', 'error'); return; }
|
|
111
|
-
try {
|
|
112
|
-
mode === 'create' ? await createTag(name, color) : await updateTag(tagId, name, color);
|
|
113
|
-
gtsCloseDialog();
|
|
114
|
-
refreshTagsList();
|
|
115
|
-
} catch (e) {
|
|
116
|
-
showToast('Error: ' + e.message, 'error');
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
window.gtsCloseDialog = () => {
|
|
120
|
-
const c = document.getElementById('tag-dialog-container');
|
|
121
|
-
if (c) c.innerHTML = '';
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
async function refreshTagsList() {
|
|
125
|
-
await listTags();
|
|
126
|
-
const c = document.getElementById('tags-list-container');
|
|
127
|
-
if (c) c.innerHTML = renderTagsList();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export async function initGlobalTags() {
|
|
131
|
-
await listTags();
|
|
132
|
-
return { createTag, updateTag, deleteTag, listTags, renderTagsList, refreshTagsList };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function renderTagsUI() {
|
|
136
|
-
return `
|
|
137
|
-
<div class="card-clean"><div class="card-clean-body">
|
|
138
|
-
<div class="flex justify-between items-center mb-4"><h2 class="card-title text-lg">Global Tags</h2><button class="btn btn-primary btn-sm" data-action="gtsOpenDialog" data-args='["create"]'>New Tag</button></div>
|
|
139
|
-
<div id="tags-list-container">${renderTagsList()}</div>
|
|
140
|
-
</div></div>
|
|
141
|
-
<div id="tag-dialog-container"></div>
|
|
142
|
-
<style>.tags-list{display:flex;flex-direction:column;gap:0.75rem}.tag-item{display:flex;align-items:center;gap:1rem;padding:0.75rem;border:1px solid var(--color-border,#e5e7eb);border-radius:0.5rem}.tag-dot{width:20px;height:20px;border-radius:50%;flex-shrink:0}.tag-info{flex:1}.tag-name{font-weight:500;display:block}.tag-usage{font-size:0.75rem;color:var(--color-text-muted,#6b7280);display:block}.tag-actions{display:flex;gap:0.5rem}.dialog-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000}.dialog-content{background:var(--color-surface,white);border-radius:0.5rem;box-shadow:0 10px 25px rgba(0,0,0,0.2);max-width:500px;width:90%;color:var(--color-text,#111)}.dialog-header{padding:1.5rem;border-bottom:1px solid var(--color-border,#e5e7eb);display:flex;justify-content:space-between;align-items:center}.dialog-body{padding:1.5rem}.dialog-footer{padding:1rem 1.5rem;border-top:1px solid var(--color-border,#e5e7eb);display:flex;justify-content:flex-end;gap:0.75rem}.form-group{margin-bottom:1rem}.label{display:block;margin-bottom:0.5rem;font-weight:500}</style>
|
|
143
|
-
`;
|
|
144
|
-
}
|