sqlite-hub 0.8.7 → 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 +11 -0
- package/frontend/index.html +1 -1
- package/frontend/js/api.js +67 -0
- package/frontend/js/app.js +1398 -999
- package/frontend/js/components/modal.js +208 -3
- package/frontend/js/components/queryEditor.js +9 -4
- package/frontend/js/components/queryHistoryDetail.js +20 -10
- package/frontend/js/components/queryHistoryPanel.js +4 -3
- package/frontend/js/components/rowEditorPanel.js +26 -5
- package/frontend/js/components/sidebar.js +49 -5
- package/frontend/js/components/structureGraph.js +614 -632
- package/frontend/js/components/tableDesignerSqlPreview.js +24 -25
- package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
- package/frontend/js/router.js +81 -71
- package/frontend/js/store.js +3091 -2184
- package/frontend/js/views/charts.js +2 -2
- package/frontend/js/views/data.js +28 -14
- package/frontend/js/views/editor.js +172 -177
- package/frontend/js/views/mediaTagging.js +861 -0
- package/frontend/styles/base.css +11 -1
- package/frontend/styles/components.css +48 -6
- package/frontend/styles/views.css +796 -0
- 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/storage/appStateStore.js +321 -2
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { escapeHtml, truncateMiddle } from "../utils/format.js";
|
|
1
|
+
import { escapeHtml, highlightSql, truncateMiddle } from "../utils/format.js";
|
|
2
2
|
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
3
3
|
import {
|
|
4
4
|
analyzeQueryChartResult,
|
|
5
5
|
getQueryChartTypeLabel,
|
|
6
6
|
QUERY_CHART_TYPES,
|
|
7
7
|
} from "../lib/queryCharts.js";
|
|
8
|
+
import {
|
|
9
|
+
hasDefaultMediaTaggingTagTable,
|
|
10
|
+
hasDefaultMediaTaggingMappingTable,
|
|
11
|
+
MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
|
|
12
|
+
MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL,
|
|
13
|
+
MEDIA_TAGGING_DEFAULT_TAG_TABLE,
|
|
14
|
+
MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
|
|
15
|
+
} from "../lib/mediaTaggingDefaults.js";
|
|
8
16
|
|
|
9
17
|
function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
|
|
10
18
|
return `
|
|
@@ -25,7 +33,7 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
|
|
|
25
33
|
|
|
26
34
|
function renderCheckboxField({ label, name, checked = false, text }) {
|
|
27
35
|
return `
|
|
28
|
-
<label class="
|
|
36
|
+
<label class="flex flex-col gap-2">
|
|
29
37
|
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
30
38
|
${escapeHtml(label)}
|
|
31
39
|
</span>
|
|
@@ -41,6 +49,27 @@ function renderCheckboxField({ label, name, checked = false, text }) {
|
|
|
41
49
|
`;
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
function renderSqlPreviewField(value, minHeightClass = "sql-highlight-shell--tall") {
|
|
53
|
+
return `
|
|
54
|
+
<div class="sql-highlight-shell ${minHeightClass}">
|
|
55
|
+
<div class="query-editor-layer sql-highlight-layer">
|
|
56
|
+
<div
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
class="query-editor-highlight sql-highlight-content"
|
|
59
|
+
data-query-editor-highlight
|
|
60
|
+
>${value ? highlightSql(value) : ""}</div>
|
|
61
|
+
<textarea
|
|
62
|
+
class="query-editor-input sql-highlight-input custom-scrollbar"
|
|
63
|
+
data-sql-highlight="true"
|
|
64
|
+
readonly
|
|
65
|
+
spellcheck="false"
|
|
66
|
+
wrap="off"
|
|
67
|
+
>${escapeHtml(value)}</textarea>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
function renderFileField({
|
|
45
74
|
label,
|
|
46
75
|
name,
|
|
@@ -70,7 +99,7 @@ function renderFileField({
|
|
|
70
99
|
function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
|
|
71
100
|
return `
|
|
72
101
|
<label class="block space-y-2">
|
|
73
|
-
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
102
|
+
<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
74
103
|
${escapeHtml(label)}
|
|
75
104
|
</span>
|
|
76
105
|
<select
|
|
@@ -687,6 +716,167 @@ function renderDeleteChartForm(modal) {
|
|
|
687
716
|
`;
|
|
688
717
|
}
|
|
689
718
|
|
|
719
|
+
function renderDeleteQueryHistoryForm(modal) {
|
|
720
|
+
return `
|
|
721
|
+
<form class="space-y-5" data-form="delete-query-history-confirm">
|
|
722
|
+
<div class="space-y-3">
|
|
723
|
+
<p class="text-sm leading-7 text-on-surface">
|
|
724
|
+
Delete query <span class="font-bold text-primary-container">${escapeHtml(
|
|
725
|
+
modal.queryTitle ?? "SQL query"
|
|
726
|
+
)}</span>?
|
|
727
|
+
</p>
|
|
728
|
+
<p class="text-sm leading-7 text-on-surface-variant/65">
|
|
729
|
+
This removes the query-history entry and all recorded runs linked to it.
|
|
730
|
+
</p>
|
|
731
|
+
</div>
|
|
732
|
+
${renderError(modal.error)}
|
|
733
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
734
|
+
<button
|
|
735
|
+
class="standard-button"
|
|
736
|
+
data-action="close-modal"
|
|
737
|
+
type="button"
|
|
738
|
+
>
|
|
739
|
+
Cancel
|
|
740
|
+
</button>
|
|
741
|
+
<button
|
|
742
|
+
class="delete-button"
|
|
743
|
+
type="submit"
|
|
744
|
+
>
|
|
745
|
+
${modal.submitting ? "Deleting..." : "Delete Query"}
|
|
746
|
+
</button>
|
|
747
|
+
</div>
|
|
748
|
+
</form>
|
|
749
|
+
`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function renderCreateMediaTaggingMappingTableForm(modal, state) {
|
|
753
|
+
const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
|
|
754
|
+
const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
|
|
755
|
+
|
|
756
|
+
return `
|
|
757
|
+
<form class="space-y-5" data-form="create-media-tagging-mapping-table">
|
|
758
|
+
<div class="space-y-3">
|
|
759
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
760
|
+
Mapping Table
|
|
761
|
+
</div>
|
|
762
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-sm text-on-surface">
|
|
763
|
+
${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)}
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="space-y-3">
|
|
767
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
768
|
+
Status
|
|
769
|
+
</div>
|
|
770
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
|
|
771
|
+
${
|
|
772
|
+
mappingExists
|
|
773
|
+
? `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} already exists in the active database.`
|
|
774
|
+
: `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} does not exist yet.`
|
|
775
|
+
}
|
|
776
|
+
${
|
|
777
|
+
readOnly && !mappingExists
|
|
778
|
+
? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
|
|
779
|
+
: ""
|
|
780
|
+
}
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
<div class="space-y-3">
|
|
784
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
785
|
+
SQL
|
|
786
|
+
</div>
|
|
787
|
+
${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL)}
|
|
788
|
+
</div>
|
|
789
|
+
${renderError(modal.error)}
|
|
790
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
791
|
+
<button
|
|
792
|
+
class="standard-button"
|
|
793
|
+
data-action="close-modal"
|
|
794
|
+
type="button"
|
|
795
|
+
>
|
|
796
|
+
Close
|
|
797
|
+
</button>
|
|
798
|
+
${
|
|
799
|
+
!mappingExists
|
|
800
|
+
? `
|
|
801
|
+
<button
|
|
802
|
+
class="standard-button"
|
|
803
|
+
type="submit"
|
|
804
|
+
${readOnly ? "disabled" : ""}
|
|
805
|
+
>
|
|
806
|
+
${modal.submitting ? "Creating..." : "Create Table"}
|
|
807
|
+
</button>
|
|
808
|
+
`
|
|
809
|
+
: ""
|
|
810
|
+
}
|
|
811
|
+
</div>
|
|
812
|
+
</form>
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function renderCreateMediaTaggingTagTableForm(modal, state) {
|
|
817
|
+
const tagTableExists = hasDefaultMediaTaggingTagTable(state.mediaTagging.schemaTables ?? []);
|
|
818
|
+
const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
|
|
819
|
+
|
|
820
|
+
return `
|
|
821
|
+
<form class="space-y-5" data-form="create-media-tagging-tag-table">
|
|
822
|
+
<div class="space-y-3">
|
|
823
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
824
|
+
Tag Table
|
|
825
|
+
</div>
|
|
826
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-sm text-on-surface">
|
|
827
|
+
${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)}
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
<div class="space-y-3">
|
|
831
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
832
|
+
Status
|
|
833
|
+
</div>
|
|
834
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
|
|
835
|
+
${
|
|
836
|
+
tagTableExists
|
|
837
|
+
? `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} already exists in the active database.`
|
|
838
|
+
: `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} does not exist yet.`
|
|
839
|
+
}
|
|
840
|
+
${
|
|
841
|
+
readOnly && !tagTableExists
|
|
842
|
+
? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
|
|
843
|
+
: ""
|
|
844
|
+
}
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
<div class="space-y-3">
|
|
848
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
849
|
+
SQL
|
|
850
|
+
</div>
|
|
851
|
+
${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL)}
|
|
852
|
+
</div>
|
|
853
|
+
${renderError(modal.error)}
|
|
854
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
855
|
+
<button
|
|
856
|
+
class="standard-button"
|
|
857
|
+
data-action="close-modal"
|
|
858
|
+
type="button"
|
|
859
|
+
>
|
|
860
|
+
Close
|
|
861
|
+
</button>
|
|
862
|
+
${
|
|
863
|
+
!tagTableExists
|
|
864
|
+
? `
|
|
865
|
+
<button
|
|
866
|
+
class="standard-button"
|
|
867
|
+
type="submit"
|
|
868
|
+
${readOnly ? "disabled" : ""}
|
|
869
|
+
>
|
|
870
|
+
${modal.submitting ? "Creating..." : "Create Table"}
|
|
871
|
+
</button>
|
|
872
|
+
`
|
|
873
|
+
: ""
|
|
874
|
+
}
|
|
875
|
+
</div>
|
|
876
|
+
</form>
|
|
877
|
+
`;
|
|
878
|
+
}
|
|
879
|
+
|
|
690
880
|
export function renderModal(state) {
|
|
691
881
|
const modal = state.modal;
|
|
692
882
|
|
|
@@ -730,6 +920,21 @@ export function renderModal(state) {
|
|
|
730
920
|
title: "Delete Chart",
|
|
731
921
|
body: renderDeleteChartForm(modal),
|
|
732
922
|
},
|
|
923
|
+
"delete-query-history": {
|
|
924
|
+
eyebrow: "History // Confirm query deletion",
|
|
925
|
+
title: "Delete Query",
|
|
926
|
+
body: renderDeleteQueryHistoryForm(modal),
|
|
927
|
+
},
|
|
928
|
+
"create-media-tagging-tag-table": {
|
|
929
|
+
eyebrow: "Media Tagging // Create default tag table",
|
|
930
|
+
title: "Create Tag Table",
|
|
931
|
+
body: renderCreateMediaTaggingTagTableForm(modal, state),
|
|
932
|
+
},
|
|
933
|
+
"create-media-tagging-mapping-table": {
|
|
934
|
+
eyebrow: "Media Tagging // Create default join table",
|
|
935
|
+
title: "Create Mapping Table",
|
|
936
|
+
body: renderCreateMediaTaggingMappingTableForm(modal, state),
|
|
937
|
+
},
|
|
733
938
|
};
|
|
734
939
|
|
|
735
940
|
const config = contentByKind[modal.kind];
|
|
@@ -19,11 +19,16 @@ function renderHighlightedQuery(query) {
|
|
|
19
19
|
|
|
20
20
|
function renderEditorSurface({ query }) {
|
|
21
21
|
return `
|
|
22
|
-
<div class="flex min-h-0 flex-1 overflow-hidden">
|
|
23
|
-
<div class="flex w-12
|
|
24
|
-
|
|
22
|
+
<div class="query-editor-shell flex min-h-0 flex-1 overflow-hidden">
|
|
23
|
+
<div class="flex w-12 min-h-0 overflow-hidden bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
|
|
24
|
+
<div
|
|
25
|
+
class="query-editor-gutter-track flex w-full flex-col items-center"
|
|
26
|
+
data-query-editor-gutter
|
|
27
|
+
>
|
|
28
|
+
${renderLineNumbers(query)}
|
|
29
|
+
</div>
|
|
25
30
|
</div>
|
|
26
|
-
<div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
|
|
31
|
+
<div class="relative min-h-0 flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
|
|
27
32
|
<div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
|
|
28
33
|
<span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
|
|
29
34
|
</div>
|
|
@@ -19,6 +19,10 @@ function renderDetailMetaItem(label, value) {
|
|
|
19
19
|
`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function canOpenQueryHistoryInCharts(item) {
|
|
23
|
+
return Boolean(item?.chartsEligible);
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
function renderRunItem(run) {
|
|
23
27
|
return `
|
|
24
28
|
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
@@ -182,15 +186,21 @@ export function renderQueryHistoryDetail({
|
|
|
182
186
|
>
|
|
183
187
|
Open In Editor
|
|
184
188
|
</button>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
${
|
|
190
|
+
canOpenQueryHistoryInCharts(item)
|
|
191
|
+
? `
|
|
192
|
+
<button
|
|
193
|
+
class="standard-button"
|
|
194
|
+
data-action="navigate"
|
|
195
|
+
data-to="/charts/${encodeURIComponent(item.id)}"
|
|
196
|
+
type="button"
|
|
197
|
+
>
|
|
198
|
+
<span class="material-symbols-outlined text-sm">bar_chart</span>
|
|
199
|
+
Open In Charts
|
|
200
|
+
</button>
|
|
201
|
+
`
|
|
202
|
+
: ""
|
|
203
|
+
}
|
|
194
204
|
<button
|
|
195
205
|
class="standard-button"
|
|
196
206
|
data-action="run-query-history"
|
|
@@ -210,7 +220,7 @@ export function renderQueryHistoryDetail({
|
|
|
210
220
|
</button>
|
|
211
221
|
<button
|
|
212
222
|
class="delete-button"
|
|
213
|
-
data-action="delete-query-history"
|
|
223
|
+
data-action="open-delete-query-history-modal"
|
|
214
224
|
data-history-id="${escapeHtml(item.id)}"
|
|
215
225
|
type="button"
|
|
216
226
|
>
|
|
@@ -20,7 +20,7 @@ function getQueryTypeTone(queryType, isDestructive) {
|
|
|
20
20
|
return "muted";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId) {
|
|
24
24
|
const isActive = Number(activeHistoryId) === Number(item.id);
|
|
25
25
|
const isSelected = Number(selectedHistoryId) === Number(item.id);
|
|
26
26
|
const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
|
|
@@ -109,6 +109,7 @@ function renderQueryHistoryTabs(activeTab, historyTotal) {
|
|
|
109
109
|
const tabs = [
|
|
110
110
|
{ id: "recent", label: "Recent" },
|
|
111
111
|
{ id: "saved", label: "Saved" },
|
|
112
|
+
{ id: "unsaved", label: "Unsaved" },
|
|
112
113
|
{ id: "failed", label: "Failed" },
|
|
113
114
|
];
|
|
114
115
|
|
|
@@ -204,9 +205,9 @@ export function renderQueryHistoryPanel({
|
|
|
204
205
|
`
|
|
205
206
|
: ""
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
+
<div class="space-y-3">
|
|
208
209
|
${items
|
|
209
|
-
.map((item) =>
|
|
210
|
+
.map((item) => renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId))
|
|
210
211
|
.join("")}
|
|
211
212
|
</div>
|
|
212
213
|
${
|
|
@@ -42,12 +42,23 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function renderReadonlyField(label, value) {
|
|
45
|
+
const badges = Array.isArray(label?.badges) ? label.badges : [];
|
|
46
|
+
const displayLabel = typeof label === "object" ? label.label : label;
|
|
45
47
|
const jsonPreview = getJsonPreview(value);
|
|
46
48
|
|
|
47
49
|
return `
|
|
48
50
|
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
49
|
-
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
50
|
-
|
|
51
|
+
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
52
|
+
<span>${escapeHtml(displayLabel)}</span>
|
|
53
|
+
${badges
|
|
54
|
+
.map(
|
|
55
|
+
(badge) => `
|
|
56
|
+
<span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
|
|
57
|
+
${escapeHtml(badge)}
|
|
58
|
+
</span>
|
|
59
|
+
`
|
|
60
|
+
)
|
|
61
|
+
.join("")}
|
|
51
62
|
</div>
|
|
52
63
|
${
|
|
53
64
|
jsonPreview
|
|
@@ -59,12 +70,22 @@ function renderReadonlyField(label, value) {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
function renderEditableField(field) {
|
|
73
|
+
const badges = Array.isArray(field.badges) ? field.badges : [];
|
|
62
74
|
const jsonPreview = getJsonPreview(field.value);
|
|
63
75
|
|
|
64
76
|
return `
|
|
65
77
|
<label class="block space-y-2">
|
|
66
|
-
<span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
67
|
-
|
|
78
|
+
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
79
|
+
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
80
|
+
${badges
|
|
81
|
+
.map(
|
|
82
|
+
(badge) => `
|
|
83
|
+
<span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
|
|
84
|
+
${escapeHtml(badge)}
|
|
85
|
+
</span>
|
|
86
|
+
`
|
|
87
|
+
)
|
|
88
|
+
.join("")}
|
|
68
89
|
</span>
|
|
69
90
|
${
|
|
70
91
|
jsonPreview
|
|
@@ -135,7 +156,7 @@ export function renderRowEditorPanel({
|
|
|
135
156
|
<button
|
|
136
157
|
class="delete-button"
|
|
137
158
|
data-action="${escapeHtml(deleteAction)}"
|
|
138
|
-
data-row-index="${escapeHtml(String(deleteRowIndex
|
|
159
|
+
${deleteRowIndex === null ? "" : `data-row-index="${escapeHtml(String(deleteRowIndex))}"`}
|
|
139
160
|
type="button"
|
|
140
161
|
${saving || deleting ? "disabled" : ""}
|
|
141
162
|
>
|
|
@@ -5,10 +5,19 @@ const sidebarItems = [
|
|
|
5
5
|
{ label: 'Connections', href: '#/connections', key: 'connections', icon: 'database' },
|
|
6
6
|
{ label: 'Overview', href: '#/overview', key: 'overview', icon: 'dashboard' },
|
|
7
7
|
{ label: 'Data', href: '#/data', key: 'data', icon: 'table_rows' },
|
|
8
|
-
{ label: 'SQL Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
|
|
9
8
|
{ label: 'Structure', href: '#/structure', key: 'structure', icon: 'account_tree' },
|
|
9
|
+
{ label: 'SQL Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
|
|
10
10
|
{ label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
|
|
11
11
|
{ label: 'Table Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
|
|
12
|
+
{
|
|
13
|
+
label: 'Media Tagging',
|
|
14
|
+
key: 'mediaTagging',
|
|
15
|
+
icon: 'sell',
|
|
16
|
+
children: [
|
|
17
|
+
{ label: 'Setup', href: '#/media-tagging', key: 'mediaTaggingSetup' },
|
|
18
|
+
{ label: 'Tagging Queue', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
12
21
|
{ label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
|
|
13
22
|
];
|
|
14
23
|
|
|
@@ -21,24 +30,59 @@ function getActiveSidebarKey(routeName) {
|
|
|
21
30
|
return 'editor';
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
if (routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue') {
|
|
34
|
+
return 'mediaTagging';
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
return routeName;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
export function renderSidebar(state) {
|
|
28
41
|
const activeKey = getActiveSidebarKey(state.route.name);
|
|
29
42
|
const activeConnection = state.connections.active;
|
|
43
|
+
const expandedKey = activeKey === 'mediaTagging' ? 'mediaTagging' : null;
|
|
30
44
|
|
|
31
45
|
return `
|
|
32
46
|
<nav class="sidebar-links">
|
|
33
47
|
${sidebarItems
|
|
34
|
-
.map(
|
|
35
|
-
item
|
|
48
|
+
.map(item => {
|
|
49
|
+
if (item.children) {
|
|
50
|
+
const isExpanded = expandedKey === item.key;
|
|
51
|
+
const isActive = activeKey === item.key;
|
|
52
|
+
return `
|
|
53
|
+
<div class="sidebar-group">
|
|
54
|
+
<a class="sidebar-link ${isActive ? 'is-active' : ''}" href="${item.children[0].href}" data-group="${item.key}">
|
|
55
|
+
<span class="material-symbols-outlined">${item.icon}</span>
|
|
56
|
+
<span>${item.label}</span>
|
|
57
|
+
<span class="material-symbols-outlined ml-auto text-[14px] ${isExpanded ? 'rotate-180' : ''}">expand_more</span>
|
|
58
|
+
</a>
|
|
59
|
+
${
|
|
60
|
+
isExpanded
|
|
61
|
+
? `
|
|
62
|
+
<div class="sidebar-sublinks">
|
|
63
|
+
${item.children
|
|
64
|
+
.map(
|
|
65
|
+
child => `
|
|
66
|
+
<a class="sidebar-sublink ${state.route.name === child.key ? 'is-active' : ''}" href="${child.href}">
|
|
67
|
+
${child.label}
|
|
68
|
+
</a>
|
|
69
|
+
`,
|
|
70
|
+
)
|
|
71
|
+
.join('')}
|
|
72
|
+
</div>
|
|
73
|
+
`
|
|
74
|
+
: ''
|
|
75
|
+
}
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
return `
|
|
36
80
|
<a class="sidebar-link ${item.key === activeKey ? 'is-active' : ''}" href="${item.href}">
|
|
37
81
|
<span class="material-symbols-outlined">${item.icon}</span>
|
|
38
82
|
<span>${item.label}</span>
|
|
39
83
|
</a>
|
|
40
|
-
|
|
41
|
-
)
|
|
84
|
+
`;
|
|
85
|
+
})
|
|
42
86
|
.join('')}
|
|
43
87
|
</nav>
|
|
44
88
|
<div class="sidebar-footer">
|