terrier-engine 4.11.0 → 4.13.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.
|
@@ -20,12 +20,18 @@ const log = new Logger("Queries")
|
|
|
20
20
|
// Query
|
|
21
21
|
////////////////////////////////////////////////////////////////////////////////
|
|
22
22
|
|
|
23
|
+
export type OrderBy = {
|
|
24
|
+
column: string
|
|
25
|
+
dir: string
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export type Query = {
|
|
24
29
|
id: string
|
|
25
30
|
name: string
|
|
26
31
|
notes: string
|
|
27
32
|
from: TableRef
|
|
28
33
|
columns?: string[]
|
|
34
|
+
order_by?: OrderBy[]
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
|
|
@@ -11,6 +11,7 @@ import {TabContainerPart} from "../../terrier/tabs"
|
|
|
11
11
|
import Messages from "tuff-core/messages"
|
|
12
12
|
import Validation, {QueryClientValidation} from "./validation"
|
|
13
13
|
import ColumnOrderModal from "./column-order-modal"
|
|
14
|
+
import RowOrderModal from "./row-order-modal"
|
|
14
15
|
|
|
15
16
|
const log = new Logger("QueryEditor")
|
|
16
17
|
|
|
@@ -78,8 +79,18 @@ class SortingPart extends ContentPart<SubEditorState> {
|
|
|
78
79
|
})
|
|
79
80
|
|
|
80
81
|
this.onClick(this.sortRowsKey, _ => {
|
|
81
|
-
|
|
82
|
+
log.info("Sorting rows")
|
|
83
|
+
this.app.showModal(RowOrderModal, {
|
|
84
|
+
query: this.state.query,
|
|
85
|
+
onSorted: (newOrderBys) => {
|
|
86
|
+
log.info(`New row sort order`, newOrderBys)
|
|
87
|
+
this.state.query.order_by = newOrderBys
|
|
88
|
+
this.state.editor.dirty()
|
|
89
|
+
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
90
|
+
}
|
|
91
|
+
})
|
|
82
92
|
})
|
|
93
|
+
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
renderContent(parent: PartTag): void {
|
|
@@ -104,8 +115,19 @@ class SortingPart extends ContentPart<SubEditorState> {
|
|
|
104
115
|
})
|
|
105
116
|
row.div(".tt-flex.gap.column.full-height", col => {
|
|
106
117
|
col.h3(".glyp-rows").text("Rows")
|
|
107
|
-
col.div(".dive-query-
|
|
108
|
-
|
|
118
|
+
col.div(".dive-query-order-bys.stretch", orderList => {
|
|
119
|
+
if (query.order_by?.length) {
|
|
120
|
+
for (const orderBy of query.order_by) {
|
|
121
|
+
orderList.div('.order-by', line => {
|
|
122
|
+
line.div(".column").text(orderBy.column)
|
|
123
|
+
const dir = orderBy.dir == 'asc' ? 'ascending' : 'descending'
|
|
124
|
+
line.div(`.dir.glyp-${dir}`).text(dir)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
orderList.div(".text-center").text("Unspecified")
|
|
130
|
+
}
|
|
109
131
|
})
|
|
110
132
|
col.a(".tt-button.shrink", button => {
|
|
111
133
|
button.i(".glyp-edit")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {ModalPart} from "../../terrier/modals"
|
|
2
|
+
import Queries, {OrderBy, Query} from "./queries"
|
|
3
|
+
import Messages from "tuff-core/messages"
|
|
4
|
+
import {PartTag} from "tuff-core/parts"
|
|
5
|
+
import {optionsForSelect, SelectOption} from "tuff-core/forms"
|
|
6
|
+
import {Logger} from "tuff-core/logging"
|
|
7
|
+
import Forms from "../../terrier/forms"
|
|
8
|
+
import SortablePlugin from "tuff-sortable/sortable-plugin";
|
|
9
|
+
|
|
10
|
+
const log = new Logger("RowOrderModal")
|
|
11
|
+
|
|
12
|
+
export type RowOrderState = {
|
|
13
|
+
query: Query
|
|
14
|
+
onSorted: (orderBys: OrderBy[]) => any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
18
|
+
|
|
19
|
+
submitKey = Messages.untypedKey()
|
|
20
|
+
newClauseKey = Messages.untypedKey()
|
|
21
|
+
changedKey = Messages.untypedKey()
|
|
22
|
+
orderBys: OrderBy[] = []
|
|
23
|
+
columnOptions: SelectOption[] = []
|
|
24
|
+
removeClauseKey = Messages.typedKey<{index: number}>()
|
|
25
|
+
|
|
26
|
+
async init() {
|
|
27
|
+
this.setTitle("Row Order")
|
|
28
|
+
this.setIcon("glyp-sort")
|
|
29
|
+
|
|
30
|
+
this.addAction({
|
|
31
|
+
title: "Apply",
|
|
32
|
+
icon: "glyp-checkmark",
|
|
33
|
+
click: {key: this.submitKey}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
this.onClick(this.submitKey, _ => {
|
|
37
|
+
this.state.onSorted(this.orderBys)
|
|
38
|
+
this.pop()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
this.addAction({
|
|
42
|
+
title: "New Clause",
|
|
43
|
+
icon: "glyp-plus",
|
|
44
|
+
click: {key: this.newClauseKey}
|
|
45
|
+
}, 'secondary')
|
|
46
|
+
|
|
47
|
+
this.onClick(this.newClauseKey, _ => {
|
|
48
|
+
this.addClause()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
this.onClick(this.removeClauseKey, m => {
|
|
52
|
+
this.removeClause(m.data.index)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// collect the column options
|
|
56
|
+
const query = this.state.query
|
|
57
|
+
Queries.eachColumn(query, (_, col) => {
|
|
58
|
+
this.columnOptions.push({title: col.name, value: col.name})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// initialize the order=bys from the query, if present
|
|
62
|
+
if (query.order_by?.length) {
|
|
63
|
+
this.orderBys = query.order_by
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// start with something by default
|
|
67
|
+
this.addClause()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// serialize on input change
|
|
71
|
+
this.onChange(this.changedKey, _ => {
|
|
72
|
+
this.serialize()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// make the list sortable
|
|
76
|
+
this.makePlugin(SortablePlugin, {
|
|
77
|
+
zoneClass: 'dive-row-sort-zone',
|
|
78
|
+
targetClass: 'order-by',
|
|
79
|
+
onSorted: (_plugin, _evt) => {
|
|
80
|
+
this.serialize()
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addClause() {
|
|
86
|
+
this.orderBys.push({column: this.columnOptions[0]?.value || '', dir: 'asc'})
|
|
87
|
+
log.info(`Added a line, orderBys is now ${this.orderBys.length} long`, this.orderBys)
|
|
88
|
+
this.dirty()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
removeClause(index: number) {
|
|
92
|
+
log.info(`Removing clause ${index}`)
|
|
93
|
+
this.orderBys.splice(index, 1)
|
|
94
|
+
log.info(`Removed line ${index}, orderBys is now ${this.orderBys.length} long`, this.orderBys)
|
|
95
|
+
this.dirty()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
serialize() {
|
|
99
|
+
log.info("Serializing...")
|
|
100
|
+
if (this.element) {
|
|
101
|
+
this.orderBys = []
|
|
102
|
+
this.element.querySelectorAll<HTMLElement>(".order-by").forEach(line => {
|
|
103
|
+
const column = line.querySelector<HTMLSelectElement>("select.column")?.value!!
|
|
104
|
+
const dir = Forms.getRadioValue(line, "input.dir") || "asc"
|
|
105
|
+
this.orderBys.push({column, dir})
|
|
106
|
+
})
|
|
107
|
+
log.info("Serialized", this.orderBys)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
renderContent(parent: PartTag): void {
|
|
112
|
+
parent.div('.tt-flex.column.padded.gap.tt-form', container => {
|
|
113
|
+
container.p().text("Drag and drop the clauses to change their order:")
|
|
114
|
+
container.div(".dive-row-sort-zone", zone => {
|
|
115
|
+
let index = 0 // for making unique radio names
|
|
116
|
+
for (const orderBy of this.orderBys) {
|
|
117
|
+
zone.div(".order-by", {data: {index: index.toString()}}, line => {
|
|
118
|
+
line.a(".drag.glyp-navicon")
|
|
119
|
+
.data({tooltip: "Re-order this clause"})
|
|
120
|
+
line.select('.column', colSelect => {
|
|
121
|
+
optionsForSelect(colSelect, this.columnOptions, orderBy.column)
|
|
122
|
+
}).emitChange(this.changedKey)
|
|
123
|
+
line.label('.caption-size', label => {
|
|
124
|
+
label.input('.dir.dir-asc', {
|
|
125
|
+
type: "radio",
|
|
126
|
+
name: `sort-dir-${index}`,
|
|
127
|
+
value: "asc",
|
|
128
|
+
checked: orderBy.dir == 'asc'
|
|
129
|
+
}).emitChange(this.changedKey)
|
|
130
|
+
label.span().text("ascending")
|
|
131
|
+
})
|
|
132
|
+
line.label('.caption-size', label => {
|
|
133
|
+
label.input('.dir.dir-desc', {
|
|
134
|
+
type: "radio",
|
|
135
|
+
name: `sort-dir-${index}`,
|
|
136
|
+
value: "desc",
|
|
137
|
+
checked: orderBy.dir == 'desc'
|
|
138
|
+
}).emitChange(this.changedKey)
|
|
139
|
+
label.span().text("descending")
|
|
140
|
+
})
|
|
141
|
+
line.a(".remove.glyp-close")
|
|
142
|
+
.data({tooltip: "Remove this clause"})
|
|
143
|
+
.emitClick(this.removeClauseKey, {index})
|
|
144
|
+
})
|
|
145
|
+
index += 1
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
package/package.json
CHANGED
package/terrier/forms.ts
CHANGED
|
@@ -11,6 +11,26 @@ import Strings from "tuff-core/strings"
|
|
|
11
11
|
|
|
12
12
|
const log = new Logger("TerrierForms")
|
|
13
13
|
|
|
14
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
15
|
+
// Utilities
|
|
16
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the value of the checked radio with the given selector.
|
|
20
|
+
* @param container contains the radios
|
|
21
|
+
* @param selector the CSS selector used to select the radios from the container
|
|
22
|
+
*/
|
|
23
|
+
function getRadioValue(container: HTMLElement, selector: string): string | undefined {
|
|
24
|
+
let value: string | undefined = undefined
|
|
25
|
+
container.querySelectorAll<HTMLInputElement>(selector).forEach(radio => {
|
|
26
|
+
if (radio.checked) {
|
|
27
|
+
value = radio.value
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
14
34
|
////////////////////////////////////////////////////////////////////////////////
|
|
15
35
|
// Options
|
|
16
36
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -112,7 +132,8 @@ export class TerrierFormFields<T extends FormPartData> extends FormFields<T> {
|
|
|
112
132
|
////////////////////////////////////////////////////////////////////////////////
|
|
113
133
|
|
|
114
134
|
const Forms = {
|
|
115
|
-
titleizeOptions
|
|
135
|
+
titleizeOptions,
|
|
136
|
+
getRadioValue
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
export default Forms
|