Skip to content

Commit 7d449b2

Browse files
centralise and improve task search and filter controls
* Add functionality to the ViewToolbar component: * Add `action=menu` for dropdowns. * Add `action=input` for text input. * Allow `icon=Object` to configure state-dependent icons. * Reduce icon spacing and improve group divider. * Move the task search & filter controls into the ViewToolbar component. * Switch the Tree and Table views over to the ViewToolbar. * Addresses #471 * Improve the task state filter control: * Add support for task modifiers - closes #1666 * Collapse the control into a single button/menu (take up less space). * Reduce the width of the task search input, automatically increase this width when focused or when text is present in it.
1 parent 2705362 commit 7d449b2

File tree

14 files changed

+547
-216
lines changed

14 files changed

+547
-216
lines changed

src/components/cylc/TaskFilter.vue

Lines changed: 0 additions & 61 deletions
This file was deleted.

src/components/cylc/ViewToolbar.vue

Lines changed: 171 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,83 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
3030
class="control"
3131
:data-cy="`control-${iControl.key}`"
3232
>
33-
<v-btn
34-
@click="iControl.callback"
35-
v-bind="btnProps"
36-
:disabled="iControl.disabled"
37-
:aria-checked="iControl.value"
38-
:color="iControl.value ? 'blue' : undefined"
39-
role="switch"
33+
<!-- menu component (to support dropdowns) -->
34+
<v-menu
35+
eager
36+
:close-on-content-click="false"
4037
>
41-
<v-icon>{{ iControl.icon }}</v-icon>
42-
<v-tooltip>{{ iControl.title }}</v-tooltip>
43-
</v-btn>
38+
<template v-slot:activator="{ props }">
39+
<!-- inputs -->
40+
<v-text-field
41+
v-if="iControl.action === 'input'"
42+
class="input"
43+
v-bind="iControl.props"
44+
clearable
45+
:prepend-inner-icon="iControl.icon"
46+
@update:modelValue="iControl.callback"
47+
@focus="autoResizeInput"
48+
@blur="autoResizeInput"
49+
/>
50+
51+
<!-- buttons -->
52+
<v-btn
53+
v-else
54+
class="control-btn"
55+
v-bind="{...$attrs, ...props, ...btnProps}"
56+
@click="(e) => {iControl.action === 'menu' ? null : iControl.callback(e)}"
57+
:disabled="iControl.disabled"
58+
:aria-checked="iControl.value"
59+
:color="isSet(iControl.value) ? 'blue' : undefined"
60+
role="switch"
61+
density="compact"
62+
>
63+
<v-icon>{{ iControl.icon[iControl.value] || iControl.icon }}</v-icon>
64+
<v-tooltip>{{ iControl.title }}</v-tooltip>
65+
</v-btn>
66+
</template>
67+
68+
<!-- dropdowns -->
69+
<v-treeview
70+
v-if="iControl.action === 'menu'"
71+
v-bind="iControl.props"
72+
@update:activated="iControl.callback"
73+
color="blue"
74+
density="compact"
75+
>
76+
<!-- task icons (for task state filters) -->
77+
<template
78+
v-slot:prepend="{ item }"
79+
v-if="iControl.props['task-state-icons']"
80+
>
81+
<Task :task="item.props" />
82+
</template>
83+
<!-- disable expansion until parent active (for task state filters) -->
84+
<template
85+
v-slot:toggle="{ props: toggleProps, isActive, isOpen }"
86+
v-if="iControl.props['task-state-icons']"
87+
>
88+
<v-icon
89+
:icon="isOpen ? $options.icons.mdiChevronUp : $options.icons.mdiChevronDown"
90+
:disabled="!isActive && !isOpen"
91+
v-bind="toggleProps"
92+
/>
93+
</template>
94+
</v-treeview>
95+
</v-menu>
4496
</div>
4597
</div>
4698
</div>
4799
</template>
48100

49101
<script>
50-
import { btnProps } from '@/utils/viewToolbar'
102+
import { btnProps, taskStateItems } from '@/utils/viewToolbar'
103+
import Task from '@/components/cylc/Task.vue'
104+
import {
105+
mdiChevronDown,
106+
mdiChevronUp,
107+
mdiFilter,
108+
mdiMagnify,
109+
} from '@mdi/js'
51110
52111
export default {
53112
name: 'ViewToolbar',
@@ -56,10 +115,20 @@ export default {
56115
'setOption'
57116
],
58117
118+
components: {
119+
Task
120+
},
121+
122+
icons: {
123+
mdiChevronDown,
124+
mdiChevronUp,
125+
mdiMagnify,
126+
},
127+
59128
props: {
60129
groups: {
61130
required: true,
62-
type: Array
131+
type: Array,
63132
/*
64133
groups: [
65134
{
@@ -70,21 +139,40 @@ export default {
70139
{
71140
// display name
72141
title: String,
142+
73143
// unique key:
74144
// * Provided with "setOption" events.
75145
// * Used by enableIf/disableIf
76146
// * Added to the control's class list for testing.
77147
key: String
148+
149+
// icon for the control:
150+
// * Either an icon.
151+
// * Or a mapping of state to an icon.
152+
// NOTE: this is autopopulated for action="taskStateFilter | taskIDFilter"
153+
icon: Icon | Object[key, Icon]
154+
78155
// action to perform when clicked:
156+
// Generic actions:
79157
// * toggle - toggle true/false
80158
// * callback - call the provided callback
159+
// * menu - open a menu (provide props: {items} in v-treeview format)
160+
// Specialised actions:
161+
// * taskIDFilter - Search box for task IDs
162+
// * taskStateFilter - open a task state filter menu
81163
action: String
164+
82165
// for use with action='callback'
83166
callback: Fuction
167+
168+
// props to be set on the control
169+
props: Object
170+
84171
// list of keys
85172
// only enable this control if all of the listed controls have
86173
// truthy values
87174
enableIf
175+
88176
// list of keys
89177
// disable this control if any of the listed controls have
90178
// truthy values
@@ -111,6 +199,7 @@ export default {
111199
let iControl
112200
let callback // callback to fire when control is activated
113201
let disabled // true if control should not be enabled
202+
let props
114203
const values = this.getValues()
115204
for (const group of this.groups) {
116205
iGroup = {
@@ -120,15 +209,43 @@ export default {
120209
for (const control of group.controls) {
121210
callback = null
122211
disabled = false
212+
props = control.props || {}
123213
124214
// set callback
125215
switch (control.action) {
126-
case 'toggle':
216+
case 'toggle': // toggle button
127217
callback = (e) => this.toggle(control, e)
128218
break
129-
case 'callback':
219+
case 'callback': // button which actions a callback
130220
callback = (e) => this.call(control, e)
131221
break
222+
case 'taskIDFilter': // specialised "input" for filtering tasks
223+
callback = (value) => this.set(control, value)
224+
control.icon = mdiMagnify
225+
control.action = 'input'
226+
props = {
227+
placeholder: 'Search',
228+
...props,
229+
}
230+
break
231+
case 'input': // text input
232+
callback = (value) => this.set(control, value)
233+
break
234+
case 'taskStateFilter': // specialised "menu" for filtering tasks
235+
control.action = 'menu'
236+
control.icon = mdiFilter
237+
props = {
238+
items: taskStateItems,
239+
'indent-lines': true,
240+
activatable: true,
241+
'active-strategy': 'independent',
242+
'item-value': 'value',
243+
'task-state-icons': true, // flag to enable special slots
244+
...props,
245+
246+
}
247+
callback = (value) => this.set(control, value)
248+
break
132249
}
133250
134251
// set disabled
@@ -147,6 +264,7 @@ export default {
147264
148265
iControl = {
149266
...control,
267+
props,
150268
callback,
151269
disabled
152270
}
@@ -186,6 +304,29 @@ export default {
186304
}
187305
return vars
188306
},
307+
set (control, value) {
308+
// update the value
309+
this.$emit('setOption', control.key, value)
310+
},
311+
autoResizeInput (e) {
312+
// enlarge a text input when focused or containing text
313+
if (e.type === 'focus') {
314+
e.target.classList.add('expanded')
315+
} else {
316+
if (e.target.value) {
317+
e.target.classList.add('expanded')
318+
} else {
319+
e.target.classList.remove('expanded')
320+
}
321+
}
322+
},
323+
isSet (value) {
324+
// determine if a control is active or not
325+
if (Array.isArray(value)) {
326+
return value.length
327+
}
328+
return value
329+
}
189330
}
190331
}
191332
</script>
@@ -204,11 +345,25 @@ export default {
204345
// place a divider between groups
205346
content: '';
206347
height: 70%;
207-
width: 2px;
208-
background: rgb(0, 0, 0, 0.22);
348+
width: 0.15em;
349+
border-radius: 0.15em;
350+
background: rgb(0, 0, 0, 0.18);
209351
// put a bit of space between the groups
210352
margin: 0 $spacing;
211353
}
354+
355+
// pack buttons more tightly than the vuetify default
356+
.control-btn {
357+
margin: 0.4em 0.25em 0.4em 0.25em;
358+
}
359+
360+
// auto expand/collapse the search bar
361+
.input {
362+
width: 8em;
363+
}
364+
.input:has(input.expanded) {
365+
width: 20em;
366+
}
212367
}
213368
}
214369
</style>

src/components/cylc/common/filter.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717

1818
/* Logic for filtering tasks. */
1919

20+
import {
21+
GenericModifierNames,
22+
TaskState,
23+
TaskStateNames,
24+
WaitingStateModifierNames,
25+
} from '@/model/TaskState.model'
26+
2027
/**
2128
* Return true if the node ID matches the given ID, or if no ID is given.
2229
*
@@ -36,8 +43,25 @@ export function matchID (node, id) {
3643
* @param {?string[]} states
3744
* @returns {boolean}
3845
*/
39-
export function matchState (node, states) {
40-
return !states?.length || states.includes(node.node.state)
46+
export function matchState (
47+
node,
48+
states = [],
49+
waitingStateModifiers = [],
50+
genericModifiers = [],
51+
) {
52+
return (
53+
(!states?.length || states.includes(node.node.state)) &&
54+
(
55+
node.node.state !== 'waiting' ||
56+
!states.includes(TaskState.WAITING.name) ||
57+
!waitingStateModifiers.length ||
58+
waitingStateModifiers.some((modifier) => node.node[modifier])
59+
) &&
60+
(
61+
!genericModifiers.length ||
62+
genericModifiers.some((modifier) => node.node[modifier])
63+
)
64+
)
4165
}
4266

4367
/**
@@ -49,6 +73,14 @@ export function matchState (node, states) {
4973
* @param {?string[]} states
5074
* @return {boolean}
5175
*/
52-
export function matchNode (node, id, states) {
53-
return matchID(node, id) && matchState(node, states)
76+
export function matchNode (node, id, states, waitingStateModifiers, genericModifiers) {
77+
return matchID(node, id) && matchState(node, states, waitingStateModifiers, genericModifiers)
78+
}
79+
80+
export function groupStateFilters (states) {
81+
return [
82+
states.filter(x => TaskStateNames.includes(x)),
83+
states.filter(x => WaitingStateModifierNames.includes(x)),
84+
states.filter(x => GenericModifierNames.includes(x)),
85+
]
5486
}

0 commit comments

Comments
 (0)