sqlite-hub 0.7.0 → 0.9.0
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/changelog.md +25 -0
- package/docs/DESIGN_GUIDELINES.md +36 -0
- package/frontend/index.html +79 -0
- package/frontend/js/api.js +67 -0
- package/frontend/js/app.js +1401 -921
- package/frontend/js/components/connectionCard.js +3 -4
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/modal.js +239 -30
- package/frontend/js/components/queryEditor.js +11 -8
- package/frontend/js/components/queryHistoryDetail.js +27 -8
- package/frontend/js/components/queryHistoryPanel.js +6 -5
- package/frontend/js/components/rowEditorPanel.js +84 -58
- package/frontend/js/components/sidebar.js +76 -33
- package/frontend/js/components/structureGraph.js +629 -715
- package/frontend/js/components/tableDesignerEditor.js +9 -8
- package/frontend/js/components/tableDesignerSidebar.js +11 -10
- package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
- package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
- package/frontend/js/router.js +81 -71
- package/frontend/js/store.js +3095 -2165
- package/frontend/js/views/charts.js +68 -40
- package/frontend/js/views/connections.js +5 -17
- package/frontend/js/views/data.js +40 -27
- package/frontend/js/views/editor.js +172 -177
- package/frontend/js/views/mediaTagging.js +861 -0
- package/frontend/js/views/overview.js +149 -10
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +74 -70
- package/frontend/js/views/tableDesigner.js +7 -2
- package/frontend/styles/base.css +73 -1
- package/frontend/styles/components.css +105 -105
- package/frontend/styles/structure-graph.css +19 -82
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +823 -30
- package/package.json +1 -1
- package/server/routes/charts.js +4 -1
- package/server/routes/data.js +14 -0
- package/server/routes/mediaTagging.js +166 -0
- package/server/routes/sql.js +1 -0
- package/server/server.js +4 -0
- package/server/services/sqlite/dataBrowserService.js +25 -0
- package/server/services/sqlite/exportService.js +31 -1
- package/server/services/sqlite/mediaTaggingService.js +1689 -0
- package/server/services/sqlite/overviewService.js +68 -0
- package/server/services/storage/appStateStore.js +321 -2
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import { escapeHtml, formatCellValue, formatNumber, highlightSql, truncateMiddle } from '../utils/format.js';
|
|
2
|
+
import {
|
|
3
|
+
hasDefaultMediaTaggingTagTable,
|
|
4
|
+
hasDefaultMediaTaggingMappingTable,
|
|
5
|
+
MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
|
|
6
|
+
MEDIA_TAGGING_DEFAULT_TAG_TABLE,
|
|
7
|
+
} from '../lib/mediaTaggingDefaults.js';
|
|
8
|
+
|
|
9
|
+
function normalizeDraft(draft = {}) {
|
|
10
|
+
return {
|
|
11
|
+
tagTable: MEDIA_TAGGING_DEFAULT_TAG_TABLE,
|
|
12
|
+
mediaTable: String(draft.mediaTable ?? '').trim(),
|
|
13
|
+
pathColumn: String(draft.pathColumn ?? '').trim(),
|
|
14
|
+
taggedColumn: String(draft.taggedColumn ?? '').trim(),
|
|
15
|
+
untaggedQuery: String(draft.untaggedQuery ?? ''),
|
|
16
|
+
taggedQuery: String(draft.taggedQuery ?? ''),
|
|
17
|
+
mappingTable: MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasConfigResetNotice(state) {
|
|
22
|
+
if (!state.mediaTagging.persistedConfig) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const persisted = normalizeDraft(state.mediaTagging.persistedConfig ?? {});
|
|
27
|
+
const draft = normalizeDraft(state.mediaTagging.draft ?? {});
|
|
28
|
+
|
|
29
|
+
return JSON.stringify(persisted) !== JSON.stringify(draft);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getMediaTaggingIssueKey(issue = {}) {
|
|
33
|
+
return `issue:${String(issue.scope ?? '').trim()}:${String(issue.code ?? '').trim()}:${String(issue.message ?? '').trim()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getMediaTaggingRouteErrorKey(error = {}) {
|
|
37
|
+
return `route:${String(error.code ?? '').trim()}:${String(error.message ?? '').trim()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderIssueCard({ code, message, toneClass = '', issueKey }) {
|
|
41
|
+
return `
|
|
42
|
+
<article class="media-tagging-issue ${toneClass}">
|
|
43
|
+
<button
|
|
44
|
+
class="media-tagging-issue__dismiss"
|
|
45
|
+
data-action="dismiss-media-tagging-issue"
|
|
46
|
+
data-issue-key="${escapeHtml(issueKey)}"
|
|
47
|
+
type="button"
|
|
48
|
+
aria-label="Hide issue"
|
|
49
|
+
title="Hide issue"
|
|
50
|
+
>
|
|
51
|
+
×
|
|
52
|
+
</button>
|
|
53
|
+
<div class="media-tagging-issue__code">${escapeHtml(code)}</div>
|
|
54
|
+
<div class="mt-2">${escapeHtml(message)}</div>
|
|
55
|
+
</article>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderIssueList(state) {
|
|
60
|
+
const routeError = state.mediaTagging.error;
|
|
61
|
+
const issues = state.mediaTagging.issues ?? [];
|
|
62
|
+
const dismissedIssueKeys = new Set(state.mediaTagging.dismissedIssueKeys ?? []);
|
|
63
|
+
|
|
64
|
+
const routeErrorMarkup =
|
|
65
|
+
routeError && !dismissedIssueKeys.has(getMediaTaggingRouteErrorKey(routeError))
|
|
66
|
+
? renderIssueCard({
|
|
67
|
+
code: routeError.code,
|
|
68
|
+
message: routeError.message,
|
|
69
|
+
toneClass: 'is-error',
|
|
70
|
+
issueKey: getMediaTaggingRouteErrorKey(routeError),
|
|
71
|
+
})
|
|
72
|
+
: '';
|
|
73
|
+
const issuesMarkup = issues
|
|
74
|
+
.filter(issue => !dismissedIssueKeys.has(getMediaTaggingIssueKey(issue)))
|
|
75
|
+
.map(issue =>
|
|
76
|
+
renderIssueCard({
|
|
77
|
+
code: issue.code,
|
|
78
|
+
message: issue.message,
|
|
79
|
+
toneClass: issue.severity === 'success' ? 'is-success' : '',
|
|
80
|
+
issueKey: getMediaTaggingIssueKey(issue),
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
.join('');
|
|
84
|
+
|
|
85
|
+
if (!routeErrorMarkup && !issuesMarkup) {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `
|
|
90
|
+
<section class="media-tagging-issues">
|
|
91
|
+
${routeErrorMarkup}
|
|
92
|
+
${issuesMarkup}
|
|
93
|
+
</section>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderSqlTextarea({ label, value = '', dataBind, dataField }) {
|
|
98
|
+
return `
|
|
99
|
+
<label class="media-tagging-field">
|
|
100
|
+
<span class="media-tagging-field__label">${escapeHtml(label)}</span>
|
|
101
|
+
<div class="sql-highlight-shell sql-highlight-shell--media">
|
|
102
|
+
<div class="query-editor-layer sql-highlight-layer">
|
|
103
|
+
<div
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
class="query-editor-highlight sql-highlight-content"
|
|
106
|
+
data-query-editor-highlight
|
|
107
|
+
>${value ? highlightSql(value) : ''}</div>
|
|
108
|
+
<textarea
|
|
109
|
+
class="query-editor-input sql-highlight-input custom-scrollbar"
|
|
110
|
+
data-bind="${escapeHtml(dataBind)}"
|
|
111
|
+
data-field="${escapeHtml(dataField)}"
|
|
112
|
+
data-sql-highlight="true"
|
|
113
|
+
spellcheck="false"
|
|
114
|
+
>${escapeHtml(value)}</textarea>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</label>
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderOptionList(options = [], selectedValue, placeholder) {
|
|
122
|
+
return [
|
|
123
|
+
`<option value="">${escapeHtml(placeholder)}</option>`,
|
|
124
|
+
...options.map(
|
|
125
|
+
option => `
|
|
126
|
+
<option value="${escapeHtml(option.value)}" ${option.value === selectedValue ? 'selected' : ''}>
|
|
127
|
+
${escapeHtml(option.label)}
|
|
128
|
+
</option>
|
|
129
|
+
`,
|
|
130
|
+
),
|
|
131
|
+
].join('');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderTagFormField(column, value) {
|
|
135
|
+
if (column.inputKind === 'checkbox') {
|
|
136
|
+
return `
|
|
137
|
+
<div class="media-tagging-field">
|
|
138
|
+
<label class="standard-checkbox table-designer-check table-designer-checkbox-override">
|
|
139
|
+
<input
|
|
140
|
+
data-bind="media-tagging-tag-form-field"
|
|
141
|
+
data-field="${escapeHtml(column.name)}"
|
|
142
|
+
type="checkbox"
|
|
143
|
+
${value ? 'checked' : ''}
|
|
144
|
+
/>
|
|
145
|
+
<span class="media-tagging-field__meta">
|
|
146
|
+
<span class="media-tagging-field__label">${escapeHtml(column.name)}</span>
|
|
147
|
+
<span class="media-tagging-field__hint">${escapeHtml(
|
|
148
|
+
column.declaredType || column.affinity || 'BOOLEAN',
|
|
149
|
+
)}</span>
|
|
150
|
+
</span>
|
|
151
|
+
</label>
|
|
152
|
+
</div>
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return `
|
|
157
|
+
<label class="media-tagging-field">
|
|
158
|
+
<span class="media-tagging-field__label">${escapeHtml(column.name)}</span>
|
|
159
|
+
<input
|
|
160
|
+
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
161
|
+
data-bind="media-tagging-tag-form-field"
|
|
162
|
+
data-field="${escapeHtml(column.name)}"
|
|
163
|
+
placeholder="name…"
|
|
164
|
+
type="${column.inputKind === 'number' ? 'number' : 'text'}"
|
|
165
|
+
value="${escapeHtml(value ?? '')}"
|
|
166
|
+
/>
|
|
167
|
+
</label>
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderParentTagFields({
|
|
172
|
+
parentToggleColumn = null,
|
|
173
|
+
parentSelectColumn = null,
|
|
174
|
+
tagFormValues = {},
|
|
175
|
+
parentTagOptions = [],
|
|
176
|
+
} = {}) {
|
|
177
|
+
if (!parentToggleColumn && !parentSelectColumn) {
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isCreatingParentTag = Boolean(parentToggleColumn ? tagFormValues[parentToggleColumn.name] : false);
|
|
182
|
+
const selectedParentTagId = parentSelectColumn ? String(tagFormValues[parentSelectColumn.name] ?? '') : '';
|
|
183
|
+
|
|
184
|
+
return `
|
|
185
|
+
${
|
|
186
|
+
parentSelectColumn
|
|
187
|
+
? `
|
|
188
|
+
<label class="media-tagging-field">
|
|
189
|
+
<span class="media-tagging-field__label">Parent Tag</span>
|
|
190
|
+
<select
|
|
191
|
+
class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
192
|
+
data-bind="media-tagging-tag-form-field"
|
|
193
|
+
data-field="${escapeHtml(parentSelectColumn.name)}"
|
|
194
|
+
${isCreatingParentTag ? 'disabled' : ''}
|
|
195
|
+
>
|
|
196
|
+
${renderOptionList(
|
|
197
|
+
parentTagOptions.map(tag => ({
|
|
198
|
+
value: String(tag.identityValue ?? ''),
|
|
199
|
+
label: tag.label,
|
|
200
|
+
})),
|
|
201
|
+
selectedParentTagId,
|
|
202
|
+
parentTagOptions.length ? 'Select a parent tag' : 'No parent tags available',
|
|
203
|
+
)}
|
|
204
|
+
</select>
|
|
205
|
+
</label>
|
|
206
|
+
`
|
|
207
|
+
: ''
|
|
208
|
+
}
|
|
209
|
+
${
|
|
210
|
+
parentToggleColumn
|
|
211
|
+
? `
|
|
212
|
+
<div class="media-tagging-field">
|
|
213
|
+
<label class="standard-checkbox table-designer-check table-designer-checkbox-override">
|
|
214
|
+
<input
|
|
215
|
+
data-bind="media-tagging-tag-form-field"
|
|
216
|
+
data-field="${escapeHtml(parentToggleColumn.name)}"
|
|
217
|
+
type="checkbox"
|
|
218
|
+
${isCreatingParentTag ? 'checked' : ''}
|
|
219
|
+
/>
|
|
220
|
+
<span class="media-tagging-field__meta">
|
|
221
|
+
<span class="media-tagging-field__label">Create Parent Tag</span>
|
|
222
|
+
</span>
|
|
223
|
+
</label>
|
|
224
|
+
</div>
|
|
225
|
+
`
|
|
226
|
+
: ''
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderTagList(tags = [], selectedTagKeys = []) {
|
|
232
|
+
const selectedSet = new Set(selectedTagKeys);
|
|
233
|
+
|
|
234
|
+
return `
|
|
235
|
+
<div class="media-tagging-tag-list custom-scrollbar">
|
|
236
|
+
${
|
|
237
|
+
tags.length
|
|
238
|
+
? tags
|
|
239
|
+
.map(
|
|
240
|
+
tag => `
|
|
241
|
+
<label
|
|
242
|
+
class="media-tagging-tag-option ${selectedSet.has(tag.key) ? 'is-selected' : ''}"
|
|
243
|
+
data-tag-search-text="${escapeHtml(
|
|
244
|
+
`${String(tag.label ?? '')} ${String(tag.parentTagLabel ?? '')}`.trim().toLowerCase(),
|
|
245
|
+
)}"
|
|
246
|
+
>
|
|
247
|
+
<input
|
|
248
|
+
class="media-tagging-tag-option__checkbox"
|
|
249
|
+
data-bind="media-tagging-tag-selection"
|
|
250
|
+
data-tag-key="${escapeHtml(tag.key)}"
|
|
251
|
+
type="checkbox"
|
|
252
|
+
${selectedSet.has(tag.key) ? 'checked' : ''}
|
|
253
|
+
/>
|
|
254
|
+
<span class="media-tagging-tag-option__content">
|
|
255
|
+
<span class="media-tagging-tag-option__text">${escapeHtml(tag.label)}</span>
|
|
256
|
+
</span>
|
|
257
|
+
${
|
|
258
|
+
tag.parentTagLabel
|
|
259
|
+
? `<span class="media-tagging-tag-option__badge">${escapeHtml(tag.parentTagLabel)}</span>`
|
|
260
|
+
: ''
|
|
261
|
+
}
|
|
262
|
+
</label>
|
|
263
|
+
`,
|
|
264
|
+
)
|
|
265
|
+
.join('')
|
|
266
|
+
: `<div class="text-sm text-on-surface-variant/55">No tags available yet.</div>`
|
|
267
|
+
}
|
|
268
|
+
</div>
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getWorkflowMetadataEntries(currentItem) {
|
|
273
|
+
return Object.entries(currentItem?.row ?? {}).filter(([key]) => key !== '__sqlite_hub_media_rowid');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderCreatedTagList(tags = [], { canRemove = false, removingTagKey = null } = {}) {
|
|
277
|
+
if (!tags.length) {
|
|
278
|
+
return `
|
|
279
|
+
<div class="text-sm text-on-surface-variant/55">
|
|
280
|
+
No tags have been created yet for the selected tag table.
|
|
281
|
+
</div>
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return `
|
|
286
|
+
<div class="media-tagging-created-tags custom-scrollbar">
|
|
287
|
+
${tags
|
|
288
|
+
.map(
|
|
289
|
+
tag => `
|
|
290
|
+
<article class="media-tagging-created-tag">
|
|
291
|
+
<div class="media-tagging-created-tag__content">
|
|
292
|
+
<div class="media-tagging-created-tag__label">${escapeHtml(tag.label)}</div>
|
|
293
|
+
${
|
|
294
|
+
tag.isParentTag || tag.parentTagLabel
|
|
295
|
+
? `
|
|
296
|
+
<div class="media-tagging-created-tag__meta">
|
|
297
|
+
${tag.isParentTag ? '<span class="media-tagging-created-tag__badge">Parent</span>' : ''}
|
|
298
|
+
${
|
|
299
|
+
tag.parentTagLabel
|
|
300
|
+
? `<span class="media-tagging-created-tag__badge">${escapeHtml(tag.parentTagLabel)}</span>`
|
|
301
|
+
: ''
|
|
302
|
+
}
|
|
303
|
+
</div>
|
|
304
|
+
`
|
|
305
|
+
: ''
|
|
306
|
+
}
|
|
307
|
+
</div>
|
|
308
|
+
<button
|
|
309
|
+
aria-label="Remove tag"
|
|
310
|
+
class="delete-button media-tagging-created-tag__remove"
|
|
311
|
+
data-action="remove-media-tag"
|
|
312
|
+
data-tag-key="${escapeHtml(tag.key)}"
|
|
313
|
+
type="button"
|
|
314
|
+
title="Remove tag"
|
|
315
|
+
${canRemove && removingTagKey !== tag.key ? '' : 'disabled'}
|
|
316
|
+
>
|
|
317
|
+
<span class="material-symbols-outlined text-[18px]">
|
|
318
|
+
${removingTagKey === tag.key ? 'hourglass_top' : 'delete'}
|
|
319
|
+
</span>
|
|
320
|
+
</button>
|
|
321
|
+
</article>
|
|
322
|
+
`,
|
|
323
|
+
)
|
|
324
|
+
.join('')}
|
|
325
|
+
</div>
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function renderPreviewMedia(currentItem, { detailsVisible = true, mediaTableName = '' } = {}) {
|
|
330
|
+
if (!currentItem) {
|
|
331
|
+
return `
|
|
332
|
+
<div class="media-tagging-preview__empty">
|
|
333
|
+
<span class="material-symbols-outlined text-4xl">photo_size_select_large</span>
|
|
334
|
+
<div class="mt-3 font-headline text-lg uppercase tracking-[0.06em] text-primary-container">
|
|
335
|
+
Queue Empty
|
|
336
|
+
</div>
|
|
337
|
+
<div class="mt-2 text-sm text-on-surface-variant/55">
|
|
338
|
+
No current media item is available with the active configuration.
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const pathValue = String(currentItem.path ?? '');
|
|
345
|
+
const fileName = pathValue.split(/[\\/]/).filter(Boolean).pop() || 'Audio file';
|
|
346
|
+
const isAudioPreview = Boolean(currentItem.previewUrl && currentItem.previewKind === 'audio');
|
|
347
|
+
let assetMarkup = `
|
|
348
|
+
<div class="media-tagging-preview__placeholder">
|
|
349
|
+
<span class="material-symbols-outlined text-5xl">perm_media</span>
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
if (currentItem.previewUrl && currentItem.previewKind === 'image') {
|
|
354
|
+
assetMarkup = `
|
|
355
|
+
<img
|
|
356
|
+
class="media-tagging-preview__asset"
|
|
357
|
+
src="${escapeHtml(currentItem.previewUrl)}"
|
|
358
|
+
alt="${escapeHtml(pathValue || 'Current media item')}"
|
|
359
|
+
/>
|
|
360
|
+
`;
|
|
361
|
+
} else if (currentItem.previewUrl && currentItem.previewKind === 'video') {
|
|
362
|
+
assetMarkup = `
|
|
363
|
+
<video class="media-tagging-preview__asset" controls preload="metadata" src="${escapeHtml(
|
|
364
|
+
currentItem.previewUrl,
|
|
365
|
+
)}"></video>
|
|
366
|
+
`;
|
|
367
|
+
} else if (currentItem.previewUrl && currentItem.previewKind === 'audio') {
|
|
368
|
+
assetMarkup = `
|
|
369
|
+
<div class="media-tagging-audio-preview">
|
|
370
|
+
<div class="media-tagging-audio-preview__icon">
|
|
371
|
+
<span class="material-symbols-outlined">audio_file</span>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="media-tagging-audio-preview__content">
|
|
374
|
+
<div class="media-tagging-audio-preview__eyebrow">Audio Preview</div>
|
|
375
|
+
<div class="media-tagging-audio-preview__title" title="${escapeHtml(fileName)}">
|
|
376
|
+
${escapeHtml(truncateMiddle(fileName, 56))}
|
|
377
|
+
</div>
|
|
378
|
+
<audio
|
|
379
|
+
class="media-tagging-audio-preview__player"
|
|
380
|
+
controls
|
|
381
|
+
preload="metadata"
|
|
382
|
+
src="${escapeHtml(currentItem.previewUrl)}"
|
|
383
|
+
></audio>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const metadata = getWorkflowMetadataEntries(currentItem);
|
|
390
|
+
const toggleLabel = detailsVisible
|
|
391
|
+
? '<span class="material-symbols-outlined">visibility_off</span> Shrink Media Viewer'
|
|
392
|
+
: 'Show Media Viewer';
|
|
393
|
+
|
|
394
|
+
return `
|
|
395
|
+
<div class="media-tagging-preview ${detailsVisible ? '' : 'media-tagging-preview--meta-hidden'}">
|
|
396
|
+
<div class="media-tagging-preview__media${isAudioPreview ? ' media-tagging-preview__media--audio' : ''}">
|
|
397
|
+
<div class="media-tagging-preview__media-toolbar">
|
|
398
|
+
<button
|
|
399
|
+
class="standard-button media-tagging-preview__toggle"
|
|
400
|
+
data-action="toggle-media-tagging-current-media"
|
|
401
|
+
data-next-value="${detailsVisible ? 'false' : 'true'}"
|
|
402
|
+
data-expanded-label="Shrink Media Viewer"
|
|
403
|
+
data-collapsed-label="Show Media Viewer"
|
|
404
|
+
aria-expanded="${detailsVisible ? 'true' : 'false'}"
|
|
405
|
+
type="button"
|
|
406
|
+
>
|
|
407
|
+
${toggleLabel}
|
|
408
|
+
</button>
|
|
409
|
+
</div>
|
|
410
|
+
${assetMarkup}
|
|
411
|
+
</div>
|
|
412
|
+
<div class="media-tagging-preview__meta">
|
|
413
|
+
<div class="media-tagging-preview__meta-header">
|
|
414
|
+
<div class="media-tagging-preview__eyebrow">Current Media</div>
|
|
415
|
+
${
|
|
416
|
+
mediaTableName && currentItem.identity
|
|
417
|
+
? `
|
|
418
|
+
<button
|
|
419
|
+
class="standard-button media-tagging-preview__meta-action"
|
|
420
|
+
data-action="open-media-tagging-current-in-data"
|
|
421
|
+
type="button"
|
|
422
|
+
>
|
|
423
|
+
Open In Data
|
|
424
|
+
</button>
|
|
425
|
+
`
|
|
426
|
+
: ''
|
|
427
|
+
}
|
|
428
|
+
</div>
|
|
429
|
+
${
|
|
430
|
+
metadata.length
|
|
431
|
+
? `
|
|
432
|
+
<div class="media-tagging-preview__metadata">
|
|
433
|
+
${metadata
|
|
434
|
+
.map(
|
|
435
|
+
([key, value]) => `
|
|
436
|
+
<div class="media-tagging-preview__metadata-row">
|
|
437
|
+
<span>${escapeHtml(key)}</span>
|
|
438
|
+
<span title="${escapeHtml(formatCellValue(value))}">
|
|
439
|
+
${escapeHtml(formatCellValue(value))}
|
|
440
|
+
</span>
|
|
441
|
+
</div>
|
|
442
|
+
`,
|
|
443
|
+
)
|
|
444
|
+
.join('')}
|
|
445
|
+
</div>
|
|
446
|
+
`
|
|
447
|
+
: ''
|
|
448
|
+
}
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function renderTagsSection(state) {
|
|
455
|
+
const tagColumns = state.mediaTagging.tagTableColumns ?? [];
|
|
456
|
+
const tagFormValues = state.mediaTagging.tagFormValues ?? {};
|
|
457
|
+
const tags = state.mediaTagging.tags ?? [];
|
|
458
|
+
const tagTableExists = hasDefaultMediaTaggingTagTable(state.mediaTagging.schemaTables ?? []);
|
|
459
|
+
const parentToggleColumn = tagColumns.find(column => column.uiRole === 'parent-toggle') ?? null;
|
|
460
|
+
const parentSelectColumn = tagColumns.find(column => column.uiRole === 'parent-select') ?? null;
|
|
461
|
+
const standardTagColumns = tagColumns.filter(column => !column.uiRole);
|
|
462
|
+
const parentTagOptions = tags.filter(tag => tag.isParentTag);
|
|
463
|
+
|
|
464
|
+
return `
|
|
465
|
+
<section class="media-tagging-card shell-section">
|
|
466
|
+
<div class="media-tagging-card__header">
|
|
467
|
+
<div>
|
|
468
|
+
<div class="media-tagging-card__eyebrow">1. Tags</div>
|
|
469
|
+
<h2 class="media-tagging-card__title">Tag Table</h2>
|
|
470
|
+
</div>
|
|
471
|
+
<div class="flex flex-col items-end gap-3">
|
|
472
|
+
<span class="status-badge ${tagTableExists ? 'status-badge--success' : ''}">
|
|
473
|
+
${tagTableExists ? 'Available' : 'Missing'}
|
|
474
|
+
</span>
|
|
475
|
+
<button
|
|
476
|
+
class="standard-button"
|
|
477
|
+
data-action="open-modal"
|
|
478
|
+
data-modal="create-media-tagging-tag-table"
|
|
479
|
+
type="button"
|
|
480
|
+
>
|
|
481
|
+
Create Tag Table
|
|
482
|
+
</button>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="media-tagging-card__body">
|
|
486
|
+
${
|
|
487
|
+
tagColumns.length
|
|
488
|
+
? `
|
|
489
|
+
<div class="media-tagging-form-grid">
|
|
490
|
+
${standardTagColumns.map(column => renderTagFormField(column, tagFormValues[column.name])).join('')}
|
|
491
|
+
${renderParentTagFields({
|
|
492
|
+
parentToggleColumn,
|
|
493
|
+
parentSelectColumn,
|
|
494
|
+
tagFormValues,
|
|
495
|
+
parentTagOptions,
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
499
|
+
<button
|
|
500
|
+
class="standard-button"
|
|
501
|
+
data-action="create-media-tag"
|
|
502
|
+
type="button"
|
|
503
|
+
${state.mediaTagging.creatingTag || state.mediaTagging.connection?.readOnly ? 'disabled' : ''}
|
|
504
|
+
><span class="material-symbols-outlined">sell</span>
|
|
505
|
+
${state.mediaTagging.creatingTag ? 'Saving...' : 'Create Tag'}
|
|
506
|
+
</button>
|
|
507
|
+
<div class="text-[11px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/45">
|
|
508
|
+
${escapeHtml(formatNumber(tags.length))} existing tag(s)
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="media-tagging-created-tags-shell">
|
|
512
|
+
<div class="media-tagging-field__label">Created Tags</div>
|
|
513
|
+
<div class="media-tagging-created-tags-frame mt-3">
|
|
514
|
+
${renderCreatedTagList(tags, {
|
|
515
|
+
canRemove: !state.mediaTagging.connection?.readOnly,
|
|
516
|
+
removingTagKey: state.mediaTagging.removingTagKey,
|
|
517
|
+
})}
|
|
518
|
+
</div>
|
|
519
|
+
<div class="mt-3">
|
|
520
|
+
<button
|
|
521
|
+
class="standard-button"
|
|
522
|
+
data-action="copy-media-tags"
|
|
523
|
+
type="button"
|
|
524
|
+
${tags.length ? '' : 'disabled'}
|
|
525
|
+
>
|
|
526
|
+
<span class="material-symbols-outlined text-sm">content_copy</span>
|
|
527
|
+
Copy Tags to clipboard
|
|
528
|
+
</button>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
`
|
|
532
|
+
: `
|
|
533
|
+
<div class="text-sm text-on-surface-variant/55">
|
|
534
|
+
${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} must exist to generate the tag creation form.
|
|
535
|
+
</div>
|
|
536
|
+
`
|
|
537
|
+
}
|
|
538
|
+
</div>
|
|
539
|
+
</section>
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function renderTaggingSection(state) {
|
|
544
|
+
const tables = state.mediaTagging.schemaTables ?? [];
|
|
545
|
+
const draft = normalizeDraft(state.mediaTagging.draft ?? {});
|
|
546
|
+
const mediaTableColumns = state.mediaTagging.mediaTableColumns ?? [];
|
|
547
|
+
const pathCandidates = state.mediaTagging.pathCandidates ?? [];
|
|
548
|
+
const booleanCandidates = state.mediaTagging.booleanCandidates ?? [];
|
|
549
|
+
|
|
550
|
+
return `
|
|
551
|
+
<section class="media-tagging-card shell-section">
|
|
552
|
+
<div class="media-tagging-card__header">
|
|
553
|
+
<div>
|
|
554
|
+
<div class="media-tagging-card__eyebrow">2. Tagging</div>
|
|
555
|
+
<h2 class="media-tagging-card__title">Media Source</h2>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="media-tagging-card__body">
|
|
559
|
+
<div class="media-tagging-form-grid">
|
|
560
|
+
<label class="media-tagging-field">
|
|
561
|
+
<span class="media-tagging-field__label">Media Table</span>
|
|
562
|
+
<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="mediaTable">
|
|
563
|
+
${renderOptionList(
|
|
564
|
+
tables
|
|
565
|
+
.map(table => ({ value: table.name, label: table.name }))
|
|
566
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
567
|
+
draft.mediaTable,
|
|
568
|
+
'Select a media table',
|
|
569
|
+
)}
|
|
570
|
+
</select>
|
|
571
|
+
</label>
|
|
572
|
+
<div></div>
|
|
573
|
+
<label class="media-tagging-field">
|
|
574
|
+
<span class="media-tagging-field__label">Path Column</span>
|
|
575
|
+
<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="pathColumn">
|
|
576
|
+
${renderOptionList(
|
|
577
|
+
mediaTableColumns
|
|
578
|
+
.map(column => ({
|
|
579
|
+
value: column.name,
|
|
580
|
+
label: pathCandidates.includes(column.name) ? `${column.name} // suggested` : column.name,
|
|
581
|
+
}))
|
|
582
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
583
|
+
draft.pathColumn,
|
|
584
|
+
'Select the media path column',
|
|
585
|
+
)}
|
|
586
|
+
</select>
|
|
587
|
+
</label>
|
|
588
|
+
<label class="media-tagging-field">
|
|
589
|
+
<span class="media-tagging-field__label">Tagged Boolean Column</span>
|
|
590
|
+
<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" data-bind="media-tagging-field" data-field="taggedColumn">
|
|
591
|
+
${renderOptionList(
|
|
592
|
+
mediaTableColumns
|
|
593
|
+
.map(column => ({
|
|
594
|
+
value: column.name,
|
|
595
|
+
label: booleanCandidates.includes(column.name) ? `${column.name} // suggested` : column.name,
|
|
596
|
+
}))
|
|
597
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
598
|
+
draft.taggedColumn,
|
|
599
|
+
mediaTableColumns.length ? 'Select the tagged flag column' : 'No columns available',
|
|
600
|
+
)}
|
|
601
|
+
</select>
|
|
602
|
+
</label>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
<div class="media-tagging-query-grid">
|
|
606
|
+
${renderSqlTextarea({
|
|
607
|
+
label: 'Untagged Query',
|
|
608
|
+
value: draft.untaggedQuery,
|
|
609
|
+
dataBind: 'media-tagging-field',
|
|
610
|
+
dataField: 'untaggedQuery',
|
|
611
|
+
})}
|
|
612
|
+
${renderSqlTextarea({
|
|
613
|
+
label: 'Tagged Query',
|
|
614
|
+
value: draft.taggedQuery,
|
|
615
|
+
dataBind: 'media-tagging-field',
|
|
616
|
+
dataField: 'taggedQuery',
|
|
617
|
+
})}
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
621
|
+
<button
|
|
622
|
+
class="standard-button"
|
|
623
|
+
data-action="reset-media-tagging-queries"
|
|
624
|
+
type="button"
|
|
625
|
+
>
|
|
626
|
+
Reset Queries to Defaults
|
|
627
|
+
</button>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
</section>
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function renderMappingSection(state) {
|
|
635
|
+
const candidates = state.mediaTagging.mappingCandidates ?? [];
|
|
636
|
+
const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
|
|
637
|
+
const mappingCandidate =
|
|
638
|
+
candidates.find(candidate => candidate.tableName === MEDIA_TAGGING_DEFAULT_MAPPING_TABLE) ?? null;
|
|
639
|
+
|
|
640
|
+
return `
|
|
641
|
+
<section class="media-tagging-card shell-section">
|
|
642
|
+
<div class="media-tagging-card__header">
|
|
643
|
+
<div>
|
|
644
|
+
<div class="media-tagging-card__eyebrow">3. Mapping</div>
|
|
645
|
+
<h2 class="media-tagging-card__title">Join Table</h2>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="flex flex-col items-end gap-3">
|
|
648
|
+
<span class="status-badge ${mappingExists ? 'status-badge--success' : ''}">
|
|
649
|
+
${mappingExists ? 'Available' : 'Missing'}
|
|
650
|
+
</span>
|
|
651
|
+
<button
|
|
652
|
+
class="standard-button"
|
|
653
|
+
data-action="open-modal"
|
|
654
|
+
data-modal="create-media-tagging-mapping-table"
|
|
655
|
+
type="button"
|
|
656
|
+
>
|
|
657
|
+
Create Mapping Table
|
|
658
|
+
</button>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
<div class="media-tagging-card__body media-tagging-card__body--mapping">
|
|
662
|
+
<article class="media-tagging-mapping-card ${mappingCandidate ? 'is-selected' : ''}">
|
|
663
|
+
<div class="font-headline text-sm uppercase tracking-[0.08em] text-primary-container">
|
|
664
|
+
${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)}
|
|
665
|
+
</div>
|
|
666
|
+
${
|
|
667
|
+
mappingCandidate
|
|
668
|
+
? `
|
|
669
|
+
<div class="mt-2 text-xs text-on-surface-variant/65">
|
|
670
|
+
Media FK:
|
|
671
|
+
${escapeHtml(
|
|
672
|
+
mappingCandidate.mediaForeignKey.mappings
|
|
673
|
+
.map(mapping => `${mapping.from} -> ${mapping.to}`)
|
|
674
|
+
.join(', '),
|
|
675
|
+
)}
|
|
676
|
+
</div>
|
|
677
|
+
<div class="mt-1 text-xs text-on-surface-variant/65">
|
|
678
|
+
Tag FK:
|
|
679
|
+
${escapeHtml(
|
|
680
|
+
mappingCandidate.tagForeignKey.mappings
|
|
681
|
+
.map(mapping => `${mapping.from} -> ${mapping.to}`)
|
|
682
|
+
.join(', '),
|
|
683
|
+
)}
|
|
684
|
+
</div>
|
|
685
|
+
`
|
|
686
|
+
: `
|
|
687
|
+
<div class="mt-2 text-xs text-on-surface-variant/65">
|
|
688
|
+
${
|
|
689
|
+
mappingExists
|
|
690
|
+
? `${MEDIA_TAGGING_DEFAULT_MAPPING_TABLE} exists, but it does not currently resolve as the active media-to-tag mapping.`
|
|
691
|
+
: `${MEDIA_TAGGING_DEFAULT_MAPPING_TABLE} is missing. Open the create flow to inspect the SQL and create it.`
|
|
692
|
+
}
|
|
693
|
+
</div>
|
|
694
|
+
`
|
|
695
|
+
}
|
|
696
|
+
</article>
|
|
697
|
+
|
|
698
|
+
<div class="media-tagging-card__footer">
|
|
699
|
+
<button
|
|
700
|
+
class="signature-button w-full"
|
|
701
|
+
data-action="save-media-tagging"
|
|
702
|
+
type="button"
|
|
703
|
+
${state.mediaTagging.saving ? 'disabled' : ''}
|
|
704
|
+
>
|
|
705
|
+
${state.mediaTagging.saving ? 'Saving...' : 'Save Configuration'}
|
|
706
|
+
</button>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
</section>
|
|
711
|
+
`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function renderWorkflowSection(state) {
|
|
715
|
+
const workflow = state.mediaTagging.workflow ?? null;
|
|
716
|
+
const selectedTagKeys = state.mediaTagging.selectedTagKeys ?? [];
|
|
717
|
+
const selectedTagCount = selectedTagKeys.length;
|
|
718
|
+
const selectableTags = (state.mediaTagging.tags ?? []).filter(tag => !tag.isParentTag);
|
|
719
|
+
const canWrite = Boolean(workflow?.canWrite);
|
|
720
|
+
const hasResetNotice = hasConfigResetNotice(state);
|
|
721
|
+
const detailsVisible = state.mediaTagging.workflowMediaDetailsVisible !== false;
|
|
722
|
+
const mediaTableName = String(state.mediaTagging.draft?.mediaTable ?? '').trim();
|
|
723
|
+
const status = workflow?.status ?? {
|
|
724
|
+
taggedCount: 0,
|
|
725
|
+
remainingCount: 0,
|
|
726
|
+
totalCount: 0,
|
|
727
|
+
ratioLabel: '0 / 0',
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
return `
|
|
731
|
+
<section class="media-tagging-card shell-section media-tagging-card--workflow">
|
|
732
|
+
<div class="media-tagging-card__body media-tagging-card__body--workflow">
|
|
733
|
+
${
|
|
734
|
+
hasResetNotice
|
|
735
|
+
? `
|
|
736
|
+
<div class="media-tagging-inline-warning">
|
|
737
|
+
Changing mapping or tagging settings replaces the stored workflow configuration for this database.
|
|
738
|
+
</div>
|
|
739
|
+
`
|
|
740
|
+
: ''
|
|
741
|
+
}
|
|
742
|
+
${renderPreviewMedia(workflow?.currentItem ?? null, { detailsVisible, mediaTableName })}
|
|
743
|
+
<div class="media-tagging-workflow-sidebar">
|
|
744
|
+
<div class="media-tagging-tag-panel">
|
|
745
|
+
<div class="media-tagging-tag-panel__header">
|
|
746
|
+
<div class="media-tagging-tag-panel__header-row">
|
|
747
|
+
<div>
|
|
748
|
+
<div class="media-tagging-field__label">Available Tags</div>
|
|
749
|
+
<div class="mt-2 text-xs uppercase tracking-[0.12em] text-on-surface-variant/45">
|
|
750
|
+
${escapeHtml(formatNumber(selectableTags.length))} selectable tag(s)
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
<div class="media-tagging-status media-tagging-status--compact">
|
|
754
|
+
<div class="media-tagging-status__value">${escapeHtml(status.ratioLabel)}</div>
|
|
755
|
+
<div class="media-tagging-status__label">
|
|
756
|
+
tagged / total
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
<label class="media-tagging-field mt-4">
|
|
761
|
+
<span class="media-tagging-field__label">Search</span>
|
|
762
|
+
<input
|
|
763
|
+
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
764
|
+
data-bind="media-tagging-tag-search"
|
|
765
|
+
placeholder="Filter available tags"
|
|
766
|
+
type="text"
|
|
767
|
+
/>
|
|
768
|
+
</label>
|
|
769
|
+
</div>
|
|
770
|
+
${renderTagList(selectableTags, selectedTagKeys)}
|
|
771
|
+
<div class="media-tagging-tag-panel__footer">
|
|
772
|
+
<div class="flex flex-wrap items-center self-end gap-3">
|
|
773
|
+
<button
|
|
774
|
+
class="standard-button"
|
|
775
|
+
data-action="skip-media-tagging-item"
|
|
776
|
+
type="button"
|
|
777
|
+
${workflow?.currentItem && canWrite && !state.mediaTagging.previewLoading && !state.mediaTagging.applying ? '' : 'disabled'}
|
|
778
|
+
>
|
|
779
|
+
${state.mediaTagging.applying ? 'Saving...' : 'Skip'}
|
|
780
|
+
</button>
|
|
781
|
+
<button
|
|
782
|
+
class="signature-button"
|
|
783
|
+
data-action="apply-media-tagging"
|
|
784
|
+
data-can-apply="${workflow?.currentItem && canWrite && !state.mediaTagging.applying ? 'true' : 'false'}"
|
|
785
|
+
type="button"
|
|
786
|
+
${workflow?.currentItem && canWrite && !state.mediaTagging.applying && selectedTagCount > 0 ? '' : 'disabled'}
|
|
787
|
+
>
|
|
788
|
+
${state.mediaTagging.applying ? 'Saving...' : `${selectedTagCount} tagged & next`}
|
|
789
|
+
</button>
|
|
790
|
+
</div>
|
|
791
|
+
${
|
|
792
|
+
workflow?.allRemainingSkipped
|
|
793
|
+
? `
|
|
794
|
+
<button
|
|
795
|
+
class="standard-button"
|
|
796
|
+
data-action="reset-skipped-media-tagging"
|
|
797
|
+
type="button"
|
|
798
|
+
>
|
|
799
|
+
Show Skipped Items Again
|
|
800
|
+
</button>
|
|
801
|
+
`
|
|
802
|
+
: ''
|
|
803
|
+
}
|
|
804
|
+
<div class="text-[11px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/45">
|
|
805
|
+
${escapeHtml(formatNumber(workflow?.status?.remainingCount ?? 0))} remaining
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
811
|
+
</section>
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function renderMediaTaggingView(state, { subView = 'setup' } = {}) {
|
|
816
|
+
const showSetup = subView === 'setup';
|
|
817
|
+
const showQueue = subView === 'queue';
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
main: `
|
|
821
|
+
<section class="view-surface media-tagging-view">
|
|
822
|
+
<div class="media-tagging-shell">
|
|
823
|
+
<header class="media-tagging-header">
|
|
824
|
+
<div class="media-tagging-header__copy">
|
|
825
|
+
<div class="text-[10px] font-bold uppercase tracking-[0.22em] text-primary-container">
|
|
826
|
+
Media Tagging
|
|
827
|
+
</div>
|
|
828
|
+
<h1 class="mt-3 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
829
|
+
${showSetup ? 'Setup' : 'Tagging Queue'}
|
|
830
|
+
</h1>
|
|
831
|
+
<p class="mt-3 max-w-3xl text-sm leading-7 text-on-surface-variant/65">
|
|
832
|
+
${
|
|
833
|
+
showSetup
|
|
834
|
+
? 'Configure tags, media queries, and mapping once per database.'
|
|
835
|
+
: 'Review the next untagged media item, select tags, skip it, or mark it as tagged.'
|
|
836
|
+
}
|
|
837
|
+
</p>
|
|
838
|
+
</div>
|
|
839
|
+
</header>
|
|
840
|
+
|
|
841
|
+
${renderIssueList(state)}
|
|
842
|
+
|
|
843
|
+
${
|
|
844
|
+
showSetup
|
|
845
|
+
? `
|
|
846
|
+
<div class="media-tagging-grid">
|
|
847
|
+
${renderTagsSection(state)}
|
|
848
|
+
${renderTaggingSection(state)}
|
|
849
|
+
${renderMappingSection(state)}
|
|
850
|
+
</div>
|
|
851
|
+
`
|
|
852
|
+
: ''
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
${showQueue ? renderWorkflowSection(state) : ''}
|
|
856
|
+
</div>
|
|
857
|
+
</section>
|
|
858
|
+
`,
|
|
859
|
+
panel: '',
|
|
860
|
+
};
|
|
861
|
+
}
|