sqlite-hub 0.9.0 → 0.9.1
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 +5 -0
- package/frontend/js/app.js +160 -4
- package/frontend/js/components/queryHistoryDetail.js +14 -2
- package/frontend/js/components/queryHistoryPanel.js +4 -8
- package/frontend/js/store.js +54 -0
- package/frontend/js/views/mediaTagging.js +146 -76
- package/frontend/styles/components.css +6 -0
- package/frontend/styles/views.css +126 -12
- package/package.json +1 -1
- package/server/services/storage/appStateStore.js +9 -6
- package/server/services/storage/queryHistoryUtils.js +165 -2
- package/shortkeys.md +5 -0
package/changelog.md
CHANGED
package/frontend/js/app.js
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
toggleChartsResultsPanel,
|
|
88
88
|
toggleChartsSqlPanel,
|
|
89
89
|
setMediaTaggingWorkflowMediaDetailsVisible,
|
|
90
|
+
setMediaTaggingWorkflowMediaRotationDegrees,
|
|
90
91
|
queueTableDesignerCsvImport,
|
|
91
92
|
showToast,
|
|
92
93
|
submitCreateConnection,
|
|
@@ -137,6 +138,7 @@ const shellRefs = {
|
|
|
137
138
|
toast: document.querySelector('#toast-root'),
|
|
138
139
|
};
|
|
139
140
|
let lastRenderedRoutePath = null;
|
|
141
|
+
let lastRenderedRouteName = null;
|
|
140
142
|
let lastRenderedTopNavMarkup = '';
|
|
141
143
|
let lastRenderedSidebarMarkup = '';
|
|
142
144
|
let lastRenderedStatusBarMarkup = '';
|
|
@@ -148,11 +150,16 @@ let lastRenderedPanelOpen = false;
|
|
|
148
150
|
let lastRenderedLockedRoute = false;
|
|
149
151
|
let pendingNewTableDesignerAutofocus = false;
|
|
150
152
|
let pendingQueryEditorFocus = false;
|
|
153
|
+
let pendingMediaTaggingTagSearchFocus = false;
|
|
151
154
|
|
|
152
155
|
function invalidateMainRenderCache() {
|
|
153
156
|
lastRenderedMainMarkup = null;
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
function isMediaTaggingRouteName(routeName) {
|
|
160
|
+
return routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue';
|
|
161
|
+
}
|
|
162
|
+
|
|
156
163
|
function resetStructureGraphForDatabaseChange() {
|
|
157
164
|
resetPersistedStructureGraphState();
|
|
158
165
|
}
|
|
@@ -258,6 +265,45 @@ function syncMediaTaggingCurrentMediaUi(button, detailsVisible) {
|
|
|
258
265
|
return true;
|
|
259
266
|
}
|
|
260
267
|
|
|
268
|
+
function normalizeMediaTaggingRotationDegrees(value) {
|
|
269
|
+
const numericValue = Number(value);
|
|
270
|
+
|
|
271
|
+
if (!Number.isFinite(numericValue)) {
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function syncMediaTaggingMediaRotationUi(node, rotationDegrees) {
|
|
279
|
+
if (!(node instanceof HTMLElement)) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const preview = node.closest('.media-tagging-preview');
|
|
284
|
+
const target = preview?.querySelector('[data-media-tagging-rotation-target]');
|
|
285
|
+
|
|
286
|
+
if (!(preview instanceof HTMLElement) || !(target instanceof HTMLElement)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const normalizedRotation = normalizeMediaTaggingRotationDegrees(rotationDegrees);
|
|
291
|
+
|
|
292
|
+
target.style.setProperty('--media-tagging-preview-rotation', `${normalizedRotation}deg`);
|
|
293
|
+
target.dataset.rotationDegrees = String(normalizedRotation);
|
|
294
|
+
target.classList.toggle('is-rotated-quarter', normalizedRotation === 90 || normalizedRotation === 270);
|
|
295
|
+
|
|
296
|
+
const resetButton = preview.querySelector(
|
|
297
|
+
'[data-action="rotate-media-tagging-current-media"][data-rotation-command="reset"]',
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (resetButton instanceof HTMLButtonElement) {
|
|
301
|
+
resetButton.disabled = normalizedRotation === 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
261
307
|
function syncMediaTaggingTagSearchUi(input) {
|
|
262
308
|
if (!(input instanceof HTMLInputElement)) {
|
|
263
309
|
return false;
|
|
@@ -615,6 +661,52 @@ function focusQueryEditorInput() {
|
|
|
615
661
|
return true;
|
|
616
662
|
}
|
|
617
663
|
|
|
664
|
+
function focusMediaTaggingTagSearchInput() {
|
|
665
|
+
const input = document.querySelector('[data-bind="media-tagging-tag-search"]');
|
|
666
|
+
|
|
667
|
+
if (!(input instanceof HTMLInputElement)) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
input.focus({ preventScroll: true });
|
|
672
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function syncSidebarActiveRoute(routeName) {
|
|
677
|
+
if (!isMediaTaggingRouteName(routeName)) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const mediaTaggingLink = shellRefs.sidebar.querySelector('a.sidebar-link[data-group="mediaTagging"]');
|
|
682
|
+
const setupLink = shellRefs.sidebar.querySelector('a.sidebar-sublink[href="#/media-tagging"]');
|
|
683
|
+
const queueLink = shellRefs.sidebar.querySelector('a.sidebar-sublink[href="#/media-tagging/queue"]');
|
|
684
|
+
|
|
685
|
+
if (
|
|
686
|
+
!(mediaTaggingLink instanceof HTMLAnchorElement) ||
|
|
687
|
+
!(setupLink instanceof HTMLAnchorElement) ||
|
|
688
|
+
!(queueLink instanceof HTMLAnchorElement)
|
|
689
|
+
) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
mediaTaggingLink.classList.add('is-active');
|
|
694
|
+
setupLink.classList.toggle('is-active', routeName === 'mediaTaggingSetup');
|
|
695
|
+
queueLink.classList.toggle('is-active', routeName === 'mediaTaggingQueue');
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function applyMediaTaggingAndFocusSearch() {
|
|
700
|
+
pendingMediaTaggingTagSearchFocus = true;
|
|
701
|
+
const result = await applyCurrentMediaTaggingSelection();
|
|
702
|
+
|
|
703
|
+
if (!result) {
|
|
704
|
+
pendingMediaTaggingTagSearchFocus = false;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
|
|
618
710
|
async function handleTableDesignerCsvImport(fileInput) {
|
|
619
711
|
if (!(fileInput instanceof HTMLInputElement)) {
|
|
620
712
|
return;
|
|
@@ -656,6 +748,7 @@ async function handleTableDesignerCsvImport(fileInput) {
|
|
|
656
748
|
|
|
657
749
|
function renderApp(state) {
|
|
658
750
|
const previousRoutePath = lastRenderedRoutePath;
|
|
751
|
+
const previousRouteName = lastRenderedRouteName;
|
|
659
752
|
const { main, panel } = resolveView(state);
|
|
660
753
|
const topNavMarkup = renderTopNav(state);
|
|
661
754
|
const sidebarMarkup = renderSidebar(state);
|
|
@@ -722,7 +815,14 @@ function renderApp(state) {
|
|
|
722
815
|
}
|
|
723
816
|
|
|
724
817
|
if (sidebarChanged) {
|
|
725
|
-
|
|
818
|
+
const sidebarSynced =
|
|
819
|
+
isMediaTaggingRouteName(previousRouteName) &&
|
|
820
|
+
isMediaTaggingRouteName(state.route.name) &&
|
|
821
|
+
syncSidebarActiveRoute(state.route.name);
|
|
822
|
+
|
|
823
|
+
if (!sidebarSynced) {
|
|
824
|
+
shellRefs.sidebar.innerHTML = sidebarMarkup;
|
|
825
|
+
}
|
|
726
826
|
}
|
|
727
827
|
|
|
728
828
|
if (statusBarChanged) {
|
|
@@ -757,6 +857,10 @@ function renderApp(state) {
|
|
|
757
857
|
if (focusQueryEditorInput()) {
|
|
758
858
|
pendingQueryEditorFocus = false;
|
|
759
859
|
}
|
|
860
|
+
} else if (pendingMediaTaggingTagSearchFocus && state.route.name === 'mediaTaggingQueue') {
|
|
861
|
+
if (focusMediaTaggingTagSearchInput()) {
|
|
862
|
+
pendingMediaTaggingTagSearchFocus = false;
|
|
863
|
+
}
|
|
760
864
|
} else if (
|
|
761
865
|
pendingNewTableDesignerAutofocus &&
|
|
762
866
|
state.route.name === 'tableDesigner' &&
|
|
@@ -771,6 +875,7 @@ function renderApp(state) {
|
|
|
771
875
|
}
|
|
772
876
|
|
|
773
877
|
lastRenderedRoutePath = state.route.path;
|
|
878
|
+
lastRenderedRouteName = state.route.name;
|
|
774
879
|
lastRenderedTopNavMarkup = topNavMarkup;
|
|
775
880
|
lastRenderedSidebarMarkup = sidebarMarkup;
|
|
776
881
|
lastRenderedStatusBarMarkup = statusBarMarkup;
|
|
@@ -1071,7 +1176,7 @@ async function handleAction(actionNode) {
|
|
|
1071
1176
|
await resetSkippedMediaTaggingItems();
|
|
1072
1177
|
return;
|
|
1073
1178
|
case 'apply-media-tagging':
|
|
1074
|
-
await
|
|
1179
|
+
await applyMediaTaggingAndFocusSearch();
|
|
1075
1180
|
return;
|
|
1076
1181
|
case 'toggle-media-tagging-current-media':
|
|
1077
1182
|
if (actionNode instanceof HTMLButtonElement) {
|
|
@@ -1080,6 +1185,20 @@ async function handleAction(actionNode) {
|
|
|
1080
1185
|
setMediaTaggingWorkflowMediaDetailsVisible(nextValue, { notify: false });
|
|
1081
1186
|
}
|
|
1082
1187
|
return;
|
|
1188
|
+
case 'rotate-media-tagging-current-media': {
|
|
1189
|
+
const currentRotation = getState().mediaTagging.workflowMediaRotationDegrees ?? 0;
|
|
1190
|
+
const command = actionNode.dataset.rotationCommand;
|
|
1191
|
+
const nextRotation =
|
|
1192
|
+
command === 'left'
|
|
1193
|
+
? currentRotation - 90
|
|
1194
|
+
: command === 'right'
|
|
1195
|
+
? currentRotation + 90
|
|
1196
|
+
: 0;
|
|
1197
|
+
|
|
1198
|
+
syncMediaTaggingMediaRotationUi(actionNode, nextRotation);
|
|
1199
|
+
setMediaTaggingWorkflowMediaRotationDegrees(nextRotation, { notify: false });
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1083
1202
|
case 'open-media-tagging-current-in-data': {
|
|
1084
1203
|
const currentState = getState();
|
|
1085
1204
|
const mediaTableName = currentState.mediaTagging.draft?.mediaTable ?? '';
|
|
@@ -1093,6 +1212,17 @@ async function handleAction(actionNode) {
|
|
|
1093
1212
|
router.navigate(`/data/${encodeURIComponent(mediaTableName)}`);
|
|
1094
1213
|
return;
|
|
1095
1214
|
}
|
|
1215
|
+
case 'open-media-tagging-current-in-structure': {
|
|
1216
|
+
const mediaTableName = String(getState().mediaTagging.draft?.mediaTable ?? '').trim();
|
|
1217
|
+
|
|
1218
|
+
if (!mediaTableName) {
|
|
1219
|
+
showToast('The current media table could not be opened in Structure.', 'alert');
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
router.navigate(`/structure/${encodeURIComponent(mediaTableName)}`);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1096
1226
|
case 'import-table-designer-csv': {
|
|
1097
1227
|
const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
|
|
1098
1228
|
|
|
@@ -1149,6 +1279,19 @@ async function handleAction(actionNode) {
|
|
|
1149
1279
|
}
|
|
1150
1280
|
}
|
|
1151
1281
|
|
|
1282
|
+
function isMediaTaggingMenuActive(routeName) {
|
|
1283
|
+
return routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue';
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function canApplyMediaTaggingShortcut(state) {
|
|
1287
|
+
return (
|
|
1288
|
+
isMediaTaggingMenuActive(state.route.name) &&
|
|
1289
|
+
Boolean(state.mediaTagging.workflow?.currentItem) &&
|
|
1290
|
+
!state.mediaTagging.applying &&
|
|
1291
|
+
(state.mediaTagging.selectedTagKeys?.length ?? 0) > 0
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1152
1295
|
document.addEventListener('click', event => {
|
|
1153
1296
|
const actionNode = event.target.closest('[data-action]');
|
|
1154
1297
|
|
|
@@ -1161,6 +1304,7 @@ document.addEventListener('click', event => {
|
|
|
1161
1304
|
|
|
1162
1305
|
document.addEventListener('keydown', event => {
|
|
1163
1306
|
const target = event.target;
|
|
1307
|
+
const state = getState();
|
|
1164
1308
|
|
|
1165
1309
|
// Handle Enter key in tag form fields to trigger create tag
|
|
1166
1310
|
if (
|
|
@@ -1181,6 +1325,20 @@ document.addEventListener('keydown', event => {
|
|
|
1181
1325
|
return;
|
|
1182
1326
|
}
|
|
1183
1327
|
|
|
1328
|
+
if (
|
|
1329
|
+
(event.key === 'Enter' || event.code === 'Enter' || event.code === 'NumpadEnter') &&
|
|
1330
|
+
event.shiftKey &&
|
|
1331
|
+
!event.altKey &&
|
|
1332
|
+
!event.ctrlKey &&
|
|
1333
|
+
!event.metaKey &&
|
|
1334
|
+
!event.defaultPrevented &&
|
|
1335
|
+
canApplyMediaTaggingShortcut(state)
|
|
1336
|
+
) {
|
|
1337
|
+
event.preventDefault();
|
|
1338
|
+
void applyMediaTaggingAndFocusSearch();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1184
1342
|
if (
|
|
1185
1343
|
event.key === 'Enter' &&
|
|
1186
1344
|
event.shiftKey &&
|
|
@@ -1204,8 +1362,6 @@ document.addEventListener('keydown', event => {
|
|
|
1204
1362
|
return;
|
|
1205
1363
|
}
|
|
1206
1364
|
|
|
1207
|
-
const state = getState();
|
|
1208
|
-
|
|
1209
1365
|
if (state.modal) {
|
|
1210
1366
|
event.preventDefault();
|
|
1211
1367
|
closeModal();
|
|
@@ -23,6 +23,18 @@ function canOpenQueryHistoryInCharts(item) {
|
|
|
23
23
|
return Boolean(item?.chartsEligible);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function getQueryTypeTone(queryType) {
|
|
27
|
+
if (queryType === "select" || queryType === "update") {
|
|
28
|
+
return "success";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (queryType === "pragma") {
|
|
32
|
+
return "primary";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return "muted";
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
function renderRunItem(run) {
|
|
27
39
|
return `
|
|
28
40
|
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
@@ -135,9 +147,9 @@ export function renderQueryHistoryDetail({
|
|
|
135
147
|
</button>
|
|
136
148
|
</div>
|
|
137
149
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
138
|
-
${renderStatusBadge(item.queryType, item.
|
|
150
|
+
${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType))}
|
|
139
151
|
${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
|
|
140
|
-
${item.isDestructive ? renderStatusBadge("destructive", "
|
|
152
|
+
${item.isDestructive ? renderStatusBadge("destructive", "warning") : ""}
|
|
141
153
|
${item.lastRun ? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success") : ""}
|
|
142
154
|
</div>
|
|
143
155
|
</div>
|
|
@@ -4,12 +4,8 @@ import {
|
|
|
4
4
|
} from "../utils/format.js";
|
|
5
5
|
import { renderStatusBadge } from "./badges.js";
|
|
6
6
|
|
|
7
|
-
function getQueryTypeTone(queryType
|
|
8
|
-
if (
|
|
9
|
-
return "alert";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (queryType === "select") {
|
|
7
|
+
function getQueryTypeTone(queryType) {
|
|
8
|
+
if (queryType === "select" || queryType === "update") {
|
|
13
9
|
return "success";
|
|
14
10
|
}
|
|
15
11
|
|
|
@@ -39,9 +35,9 @@ export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistor
|
|
|
39
35
|
<span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">
|
|
40
36
|
${escapeHtml(item.displayTitle)}
|
|
41
37
|
</span>
|
|
42
|
-
${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType
|
|
38
|
+
${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType))}
|
|
43
39
|
${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
|
|
44
|
-
${item.isDestructive ? renderStatusBadge("destructive", "
|
|
40
|
+
${item.isDestructive ? renderStatusBadge("destructive", "warning") : ""}
|
|
45
41
|
</div>
|
|
46
42
|
<p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">
|
|
47
43
|
${escapeHtml(item.previewSql)}
|
package/frontend/js/store.js
CHANGED
|
@@ -194,6 +194,7 @@ const state = {
|
|
|
194
194
|
dismissedIssueKeys: [],
|
|
195
195
|
selectedTagKeys: [],
|
|
196
196
|
workflowMediaDetailsVisible: true,
|
|
197
|
+
workflowMediaRotationDegrees: 0,
|
|
197
198
|
skippedMediaKeys: [],
|
|
198
199
|
tagFormValues: {},
|
|
199
200
|
},
|
|
@@ -348,6 +349,35 @@ function requiresActiveDatabase(routeName) {
|
|
|
348
349
|
].includes(routeName);
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
function isMediaTaggingRouteName(routeName) {
|
|
353
|
+
return routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getConnectionIdentity(connection) {
|
|
357
|
+
return connection?.id ?? connection?.path ?? null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function hasLoadedMediaTaggingForActiveConnection() {
|
|
361
|
+
const activeConnectionId = getConnectionIdentity(state.connections.active);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
Boolean(activeConnectionId) &&
|
|
365
|
+
getConnectionIdentity(state.mediaTagging.connection) === activeConnectionId &&
|
|
366
|
+
!state.mediaTagging.loading &&
|
|
367
|
+
(state.mediaTagging.draft !== null || state.mediaTagging.suggestedConfig !== null)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizeMediaTaggingRotationDegrees(value) {
|
|
372
|
+
const numericValue = Number(value);
|
|
373
|
+
|
|
374
|
+
if (!Number.isFinite(numericValue)) {
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
|
|
379
|
+
}
|
|
380
|
+
|
|
351
381
|
function normalizeDataPageSize(value, fallback = 50) {
|
|
352
382
|
const numericValue = Number(value);
|
|
353
383
|
|
|
@@ -844,6 +874,7 @@ function setMissingDatabaseState() {
|
|
|
844
874
|
state.mediaTagging.issues = [];
|
|
845
875
|
state.mediaTagging.dismissedIssueKeys = [];
|
|
846
876
|
state.mediaTagging.selectedTagKeys = [];
|
|
877
|
+
state.mediaTagging.workflowMediaRotationDegrees = 0;
|
|
847
878
|
state.mediaTagging.skippedMediaKeys = [];
|
|
848
879
|
state.mediaTagging.tagFormValues = {};
|
|
849
880
|
state.mediaTagging.error = error;
|
|
@@ -1579,6 +1610,9 @@ function applyMediaTaggingResponse(data, options = {}) {
|
|
|
1579
1610
|
};
|
|
1580
1611
|
state.mediaTagging.tags = data?.tags ?? [];
|
|
1581
1612
|
state.mediaTagging.workflow = data?.workflow ?? null;
|
|
1613
|
+
if (previousCurrentKey !== nextCurrentKey) {
|
|
1614
|
+
state.mediaTagging.workflowMediaRotationDegrees = 0;
|
|
1615
|
+
}
|
|
1582
1616
|
state.mediaTagging.issues = data?.issues ?? [];
|
|
1583
1617
|
state.mediaTagging.error = null;
|
|
1584
1618
|
syncDismissedMediaTaggingIssues();
|
|
@@ -1650,6 +1684,7 @@ async function loadMediaTagging(version) {
|
|
|
1650
1684
|
state.mediaTagging.issues = [];
|
|
1651
1685
|
state.mediaTagging.dismissedIssueKeys = [];
|
|
1652
1686
|
state.mediaTagging.selectedTagKeys = [];
|
|
1687
|
+
state.mediaTagging.workflowMediaRotationDegrees = 0;
|
|
1653
1688
|
state.mediaTagging.skippedMediaKeys = [];
|
|
1654
1689
|
state.mediaTagging.tagFormValues = {};
|
|
1655
1690
|
state.mediaTagging.removingTagKey = null;
|
|
@@ -1759,6 +1794,7 @@ function invalidateDatabaseCaches() {
|
|
|
1759
1794
|
state.mediaTagging.issues = [];
|
|
1760
1795
|
state.mediaTagging.dismissedIssueKeys = [];
|
|
1761
1796
|
state.mediaTagging.selectedTagKeys = [];
|
|
1797
|
+
state.mediaTagging.workflowMediaRotationDegrees = 0;
|
|
1762
1798
|
state.mediaTagging.skippedMediaKeys = [];
|
|
1763
1799
|
state.mediaTagging.tagFormValues = {};
|
|
1764
1800
|
}
|
|
@@ -1772,6 +1808,10 @@ async function loadRouteData(route) {
|
|
|
1772
1808
|
return;
|
|
1773
1809
|
}
|
|
1774
1810
|
|
|
1811
|
+
if (isMediaTaggingRouteName(route.name) && hasLoadedMediaTaggingForActiveConnection()) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1775
1815
|
const version = ++routeLoadVersion;
|
|
1776
1816
|
|
|
1777
1817
|
if (route.name === 'landing' || route.name === 'connections') {
|
|
@@ -2878,6 +2918,20 @@ export function setMediaTaggingWorkflowMediaDetailsVisible(value, options = {})
|
|
|
2878
2918
|
}
|
|
2879
2919
|
}
|
|
2880
2920
|
|
|
2921
|
+
export function setMediaTaggingWorkflowMediaRotationDegrees(value, options = {}) {
|
|
2922
|
+
const nextValue = normalizeMediaTaggingRotationDegrees(value);
|
|
2923
|
+
|
|
2924
|
+
if (state.mediaTagging.workflowMediaRotationDegrees === nextValue) {
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
state.mediaTagging.workflowMediaRotationDegrees = nextValue;
|
|
2929
|
+
|
|
2930
|
+
if (options.notify !== false) {
|
|
2931
|
+
emitChange();
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2881
2935
|
export async function refreshMediaTaggingPreview() {
|
|
2882
2936
|
return previewMediaTaggingDraft({
|
|
2883
2937
|
keepSelectedTags: true,
|
|
@@ -326,7 +326,10 @@ function renderCreatedTagList(tags = [], { canRemove = false, removingTagKey = n
|
|
|
326
326
|
`;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
function renderPreviewMedia(
|
|
329
|
+
function renderPreviewMedia(
|
|
330
|
+
currentItem,
|
|
331
|
+
{ detailsVisible = true, mediaTableName = '', rotationDegrees = 0, status = null } = {},
|
|
332
|
+
) {
|
|
330
333
|
if (!currentItem) {
|
|
331
334
|
return `
|
|
332
335
|
<div class="media-tagging-preview__empty">
|
|
@@ -344,6 +347,15 @@ function renderPreviewMedia(currentItem, { detailsVisible = true, mediaTableName
|
|
|
344
347
|
const pathValue = String(currentItem.path ?? '');
|
|
345
348
|
const fileName = pathValue.split(/[\\/]/).filter(Boolean).pop() || 'Audio file';
|
|
346
349
|
const isAudioPreview = Boolean(currentItem.previewUrl && currentItem.previewKind === 'audio');
|
|
350
|
+
const isRotatablePreview = Boolean(
|
|
351
|
+
currentItem.previewUrl && (currentItem.previewKind === 'image' || currentItem.previewKind === 'video'),
|
|
352
|
+
);
|
|
353
|
+
const numericRotation = Number(rotationDegrees);
|
|
354
|
+
const normalizedRotation = Number.isFinite(numericRotation)
|
|
355
|
+
? ((Math.round(numericRotation / 90) * 90) % 360 + 360) % 360
|
|
356
|
+
: 0;
|
|
357
|
+
const rotationStyle = `--media-tagging-preview-rotation: ${normalizedRotation}deg;`;
|
|
358
|
+
const rotationClass = normalizedRotation === 90 || normalizedRotation === 270 ? ' is-rotated-quarter' : '';
|
|
347
359
|
let assetMarkup = `
|
|
348
360
|
<div class="media-tagging-preview__placeholder">
|
|
349
361
|
<span class="material-symbols-outlined text-5xl">perm_media</span>
|
|
@@ -353,16 +365,27 @@ function renderPreviewMedia(currentItem, { detailsVisible = true, mediaTableName
|
|
|
353
365
|
if (currentItem.previewUrl && currentItem.previewKind === 'image') {
|
|
354
366
|
assetMarkup = `
|
|
355
367
|
<img
|
|
356
|
-
class="media-tagging-preview__asset"
|
|
368
|
+
class="media-tagging-preview__asset${rotationClass}"
|
|
369
|
+
data-media-tagging-rotation-target="true"
|
|
370
|
+
data-rotation-degrees="${escapeHtml(String(normalizedRotation))}"
|
|
371
|
+
style="${escapeHtml(rotationStyle)}"
|
|
357
372
|
src="${escapeHtml(currentItem.previewUrl)}"
|
|
358
373
|
alt="${escapeHtml(pathValue || 'Current media item')}"
|
|
359
374
|
/>
|
|
360
375
|
`;
|
|
361
376
|
} else if (currentItem.previewUrl && currentItem.previewKind === 'video') {
|
|
362
377
|
assetMarkup = `
|
|
363
|
-
<video
|
|
378
|
+
<video
|
|
379
|
+
class="media-tagging-preview__asset${rotationClass}"
|
|
380
|
+
data-media-tagging-rotation-target="true"
|
|
381
|
+
data-rotation-degrees="${escapeHtml(String(normalizedRotation))}"
|
|
382
|
+
style="${escapeHtml(rotationStyle)}"
|
|
383
|
+
controls
|
|
384
|
+
preload="metadata"
|
|
385
|
+
src="${escapeHtml(
|
|
364
386
|
currentItem.previewUrl,
|
|
365
|
-
)}"
|
|
387
|
+
)}"
|
|
388
|
+
></video>
|
|
366
389
|
`;
|
|
367
390
|
} else if (currentItem.previewUrl && currentItem.previewKind === 'audio') {
|
|
368
391
|
assetMarkup = `
|
|
@@ -395,36 +418,100 @@ function renderPreviewMedia(currentItem, { detailsVisible = true, mediaTableName
|
|
|
395
418
|
<div class="media-tagging-preview ${detailsVisible ? '' : 'media-tagging-preview--meta-hidden'}">
|
|
396
419
|
<div class="media-tagging-preview__media${isAudioPreview ? ' media-tagging-preview__media--audio' : ''}">
|
|
397
420
|
<div class="media-tagging-preview__media-toolbar">
|
|
398
|
-
<
|
|
399
|
-
class="
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
421
|
+
<div class="media-tagging-status media-tagging-status--compact media-tagging-status--preview">
|
|
422
|
+
<div class="media-tagging-status__value">${escapeHtml(status?.ratioLabel ?? '0 / 0')}</div>
|
|
423
|
+
<div class="media-tagging-status__label">
|
|
424
|
+
tagged / total
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="media-tagging-preview__toolbar-actions">
|
|
428
|
+
${
|
|
429
|
+
isRotatablePreview
|
|
430
|
+
? `
|
|
431
|
+
<div class="media-tagging-preview__rotation-controls" aria-label="Rotate media preview">
|
|
432
|
+
<button
|
|
433
|
+
class="standard-button media-tagging-preview__icon-button"
|
|
434
|
+
data-action="rotate-media-tagging-current-media"
|
|
435
|
+
data-rotation-command="left"
|
|
436
|
+
type="button"
|
|
437
|
+
aria-label="Rotate left 90 degrees"
|
|
438
|
+
title="Rotate left 90 degrees"
|
|
439
|
+
>
|
|
440
|
+
<span class="material-symbols-outlined">rotate_left</span>
|
|
441
|
+
</button>
|
|
442
|
+
<button
|
|
443
|
+
class="standard-button media-tagging-preview__icon-button"
|
|
444
|
+
data-action="rotate-media-tagging-current-media"
|
|
445
|
+
data-rotation-command="right"
|
|
446
|
+
type="button"
|
|
447
|
+
aria-label="Rotate right 90 degrees"
|
|
448
|
+
title="Rotate right 90 degrees"
|
|
449
|
+
>
|
|
450
|
+
<span class="material-symbols-outlined">rotate_right</span>
|
|
451
|
+
</button>
|
|
452
|
+
<button
|
|
453
|
+
class="standard-button media-tagging-preview__icon-button"
|
|
454
|
+
data-action="rotate-media-tagging-current-media"
|
|
455
|
+
data-rotation-command="reset"
|
|
456
|
+
type="button"
|
|
457
|
+
aria-label="Reset rotation"
|
|
458
|
+
title="Reset rotation"
|
|
459
|
+
${normalizedRotation === 0 ? 'disabled' : ''}
|
|
460
|
+
>
|
|
461
|
+
<span class="material-symbols-outlined">restart_alt</span>
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
`
|
|
465
|
+
: ''
|
|
466
|
+
}
|
|
467
|
+
<button
|
|
468
|
+
class="standard-button media-tagging-preview__toggle"
|
|
469
|
+
data-action="toggle-media-tagging-current-media"
|
|
470
|
+
data-next-value="${detailsVisible ? 'false' : 'true'}"
|
|
471
|
+
data-expanded-label="Shrink Media Viewer"
|
|
472
|
+
data-collapsed-label="Show Media Viewer"
|
|
473
|
+
aria-expanded="${detailsVisible ? 'true' : 'false'}"
|
|
474
|
+
type="button"
|
|
475
|
+
>
|
|
476
|
+
${toggleLabel}
|
|
477
|
+
</button>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="media-tagging-preview__asset-shell">
|
|
481
|
+
${assetMarkup}
|
|
409
482
|
</div>
|
|
410
|
-
${assetMarkup}
|
|
411
483
|
</div>
|
|
412
484
|
<div class="media-tagging-preview__meta">
|
|
413
485
|
<div class="media-tagging-preview__meta-header">
|
|
414
486
|
<div class="media-tagging-preview__eyebrow">Current Media</div>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
487
|
+
<div class="media-tagging-preview__meta-actions">
|
|
488
|
+
${
|
|
489
|
+
mediaTableName
|
|
490
|
+
? `
|
|
491
|
+
<button
|
|
492
|
+
class="standard-button media-tagging-preview__meta-action"
|
|
493
|
+
data-action="open-media-tagging-current-in-structure"
|
|
494
|
+
type="button"
|
|
495
|
+
>
|
|
496
|
+
Open Structure
|
|
497
|
+
</button>
|
|
498
|
+
`
|
|
499
|
+
: ''
|
|
500
|
+
}
|
|
501
|
+
${
|
|
502
|
+
mediaTableName && currentItem.identity
|
|
503
|
+
? `
|
|
504
|
+
<button
|
|
505
|
+
class="standard-button media-tagging-preview__meta-action"
|
|
506
|
+
data-action="open-media-tagging-current-in-data"
|
|
507
|
+
type="button"
|
|
508
|
+
>
|
|
509
|
+
Open In Data
|
|
510
|
+
</button>
|
|
511
|
+
`
|
|
512
|
+
: ''
|
|
513
|
+
}
|
|
514
|
+
</div>
|
|
428
515
|
</div>
|
|
429
516
|
${
|
|
430
517
|
metadata.length
|
|
@@ -548,7 +635,7 @@ function renderTaggingSection(state) {
|
|
|
548
635
|
const booleanCandidates = state.mediaTagging.booleanCandidates ?? [];
|
|
549
636
|
|
|
550
637
|
return `
|
|
551
|
-
<section class="media-tagging-card shell-section">
|
|
638
|
+
<section class="media-tagging-card media-tagging-card--tagging shell-section">
|
|
552
639
|
<div class="media-tagging-card__header">
|
|
553
640
|
<div>
|
|
554
641
|
<div class="media-tagging-card__eyebrow">2. Tagging</div>
|
|
@@ -739,7 +826,12 @@ function renderWorkflowSection(state) {
|
|
|
739
826
|
`
|
|
740
827
|
: ''
|
|
741
828
|
}
|
|
742
|
-
${renderPreviewMedia(workflow?.currentItem ?? null, {
|
|
829
|
+
${renderPreviewMedia(workflow?.currentItem ?? null, {
|
|
830
|
+
detailsVisible,
|
|
831
|
+
mediaTableName,
|
|
832
|
+
rotationDegrees: state.mediaTagging.workflowMediaRotationDegrees,
|
|
833
|
+
status,
|
|
834
|
+
})}
|
|
743
835
|
<div class="media-tagging-workflow-sidebar">
|
|
744
836
|
<div class="media-tagging-tag-panel">
|
|
745
837
|
<div class="media-tagging-tag-panel__header">
|
|
@@ -750,12 +842,6 @@ function renderWorkflowSection(state) {
|
|
|
750
842
|
${escapeHtml(formatNumber(selectableTags.length))} selectable tag(s)
|
|
751
843
|
</div>
|
|
752
844
|
</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
845
|
</div>
|
|
760
846
|
<label class="media-tagging-field mt-4">
|
|
761
847
|
<span class="media-tagging-field__label">Search</span>
|
|
@@ -769,24 +855,29 @@ function renderWorkflowSection(state) {
|
|
|
769
855
|
</div>
|
|
770
856
|
${renderTagList(selectableTags, selectedTagKeys)}
|
|
771
857
|
<div class="media-tagging-tag-panel__footer">
|
|
772
|
-
<div class="
|
|
773
|
-
<
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
858
|
+
<div class="media-tagging-tag-panel__footer-main">
|
|
859
|
+
<div class="media-tagging-tag-panel__remaining">
|
|
860
|
+
${escapeHtml(formatNumber(workflow?.status?.remainingCount ?? 0))} remaining
|
|
861
|
+
</div>
|
|
862
|
+
<div class="media-tagging-tag-panel__actions">
|
|
863
|
+
<button
|
|
864
|
+
class="standard-button"
|
|
865
|
+
data-action="skip-media-tagging-item"
|
|
866
|
+
type="button"
|
|
867
|
+
${workflow?.currentItem && canWrite && !state.mediaTagging.previewLoading && !state.mediaTagging.applying ? '' : 'disabled'}
|
|
868
|
+
>
|
|
869
|
+
${state.mediaTagging.applying ? 'Saving...' : 'Skip'}
|
|
870
|
+
</button>
|
|
871
|
+
<button
|
|
872
|
+
class="signature-button"
|
|
873
|
+
data-action="apply-media-tagging"
|
|
874
|
+
data-can-apply="${workflow?.currentItem && canWrite && !state.mediaTagging.applying ? 'true' : 'false'}"
|
|
875
|
+
type="button"
|
|
876
|
+
${workflow?.currentItem && canWrite && !state.mediaTagging.applying && selectedTagCount > 0 ? '' : 'disabled'}
|
|
877
|
+
>
|
|
878
|
+
${state.mediaTagging.applying ? 'Saving...' : `${selectedTagCount} tagged & next`}
|
|
879
|
+
</button>
|
|
880
|
+
</div>
|
|
790
881
|
</div>
|
|
791
882
|
${
|
|
792
883
|
workflow?.allRemainingSkipped
|
|
@@ -801,9 +892,6 @@ function renderWorkflowSection(state) {
|
|
|
801
892
|
`
|
|
802
893
|
: ''
|
|
803
894
|
}
|
|
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
895
|
</div>
|
|
808
896
|
</div>
|
|
809
897
|
</div>
|
|
@@ -820,24 +908,6 @@ export function renderMediaTaggingView(state, { subView = 'setup' } = {}) {
|
|
|
820
908
|
main: `
|
|
821
909
|
<section class="view-surface media-tagging-view">
|
|
822
910
|
<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
911
|
${renderIssueList(state)}
|
|
842
912
|
|
|
843
913
|
${
|
|
@@ -287,6 +287,12 @@
|
|
|
287
287
|
color: var(--color-error);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
.status-badge--warning {
|
|
291
|
+
background: rgba(245, 158, 11, 0.12);
|
|
292
|
+
border-color: rgba(245, 158, 11, 0.28);
|
|
293
|
+
color: #f59e0b;
|
|
294
|
+
}
|
|
295
|
+
|
|
290
296
|
.status-badge--success {
|
|
291
297
|
background: rgba(0, 220, 225, 0.1);
|
|
292
298
|
color: var(--color-tertiary-dim);
|
|
@@ -389,11 +389,6 @@
|
|
|
389
389
|
padding: var(--spacing-6);
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
.media-tagging-header {
|
|
393
|
-
border-bottom: 1px solid rgba(75, 71, 50, 0.18);
|
|
394
|
-
padding-bottom: var(--spacing-4);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
392
|
.media-tagging-grid {
|
|
398
393
|
align-content: stretch;
|
|
399
394
|
display: grid;
|
|
@@ -415,6 +410,7 @@
|
|
|
415
410
|
}
|
|
416
411
|
|
|
417
412
|
.media-tagging-card--workflow {
|
|
413
|
+
flex: 1;
|
|
418
414
|
min-height: 32rem;
|
|
419
415
|
}
|
|
420
416
|
|
|
@@ -602,6 +598,11 @@
|
|
|
602
598
|
font-size: 1.25rem;
|
|
603
599
|
}
|
|
604
600
|
|
|
601
|
+
.media-tagging-status--preview {
|
|
602
|
+
align-items: flex-start;
|
|
603
|
+
min-width: max-content;
|
|
604
|
+
}
|
|
605
|
+
|
|
605
606
|
.media-tagging-mapping-list {
|
|
606
607
|
display: grid;
|
|
607
608
|
gap: var(--spacing-3);
|
|
@@ -641,7 +642,7 @@
|
|
|
641
642
|
border: 1px solid rgba(75, 71, 50, 0.16);
|
|
642
643
|
display: flex;
|
|
643
644
|
flex-direction: column;
|
|
644
|
-
justify-content:
|
|
645
|
+
justify-content: flex-start;
|
|
645
646
|
min-height: 26rem;
|
|
646
647
|
overflow: hidden;
|
|
647
648
|
padding: var(--spacing-4);
|
|
@@ -653,17 +654,52 @@
|
|
|
653
654
|
}
|
|
654
655
|
|
|
655
656
|
.media-tagging-preview__media--audio .media-tagging-preview__media-toolbar {
|
|
656
|
-
justify-content:
|
|
657
|
-
position: static;
|
|
657
|
+
justify-content: space-between;
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
.media-tagging-preview__media-toolbar {
|
|
661
|
+
align-items: center;
|
|
662
|
+
border-bottom: 1px solid rgba(75, 71, 50, 0.18);
|
|
661
663
|
display: flex;
|
|
664
|
+
flex: 0 0 auto;
|
|
665
|
+
flex-wrap: wrap;
|
|
662
666
|
gap: var(--spacing-3);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
+
justify-content: space-between;
|
|
668
|
+
margin: calc(var(--spacing-4) * -1) calc(var(--spacing-4) * -1) var(--spacing-4);
|
|
669
|
+
min-height: calc(var(--control-height) + var(--spacing-4));
|
|
670
|
+
padding: var(--spacing-2) var(--spacing-4);
|
|
671
|
+
width: calc(100% + (var(--spacing-4) * 2));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.media-tagging-preview__toolbar-actions {
|
|
675
|
+
display: flex;
|
|
676
|
+
flex-wrap: wrap;
|
|
677
|
+
gap: var(--spacing-3);
|
|
678
|
+
justify-content: flex-end;
|
|
679
|
+
margin-left: auto;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.media-tagging-preview__asset-shell {
|
|
683
|
+
align-items: center;
|
|
684
|
+
display: flex;
|
|
685
|
+
flex: 1;
|
|
686
|
+
justify-content: center;
|
|
687
|
+
min-height: 0;
|
|
688
|
+
overflow: hidden;
|
|
689
|
+
width: 100%;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.media-tagging-preview__rotation-controls {
|
|
693
|
+
display: flex;
|
|
694
|
+
gap: var(--spacing-2);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.media-tagging-preview__icon-button {
|
|
698
|
+
backdrop-filter: blur(8px);
|
|
699
|
+
background: rgba(20, 20, 20, 0.78);
|
|
700
|
+
min-width: var(--control-height);
|
|
701
|
+
padding: 0;
|
|
702
|
+
width: var(--control-height);
|
|
667
703
|
}
|
|
668
704
|
|
|
669
705
|
.media-tagging-preview__toggle {
|
|
@@ -674,10 +710,19 @@
|
|
|
674
710
|
.media-tagging-preview__asset {
|
|
675
711
|
flex: 1;
|
|
676
712
|
height: 100%;
|
|
713
|
+
min-height: 0;
|
|
677
714
|
object-fit: contain;
|
|
715
|
+
transform: rotate(var(--media-tagging-preview-rotation, 0deg));
|
|
716
|
+
transform-origin: center;
|
|
717
|
+
transition: transform var(--transition-standard);
|
|
678
718
|
width: 100%;
|
|
679
719
|
}
|
|
680
720
|
|
|
721
|
+
.media-tagging-preview__asset.is-rotated-quarter {
|
|
722
|
+
height: min(100%, 72vw);
|
|
723
|
+
width: min(100%, 72vh);
|
|
724
|
+
}
|
|
725
|
+
|
|
681
726
|
.media-tagging-audio-preview {
|
|
682
727
|
align-items: center;
|
|
683
728
|
align-self: center;
|
|
@@ -765,6 +810,7 @@
|
|
|
765
810
|
.media-tagging-preview__meta-header {
|
|
766
811
|
align-items: center;
|
|
767
812
|
display: flex;
|
|
813
|
+
flex-wrap: wrap;
|
|
768
814
|
gap: var(--spacing-3);
|
|
769
815
|
justify-content: space-between;
|
|
770
816
|
}
|
|
@@ -782,6 +828,13 @@
|
|
|
782
828
|
flex: 0 0 auto;
|
|
783
829
|
}
|
|
784
830
|
|
|
831
|
+
.media-tagging-preview__meta-actions {
|
|
832
|
+
display: flex;
|
|
833
|
+
flex-wrap: wrap;
|
|
834
|
+
gap: var(--spacing-2);
|
|
835
|
+
justify-content: flex-end;
|
|
836
|
+
}
|
|
837
|
+
|
|
785
838
|
.media-tagging-preview__metadata {
|
|
786
839
|
border-top: 1px solid rgba(75, 71, 50, 0.14);
|
|
787
840
|
display: flex;
|
|
@@ -858,6 +911,32 @@
|
|
|
858
911
|
gap: var(--spacing-3);
|
|
859
912
|
}
|
|
860
913
|
|
|
914
|
+
.media-tagging-tag-panel__footer-main {
|
|
915
|
+
align-items: center;
|
|
916
|
+
display: flex;
|
|
917
|
+
gap: var(--spacing-3);
|
|
918
|
+
justify-content: space-between;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.media-tagging-tag-panel__remaining {
|
|
922
|
+
color: rgba(205, 199, 171, 0.45);
|
|
923
|
+
flex: 1 1 auto;
|
|
924
|
+
font-family: var(--font-family-mono);
|
|
925
|
+
font-size: 0.6875rem;
|
|
926
|
+
letter-spacing: 0.14em;
|
|
927
|
+
min-width: 0;
|
|
928
|
+
text-transform: uppercase;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.media-tagging-tag-panel__actions {
|
|
932
|
+
align-items: center;
|
|
933
|
+
display: flex;
|
|
934
|
+
flex: 0 0 auto;
|
|
935
|
+
flex-wrap: wrap;
|
|
936
|
+
gap: var(--spacing-3);
|
|
937
|
+
justify-content: flex-end;
|
|
938
|
+
}
|
|
939
|
+
|
|
861
940
|
.media-tagging-tag-list {
|
|
862
941
|
align-content: start;
|
|
863
942
|
display: grid;
|
|
@@ -1139,9 +1218,44 @@
|
|
|
1139
1218
|
grid-template-columns: 1fr;
|
|
1140
1219
|
}
|
|
1141
1220
|
|
|
1221
|
+
.sql-highlight-shell--media {
|
|
1222
|
+
min-height: 9rem;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1142
1225
|
.media-tagging-audio-preview {
|
|
1143
1226
|
grid-template-columns: 1fr;
|
|
1144
1227
|
justify-items: center;
|
|
1145
1228
|
text-align: center;
|
|
1146
1229
|
}
|
|
1147
1230
|
}
|
|
1231
|
+
|
|
1232
|
+
@media (min-width: 1024px) and (max-height: 900px) {
|
|
1233
|
+
.media-tagging-card--tagging .media-tagging-card__header,
|
|
1234
|
+
.media-tagging-card--tagging .media-tagging-card__body {
|
|
1235
|
+
padding: var(--spacing-4);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.media-tagging-card--tagging .media-tagging-card__body {
|
|
1239
|
+
gap: var(--spacing-3);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.media-tagging-card--tagging .media-tagging-form-grid,
|
|
1243
|
+
.media-tagging-card--tagging .media-tagging-query-grid {
|
|
1244
|
+
gap: var(--spacing-3);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.media-tagging-card--tagging .media-tagging-field {
|
|
1248
|
+
gap: 0.35rem;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.media-tagging-card--tagging .sql-highlight-shell--media {
|
|
1252
|
+
min-height: 4.5rem;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.media-tagging-card--tagging .sql-highlight-content,
|
|
1256
|
+
.media-tagging-card--tagging .sql-highlight-input {
|
|
1257
|
+
font-size: 0.72rem;
|
|
1258
|
+
line-height: 1.5;
|
|
1259
|
+
padding: 0.65rem 0.75rem;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
package/package.json
CHANGED
|
@@ -100,8 +100,11 @@ function normalizeMediaTaggingConfigRecord(config = {}) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function
|
|
104
|
-
return
|
|
103
|
+
function isChartCompatibleQuery(queryType, rawSql = "") {
|
|
104
|
+
return (
|
|
105
|
+
String(queryType ?? "").trim().toLowerCase() === "select" ||
|
|
106
|
+
detectQueryType(rawSql) === "select"
|
|
107
|
+
);
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
class AppStateStore {
|
|
@@ -752,7 +755,7 @@ class AppStateStore {
|
|
|
752
755
|
previewSql: buildSqlPreview(row.raw_sql ?? row.rawSql),
|
|
753
756
|
lastRun,
|
|
754
757
|
chartsEligible:
|
|
755
|
-
|
|
758
|
+
isChartCompatibleQuery(queryType, row.raw_sql ?? row.rawSql) &&
|
|
756
759
|
(!lastRun || String(lastRun.status ?? "").trim().toLowerCase() !== "error"),
|
|
757
760
|
};
|
|
758
761
|
}
|
|
@@ -1252,12 +1255,12 @@ class AppStateStore {
|
|
|
1252
1255
|
LIMIT 1
|
|
1253
1256
|
)
|
|
1254
1257
|
WHERE q.database_key = ?
|
|
1255
|
-
AND q.query_type = 'select'
|
|
1256
1258
|
AND COALESCE(latest.status, 'success') != 'error'
|
|
1257
1259
|
ORDER BY q.last_used_at DESC, q.id DESC
|
|
1258
1260
|
`)
|
|
1259
1261
|
.all(normalizedDatabaseKey)
|
|
1260
|
-
.map((row) => this.decorateQueryHistoryRow(row))
|
|
1262
|
+
.map((row) => this.decorateQueryHistoryRow(row))
|
|
1263
|
+
.filter((item) => item.chartsEligible);
|
|
1261
1264
|
}
|
|
1262
1265
|
|
|
1263
1266
|
getQueryHistoryItemById(historyId) {
|
|
@@ -1367,7 +1370,7 @@ class AppStateStore {
|
|
|
1367
1370
|
getChartQueryHistoryItemForDatabase(historyId, databaseKey) {
|
|
1368
1371
|
const item = this.getQueryHistoryItemForDatabase(historyId, databaseKey);
|
|
1369
1372
|
|
|
1370
|
-
if (!
|
|
1373
|
+
if (!isChartCompatibleQuery(item.queryType, item.rawSql)) {
|
|
1371
1374
|
throw new ValidationError("Only SELECT queries can be opened in Charts.");
|
|
1372
1375
|
}
|
|
1373
1376
|
|
|
@@ -37,11 +37,174 @@ function getLeadingKeywords(sql = "", limit = 3) {
|
|
|
37
37
|
return matches.slice(0, limit).map((token) => token.toLowerCase());
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function readWordAt(value = "", index = 0) {
|
|
41
|
+
const match = String(value).slice(index).match(/^[A-Za-z]+/);
|
|
42
|
+
return match ? match[0].toLowerCase() : "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function skipWhitespace(value = "", index = 0) {
|
|
46
|
+
let cursor = index;
|
|
47
|
+
|
|
48
|
+
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
49
|
+
cursor += 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cursor;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function advanceQuotedSql(value = "", index = 0) {
|
|
56
|
+
const quote = value[index];
|
|
57
|
+
const closingQuote = quote === "[" ? "]" : quote;
|
|
58
|
+
let cursor = index + 1;
|
|
59
|
+
|
|
60
|
+
while (cursor < value.length) {
|
|
61
|
+
if (value[cursor] === closingQuote) {
|
|
62
|
+
if ((quote === "'" || quote === '"') && value[cursor + 1] === closingQuote) {
|
|
63
|
+
cursor += 2;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cursor + 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
cursor += 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isSqlWordBoundary(value = "", index = 0) {
|
|
77
|
+
return index < 0 || index >= value.length || !/[A-Za-z0-9_]/.test(value[index]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findTopLevelWord(value = "", word = "", startIndex = 0) {
|
|
81
|
+
const normalizedWord = String(word).toLowerCase();
|
|
82
|
+
let cursor = startIndex;
|
|
83
|
+
let depth = 0;
|
|
84
|
+
|
|
85
|
+
while (cursor < value.length) {
|
|
86
|
+
const char = value[cursor];
|
|
87
|
+
|
|
88
|
+
if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
89
|
+
cursor = advanceQuotedSql(value, cursor);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (char === "(") {
|
|
94
|
+
depth += 1;
|
|
95
|
+
cursor += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (char === ")") {
|
|
100
|
+
depth = Math.max(0, depth - 1);
|
|
101
|
+
cursor += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
depth === 0 &&
|
|
107
|
+
value.slice(cursor, cursor + normalizedWord.length).toLowerCase() === normalizedWord &&
|
|
108
|
+
isSqlWordBoundary(value, cursor - 1) &&
|
|
109
|
+
isSqlWordBoundary(value, cursor + normalizedWord.length)
|
|
110
|
+
) {
|
|
111
|
+
return cursor;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cursor += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findMatchingParen(value = "", openIndex = 0) {
|
|
121
|
+
let cursor = openIndex;
|
|
122
|
+
let depth = 0;
|
|
123
|
+
|
|
124
|
+
while (cursor < value.length) {
|
|
125
|
+
const char = value[cursor];
|
|
126
|
+
|
|
127
|
+
if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
128
|
+
cursor = advanceQuotedSql(value, cursor);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (char === "(") {
|
|
133
|
+
depth += 1;
|
|
134
|
+
} else if (char === ")") {
|
|
135
|
+
depth -= 1;
|
|
136
|
+
|
|
137
|
+
if (depth === 0) {
|
|
138
|
+
return cursor;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
cursor += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveCteStatementKeyword(sql = "") {
|
|
149
|
+
const stripped = stripBlockComments(stripLineComments(sql)).trim();
|
|
150
|
+
let cursor = skipWhitespace(stripped, 0);
|
|
151
|
+
|
|
152
|
+
if (readWordAt(stripped, cursor) !== "with") {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
cursor += 4;
|
|
157
|
+
cursor = skipWhitespace(stripped, cursor);
|
|
158
|
+
|
|
159
|
+
if (readWordAt(stripped, cursor) === "recursive") {
|
|
160
|
+
cursor += "recursive".length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
while (cursor < stripped.length) {
|
|
164
|
+
const asIndex = findTopLevelWord(stripped, "as", cursor);
|
|
165
|
+
|
|
166
|
+
if (asIndex < 0) {
|
|
167
|
+
return "with";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
cursor = skipWhitespace(stripped, asIndex + 2);
|
|
171
|
+
|
|
172
|
+
if (readWordAt(stripped, cursor) === "not") {
|
|
173
|
+
cursor = skipWhitespace(stripped, cursor + 3);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (readWordAt(stripped, cursor) === "materialized") {
|
|
177
|
+
cursor = skipWhitespace(stripped, cursor + "materialized".length);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (stripped[cursor] !== "(") {
|
|
181
|
+
return "with";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const closeIndex = findMatchingParen(stripped, cursor);
|
|
185
|
+
|
|
186
|
+
if (closeIndex < 0) {
|
|
187
|
+
return "with";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
cursor = skipWhitespace(stripped, closeIndex + 1);
|
|
191
|
+
|
|
192
|
+
if (stripped[cursor] === ",") {
|
|
193
|
+
cursor = skipWhitespace(stripped, cursor + 1);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return readWordAt(stripped, cursor) || "with";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return "with";
|
|
201
|
+
}
|
|
202
|
+
|
|
40
203
|
function resolveEffectiveKeyword(sql = "") {
|
|
41
|
-
const [firstKeyword, secondKeyword
|
|
204
|
+
const [firstKeyword, secondKeyword] = getLeadingKeywords(sql, 3);
|
|
42
205
|
|
|
43
206
|
if (firstKeyword === "with") {
|
|
44
|
-
return
|
|
207
|
+
return resolveCteStatementKeyword(sql);
|
|
45
208
|
}
|
|
46
209
|
|
|
47
210
|
if (firstKeyword === "explain") {
|