Skip to content

Commit f017677

Browse files
authored
Merge pull request #21728 from itisAliRH/btable-to-gtable-dataset-index
Migrate DatasetIndex from BTable to GTable and Add Local Sorting to GCard
2 parents cc51843 + 269fd94 commit f017677

4 files changed

Lines changed: 265 additions & 11 deletions

File tree

client/src/components/Common/GTable.vue

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ interface Props {
103103
*/
104104
overlayLoading?: boolean;
105105
106+
/**
107+
* Whether to use local sorting (client-side) or rely on external sorting (server-side)
108+
* @default true
109+
*/
110+
localSorting?: boolean;
111+
106112
/**
107113
* Whether to show striped rows
108114
* @default true
@@ -167,6 +173,7 @@ const props = withDefaults(defineProps<Props>(), {
167173
loadingMessage: "Loading...",
168174
loadMoreLoading: false,
169175
loadMoreMessage: "Loading more...",
176+
localSorting: true,
170177
overlayLoading: false,
171178
selectable: false,
172179
selectedItems: () => [],
@@ -210,7 +217,64 @@ const emit = defineEmits<{
210217
const sortBy = ref<string>(props.sortBy || "update_time");
211218
const sortDesc = ref<boolean>(props.sortDesc || true);
212219
213-
const localItems = computed(() => props.items || []);
220+
const localItems = computed(() => {
221+
const items = props.items || [];
222+
223+
// If local sorting is disabled, return items as-is
224+
if (!props.localSorting) {
225+
return items;
226+
}
227+
228+
// If no sort field is set, return items as-is
229+
if (!sortBy.value) {
230+
return items;
231+
}
232+
233+
// Create a shallow copy to avoid mutating the original array
234+
const sortedItems = [...items];
235+
236+
// Find the field definition for the current sort key
237+
const field = props.fields.find((f) => f.key === sortBy.value);
238+
239+
// Sort the items
240+
sortedItems.sort((a, b) => {
241+
let aVal = a[sortBy.value];
242+
let bVal = b[sortBy.value];
243+
244+
// Apply formatter if available
245+
if (field?.formatter) {
246+
aVal = field.formatter(aVal, sortBy.value, a);
247+
bVal = field.formatter(bVal, sortBy.value, b);
248+
}
249+
250+
// Handle null/undefined values
251+
if (aVal == null && bVal == null) {
252+
return 0;
253+
}
254+
if (aVal == null) {
255+
return 1;
256+
}
257+
if (bVal == null) {
258+
return -1;
259+
}
260+
261+
// Compare values
262+
let comparison = 0;
263+
if (typeof aVal === "string" && typeof bVal === "string") {
264+
comparison = aVal.localeCompare(bVal);
265+
} else if (typeof aVal === "number" && typeof bVal === "number") {
266+
comparison = aVal - bVal;
267+
} else {
268+
// Convert to strings for comparison
269+
comparison = String(aVal).localeCompare(String(bVal));
270+
}
271+
272+
return sortDesc.value ? -comparison : comparison;
273+
});
274+
275+
return sortedItems;
276+
});
277+
214278
const selectAllDisabled = computed(() => {
215279
return props.selectable && localItems.value.length === 0;
216280
});
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { getLocalVue } from "@tests/vitest/helpers";
2+
import { shallowMount } from "@vue/test-utils";
3+
import flushPromises from "flush-promises";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
5+
import { computed } from "vue";
6+
7+
import type { PathDestination } from "@/composables/datasetPathDestination";
8+
import * as datasetPathDestinationModule from "@/composables/datasetPathDestination";
9+
10+
import DatasetIndex from "./DatasetIndex.vue";
11+
12+
const localVue = getLocalVue();
13+
14+
vi.mock("@/composables/datasetPathDestination");
15+
16+
describe("DatasetIndex", () => {
17+
let mockDatasetPathDestination: ReturnType<typeof vi.fn>;
18+
19+
beforeEach(() => {
20+
// Reset mock before each test
21+
mockDatasetPathDestination = vi.fn();
22+
vi.mocked(datasetPathDestinationModule.useDatasetPathDestination).mockReturnValue({
23+
datasetPathDestination: computed(() => mockDatasetPathDestination) as any,
24+
});
25+
});
26+
27+
it("renders GTable when dataset has valid content", async () => {
28+
const mockPathDestination: PathDestination = {
29+
datasetContent: [
30+
{ path: "file1.txt", class: "File" },
31+
{ path: "file2.csv", class: "File" },
32+
],
33+
isDirectory: false,
34+
};
35+
36+
mockDatasetPathDestination.mockResolvedValue(mockPathDestination);
37+
38+
const wrapper = shallowMount(DatasetIndex as object, {
39+
propsData: {
40+
historyDatasetId: "test-dataset-123",
41+
},
42+
localVue,
43+
});
44+
45+
// Wait for async computedAsync to resolve
46+
await flushPromises();
47+
48+
// Table renders (shallowMount converts it to anonymous-stub)
49+
const gTable = wrapper.find("#g-table-0");
50+
expect(gTable.exists()).toBe(true);
51+
expect(gTable.attributes("fields")).toBeDefined();
52+
});
53+
54+
it("displays error message when dataset is not composite", async () => {
55+
mockDatasetPathDestination.mockResolvedValue(null);
56+
57+
const wrapper = shallowMount(DatasetIndex as object, {
58+
propsData: {
59+
historyDatasetId: "test-dataset-123",
60+
},
61+
localVue,
62+
});
63+
64+
await flushPromises();
65+
66+
expect(wrapper.text()).toContain("Dataset is not composite!");
67+
const gTable = wrapper.find("gtable-stub");
68+
expect(gTable.exists()).toBe(false);
69+
});
70+
71+
it("displays error message when path points to a file", async () => {
72+
const mockPathDestination: PathDestination = {
73+
datasetContent: [{ path: "file1.txt", class: "File" }],
74+
isDirectory: false,
75+
fileLink: "/api/datasets/123/file1.txt",
76+
};
77+
78+
mockDatasetPathDestination.mockResolvedValue(mockPathDestination);
79+
80+
const wrapper = shallowMount(DatasetIndex as object, {
81+
propsData: {
82+
historyDatasetId: "test-dataset-123",
83+
path: "file1.txt",
84+
},
85+
localVue,
86+
});
87+
88+
await flushPromises();
89+
90+
expect(wrapper.text()).toContain("is not a directory!");
91+
const gTable = wrapper.find("gtable-stub");
92+
expect(gTable.exists()).toBe(false);
93+
});
94+
95+
it("displays error message when path is not found", async () => {
96+
const mockPathDestination: PathDestination = {
97+
datasetContent: [{ path: "other.txt", class: "File" }],
98+
isDirectory: false,
99+
filepath: "nonexistent.txt",
100+
};
101+
102+
mockDatasetPathDestination.mockResolvedValue(mockPathDestination);
103+
104+
const wrapper = shallowMount(DatasetIndex as object, {
105+
propsData: {
106+
historyDatasetId: "test-dataset-123",
107+
path: "nonexistent.txt",
108+
},
109+
localVue,
110+
});
111+
112+
await flushPromises();
113+
114+
expect(wrapper.text()).toContain("nonexistent.txt");
115+
expect(wrapper.text()).toContain("is not found!");
116+
const gTable = wrapper.find("gtable-stub");
117+
expect(gTable.exists()).toBe(false);
118+
});
119+
120+
it("renders GTable with correct fields configuration", async () => {
121+
const mockPathDestination: PathDestination = {
122+
datasetContent: [
123+
{ path: "test.txt", class: "File" },
124+
{ path: "data.csv", class: "File" },
125+
],
126+
isDirectory: false,
127+
};
128+
129+
mockDatasetPathDestination.mockResolvedValue(mockPathDestination);
130+
131+
const wrapper = shallowMount(DatasetIndex as object, {
132+
propsData: {
133+
historyDatasetId: "test-dataset-123",
134+
},
135+
localVue,
136+
});
137+
138+
await flushPromises();
139+
140+
const html = wrapper.html();
141+
expect(html).toContain("fields");
142+
expect(html).toContain("items");
143+
expect(html).toContain("[object Object]");
144+
});
145+
146+
it("filters directory content when viewing a subdirectory", async () => {
147+
const mockPathDestination: PathDestination = {
148+
datasetContent: [
149+
{ path: "subfolder/nested.txt", class: "File" },
150+
{ path: "subfolder/another.csv", class: "File" },
151+
],
152+
isDirectory: true,
153+
filepath: "subfolder",
154+
};
155+
156+
mockDatasetPathDestination.mockResolvedValue(mockPathDestination);
157+
158+
const wrapper = shallowMount(DatasetIndex as object, {
159+
propsData: {
160+
historyDatasetId: "test-dataset-123",
161+
path: "subfolder",
162+
},
163+
localVue,
164+
});
165+
166+
await flushPromises();
167+
168+
// Check that table renders (shallowMount converts GTable to anonymous-stub)
169+
expect(wrapper.html()).toContain("fields");
170+
expect(wrapper.html()).not.toContain("is not found");
171+
expect(wrapper.html()).not.toContain("is not a directory");
172+
});
173+
174+
it("does not render GTable when error message is displayed", async () => {
175+
mockDatasetPathDestination.mockResolvedValue(null);
176+
177+
const wrapper = shallowMount(DatasetIndex as object, {
178+
propsData: {
179+
historyDatasetId: "test-dataset-123",
180+
},
181+
localVue,
182+
});
183+
184+
await flushPromises();
185+
186+
const gTable = wrapper.find("gtable-stub");
187+
const errorDiv = wrapper.find("div");
188+
189+
expect(gTable.exists()).toBe(false);
190+
expect(errorDiv.exists()).toBe(true);
191+
expect(errorDiv.text()).toContain("Dataset is not composite!");
192+
});
193+
});

client/src/components/Dataset/DatasetIndex/DatasetIndex.vue

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { computedAsync } from "@vueuse/core";
33
import { computed } from "vue";
44
55
import type { DatasetExtraFiles } from "@/api/datasets";
6+
import type { TableField } from "@/components/Common/GTable.types";
67
import { type PathDestination, useDatasetPathDestination } from "@/composables/datasetPathDestination";
78
9+
import GTable from "@/components/Common/GTable.vue";
10+
811
interface Props {
912
historyDatasetId: string;
1013
path?: string;
@@ -62,9 +65,10 @@ function removeParentDirectory(datasetContent: DatasetExtraFiles, filepath?: str
6265
});
6366
}
6467
65-
const fields = [
68+
const fields: TableField[] = [
6669
{
6770
key: "path",
71+
label: "Path",
6872
sortable: true,
6973
},
7074
{
@@ -77,16 +81,9 @@ const fields = [
7781

7882
<template>
7983
<div>
80-
<b-table
81-
v-if="directoryContent && !errorMessage"
82-
thead-class="hidden_header"
83-
striped
84-
hover
85-
:fields="fields"
86-
:items="directoryContent">
87-
</b-table>
8884
<div v-if="errorMessage">
8985
<b v-if="path">{{ path }}</b> {{ errorMessage }}
9086
</div>
87+
<GTable v-else-if="directoryContent" :fields="fields" :items="directoryContent" />
9188
</div>
9289
</template>

client/src/components/Dataset/DatasetList.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ onMounted(() => {
296296
selectable
297297
show-select-all
298298
no-sort-reset
299-
no-local-sorting
299+
:local-sorting="false"
300300
:fields="fields"
301301
:items="rows"
302302
:sort-by="sortBy"

0 commit comments

Comments
 (0)