Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(VsTable): fix search target object bug (215845, 213519) #273

Merged
merged 5 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/vlossom/src/components/vs-table/VsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
<script lang="ts">
import { ComputedRef, PropType, Ref, computed, defineComponent, ref, toRefs, watch } from 'vue';
import { getResponsiveProps, useColorScheme, useStyleSet } from '@/composables';
import { useTableParams } from './composables/useTableParams';
import { useTableParams } from './composables';
import { VsComponent, type ColorScheme, LabelValue } from '@/declaration';
import { utils } from '@/utils';
import { DEFAULT_TABLE_ITEMS_PER_PAGE, DEFAULT_TABLE_PAGE_COUNT } from './constant';
Expand Down
14 changes: 8 additions & 6 deletions packages/vlossom/src/components/vs-table/VsTableBody.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@
import draggable from 'vuedraggable/src/vuedraggable';
import { computed, ComputedRef, defineComponent, PropType, ref, Ref, toRefs, watch, WritableComputedRef } from 'vue';
import VsTableBodyRow from './VsTableBodyRow.vue';
import { useTableSearch } from './composables/useTableSearch';
import { useTableFilter } from './composables/useTableFilter';
import { useTableSort } from './composables/useTableSort';
import { useTableExpand } from './composables/useTableExpand';
import { useTableSelect } from './composables/useTableSelect';
import { useTablePagination } from './composables/useTablePagination';
import {
useTableSearch,
useTableFilter,
useTableSort,
useTableExpand,
useTableSelect,
useTablePagination,
} from './composables';
import { VsIcon } from '@/icons';
import { VsCheckboxNode } from '@/nodes';
import { utils } from '@/utils';
Expand Down
3 changes: 2 additions & 1 deletion packages/vlossom/src/components/vs-table/VsTableBodyRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<script lang="ts">
import { computed, ComputedRef, defineComponent, PropType, toRefs } from 'vue';
import { ColorScheme, UIState } from '@/declaration';
import { utils } from '@/utils';
import { VsIcon } from '@/icons';
import VsSkeleton from '@/components/vs-skeleton/VsSkeleton.vue';

Expand Down Expand Up @@ -96,7 +97,7 @@ export default defineComponent({

return headers.value.map((header) => {
const { key } = header;
return { key, value: row[key] };
return { key, value: utils.object.get(row, key) };
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/vlossom/src/components/vs-table/VsTableHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { defineComponent, PropType, toRefs } from 'vue';
import { VsIcon } from '@/icons';
import VsInput from '@/components/vs-input/VsInput.vue';
import { SortType, type TableHeader } from './types';
import { useSortableHeader } from './composables/useSortableHeader';
import { useSortableHeader } from './composables';

export default defineComponent({
name: 'VsTableHeader',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const items: { [key: string]: any }[] = [
];

describe('VsTable', () => {
describe('items / headers', () => {
describe('headers / items', () => {
it('headers와 items 배열을 props로 할당하면 적절하게 table을 렌더할 수 있다', async () => {
// given
const wrapper: ReturnType<typeof mountComponent> = mount(VsTable, {
Expand Down Expand Up @@ -70,6 +70,33 @@ describe('VsTable', () => {
expect(td.text()).toBe(itemData);
});
});

it('path 형태의 header key를 설정해서 item을 만들 수 있다', () => {
// given
const pathHeaders: TableHeader[] = [{ label: 'Test', key: 'test.a.b', width: '7rem' }];

const pathItems: { [key: string]: any }[] = [
{
test: {
a: {
b: 'value',
},
},
},
];

// when
const wrapper: ReturnType<typeof mountComponent> = mount(VsTable, {
props: {
headers: pathHeaders,
items: pathItems,
},
});

// then
const tdElements = wrapper.findAll('tbody td');
expect(tdElements[0].text()).toBe('value');
});
});

describe('caption', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { describe, expect, it } from 'vitest';
import { Ref, ref } from 'vue';
import { useTableSearch } from './../table-search-composable';
import { TableHeader, TableItem } from './../../types';

describe('useTableSearch', () => {
describe('search targets', () => {
it('기본적으로 모든 header의 key는 검색 가능한 대상이다', () => {
// given
const headers: Ref<TableHeader[]> = ref([
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'age', label: 'Age' },
{ key: 'shopping', label: 'Shopping' },
]);

// when
const { searchTargetKeys } = useTableSearch(headers, ref([]));

// then
expect(searchTargetKeys.value).toEqual(['id', 'name', 'age', 'shopping']);
});

it('searchable: false 설정으로 검색 대상에서 제외할 수 있다', () => {
// given
const headers: Ref<TableHeader[]> = ref([
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'age', label: 'Age', searchable: false },
{ key: 'shopping', label: 'Shopping', searchable: false },
]);

// when
const { searchTargetKeys } = useTableSearch(headers, ref([]));

// then
expect(searchTargetKeys.value).toEqual(['id', 'name']);
});

it('기본 header의 key값 외에도 검색 가능한 key를 추가할 수 있다', () => {
// given
const headers: Ref<TableHeader[]> = ref([
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
]);

// when
const { searchTargetKeys } = useTableSearch(headers, ref(['email', 'tel']));

// then
expect(searchTargetKeys.value).toEqual(['id', 'name', 'email', 'tel']);
});

it('path 형태의 key값을 설정할 수 있다', () => {
// given
const headers: Ref<TableHeader[]> = ref([
{ key: 'id', label: 'ID' },
{ key: 'shopping.item', label: 'Shopping Item' },
{ key: 'shopping.ingredients', label: 'Ingredients' },
{ key: 'shopping.ingredients.fat', label: 'Fat' },
]);

// when
const { searchTargetKeys } = useTableSearch(headers, ref([]));

// then
expect(searchTargetKeys.value).toEqual([
'id',
'shopping.item',
'shopping.ingredients',
'shopping.ingredients.fat',
]);
});
});

describe('getSearchedTableItems', () => {
const items: TableItem[] = [
{
id: 'item-1',
data: {
name: 'John Doe',
shopping: {
date: '2021-09-01',
items: [
{ item: 'Apple', price: 1.5, ingredients: { fat: '5%', sugar: '10%' } },
{ item: 'Banana', price: 2.5, ingredients: { fat: '10%', sugar: '4%' } },
],
},
},
},
{
id: 'item-2',
data: {
name: 'Jane Doe',
shopping: {
date: '2021-09-02',
items: [
{ item: 'Orange', price: 3.5, ingredients: { fat: '15%', sugar: '8%' } },
{ item: 'Grapes', price: 4.5, ingredients: { fat: '20%', sugar: '12%' } },
],
},
},
},
{
id: 'item-3',
data: {
name: 'Alice',
shopping: {
date: '2021-09-03',
items: [
{ item: 'Pineapple', price: 5.5, ingredients: { fat: '25%', sugar: '16%' } },
{ item: 'Mango', price: 6.5, ingredients: { fat: '30%', sugar: '20%' } },
],
},
},
},
{ id: 'item-4', data: { name: 'Bob', shopping: {} } },
];

it('keyword가 빈 값이면 table items를 그대로 반환한다', () => {
// given
const headers: Ref<TableHeader[]> = ref([{ key: 'name', label: 'Name' }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref(''));

// then
expect(result).toEqual(items);
});

it('keyword가 빈 스페이스로 이루어져 있으면 table items를 그대로 반환한다', () => {
// given
const headers: Ref<TableHeader[]> = ref([{ key: 'name', label: 'Name' }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref(' '));

// then
expect(result).toEqual(items);
});

it('keyword가 data에 포함되어 있으면 해당 아이템을 반환한다', () => {
// given
const headers: Ref<TableHeader[]> = ref([{ key: 'name', label: 'Name' }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref('Doe'));

// then
expect(result).length(2);
expect(result).toEqual([items[0], items[1]]);
});

it('검색 가능하지 않은 key(searchable: false)의 값은 검색되지 않는다', () => {
/// given
const headers: Ref<TableHeader[]> = ref([{ key: 'name', label: 'Name', searchable: false }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref('Doe'));

// then
expect(result).length(0);
expect(result).toEqual([]);
});

it('header에 없더라도 searchableKeys로 추가된 data를 검색할 수 있다', () => {
// given
const headers: Ref<TableHeader[]> = ref([{ key: 'name', label: 'Name' }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref(['shopping']));
const result = getSearchedTableItems(items, ref('Banana'));

// then
expect(result).length(1);
expect(result).toEqual([items[0]]);
});

it('value가 object 형태일 때 keyword에 key 자체를 입력해도 검색된다', () => {
// given
const headers: Ref<TableHeader[]> = ref([
{ key: 'name', label: 'Name' },
{ key: 'shopping', label: 'Shopping' },
]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref('date'));

// then
expect(result).length(3);
});

it('key가 path 형태일 때도 검색이 가능하다', () => {
// given
const headers: Ref<TableHeader[]> = ref([{ key: 'shopping.items', label: 'Shopping Item' }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref('Banana'));

// then
expect(result).length(1);
expect(result).toEqual([items[0]]);
});

it('key값으로 등록되지 않은 path의 경우엔 검색되지 않는다', () => {
// given
const headers: Ref<TableHeader[]> = ref([{ key: 'shopping.items', label: 'Shopping Item' }]);

// when
const { getSearchedTableItems } = useTableSearch(headers, ref([]));
const result = getSearchedTableItems(items, ref('2021-09-01'));

// then
expect(result).length(0);
});
});
});
8 changes: 8 additions & 0 deletions packages/vlossom/src/components/vs-table/composables/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from './table-expand-composable';
export * from './table-filter-composable';
export * from './table-pagination-composable';
export * from './table-params-composable';
export * from './table-search-composable';
export * from './table-select-composable';
export * from './table-sort-composable';
export * from './table-sortable-header-composable';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ref } from 'vue';
import { TableItem, TableFilter } from './../types';
import { TableItem, TableFilter } from '../types';

export function useTableFilter() {
function getFilteredTableItems(tableItems: TableItem[], filter: Ref<TableFilter>): TableItem[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ref } from 'vue';
import { TableItem } from './../types';
import { TableItem } from '../types';

export function useTablePagination() {
function getPagedTableItems(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ComputedRef, Ref, computed, watch } from 'vue';
import { utils } from '@/utils';
import { SortType, TableParams } from './../types';
import { SortType, TableParams } from '../types';

export function useTableParams(
page: Ref<number>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,27 @@ export function useTableSearch(headers: Ref<TableHeader[]>, searchableKeys: Ref<
});

function getSearchedTableItems(tableItems: TableItem[], keyword: Ref<string>): TableItem[] {
if (keyword.value.trim() === '') {
if (!keyword.value || keyword.value.trim() === '') {
return tableItems;
}
const lowercaseKeyword = keyword.value.trim().toLowerCase();
return tableItems.filter(({ data }) => {
const searchableData = utils.object.pick(data, searchTargetKeys.value);
const target = Object.values(searchableData).join(' ').toLowerCase();
const searchableData = utils.object.pickWithPath(data, searchTargetKeys.value);
const target = Object.values(searchableData)
.map((v: any) => {
if (utils.object.isPlainObject(v) || utils.object.isArray(v)) {
return JSON.stringify(v);
}
return v;
})
.join(' ')
.toLowerCase();
return target.includes(lowercaseKeyword);
});
}

return {
searchTargetKeys,
getSearchedTableItems,
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ref, WritableComputedRef, computed, ComputedRef, ref, watch } from 'vue';
import type { TableItem, TableRow } from './../types';
import type { TableItem, TableRow } from '../types';
import { utils } from '@/utils';

export function useTableSelect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ref } from 'vue';
import { TableItem, SortType } from './../types';
import { TableItem, SortType } from '../types';

export function useTableSort() {
function getSortedTableItems(items: TableItem[], sortTypes: Ref<{ [key: string]: SortType }>) {
Expand Down
Loading