Skip to content

Commit 948092b

Browse files
add grid view to tools list
1 parent 02c49a1 commit 948092b

4 files changed

Lines changed: 87 additions & 23 deletions

File tree

client/src/components/ScrollList/ScrollList.vue

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ interface Props<T> {
4141
showCountInFooter?: boolean;
4242
/** A key on which the `scrollTop` is reset to 0 */
4343
scrollTopResetKey?: string;
44+
/** Whether to use grid view (div instead of `BListGroup` for better flex/grid support) */
45+
gridView?: boolean;
4446
}
4547
4648
// TODO: In Vue 3, we'll be able to use generic types directly in the template, so we can remove this type assertion
@@ -60,6 +62,7 @@ const props = withDefaults(defineProps<Props<T>>(), {
6062
propScrollTop: 0,
6163
showCountInFooter: false,
6264
scrollTopResetKey: undefined,
65+
gridView: false,
6366
});
6467
6568
const emit = defineEmits<{
@@ -212,28 +215,36 @@ watch(
212215
}"
213216
role="list">
214217
<BAlert v-if="errorMessage" variant="danger" show>{{ errorMessage }}</BAlert>
215-
<BListGroup v-else>
218+
<template v-else>
216219
<slot v-if="items.length === 0" name="loading">
217220
<BAlert v-if="busy" variant="info" show>
218221
<LoadingSpan :message="`Loading ${props.namePlural}`" />
219222
</BAlert>
220223
</slot>
224+
<component
225+
:is="props.gridView ? 'div' : BListGroup"
226+
:class="{ 'card-list d-flex flex-wrap': props.gridView }">
227+
<!-- Use component wrapper with v-for to provide proper keying while avoiding layout interference -->
228+
<component
229+
:is="'div'"
230+
v-for="(item, index) in items"
231+
:key="itemKey(item)"
232+
class=""
233+
style="display: contents">
234+
<slot name="item" :item="item" :index="index" />
235+
</component>
221236

222-
<!-- Wrap slot in a template to use v-for -->
223-
<div v-for="(item, index) in items" :key="itemKey(item)">
224-
<slot name="item" :item="item" :index="index" />
225-
</div>
237+
<template v-if="!busy">
238+
<slot v-if="allLoaded && items.length === 0" name="none-loaded-footer">
239+
<div class="list-end">- No {{ props.namePlural }} found -</div>
240+
</slot>
226241

227-
<template v-if="!busy">
228-
<slot v-if="allLoaded && items.length === 0" name="none-loaded-footer">
229-
<div class="list-end">- No {{ props.namePlural }} found -</div>
230-
</slot>
231-
232-
<slot v-else-if="allLoaded" name="all-loaded-footer">
233-
<div class="list-end">- {{ listEndText }} -</div>
234-
</slot>
235-
</template>
236-
</BListGroup>
242+
<slot v-else-if="allLoaded" name="all-loaded-footer">
243+
<div class="list-end">- {{ listEndText }} -</div>
244+
</slot>
245+
</template>
246+
</component>
247+
</template>
237248
</div>
238249
<ScrollToTopButton :offset="scrollTop" @click="scrollToTop" />
239250
</div>
@@ -262,3 +273,9 @@ watch(
262273
</div>
263274
</div>
264275
</template>
276+
277+
<style lang="scss" scoped>
278+
.card-list {
279+
container: cards-list / inline-size;
280+
}
281+
</style>

client/src/components/ToolsList/ToolsList.vue

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
<script setup lang="ts">
2-
import { faSitemap, faStar } from "@fortawesome/free-solid-svg-icons";
2+
import { faBars, faGripVertical, faSitemap, faStar } from "@fortawesome/free-solid-svg-icons";
33
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
44
import { storeToRefs } from "pinia";
55
import { computed, ref, watch } from "vue";
66
import { useRouter } from "vue-router/composables";
77
88
import { type FilterSettings, type Tool, useToolStore } from "@/stores/toolStore";
9-
import { useUserStore } from "@/stores/userStore";
9+
import { type ListViewMode, useUserStore } from "@/stores/userStore";
1010
import Filtering, { contains, type ValidFilter } from "@/utils/filtering";
1111
1212
import { createWhooshQuery, FAVORITES_KEYS } from "../Panels/utilities";
1313
1414
import GButton from "../BaseComponents/GButton.vue";
15+
import GButtonGroup from "../BaseComponents/GButtonGroup.vue";
1516
import FilterMenu from "../Common/FilterMenu.vue";
1617
import Heading from "../Common/Heading.vue";
1718
import ToolsListSectionFilters from "./ToolsListSectionFilters.vue";
@@ -39,7 +40,10 @@ const props = withDefaults(defineProps<Props>(), {
3940
4041
const router = useRouter();
4142
42-
const { isAnonymous } = storeToRefs(useUserStore());
43+
const userStore = useUserStore();
44+
const { isAnonymous, currentListViewPreferences } = storeToRefs(userStore);
45+
46+
const currentListViewMode = computed(() => currentListViewPreferences.value["tools"] || "list");
4347
4448
const toolStore = useToolStore();
4549
const { loading } = storeToRefs(toolStore);
@@ -142,6 +146,10 @@ async function searchTools() {
142146
function applyFilter(filter: string, value: string) {
143147
filterText.value = ToolFilters.value.setFilterValue(filterText.value, filter, value);
144148
}
149+
150+
function onToggleView(newView: ListViewMode) {
151+
userStore.setListViewPreference("tools", newView);
152+
}
145153
</script>
146154

147155
<template>
@@ -249,18 +257,51 @@ function applyFilter(filter: string, value: string) {
249257
</GButton>
250258
</div>
251259

252-
<ToolsListSectionFilters
253-
:filter-class="ToolFilters"
254-
:filter-text="filterText"
255-
:disabled="loading"
256-
@apply-filter="applyFilter" />
260+
<div class="d-flex justify-content-between align-items-center">
261+
<ToolsListSectionFilters
262+
:filter-class="ToolFilters"
263+
:filter-text="filterText"
264+
:disabled="loading"
265+
@apply-filter="applyFilter" />
266+
267+
<!-- TODO: This div here and in ListHeader.vue needs to be a reusable component -->
268+
<div>
269+
Display:
270+
<GButtonGroup>
271+
<GButton
272+
id="view-grid"
273+
tooltip
274+
title="Grid view"
275+
size="small"
276+
:pressed="currentListViewMode === 'grid'"
277+
outline
278+
color="blue"
279+
@click="onToggleView('grid')">
280+
<FontAwesomeIcon :icon="faGripVertical" />
281+
</GButton>
282+
283+
<GButton
284+
id="view-list"
285+
tooltip
286+
title="List view"
287+
size="small"
288+
:pressed="currentListViewMode === 'list'"
289+
outline
290+
color="blue"
291+
@click="onToggleView('list')">
292+
<FontAwesomeIcon :icon="faBars" />
293+
</GButton>
294+
</GButtonGroup>
295+
</div>
296+
</div>
257297
</div>
258298

259299
<div class="tools-list-body">
260300
<ToolsListTable
261301
:tools="itemsLoaded"
262302
:loading="loading"
263303
:has-owner-filter="hasOwnerFilter"
304+
:grid-view="currentListViewMode === 'grid'"
264305
@apply-filter="applyFilter" />
265306
</div>
266307
</section>

client/src/components/ToolsList/ToolsListCard.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ interface Props {
4646
local: boolean;
4747
owner?: string;
4848
fetching: boolean;
49+
gridView?: boolean;
4950
}
5051
5152
const props = withDefaults(defineProps<Props>(), {
@@ -62,6 +63,7 @@ const props = withDefaults(defineProps<Props>(), {
6263
local: false,
6364
owner: undefined,
6465
fetching: false,
66+
gridView: false,
6567
});
6668
6769
const emit = defineEmits<{
@@ -166,6 +168,7 @@ const {
166168
:id="props.id"
167169
class="tool-list-item"
168170
:badges="toolBadges"
171+
:grid-view="props.gridView"
169172
:bookmarked="bookmark.label === 'Unfavorite'"
170173
:show-bookmark="!bookmark.title.includes('Login')"
171174
:primary-actions="toolsListCardPrimaryActions"

client/src/components/ToolsList/ToolsListTable.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const props = defineProps<{
1919
tools: Tool[];
2020
loading?: boolean;
2121
hasOwnerFilter?: boolean;
22+
gridView?: boolean;
2223
}>();
2324
2425
const toolStore = useToolStore();
@@ -62,6 +63,7 @@ watchEffect(() => {
6263
name-plural="tools"
6364
:load-disabled="!props.tools.length"
6465
show-count-in-footer
66+
:grid-view="props.gridView"
6567
no-footer>
6668
<template v-slot:loading>
6769
<BAlert v-if="props.tools.length" show>
@@ -87,6 +89,7 @@ watchEffect(() => {
8789
:owner="props.hasOwnerFilter && item.tool_shed_repository ? item.tool_shed_repository.owner : undefined"
8890
:workflow-compatible="item.is_workflow_compatible"
8991
:version="item.version"
92+
:grid-view="props.gridView"
9093
@apply-filter="(filter, value) => $emit('apply-filter', filter, value)" />
9194
</template>
9295
</ScrollList>

0 commit comments

Comments
 (0)