Merge pull request #21766 from itisAliRH/btable-to-gtable-toolshed

This commit is contained in:
Alireza Heidari
2026-02-06 18:46:08 +01:00
committed by GitHub
10 changed files with 340 additions and 211 deletions
+169 -81
View File
@@ -61,12 +61,24 @@ interface Props {
*/
fields?: TableField[];
/**
* Filter string to filter items
* @default ""
*/
filter?: string;
/**
* Whether to show hover effect on rows
* @default true
*/
hover?: boolean;
/**
* Whether to hide the table header
* @default false
*/
hideHeader?: boolean;
/**
* Table data items
* @default []
@@ -103,6 +115,12 @@ interface Props {
*/
overlayLoading?: boolean;
/**
* Whether to use local filtering (client-side) or rely on external filtering (server-side)
* @default true
*/
localFiltering?: boolean;
/**
* Whether to use local sorting (client-side) or rely on external sorting (server-side)
* @default true
@@ -167,12 +185,15 @@ const props = withDefaults(defineProps<Props>(), {
containerClass: "",
emptyState: () => ({ message: "No data available" }),
fields: () => [],
filter: "",
hover: true,
hideHeader: false,
items: () => [],
loading: false,
loadingMessage: "Loading...",
loadMoreLoading: false,
loadMoreMessage: "Loading more...",
localFiltering: true,
localSorting: true,
overlayLoading: false,
selectable: false,
@@ -189,6 +210,12 @@ const props = withDefaults(defineProps<Props>(), {
* Events emitted by the GTable component
*/
const emit = defineEmits<{
/**
* Emitted when items are filtered
* @event filtered
*/
(e: "filtered", filteredItems: T[]): void;
/**
* Emitted when sort changes
* @event sort-changed
@@ -216,15 +243,33 @@ const emit = defineEmits<{
const sortBy = ref<string>(props.sortBy || "update_time");
const sortDesc = ref<boolean>(props.sortDesc || true);
const expandedRows = ref<Set<number>>(new Set());
const localItems = computed(() => {
const items = props.items || [];
let items = props.items || [];
// If local sorting is disabled, return items as-is
if (!props.localSorting) {
return items;
}
// Apply local filtering if enabled and filter string is provided
if (props.localFiltering && props.filter && props.filter.trim() !== "") {
const filterLower = props.filter.toLowerCase().trim();
items = items.filter((item) => {
// Search through all field values
return Object.values(item).some((value) => {
if (value == null) {
return false;
}
return String(value).toLowerCase().includes(filterLower);
});
});
// Emit filtered event with the filtered items
emit("filtered", items);
}
// If no sort field is set, return items as-is
if (!sortBy.value) {
return items;
@@ -341,6 +386,26 @@ function onRowSelect(item: T, index: number) {
emit("row-select", { item, index, selected: !isSelected });
}
/**
* Check if row details are expanded
*/
function isRowExpanded(index: number) {
return expandedRows.value.has(index);
}
/**
* Toggle row details expansion
*/
function toggleRowDetails(index: number) {
if (isRowExpanded(index)) {
expandedRows.value.delete(index);
} else {
expandedRows.value.add(index);
}
// Trigger reactivity
expandedRows.value = new Set(expandedRows.value);
}
/**
* Get cell value with optional formatter
*/
@@ -415,7 +480,7 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
</script>
<template>
<div :id="`g-table-container-${props.id}`" class="w-100" :class="containerClass">
<div :id="`g-table-container-${props.id}`" :class="containerClass">
<!-- Table wrapper -->
<BOverlay :show="overlayLoading" rounded="sm" class="position-relative w-100">
<div :id="`g-table-wrapper-${props.id}`" class="position-relative w-100">
@@ -428,7 +493,7 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
{ 'table-bordered': bordered },
tableClass,
]">
<thead>
<thead v-if="!props.hideHeader">
<tr>
<th v-if="selectable" class="g-table-select-column">
<slot name="head-select">
@@ -479,87 +544,106 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
</thead>
<tbody v-if="localItems.length > 0">
<tr
v-for="(item, index) in localItems"
:id="getRowId(props.id, index)"
:key="index"
:class="{
'g-table-row-clickable': clickableRows,
'g-table-row-selected': isRowSelected(index),
}"
@click="onRowClick(item, index, $event)">
<!-- Selection checkbox column -->
<td v-if="selectable" class="g-table-select-column">
<BFormCheckbox
:id="`${getRowId(props.id, index)}-select`"
v-b-tooltip.hover.noninteractive
:checked="isRowSelected(index)"
title="Select for bulk actions"
@click.stop
@change="onRowSelect(item, index)" />
</td>
<template v-for="(item, index) in localItems">
<template>
<tr
:id="getRowId(props.id, index)"
:key="`tr` + index"
:class="{
'g-table-row-clickable': clickableRows,
'g-table-row-selected': isRowSelected(index),
}"
@click="onRowClick(item, index, $event)">
<!-- Selection checkbox column -->
<td v-if="selectable" class="g-table-select-column">
<BFormCheckbox
:id="`${getRowId(props.id, index)}-select`"
v-b-tooltip.hover.noninteractive
:checked="isRowSelected(index)"
title="Select for bulk actions"
@click.stop
@change="onRowSelect(item, index)" />
</td>
<!-- Data columns -->
<td
v-for="(field, fieldIndex) in props.fields"
:id="getCellId(props.id, field.key, index)"
:key="field.key"
:class="[
field.cellClass,
field.class,
getAlignmentClass(field.align),
{ 'hide-on-small': field.hideOnSmall },
]">
<template v-if="fieldIndex === 0 && getStatusIcon(item, index)">
<FontAwesomeIcon
v-if="getStatusIcon(item, index)"
v-b-tooltip.hover.noninteractive
v-bind="getIconProps(item, index)"
fixed-width />
</template>
<slot :name="`cell(${field.key})`" :value="item[field.key]" :item="item" :index="index">
<span>{{ getCellValue(item, field) }}</span>
</slot>
</td>
<!-- Actions column -->
<td v-if="props.actions" class="g-table-actions-column">
<slot name="actions" :item="item" :index="index">
<BDropdown
v-b-tooltip.hover.noninteractive
no-caret
right
title="More actions"
variant="link"
size="lg"
toggle-class="text-decoration-none p-0"
@click.stop>
<template v-slot:button-content>
<FontAwesomeIcon :icon="faEllipsisV" fixed-width />
<!-- Data columns -->
<td
v-for="(field, fieldIndex) in props.fields"
:id="getCellId(props.id, field.key, index)"
:key="field.key"
:class="[
field.cellClass,
field.class,
getAlignmentClass(field.align),
{ 'hide-on-small': field.hideOnSmall },
]">
<template v-if="fieldIndex === 0 && getStatusIcon(item, index)">
<FontAwesomeIcon
v-if="getStatusIcon(item, index)"
v-b-tooltip.hover.noninteractive
v-bind="getIconProps(item, index)"
fixed-width />
</template>
<template v-for="ac in props.actions">
<BDropdownItem
v-if="ac.visible ?? true"
:id="ac.id"
:key="ac.id"
:disabled="ac.disabled"
:size="ac.size || 'sm'"
:variant="ac.variant || 'link'"
:to="ac.to"
:title="ac.title"
:href="ac.href"
:target="ac.externalLink ? '_blank' : undefined"
@click.stop="ac.handler && ac.handler(item, index)">
<FontAwesomeIcon v-if="ac.icon" :icon="ac.icon" fixed-width />
{{ ac.label }}
</BDropdownItem>
</template>
</BDropdown>
</slot>
</td>
</tr>
<slot
:name="`cell(${field.key})`"
:value="item[field.key]"
:item="item"
:index="index"
:toggle-details="() => toggleRowDetails(index)">
<span>{{ getCellValue(item, field) }}</span>
</slot>
</td>
<!-- Actions column -->
<td v-if="props.actions" class="g-table-actions-column">
<slot name="actions" :item="item" :index="index">
<BDropdown
v-b-tooltip.hover.noninteractive
no-caret
right
title="More actions"
variant="link"
size="lg"
toggle-class="text-decoration-none p-0"
@click.stop>
<template v-slot:button-content>
<FontAwesomeIcon :icon="faEllipsisV" fixed-width />
</template>
<template v-for="ac in props.actions">
<BDropdownItem
v-if="ac.visible ?? true"
:id="ac.id"
:key="ac.id"
:disabled="ac.disabled"
:size="ac.size || 'sm'"
:variant="ac.variant || 'link'"
:to="ac.to"
:title="ac.title"
:href="ac.href"
:target="ac.externalLink ? '_blank' : undefined"
@click.stop="ac.handler && ac.handler(item, index)">
<FontAwesomeIcon v-if="ac.icon" :icon="ac.icon" fixed-width />
{{ ac.label }}
</BDropdownItem>
</template>
</BDropdown>
</slot>
</td>
</tr>
</template>
<!-- Row details expansion -->
<tr v-if="isRowExpanded(index)" :key="`details-${index}`" class="g-table-details-row">
<td :colspan="props.fields.length + (selectable ? 1 : 0) + (props.actions ? 1 : 0)">
<slot
name="row-details"
:item="item"
:index="index"
:toggle-details="() => toggleRowDetails(index)" />
</td>
</tr>
</template>
</tbody>
</table>
@@ -616,6 +700,10 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
&.g-table-row-selected {
background-color: $brand-light;
}
&.g-table-details-row {
background-color: lighten($brand-light, 0.3);
}
}
td {
@@ -3,6 +3,7 @@ import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import Index from "./Index.vue";
import GLink from "@/components/BaseComponents/GLink.vue";
vi.mock("app");
vi.mock("onload/loadConfig", () => ({
@@ -42,20 +43,22 @@ describe("InstalledList", () => {
},
stubs: {
RepositoryDetails: true,
GLink,
LoadingSpan: true,
},
localVue,
});
expect(wrapper.find(".loading-message").text()).toBe("Loading installed repositories...");
expect(wrapper.find("loadingspan-stub").attributes("message")).toBe("Loading installed repositories");
await wrapper.vm.$nextTick();
expect(wrapper.find(".installed-message").text()).toBe("2 repositories installed on this instance.");
const names = wrapper.findAll(".name");
expect(names.length).toBe(2);
expect(names.at(0).text()).toBe("name_0");
expect(names.at(1).text()).toBe("name_1");
const links = wrapper.findAll("a");
const links = wrapper.findAllComponents(GLink);
expect(links.length).toBe(3);
const badge = links.at(1).find(".badge");
expect(badge.text()).toBe("Newer version available!");
expect(wrapper.find('th[role="columnheader"][aria-colindex="3"] > div').text()).toBe("Tool Shed");
expect(wrapper.vm.fields.some((field) => field.key === "tool_shed")).toBe(true);
});
});
@@ -4,44 +4,50 @@
<div v-else>
<LoadingSpan v-if="loading" message="Loading installed repositories" />
<div v-else>
<b-alert :variant="messageVariant" :show="showMessage">{{ message }}</b-alert>
<BAlert :variant="messageVariant" :show="showMessage">{{ message }}</BAlert>
<div class="m-1">
<span class="installed-message text-muted">
{{ repositories.length }} repositories installed on this instance.
</span>
<b-link class="font-weight-bold" @click="toggleMonitor">
<GLink class="font-weight-bold" @click="toggleMonitor">
<span v-if="showMonitor">
<span class="fa fa-angle-double-up" />
<FontAwesomeIcon :icon="faAngleDoubleUp" />
<span>Hide installation progress.</span>
</span>
<span v-else>
<span class="fa fa-angle-double-down" />
<FontAwesomeIcon :icon="faAngleDoubleDown" />
<span>Show installation progress.</span>
</span>
</b-link>
</GLink>
</div>
<Monitor v-if="showMonitor" @onQuery="onQuery" />
<b-table
<GTable
id="repository-table"
striped
:fields="fields"
:sort-by="sortBy"
:items="repositories"
:filter="filter"
@filtered="filtered">
<template v-slot:cell(name)="row">
<b-link href="#" role="button" class="font-weight-bold" @click="row.toggleDetails">
<div v-if="!isLatest(row.item)">
<b-badge variant="danger" class="mb-2"> Newer version available! </b-badge>
<template v-slot:cell(name)="{ item, toggleDetails }">
<GLink class="font-weight-bold" @click.prevent="toggleDetails">
<div v-if="!isLatest(item)">
<BBadge variant="danger" class="mb-2"> Newer version available! </BBadge>
</div>
<div class="name">{{ row.item.name }}</div>
</b-link>
<div>{{ row.item.description }}</div>
<div class="name">{{ item.name }}</div>
</GLink>
<div>{{ item.description }}</div>
</template>
<template v-slot:row-details="row">
<RepositoryDetails :repo="row.item" />
<template v-slot:row-details="{ item }">
<RepositoryDetails :repo="item" />
</template>
</b-table>
</GTable>
<div v-if="showNotFound">
No matching entries found for: <span class="font-weight-bold">{{ filter }}</span
>.
@@ -51,9 +57,11 @@
</div>
</div>
</template>
<script>
import BootstrapVue from "bootstrap-vue";
import Vue from "vue";
import { faAngleDoubleDown, faAngleDoubleUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BBadge } from "bootstrap-vue";
import { getAppRoot } from "@/onload/loadConfig";
@@ -61,12 +69,17 @@ import { Services } from "../services";
import RepositoryDetails from "./Details.vue";
import Monitor from "./Monitor.vue";
import GLink from "@/components/BaseComponents/GLink.vue";
import GTable from "@/components/Common/GTable.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
Vue.use(BootstrapVue);
export default {
components: {
BAlert,
BBadge,
FontAwesomeIcon,
GLink,
GTable,
LoadingSpan,
Monitor,
RepositoryDetails,
@@ -79,6 +92,8 @@ export default {
},
data() {
return {
faAngleDoubleDown,
faAngleDoubleUp,
error: null,
loading: true,
message: null,
@@ -86,7 +101,6 @@ export default {
nRepositories: 0,
repositories: [],
showMonitor: false,
sortBy: "name",
};
},
computed: {
@@ -110,13 +124,12 @@ export default {
const fields = [
{
key: "name",
label: "Name",
sortable: true,
sortByFormatted: (value, key, item) => {
return `${this.isLatest(item)}_${value}`;
},
},
{
key: "owner",
label: "Owner",
sortable: true,
},
];
@@ -1,5 +1,5 @@
import { getLocalVue } from "@tests/vitest/helpers";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { describe, expect, it, vi } from "vitest";
import Monitor from "./Monitor.vue";
@@ -31,19 +31,18 @@ vi.mock("../services", () => ({
describe("Monitor", () => {
it("test monitor", async () => {
const localVue = getLocalVue();
const wrapper = mount(Monitor, { localVue });
await localVue.nextTick();
const wrapper = mount(Monitor);
await flushPromises();
const headers = wrapper.findAll("th");
expect(headers.length).toBe(3);
expect(headers.at(0).text()).toBe("Name");
expect(headers.at(1).text()).toBe("Status");
expect(headers.length).toBe(0);
const cells = wrapper.findAll("td");
expect(cells.length).toBe(6);
expect(cells.at(0).text()).toBe("name_0 (owner_0)");
expect(cells.at(0).text()).toContain("name_0 (owner_0)");
expect(cells.at(1).text()).toContain("status_0_0");
expect(cells.at(3).text()).toBe("name_1 (owner_1)");
expect(cells.at(3).text()).toContain("name_1 (owner_1)");
expect(cells.at(4).text()).toContain("status_1");
});
});
@@ -1,45 +1,52 @@
<template>
<div>
<b-alert v-if="error" variant="danger" show>
<BAlert v-if="error" variant="danger" show>
{{ error }}
</b-alert>
<b-card v-if="showItems" no-body class="my-2">
</BAlert>
<GCard v-if="showItems" class="my-2">
<h2 class="m-3 h-text">Currently installing...</h2>
<b-table
class="mx-3 mb-0"
sticky-header
thead-class="installation-monitor-header"
:items="items"
:fields="fields">
<GTable :items="items" :fields="fields" hide-header class="m-2">
<template v-slot:cell(name)="row">
<b-link @click="onQuery(row.item.name)"> {{ row.item.name }} ({{ row.item.owner }}) </b-link>
<GLink tooltip title="Show all repositories with this name" @click="onQuery(row.item.name)">
{{ row.item.name }} ({{ row.item.owner }})
</GLink>
</template>
<template v-slot:cell(status)="row">
<b>Status: </b><span>{{ row.item.status }}</span>
</template>
<template v-slot:cell(actions)="row">
<InstallationActions
class="float-right"
:status="row.item.status"
@onUninstall="uninstallRepository(row.item)" />
</template>
</b-table>
</b-card>
<b-alert v-if="showEmpty" variant="info" show> Currently there are no installing repositories. </b-alert>
</GTable>
</GCard>
<BAlert v-if="showEmpty" variant="info" show> Currently there are no installing repositories. </BAlert>
</div>
</template>
<script>
import BootstrapVue from "bootstrap-vue";
import Vue from "vue";
import { BAlert } from "bootstrap-vue";
import { Services } from "../services";
import InstallationActions from "../RepositoryDetails/InstallationActions.vue";
Vue.use(BootstrapVue);
import GLink from "@/components/BaseComponents/GLink.vue";
import GCard from "@/components/Common/GCard.vue";
import GTable from "@/components/Common/GTable.vue";
export default {
components: {
BAlert,
GCard,
GLink,
GTable,
InstallationActions,
},
data() {
@@ -48,7 +55,20 @@ export default {
loading: true,
error: null,
items: [],
fields: ["name", "status", "actions"],
fields: [
{
key: "name",
label: "Name",
},
{
key: "status",
label: "Status",
},
{
key: "actions",
label: "Actions",
},
],
};
},
computed: {
@@ -103,8 +123,3 @@ export default {
},
};
</script>
<style>
.installation-monitor-header {
display: none;
}
</style>
@@ -34,7 +34,7 @@ describe("RepositoryDetails", () => {
const localVue = getLocalVue();
const pinia = createPinia();
const wrapper = shallowMount(Index, {
const wrapper = shallowMount(Index as object, {
propsData: {
repo: {
id: "id",
@@ -47,7 +47,7 @@ describe("RepositoryDetails", () => {
localVue,
pinia,
});
expect(wrapper.find(".loading-message").text()).toBe("Loading repository details...");
expect(wrapper.find("loadingspan-stub").attributes("message")).toBe("Loading repository details");
await flushPromises();
expect(wrapper.findAll(".alert").length).toBe(0);
});
@@ -1,4 +1,6 @@
<script setup>
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { useResourceWatcher } from "@/composables/resourceWatcher";
@@ -10,6 +12,8 @@ import { Services } from "../services";
import InstallationActions from "./InstallationActions.vue";
import InstallationSettings from "./InstallationSettings.vue";
import RepositoryTools from "./RepositoryTools.vue";
import GTable from "@/components/Common/GTable.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
const props = defineProps({
repo: {
@@ -31,8 +35,6 @@ const repositoryWatcher = useResourceWatcher(loadInstalledRepositories, {
enableBackgroundPolling: false,
});
const repoChecked = "fa fa-check text-success";
const repoUnchecked = "fa fa-times text-danger";
const repoFields = [
{ key: "numeric_revision", label: "Revision" },
{ key: "tools", label: "Tools and Versions" },
@@ -219,35 +221,31 @@ function stopWatchingRepository() {
<b-link :href="repo.repository_url" target="_blank">Show additional details and dependencies.</b-link>
</div>
<div>
<span v-if="loading">
<span class="fa fa-spinner fa-spin" />
<span class="loading-message">Loading repository details...</span>
</span>
<LoadingSpan v-if="loading" message="Loading repository details" />
<div v-else>
<b-alert v-if="error" variant="danger" show>
{{ error }}
</b-alert>
<div v-else class="border rounded">
<b-table borderless :items="repoTable" :fields="repoFields" class="text-center m-0">
<template v-slot:cell(numeric_revision)="data">
<span class="font-weight-bold">{{ data.value }}</span>
<GTable borderless :items="repoTable" :fields="repoFields" class="text-center m-0">
<template v-slot:cell(numeric_revision)="row">
<span class="font-weight-bold">{{ row.value }}</span>
</template>
<template v-slot:cell(tools)="data">
<RepositoryTools :tools="data.value" />
<template v-slot:cell(tools)="row">
<RepositoryTools :tools="row.value" />
</template>
<template v-slot:cell(profile)="data">
{{ data.value ? `+${data.value}` : "-" }}
<template v-slot:cell(profile)="row">
{{ row.value ? `+${row.value}` : "-" }}
</template>
<template v-slot:cell(missing_test_components)="data">
<span v-if="!data.value" :class="repoChecked" />
<span v-else :class="repoUnchecked" />
<template v-slot:cell(missing_test_components)="row">
<FontAwesomeIcon v-if="!row.value" :icon="faCheck" class="text-success" />
<FontAwesomeIcon v-else :icon="faTimes" class="text-danger" />
</template>
<template v-slot:cell(status)="row">
<span v-if="row.item.status">
<span v-if="!isFinalState(row.item.status)" class="fa fa-spinner fa-spin" />
{{ row.item.status }}
</span>
<span v-else> - </span>
<template v-if="row.item.status">
<LoadingSpan v-if="!isFinalState(row.item.status)" :message="row.item.status" />
</template>
<template v-else> - </template>
</template>
<template v-slot:cell(actions)="row">
<InstallationActions
@@ -257,7 +255,8 @@ function stopWatchingRepository() {
@onUninstall="uninstallRepository(row.item)"
@onReset="resetRepository(row.item)" />
</template>
</b-table>
</GTable>
<InstallationSettings
v-if="showSettings && selectedChangeset"
:repo="repo"
@@ -2,6 +2,7 @@ import { createLocalVue, mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import Categories from "./Categories.vue";
import GLink from "@/components/BaseComponents/GLink.vue";
vi.mock("../services", () => ({
Services: class Services {
@@ -35,8 +36,11 @@ describe("Categories", () => {
toolshedUrl: "toolshedUrl",
},
localVue,
stubs: {
LoadingSpan: true,
},
});
expect(wrapper.find(".loading-message").text()).toBe("Loading categories...");
expect(wrapper.find("loadingspan-stub").attributes("message")).toBe("Loading categories");
});
it("test categories table", async () => {
@@ -46,12 +50,17 @@ describe("Categories", () => {
toolshedUrl: "toolshedUrl",
},
localVue,
stubs: {
GLink,
},
});
await localVue.nextTick();
const links = wrapper.findAll("a");
const links = wrapper.findAllComponents(GLink);
expect(links.length).toBe(2);
expect(links.at(0).text()).toBe("name_0");
expect(links.at(1).text()).toBe("name_1");
expect(links.at(0).text()).toContain("name_0");
expect(links.at(1).text()).toContain("name_1");
const rows = wrapper.findAll("tr");
expect(rows.length).toBe(3);
const cells = rows.at(1).findAll("td");
@@ -1,31 +1,35 @@
<template>
<div>
<LoadingSpan v-if="loading" message="Loading categories" />
<b-table v-else striped no-sort-reset :items="categories" :fields="fields">
<template v-slot:cell(name)="data">
<b-link
href="javascript:void(0)"
role="button"
class="font-weight-bold"
@click="onCategory(data.value)">
{{ data.value }}
</b-link>
</template>
</b-table>
<div v-else>
<GTable :items="categories" :fields="fields">
<template v-slot:cell(name)="data">
<GLink
href="#"
tooltip
title="View repositories in this category"
@click.prevent="onCategory(data.item.name)">
{{ data.item.name }}
</GLink>
</template>
</GTable>
</div>
</div>
</template>
<script>
import BootstrapVue from "bootstrap-vue";
import Vue from "vue";
import { Services } from "../services";
import { Services } from "@/components/Toolshed/services";
import GLink from "@/components/BaseComponents/GLink.vue";
import GTable from "@/components/Common/GTable.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
Vue.use(BootstrapVue);
export default {
components: { LoadingSpan },
components: {
GLink,
GTable,
LoadingSpan,
},
props: {
toolshedUrl: {
type: String,
@@ -41,8 +45,8 @@ export default {
categories: [],
fields: [
{ key: "name", label: "Category", sortable: true },
{ key: "description", sortable: false },
{ key: "repositories", sortable: true },
{ key: "description", label: "Description", sortable: false },
{ key: "repositories", label: "Repositories", sortable: true },
],
};
},
@@ -1,30 +1,27 @@
<template>
<div>
<b-table id="shed-search-results" striped :items="repositories" :fields="fields">
<template v-slot:cell(name)="row">
<b-link href="javascript:void(0)" role="button" class="font-weight-bold" @click="row.toggleDetails">
{{ row.item.name }}
</b-link>
<div>{{ row.item.description }}</div>
</template>
<template v-slot:row-details="row">
<RepositoryDetails :repo="row.item" :toolshed-url="toolshedUrl" />
</template>
</b-table>
<div v-if="noResultsFound" class="unavailable-message">No matching repositories found.</div>
<LoadingSpan v-if="pageLoading" message="Loading repositories" />
<GTable v-else id="shed-search-results" :items="repositories" :fields="fields">
<template v-slot:cell(name)="{ item, toggleDetails }">
<GLink href="#" @click.prevent="toggleDetails">{{ item.name }}</GLink>
<div>{{ item.description }}</div>
</template>
<template v-slot:row-details="{ item }">
<RepositoryDetails :repo="item" :toolshed-url="toolshedUrl" />
</template>
</GTable>
</div>
</template>
<script>
import BootstrapVue from "bootstrap-vue";
import Vue from "vue";
<script>
import { Services } from "../services";
import RepositoryDetails from "../RepositoryDetails/Index.vue";
import GLink from "@/components/BaseComponents/GLink.vue";
import GTable from "@/components/Common/GTable.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
Vue.use(BootstrapVue);
import RepositoryDetails from "@/components/Toolshed/RepositoryDetails/Index.vue";
const READY = 0;
const LOADING = 1;
@@ -32,6 +29,8 @@ const COMPLETE = 2;
export default {
components: {
LoadingSpan,
GLink,
GTable,
RepositoryDetails,
},
props: {
@@ -52,10 +51,10 @@ export default {
return {
repositories: [],
fields: [
{ key: "name" },
{ key: "owner", label: "Owner" },
{ key: "times_downloaded", label: "Downloaded" },
{ key: "last_updated", label: "Updated" },
{ key: "name", label: "Name", sortable: true },
{ key: "owner", label: "Owner", sortable: true },
{ key: "times_downloaded", label: "Downloaded", sortable: true },
{ key: "last_updated", label: "Updated", sortable: true },
],
page: 1,
pageSize: 50,