Skip to content

Commit af2a960

Browse files
Use selectedItems composable in GridList
Builds on galaxyproject#19973
1 parent 10b73b1 commit af2a960

5 files changed

Lines changed: 84 additions & 37 deletions

File tree

client/src/components/Grid/GridList.vue

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { BAlert, BButton, BCard, BFormCheckbox, BOverlay, BPagination } from "bo
77
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
88
import { useRouter } from "vue-router/composables";
99
10+
import { useSelectedItems } from "@/composables/selectedItems/selectedItems";
11+
1012
import type { BatchOperation, FieldEntry, FieldHandler, GridConfig, Operation, RowData } from "./configs/types";
1113
1214
import HelpText from "../Help/HelpText.vue";
@@ -67,11 +69,6 @@ const errorMessage = ref("");
6769
const operationMessage = ref("");
6870
const operationStatus = ref("");
6971
70-
// selection references
71-
const selected = ref(new Set<RowData>());
72-
const selectedAll = computed(() => gridData.value.length === selected.value.size);
73-
const selectedIndeterminate = computed(() => ![0, gridData.value.length].includes(selected.value.size));
74-
7572
// expand references
7673
const expanded = ref(new Set<RowData>());
7774
@@ -109,6 +106,54 @@ const hideMessage = useDebounceFn(() => {
109106
operationMessage.value = "";
110107
}, props.delay);
111108
109+
// selection references
110+
const {
111+
allSelected,
112+
initSelectedItem,
113+
itemRefs,
114+
selectedItems: selected,
115+
isSelected,
116+
onClick,
117+
onKeyDown,
118+
resetSelection,
119+
selectAllInCurrentQuery,
120+
selectionSize,
121+
setSelected,
122+
} = useSelectedItems<RowData, typeof HTMLTableRowElement>({
123+
scopeKey: computed(() => `grid-${props.gridConfig.id}`),
124+
getItemKey: props.gridConfig.getItemKey || getItemKey,
125+
filterText: filterText,
126+
totalItemsInQuery: computed(() => totalRows.value),
127+
allItems: gridData,
128+
filterClass: filterClass,
129+
selectable: computed(() => Boolean(props.gridConfig.batch)),
130+
onDelete: (item) => {
131+
// TODO: Is this a naive way to find the delete operation?
132+
const fieldEntry = props.gridConfig.fields.find((f) => f.type === "operations");
133+
const operation = fieldEntry?.operations?.find((o) => o.title === "Delete");
134+
if (operation) {
135+
operation.handler(item);
136+
}
137+
},
138+
expectedKeyDownClass: "grid-data-tbody",
139+
});
140+
const selectedIndeterminate = computed(() => ![0, gridData.value.length].includes(selectionSize.value));
141+
142+
function isRangeSelectAnchor(rowData: RowData): boolean {
143+
if (initSelectedItem.value) {
144+
const itemKey = props.gridConfig.getItemKey ?? getItemKey;
145+
return itemKey(rowData) === itemKey(initSelectedItem.value);
146+
}
147+
return false;
148+
}
149+
150+
function getItemKey(item: RowData): string {
151+
if (item && typeof item === "object" && "id" in item) {
152+
return String(item.id);
153+
}
154+
return JSON.stringify(item);
155+
}
156+
112157
/**
113158
* Manually set filter value, used for tags and `SharingIndicators`
114159
*/
@@ -159,7 +204,7 @@ function fieldTitle(fieldEntry: FieldEntry): string | null {
159204
*/
160205
async function getGridData() {
161206
resultsLoading.value = true;
162-
selected.value = new Set();
207+
resetSelection();
163208
if (props.gridConfig) {
164209
if (hasInvalidFilters.value) {
165210
// there are invalid filters, so we don't want to search
@@ -250,24 +295,6 @@ function onFilter(filter?: string) {
250295
}
251296
}
252297
253-
// Select multiple rows
254-
function onSelect(rowData: RowData) {
255-
if (selected.value.has(rowData)) {
256-
selected.value.delete(rowData);
257-
} else {
258-
selected.value.add(rowData);
259-
}
260-
selected.value = new Set(selected.value);
261-
}
262-
263-
function onSelectAll(current: boolean): void {
264-
if (current) {
265-
selected.value = new Set(gridData.value);
266-
} else {
267-
selected.value = new Set();
268-
}
269-
}
270-
271298
/**
272299
* Show details for a row
273300
*/
@@ -396,9 +423,9 @@ watch(operationMessage, () => {
396423
<th v-if="!!gridConfig.batch">
397424
<BFormCheckbox
398425
class="m-2"
399-
:checked="selectedAll"
426+
:checked="allSelected"
400427
:indeterminate="selectedIndeterminate"
401-
@change="onSelectAll" />
428+
@change="selectAllInCurrentQuery" />
402429
</th>
403430
<th
404431
v-for="(fieldEntry, fieldIndex) in gridConfig.fields"
@@ -424,14 +451,25 @@ watch(operationMessage, () => {
424451
<span v-else-if="fieldTitle(fieldEntry)">{{ fieldTitle(fieldEntry) }}</span>
425452
</th>
426453
</thead>
427-
<tbody v-for="(rowData, rowIndex) in gridData" :key="rowIndex" data-description="grid item">
454+
<tbody
455+
v-for="(rowData, rowIndex) in gridData"
456+
:id="gridConfig.getItemKey ? gridConfig.getItemKey(rowData) : getItemKey(rowData)"
457+
:ref="itemRefs[gridConfig.getItemKey ? gridConfig.getItemKey(rowData) : getItemKey(rowData)]"
458+
:key="rowIndex"
459+
class="grid-data-tbody"
460+
:class="{ 'range-select-anchor-item': isRangeSelectAnchor(rowData) }"
461+
data-description="grid item"
462+
tabindex="0"
463+
role="row"
464+
@keydown="onKeyDown(rowData, $event)"
465+
@click="onClick(rowData, $event)">
428466
<tr :class="{ 'grid-dark-row': rowIndex % 2 }">
429467
<td v-if="!!gridConfig.batch">
430468
<BFormCheckbox
431-
:checked="selected.has(rowData)"
469+
:checked="isSelected(rowData)"
432470
class="m-2 cursor-pointer"
433471
data-description="grid selected"
434-
@change="onSelect(rowData)" />
472+
@change="setSelected(rowData, !isSelected(rowData))" />
435473
</td>
436474
<td
437475
v-for="(fieldEntry, fieldIndex) in gridConfig.fields"
@@ -517,16 +555,16 @@ watch(operationMessage, () => {
517555
<div v-for="(batchOperation, batchIndex) in gridConfig.batch" :key="batchIndex">
518556
<GButton
519557
v-if="
520-
selected.size > 0 &&
521-
(!batchOperation.condition || batchOperation.condition(Array.from(selected)))
558+
selectionSize > 0 &&
559+
(!batchOperation.condition || batchOperation.condition(Array.from(selected.values())))
522560
"
523561
class="mr-2"
524562
size="small"
525563
color="blue"
526564
:data-description="`grid batch ${batchOperation.title.toLowerCase()}`"
527-
@click="onBatchOperation(batchOperation, Array.from(selected))">
565+
@click="onBatchOperation(batchOperation, Array.from(selected.values()))">
528566
<Icon :icon="batchOperation.icon" class="mr-1" />
529-
<span v-localize>{{ batchOperation.title }} ({{ selected.size }})</span>
567+
<span v-localize>{{ batchOperation.title }} ({{ selectionSize }})</span>
530568
</GButton>
531569
</div>
532570
</div>
@@ -568,4 +606,10 @@ watch(operationMessage, () => {
568606
.grid-dark-row {
569607
background: $gray-200;
570608
}
609+
610+
.grid-data-tbody {
611+
&.range-select-anchor-item {
612+
box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75);
613+
}
614+
}
571615
</style>

client/src/components/Grid/configs/histories.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ const gridConfig: GridConfig = {
356356
sortDesc: true,
357357
sortKeys: ["create_time", "name", "update_time"],
358358
title: "Histories",
359+
getItemKey: (data: HistoryEntry) => String(data.id),
359360
};
360361

361362
export default gridConfig;

client/src/components/Grid/configs/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface GridConfig {
3030
sortKeys: Array<string>;
3131
sortDesc: boolean;
3232
title: string;
33+
getItemKey?: (data: RowData) => string;
3334
}
3435

3536
export type FieldArray = Array<FieldEntry>;

client/src/composables/selectedItems/selectedItems.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function useSelectedItems<T, ComponentType extends ComponentInstanceExten
1313
filterText,
1414
totalItemsInQuery,
1515
allItems,
16-
filterClass,
16+
filterClass = undefined,
1717
selectable,
1818
querySelectionBreak = () => {},
1919
onDelete,
@@ -37,7 +37,7 @@ export function useSelectedItems<T, ComponentType extends ComponentInstanceExten
3737

3838
const selectionSize = computed(() => (isQuerySelection.value ? totalItemsInQuery.value : selectedItems.value.size));
3939
const isQuerySelection = computed(() => allSelected.value && totalItemsInQuery.value !== selectedItems.value.size);
40-
const currentFilters = computed(() => filterClass.getFiltersForText(filterText.value));
40+
const currentFilters = computed(() => filterClass?.getFiltersForText(filterText.value) ?? []);
4141
const initSelectedKey = computed(() => (initSelectedItem.value ? getItemKey(initSelectedItem.value as T) : null)); // TODO: Weird Unwrap ref type
4242
const lastInRangeIndex = computed(() =>
4343
lastInRange.value ? allItems.value.indexOf(lastInRange.value as T) : null
@@ -66,7 +66,7 @@ export function useSelectedItems<T, ComponentType extends ComponentInstanceExten
6666
}
6767

6868
function isSelected(item: T) {
69-
if (isQuerySelection.value) {
69+
if (isQuerySelection.value && filterClass) {
7070
return filterClass.testFilters(currentFilters.value, item as Record<string, unknown>);
7171
}
7272
const key = getItemKey(item as T);
@@ -448,6 +448,7 @@ export function useSelectedItems<T, ComponentType extends ComponentInstanceExten
448448
initSelectedItem,
449449
isQuerySelection,
450450
itemRefs,
451+
allSelected,
451452
arrowNavigate,
452453
setShowSelection,
453454
selectAllInCurrentQuery,

client/src/composables/selectedItems/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface SelectedItemsProps<T> {
1414
/** A list of all items. */
1515
allItems: Ref<T[]>;
1616
/** The filtering class used to check query selection. */
17-
filterClass: Filtering<any>;
17+
filterClass?: Filtering<any>;
1818
/** If the items are selectable. */
1919
selectable: Ref<boolean>;
2020
/** A method called when the "Query Selection Mode" is broken. */

0 commit comments

Comments
 (0)