Skip to content

Commit 4d9ab09

Browse files
add grid view to tools list
1 parent c8b83c7 commit 4d9ab09

4 files changed

Lines changed: 86 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: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
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";
@@ -39,7 +39,10 @@ const props = withDefaults(defineProps<Props>(), {
3939
4040
const router = useRouter();
4141
42-
const { isAnonymous } = storeToRefs(useUserStore());
42+
const userStore = useUserStore();
43+
const { isAnonymous, currentListViewPreferences } = storeToRefs(userStore);
44+
45+
const currentListViewMode = computed(() => currentListViewPreferences.value["tools"] || "list");
4346
4447
const toolStore = useToolStore();
4548
const { loading } = storeToRefs(toolStore);
@@ -142,6 +145,10 @@ async function searchTools() {
142145
function applyFilter(filter: string, value: string) {
143146
filterText.value = ToolFilters.value.setFilterValue(filterText.value, filter, value);
144147
}
148+
149+
function onToggleView(newView: ListViewMode) {
150+
userStore.setListViewPreference("tools", newView);
151+
}
145152
</script>
146153

147154
<template>
@@ -249,18 +256,51 @@ function applyFilter(filter: string, value: string) {
249256
</GButton>
250257
</div>
251258

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

259298
<div class="tools-list-body">
260299
<ToolsListTable
261300
:tools="itemsLoaded"
262301
:loading="loading"
263302
:has-owner-filter="hasOwnerFilter"
303+
:grid-view="currentListViewMode === 'grid'"
264304
@apply-filter="applyFilter" />
265305
</div>
266306
</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)