mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-05 06:18:04 +03:00
Preserve filters and sorts while switching between tables (and load from URL)
[GCU-47]: https://hasurahq.atlassian.net/browse/GCU-47?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7522 GitOrigin-RevId: 92ade94f2fbd6013986b28b5a97ba5683a4fdd2e
This commit is contained in:
parent
611bd0363f
commit
664db29bcc
@ -3,6 +3,7 @@ import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { BrowseRows } from './BrowseRows';
|
||||
import { handlers } from './__mocks__/handlers.mock';
|
||||
|
||||
@ -20,6 +21,7 @@ export const Basic: ComponentStory<typeof BrowseRows> = () => {
|
||||
table={['Album']}
|
||||
dataSourceName="sqlite_test"
|
||||
primaryKeys={[]}
|
||||
onUpdateOptions={action('onUpdateOptions')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -30,6 +32,7 @@ export const BasicDisplayTest: ComponentStory<typeof BrowseRows> = () => {
|
||||
table={['Album']}
|
||||
dataSourceName="sqlite_test"
|
||||
primaryKeys={[]}
|
||||
onUpdateOptions={action('onUpdateOptions')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { getTableDisplayName } from '@/features/DatabaseRelationships';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import produce from 'immer';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { setWhereAndSortToUrl } from './BrowseRows.utils';
|
||||
import {
|
||||
DataGrid,
|
||||
DataGridOptions,
|
||||
@ -9,12 +10,13 @@ import {
|
||||
} from './components/DataGrid/DataGrid';
|
||||
import { TableTabView } from './components/DataGrid/parts/TableTabView';
|
||||
|
||||
interface BrowseRowsProps {
|
||||
type BrowseRowsProps = {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
options?: DataGridOptions;
|
||||
primaryKeys: string[];
|
||||
}
|
||||
onUpdateOptions: (options: DataGridOptions) => void;
|
||||
};
|
||||
|
||||
type TabState = { name: string; details: DataGridProps; parentValue: string };
|
||||
type OpenNewRelationshipTabProps = {
|
||||
@ -94,9 +96,13 @@ const onTabClose = (
|
||||
}
|
||||
};
|
||||
|
||||
export const BrowseRows = (props: BrowseRowsProps) => {
|
||||
const { dataSourceName, table, options, primaryKeys } = props;
|
||||
|
||||
export const BrowseRows = ({
|
||||
dataSourceName,
|
||||
table,
|
||||
options,
|
||||
primaryKeys,
|
||||
onUpdateOptions,
|
||||
}: BrowseRowsProps) => {
|
||||
const defaultTabState = {
|
||||
name: getTableDisplayName(table),
|
||||
details: { dataSourceName, table, options, primaryKeys: [] as string[] },
|
||||
@ -117,6 +123,21 @@ export const BrowseRows = (props: BrowseRowsProps) => {
|
||||
const defaultActiveTab = getTableDisplayName(table);
|
||||
const [activeTab, setActiveTab] = useState(defaultActiveTab);
|
||||
|
||||
useEffect(() => {
|
||||
setOriginalTableOptions(options);
|
||||
const currentTab = openTabs[0];
|
||||
|
||||
setOpenTabs([
|
||||
{
|
||||
...currentTab,
|
||||
details: {
|
||||
...currentTab.details,
|
||||
options,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}, [options]);
|
||||
|
||||
/**
|
||||
* when relationships are open, disable sorting and searching through all the views
|
||||
*/
|
||||
@ -150,43 +171,51 @@ export const BrowseRows = (props: BrowseRowsProps) => {
|
||||
setActiveTab(`${openTab.name}.${relationshipName}`);
|
||||
};
|
||||
|
||||
const onUpdateOptionsGenerator =
|
||||
(index: number) =>
|
||||
(_options: DataGridOptions): void => {
|
||||
setWhereAndSortToUrl(_options);
|
||||
|
||||
setOriginalTableOptions(_options);
|
||||
|
||||
setOpenTabs(_openTabs =>
|
||||
produce(_openTabs, draft => {
|
||||
draft[index].details.options = _options;
|
||||
})
|
||||
);
|
||||
|
||||
onUpdateOptions(_options);
|
||||
};
|
||||
|
||||
const tableTabItems = openTabs.map((openTab, index) => {
|
||||
const innerOnUpdateOptions = onUpdateOptionsGenerator(index);
|
||||
return {
|
||||
value: openTab.name,
|
||||
label: openTab.name,
|
||||
parentValue: openTab.parentValue,
|
||||
content: (
|
||||
<DataGrid
|
||||
key={JSON.stringify(openTab)}
|
||||
table={openTab.details.table}
|
||||
dataSourceName={openTab.details.dataSourceName}
|
||||
options={openTab.details.options}
|
||||
activeRelationships={openTab.details.activeRelationships}
|
||||
onRelationshipOpen={data => openNewRelationshipTab({ data, openTab })}
|
||||
onRelationshipClose={relationshipName =>
|
||||
setActiveTab(`${openTab.name}.${relationshipName}`)
|
||||
}
|
||||
disableRunQuery={disableRunQuery}
|
||||
updateOptions={innerOnUpdateOptions}
|
||||
primaryKeys={primaryKeys}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableTabView
|
||||
items={openTabs.map((openTab, index) => ({
|
||||
value: openTab.name,
|
||||
label: openTab.name,
|
||||
parentValue: openTab.parentValue,
|
||||
content: (
|
||||
<DataGrid
|
||||
key={JSON.stringify(openTab)}
|
||||
table={openTab.details.table}
|
||||
dataSourceName={openTab.details.dataSourceName}
|
||||
options={openTab.details.options}
|
||||
activeRelationships={openTab.details.activeRelationships}
|
||||
onRelationshipOpen={data =>
|
||||
openNewRelationshipTab({ data, openTab })
|
||||
}
|
||||
onRelationshipClose={relationshipName =>
|
||||
setActiveTab(`${openTab.name}.${relationshipName}`)
|
||||
}
|
||||
disableRunQuery={disableRunQuery}
|
||||
updateOptions={_options => {
|
||||
if (index === 0) {
|
||||
// Save a copy of the parent filters before opening
|
||||
setOriginalTableOptions(_options);
|
||||
}
|
||||
|
||||
setOpenTabs(_openTabs =>
|
||||
produce(_openTabs, draft => {
|
||||
draft[index].details.options = _options;
|
||||
})
|
||||
);
|
||||
}}
|
||||
primaryKeys={primaryKeys}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
items={tableTabItems}
|
||||
activeTab={activeTab}
|
||||
onTabClick={value => {
|
||||
setActiveTab(value);
|
||||
|
18
console/src/features/BrowseRows/BrowseRows.utils.ts
Normal file
18
console/src/features/BrowseRows/BrowseRows.utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { DataGridOptions } from './components/DataGrid/DataGrid';
|
||||
import { applyWhereAndSortConditionsToQueryString } from './components/DataGrid/DataGrid.utils';
|
||||
|
||||
export const setWhereAndSortToUrl = (options: DataGridOptions) => {
|
||||
const searchQueryString = applyWhereAndSortConditionsToQueryString({
|
||||
options,
|
||||
search: window.location.search,
|
||||
});
|
||||
|
||||
if (window.history.pushState) {
|
||||
const {
|
||||
location: { protocol, host, pathname },
|
||||
} = window;
|
||||
|
||||
const newUrl = `${protocol}//${host}${pathname}?${searchQueryString}`;
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
}
|
||||
};
|
@ -2,6 +2,7 @@ import { Table } from '@/features/hasura-metadata-types';
|
||||
import React from 'react';
|
||||
import { BrowseRows } from '../../BrowseRows';
|
||||
import { useTableColumns } from '../../hooks';
|
||||
import { useInitialWhereAndOrderBy } from './hooks/useInitialWhereAndOrderBy';
|
||||
|
||||
interface BrowseRowsContainerProps {
|
||||
table: Table;
|
||||
@ -22,12 +23,20 @@ export const BrowseRowsContainer = ({
|
||||
.map(column => column.graphQLProperties?.name)
|
||||
.filter(columnName => columnName !== undefined) as string[];
|
||||
|
||||
const { options, onUpdateOptions } = useInitialWhereAndOrderBy({
|
||||
columns: tableColumns?.columns,
|
||||
table,
|
||||
dataSourceName,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<BrowseRows
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
primaryKeys={primaryKeys}
|
||||
options={options}
|
||||
onUpdateOptions={onUpdateOptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,130 @@
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
import { Table } from '@/features/hasura-metadata-types';
|
||||
import { getLSItem, setLSItem } from '@/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DataGridOptions } from '../../DataGrid/DataGrid';
|
||||
import { convertUrlToDataGridOptions } from '../../DataGrid/DataGrid.utils';
|
||||
|
||||
type GetUniqueTableKeyProps = {
|
||||
table: Table;
|
||||
dataSourceName: string;
|
||||
};
|
||||
|
||||
const getUniqueTableKey = ({
|
||||
table,
|
||||
dataSourceName,
|
||||
}: GetUniqueTableKeyProps) => {
|
||||
if (Array.isArray(table)) {
|
||||
return `${dataSourceName}.${table.join('-')}.query`;
|
||||
}
|
||||
|
||||
return `${dataSourceName}.${table}.query`;
|
||||
};
|
||||
|
||||
type WhereAndOrderBy = Pick<DataGridOptions, 'where' | 'order_by'>;
|
||||
|
||||
const getWhereAndOrderByFromLocalStorage = ({
|
||||
table,
|
||||
dataSourceName,
|
||||
}: GetUniqueTableKeyProps): WhereAndOrderBy | undefined => {
|
||||
const localStorageKey = getUniqueTableKey({ table, dataSourceName });
|
||||
const localUserQueryString = localStorageKey
|
||||
? getLSItem(localStorageKey)
|
||||
: '';
|
||||
|
||||
if (localUserQueryString) {
|
||||
return JSON.parse(localUserQueryString) as WhereAndOrderBy;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type SetWhereAndOrderByToLocalStorage = {
|
||||
whereAndOrderBy: WhereAndOrderBy;
|
||||
} & GetUniqueTableKeyProps;
|
||||
|
||||
const setWhereAndOrderByToLocalStorage = ({
|
||||
table,
|
||||
dataSourceName,
|
||||
whereAndOrderBy,
|
||||
}: SetWhereAndOrderByToLocalStorage) => {
|
||||
const localStorageKey = getUniqueTableKey({ table, dataSourceName });
|
||||
setLSItem(localStorageKey, JSON.stringify(whereAndOrderBy));
|
||||
};
|
||||
|
||||
type UseInitialWhereAndOrderByProps = {
|
||||
columns: TableColumn[] | undefined;
|
||||
table: Table;
|
||||
dataSourceName: string;
|
||||
};
|
||||
|
||||
export const useInitialWhereAndOrderBy = ({
|
||||
columns,
|
||||
table,
|
||||
dataSourceName,
|
||||
}: UseInitialWhereAndOrderByProps) => {
|
||||
const [options, setOptions] = useState<DataGridOptions | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const localStorageWhereAndOrderBy = getWhereAndOrderByFromLocalStorage({
|
||||
table,
|
||||
dataSourceName,
|
||||
});
|
||||
const [initialWhereAndOrderBy] = useState(localStorageWhereAndOrderBy);
|
||||
|
||||
const [initialUrlSearchParams] = useState(window.location.search);
|
||||
|
||||
useEffect(() => {
|
||||
if (columns) {
|
||||
if (initialUrlSearchParams) {
|
||||
const newOptions = convertUrlToDataGridOptions(
|
||||
initialUrlSearchParams,
|
||||
columns
|
||||
);
|
||||
|
||||
const hasWhere = newOptions.where && newOptions.where?.length > 0;
|
||||
const hasOrderBy =
|
||||
newOptions.order_by && newOptions.order_by?.length > 0;
|
||||
|
||||
if (hasWhere || hasOrderBy) {
|
||||
setWhereAndOrderByToLocalStorage({
|
||||
table,
|
||||
dataSourceName,
|
||||
whereAndOrderBy: {
|
||||
where: newOptions?.where || [],
|
||||
order_by: newOptions?.order_by || [],
|
||||
},
|
||||
});
|
||||
setOptions(newOptions);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (initialWhereAndOrderBy) {
|
||||
const newOptions: DataGridOptions = {
|
||||
where: initialWhereAndOrderBy.where,
|
||||
order_by: initialWhereAndOrderBy.order_by,
|
||||
};
|
||||
setOptions(newOptions);
|
||||
}
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const onUpdateOptions = (_options: DataGridOptions) => {
|
||||
const whereAndOrderBy: WhereAndOrderBy = {
|
||||
where: _options?.where || [],
|
||||
order_by: _options?.order_by || [],
|
||||
};
|
||||
setWhereAndOrderByToLocalStorage({
|
||||
table,
|
||||
dataSourceName,
|
||||
whereAndOrderBy,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
options,
|
||||
onUpdateOptions,
|
||||
};
|
||||
};
|
@ -107,6 +107,12 @@ export const DataGrid = (props: DataGridProps) => {
|
||||
setSorting(DEFAULT_SORT_CLAUSES);
|
||||
setWhereClauses(DEFAULT_WHERE_CLAUSES);
|
||||
setOrderClauses(DEFAULT_ORDER_BY_CLAUSES);
|
||||
updateOptions?.({
|
||||
limit: pageSize,
|
||||
offset: pageIndex * pageSize,
|
||||
where: DEFAULT_WHERE_CLAUSES,
|
||||
order_by: DEFAULT_ORDER_BY_CLAUSES,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -309,10 +315,24 @@ export const DataGrid = (props: DataGridProps) => {
|
||||
whereClauses,
|
||||
supportedOperators: tableColumnQueryResult?.supportedOperators ?? [],
|
||||
removeWhereClause: id => {
|
||||
const newWhereClauses = whereClauses.filter((_, i) => i !== id);
|
||||
setWhereClauses(whereClauses.filter((_, i) => i !== id));
|
||||
updateOptions?.({
|
||||
limit: pageSize,
|
||||
offset: pageIndex * pageSize,
|
||||
where: newWhereClauses,
|
||||
order_by: orderByClauses,
|
||||
});
|
||||
},
|
||||
removeOrderByClause: id => {
|
||||
setOrderClauses(orderByClauses.filter((_, i) => i !== id));
|
||||
const newOrderByClauses = orderByClauses.filter((_, i) => i !== id);
|
||||
setOrderClauses(newOrderByClauses);
|
||||
updateOptions?.({
|
||||
limit: pageSize,
|
||||
offset: pageIndex * pageSize,
|
||||
where: whereClauses,
|
||||
order_by: newOrderByClauses,
|
||||
});
|
||||
},
|
||||
onExportRows,
|
||||
onExportSelectedRows,
|
||||
|
@ -1,7 +1,18 @@
|
||||
import { TableRow, WhereClause } from '../../../../features/DataSource';
|
||||
import {
|
||||
TableColumn,
|
||||
TableRow,
|
||||
WhereClause,
|
||||
} from '../../../../features/DataSource';
|
||||
import { DataGridOptions } from './DataGrid';
|
||||
import {
|
||||
adaptSelectedRowIdsToWhereClause,
|
||||
AdaptSelectedRowIdsToWhereClauseArgs,
|
||||
mapWhereAndSortConditions,
|
||||
FilterConditions,
|
||||
replaceFiltersInUrl,
|
||||
applyWhereAndSortConditionsToQueryString,
|
||||
convertUrlToDataGridOptions,
|
||||
convertValueToGraphQL,
|
||||
} from './DataGrid.utils';
|
||||
|
||||
describe('adaptSelectedRowIdsToWhereClause', () => {
|
||||
@ -50,3 +61,396 @@ describe('adaptSelectedRowIdsToWhereClause', () => {
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapWhereAndSortConditions', () => {
|
||||
describe('when where and sort conditions are defined', () => {
|
||||
it('returns the query string', () => {
|
||||
const options: DataGridOptions = {
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
where: [
|
||||
{
|
||||
AlbumId: {
|
||||
_gte: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: {
|
||||
_like: '%foo%',
|
||||
},
|
||||
},
|
||||
],
|
||||
order_by: [
|
||||
{
|
||||
column: 'AlbumId',
|
||||
type: 'desc',
|
||||
},
|
||||
{
|
||||
column: 'Title',
|
||||
type: 'asc',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(mapWhereAndSortConditions(options)).toEqual([
|
||||
{
|
||||
filter: 'AlbumId;_gte;2',
|
||||
},
|
||||
{
|
||||
filter: 'Title;_like;%foo%',
|
||||
},
|
||||
{
|
||||
sort: 'AlbumId;desc',
|
||||
},
|
||||
{
|
||||
sort: 'Title;asc',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when only where conditions are defined', () => {
|
||||
it('returns the query string', () => {
|
||||
const options: DataGridOptions = {
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
where: [
|
||||
{
|
||||
AlbumId: {
|
||||
_gte: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: {
|
||||
_like: '%foo%',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(mapWhereAndSortConditions(options)).toEqual([
|
||||
{
|
||||
filter: 'AlbumId;_gte;2',
|
||||
},
|
||||
{
|
||||
filter: 'Title;_like;%foo%',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when only sort conditions are defined', () => {
|
||||
it('returns the query string', () => {
|
||||
const options: DataGridOptions = {
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
order_by: [
|
||||
{
|
||||
column: 'AlbumId',
|
||||
type: 'desc',
|
||||
},
|
||||
{
|
||||
column: 'Title',
|
||||
type: 'asc',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(mapWhereAndSortConditions(options)).toEqual([
|
||||
{
|
||||
sort: 'AlbumId;desc',
|
||||
},
|
||||
{
|
||||
sort: 'Title;asc',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceFiltersInUrl', () => {
|
||||
describe('when filter and sort conditions are provided', () => {
|
||||
it('returns the query string', () => {
|
||||
const filterConditions: FilterConditions = [
|
||||
{ filter: 'AlbumId;_gte;1' },
|
||||
{ filter: 'Title;_like;%foo%' },
|
||||
{ sort: 'AlbumId;asc' },
|
||||
];
|
||||
|
||||
expect(
|
||||
replaceFiltersInUrl('?database=Chinook&table=Album', filterConditions)
|
||||
).toBe(
|
||||
'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Basc'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyWhereAndSortConditionsToQueryString', () => {
|
||||
it('returns the query string', () => {
|
||||
const options: DataGridOptions = {
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
where: [
|
||||
{
|
||||
AlbumId: {
|
||||
_gte: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: {
|
||||
_like: '%foo%',
|
||||
},
|
||||
},
|
||||
],
|
||||
order_by: [
|
||||
{
|
||||
column: 'AlbumId',
|
||||
type: 'desc',
|
||||
},
|
||||
{
|
||||
column: 'Title',
|
||||
type: 'asc',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const search = '?database=Chinook&table=%5B%22Album%22%5D';
|
||||
|
||||
expect(
|
||||
applyWhereAndSortConditionsToQueryString({
|
||||
options,
|
||||
search,
|
||||
})
|
||||
).toBe(
|
||||
'database=Chinook&table=%5B%22Album%22%5D&filter=AlbumId%3B_gte%3B2&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Bdesc&sort=Title%3Basc'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertUrlToDataGridOptions', () => {
|
||||
describe('when filters and sort are defined', () => {
|
||||
it('returns the options', () => {
|
||||
const search =
|
||||
'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Basc';
|
||||
|
||||
const expected: DataGridOptions = {
|
||||
where: [
|
||||
{
|
||||
AlbumId: {
|
||||
_gte: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: {
|
||||
_like: '%foo%',
|
||||
},
|
||||
},
|
||||
],
|
||||
order_by: [
|
||||
{
|
||||
column: 'AlbumId',
|
||||
type: 'asc',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(convertUrlToDataGridOptions(search)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when filters are defined', () => {
|
||||
it('returns the options', () => {
|
||||
const search =
|
||||
'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25';
|
||||
|
||||
const expected: DataGridOptions = {
|
||||
where: [
|
||||
{
|
||||
AlbumId: {
|
||||
_gte: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: {
|
||||
_like: '%foo%',
|
||||
},
|
||||
},
|
||||
],
|
||||
order_by: [],
|
||||
};
|
||||
expect(convertUrlToDataGridOptions(search)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort are defined', () => {
|
||||
it('returns the options', () => {
|
||||
const search =
|
||||
'database=Chinook&table=Album&sort=AlbumId%3Basc&sort=Title%3Bdesc';
|
||||
|
||||
const expected: DataGridOptions = {
|
||||
where: [],
|
||||
order_by: [
|
||||
{
|
||||
column: 'AlbumId',
|
||||
type: 'asc',
|
||||
},
|
||||
{
|
||||
column: 'Title',
|
||||
type: 'desc',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(convertUrlToDataGridOptions(search)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when filters and sort are not defined', () => {
|
||||
it('returns the options', () => {
|
||||
const search = 'database=Chinook&table=Album';
|
||||
|
||||
const expected: DataGridOptions = {
|
||||
where: [],
|
||||
order_by: [],
|
||||
};
|
||||
expect(convertUrlToDataGridOptions(search)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when table columns are provided', () => {
|
||||
it('returns the options', () => {
|
||||
const search =
|
||||
'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Basc';
|
||||
|
||||
const expected: DataGridOptions = {
|
||||
where: [
|
||||
{
|
||||
AlbumId: {
|
||||
_gte: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: {
|
||||
_like: '%foo%',
|
||||
},
|
||||
},
|
||||
],
|
||||
order_by: [
|
||||
{
|
||||
column: 'AlbumId',
|
||||
type: 'asc',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tableColumns: TableColumn[] = [
|
||||
{
|
||||
name: 'AlbumId',
|
||||
dataType: 'number',
|
||||
graphQLProperties: { name: 'AlbumId', scalarType: 'decimal' },
|
||||
},
|
||||
{
|
||||
name: 'Title',
|
||||
dataType: 'string',
|
||||
graphQLProperties: { name: 'Title', scalarType: 'String' },
|
||||
},
|
||||
];
|
||||
|
||||
expect(convertUrlToDataGridOptions(search, tableColumns)).toEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertValueToGraphQL', () => {
|
||||
it('converts decimal', () => {
|
||||
const value = '1';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'number',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'decimal',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe(1);
|
||||
});
|
||||
|
||||
it('converts float', () => {
|
||||
const value = '1';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'number',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'float',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe(1);
|
||||
});
|
||||
|
||||
it('converts boolean', () => {
|
||||
const value = 'true';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'bool',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'boolean',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe(true);
|
||||
});
|
||||
|
||||
it('converts string', () => {
|
||||
const value = 'aString';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'string',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'string',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe('aString');
|
||||
});
|
||||
|
||||
it('converts array of strings', () => {
|
||||
const value = '[1, 2, 3, 4]';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'string',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'string',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe('["1","2","3","4"]');
|
||||
});
|
||||
|
||||
it('converts array of int', () => {
|
||||
const value = '[1, 2, 3, 4]';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'number',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'int',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe('[1,2,3,4]');
|
||||
});
|
||||
|
||||
it('converts array of float', () => {
|
||||
const value = '[1.1, 2.2, 3.3, 4.4]';
|
||||
const tableColumn: TableColumn = {
|
||||
name: 'AlbumId',
|
||||
dataType: 'number',
|
||||
graphQLProperties: {
|
||||
name: 'AlbumId',
|
||||
scalarType: 'float',
|
||||
},
|
||||
};
|
||||
expect(convertValueToGraphQL(value, tableColumn)).toBe('[1.1,2.2,3.3,4.4]');
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { TableRow, WhereClause } from '@/features/DataSource';
|
||||
import { TableRow, WhereClause, TableColumn } from '@/features/DataSource';
|
||||
import { DataGridOptions } from './DataGrid';
|
||||
|
||||
export type AdaptSelectedRowIdsToWhereClauseArgs = {
|
||||
rowsId: Record<number, boolean>;
|
||||
@ -39,3 +40,167 @@ export const adaptSelectedRowIdsToWhereClause = ({
|
||||
|
||||
return whereClause;
|
||||
};
|
||||
|
||||
export type FilterConditions = ({ filter: string } | { sort: string })[];
|
||||
|
||||
export const mapWhereAndSortConditions = (
|
||||
options: DataGridOptions
|
||||
): FilterConditions => {
|
||||
const { where = [], order_by = [] } = options;
|
||||
|
||||
const whereQueryString = where.map(whereCondition => {
|
||||
const columnName = Object.keys(whereCondition)[0];
|
||||
const operator = Object.keys(whereCondition[columnName])[0];
|
||||
const value = whereCondition[columnName][operator];
|
||||
|
||||
const filterQueryString = `${columnName};${operator};${value}`;
|
||||
|
||||
return { filter: filterQueryString };
|
||||
});
|
||||
|
||||
const orderQueryString = order_by.map(orderCondition => {
|
||||
const columnName = orderCondition.column;
|
||||
const sortOrder = orderCondition.type;
|
||||
|
||||
const sortQueryString = `${columnName};${sortOrder}`;
|
||||
|
||||
return { sort: sortQueryString };
|
||||
});
|
||||
|
||||
return [...whereQueryString, ...orderQueryString];
|
||||
};
|
||||
|
||||
export const replaceFiltersInUrl = (
|
||||
currentSearch: string,
|
||||
newFilterConditions: FilterConditions
|
||||
): string => {
|
||||
const searchParams = new URLSearchParams(currentSearch);
|
||||
searchParams.delete('filter');
|
||||
searchParams.delete('sort');
|
||||
|
||||
newFilterConditions.forEach(newFilterCondition => {
|
||||
if ('filter' in newFilterCondition) {
|
||||
searchParams.append('filter', newFilterCondition.filter);
|
||||
}
|
||||
|
||||
if ('sort' in newFilterCondition) {
|
||||
searchParams.append('sort', newFilterCondition.sort);
|
||||
}
|
||||
});
|
||||
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
type Args = {
|
||||
options: DataGridOptions;
|
||||
search: string;
|
||||
};
|
||||
|
||||
export const applyWhereAndSortConditionsToQueryString = ({
|
||||
options,
|
||||
search,
|
||||
}: Args) => {
|
||||
const whereAndSortMap = mapWhereAndSortConditions(options);
|
||||
const searchQueryString = replaceFiltersInUrl(search, whereAndSortMap);
|
||||
return searchQueryString;
|
||||
};
|
||||
|
||||
export const convertValueToGraphQL = (
|
||||
value: string,
|
||||
column: TableColumn
|
||||
): number | string | boolean => {
|
||||
const scalarType = column.graphQLProperties?.scalarType || column.dataType;
|
||||
|
||||
if (value.includes('[')) {
|
||||
const values = value.replace('[', '').replace(']', '').split(',');
|
||||
if (scalarType === 'decimal' || scalarType === 'float') {
|
||||
return JSON.stringify(values.map(_value => parseFloat(_value)));
|
||||
}
|
||||
|
||||
if (scalarType === 'int') {
|
||||
return JSON.stringify(values.map(_value => parseInt(_value, 10)));
|
||||
}
|
||||
|
||||
if (scalarType === 'string') {
|
||||
return JSON.stringify(values.map(_value => _value.trim().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
if (scalarType === 'decimal' || scalarType === 'float') {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
if (scalarType === 'int') {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
if (scalarType === 'boolean') {
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const convertUrlToDataGridOptions = (
|
||||
search: string,
|
||||
columns: TableColumn[] | undefined = []
|
||||
): DataGridOptions => {
|
||||
const searchParams = new URLSearchParams(search);
|
||||
|
||||
const baseOption: DataGridOptions = {
|
||||
where: [],
|
||||
order_by: [],
|
||||
};
|
||||
|
||||
const searchParamsArray: [string, string][] = Array.from(
|
||||
searchParams.entries()
|
||||
);
|
||||
|
||||
return searchParamsArray.reduce<DataGridOptions>((acc, value) => {
|
||||
const key = value[0];
|
||||
if (key === 'database' || key === 'table') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (key === 'filter') {
|
||||
const where = acc?.where || [];
|
||||
const [columnName, operator, filterValue] = value[1].split(';');
|
||||
|
||||
const column = columns.find(_column => _column.name === columnName);
|
||||
const convertedValue = column
|
||||
? convertValueToGraphQL(filterValue, column)
|
||||
: filterValue;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
where: [
|
||||
...where,
|
||||
{
|
||||
[columnName]: {
|
||||
[operator]: convertedValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'sort') {
|
||||
const order_by = acc?.order_by || [];
|
||||
const [columnName, orderType] = value[1].split(';');
|
||||
if (orderType === 'asc' || orderType === 'desc') {
|
||||
return {
|
||||
...acc,
|
||||
order_by: [
|
||||
...order_by,
|
||||
{
|
||||
column: columnName,
|
||||
type: orderType,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, baseOption);
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
FaFilter,
|
||||
FaRegTimesCircle,
|
||||
FaSearch,
|
||||
FaSortAmountDownAlt,
|
||||
FaSortAmountUpAlt,
|
||||
FaTimes,
|
||||
} from 'react-icons/fa';
|
||||
@ -95,7 +96,11 @@ const DisplayOrderByClauses = ({
|
||||
<Badge color="yellow" key={id}>
|
||||
<div className={`gap-3 ${twFlexCenter}`}>
|
||||
<span className={`min-h-3 ${twFlexCenter}`}>
|
||||
<FaSortAmountUpAlt />
|
||||
{orderByClause.type === 'desc' ? (
|
||||
<FaSortAmountDownAlt />
|
||||
) : (
|
||||
<FaSortAmountUpAlt />
|
||||
)}
|
||||
</span>
|
||||
<span className={twFlexCenter}>
|
||||
{orderByClause.column} ({orderByClause.type})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { DropdownMenu } from '@/new-components/DropdownMenu';
|
||||
import { FaEllipsisV } from 'react-icons/fa';
|
||||
import { RiMore2Fill } from 'react-icons/ri';
|
||||
|
||||
export const RowOptionsButton: React.VFC<{
|
||||
row: Record<string, any>;
|
||||
@ -19,7 +19,7 @@ export const RowOptionsButton: React.VFC<{
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="mx-2 my-1 cursor-pointer group-hover:opacity-100">
|
||||
<FaEllipsisV />
|
||||
<RiMore2Fill size="14px" />
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
|
@ -25,8 +25,6 @@ export const fetchRows = async ({
|
||||
table,
|
||||
});
|
||||
|
||||
console.log('>>>', columns, tableColumns);
|
||||
|
||||
const result = await DataSource(httpClient).getTableRows({
|
||||
dataSourceName,
|
||||
table,
|
||||
@ -34,8 +32,6 @@ export const fetchRows = async ({
|
||||
options,
|
||||
});
|
||||
|
||||
console.log('>>>', result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user