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

Graphql data provider #202

Closed
wants to merge 13 commits into from
Prev Previous commit
Next Next commit
Add user list and pagination.
  • Loading branch information
yildirayunlu committed Mar 3, 2021
commit 6d4d295f5c81fc940b241a7183ecedea15ea7cd3
23 changes: 1 addition & 22 deletions example-graphql/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,35 +60,14 @@ function App() {
ready={ReadyPage}
>
<Resource
name="posts"
name="Posts"
list={PostList}
create={PostCreate}
edit={PostEdit}
show={PostShow}
canDelete
options={{ label: "My Posts" }}
/>
<Resource
name="categories"
list={CategoryList}
create={CategoryCreate}
canDelete
/>
<Resource
name="users"
list={UserList}
show={UserShow}
icon={<Icons.UserOutlined />}
/>
<Resource
name="tags"
list={TagList}
edit={TagEdit}
create={TagCreate}
canDelete
/>
<Resource name="images" list={ImagesList} />
<Resource name="files" list={FilesList} />
</Admin>
);
}
Expand Down
78 changes: 21 additions & 57 deletions example-graphql/src/components/pages/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ import {
useApiUrl,
} from "readmin";

import { ShowAside, ShowComponent } from "../show";
import { ShowAside } from "../show";

export const PostList = (props: any) => {
return (
<List {...props}>
<Table
rowKey="id"
pagination={{
pageSize: 20,
pageSize: 5,
position: ["bottomCenter"],
size: "small",
}}
Expand All @@ -40,72 +40,36 @@ export const PostList = (props: any) => {
dataIndex="id"
title="ID"
key="id"
sorter={{
multiple: 3,
}}
defaultSortOrder="descend"
sorter
defaultSortOrder="ascend"
/>
<Column
dataIndex="title"
title="Title"
key="title"
render={(value) => <TextField value={value} />}
sorter={{
multiple: 1,
}}
sorter
/>
<Column
dataIndex="slug"
title="Slug"
key="slug"
dataIndex="views"
title="Views"
key="views"
render={(value) => <TextField value={value} />}
sorter={{
multiple: 2,
}}
sorter
/>
<Column
dataIndex="categoryId"
title="Category"
key="categoryId"
render={(value) => (
<ReferenceField resource="categories" value={value}>
<TextField renderRecordKey="title" />
</ReferenceField>
)}
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Reference
reference="categories"
optionText="title"
sort={{
field: "title",
order: "asc",
}}
>
<Select
style={{ minWidth: 200 }}
showSearch
mode="multiple"
placeholder="Select Category"
/>
</Reference>
</FilterDropdown>
)}
/>
<Column
dataIndex="status"
title="Status"
key="status"
render={(value) => <TagField value={value} />}
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Radio.Group>
<Radio value="active">Active</Radio>
<Radio value="draft">Draft</Radio>
</Radio.Group>
</FilterDropdown>
)}
defaultFilteredValue={["active"]}
dataIndex="User.name"
title="User"
key="userName"
render={(value, record) => {
return (
<TextField
record={record.User}
renderRecordKey="name"
/>
);
}}
sorter
/>
</Table>
</List>
Expand Down
10 changes: 10 additions & 0 deletions src/components/table/table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export const Table: React.FC<TableProps> = ({
throw new Error(`resource not found!`);
}

// get all fields for graphql
const fields: string[] = [];
React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
// TODO: Check Column component.
fields.push(child.props.dataIndex);
}
});

const [current, setCurrent] = useState(
(pagination && pagination.current) || defaultCurrent,
);
Expand All @@ -52,6 +61,7 @@ export const Table: React.FC<TableProps> = ({
pagination: { current, pageSize },
filters,
sort,
fields,
});

const onChange = (
Expand Down
5 changes: 3 additions & 2 deletions src/contexts/data/IDataContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ export interface DeleteManyResponse {
}

export interface IDataContext {
getList: <TData extends BaseRecord = BaseRecord>(
getList: (
resource: string,
params: {
pagination?: Pagination;
search?: string;
sort?: Sort;
filters?: Filters;
fields?: string[];
},
fields?: string[],
) => Promise<GetListResponse<TData>>;
) => Promise<GetListResponse>;
getMany: <TData extends BaseRecord = BaseRecord>(
resource: string,
ids: Identifier[],
Expand Down
90 changes: 43 additions & 47 deletions src/dataProviders/jsonGraphqlServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,67 @@ import axios from "axios";
import { stringify } from "query-string";
import { GraphQLClient } from "graphql-request";
import * as gql from "gql-query-builder";
import IQueryBuilderOptions from "gql-query-builder/build/IQueryBuilderOptions";

import { IDataContext } from "@interfaces";
import { BaseRecord, IDataContext } from "@interfaces";
import { generateFieldsArray } from "@definitions/graphql";

const JsonGraphqlServer = (apiUrl: string): IDataContext => {
const client = new GraphQLClient("http:https://localhost:3000");

const request = async (
options: IQueryBuilderOptions | IQueryBuilderOptions[],
) => {
const qb = gql.query(options);

return client.request(qb.query, qb.variables);
};

return {
getList: async (resource, params, fields) => {
const url = `${apiUrl}/${resource}`;
getList: async (resource, params) => {
const operation = `all${resource}`;

// search
const q = params.search;
// const q = params.search;

// pagination
const current = params.pagination?.current || 1;
const pageSize = params.pagination?.pageSize || 10;

const qb = gql.query({
operation: "allPosts",
fields: ["id", "title", "views"],
});

client
.request(qb.query, qb.variables)
.then((e: any) => console.log("e", e));
const page = params.pagination?.current || 1;
const perPage = params.pagination?.pageSize || 10;

// sort
let _sort = ["id"]; // default sorting field
let _order = ["desc"]; // default sorting

const { sort } = params;
let sortField = "id"; // default sorting field
let sortOrder = "desc"; // default sorting

if (Array.isArray(sort) || sort?.field) {
_sort = [];
_order = [];

if (Array.isArray(sort)) {
sort.map((item) => {
_sort.push(`${item.field}`);
_order.push(`${item.order}`.replace("end", "")); // replace -> [ascend, descend] -> [asc,desc]
});
} else {
_sort.push(`${sort.field}`);
_order.push(`${sort.order}`.replace("end", "")); // replace -> [ascend, descend] -> [asc,desc]
}
// multiple sorting not support marmelab/json-graphql-server
if (!Array.isArray(sort) && sort && sort.field && sort.order) {
sortField = String(sort.field);
sortOrder = sort.order.replace("end", ""); // replace -> [ascend, descend] -> [asc,desc];
}

// filter
const filters = stringify(params.filters || {}, {
skipNull: true,
// fields
const fields = generateFieldsArray(params.fields || []);

let data: BaseRecord[] = [];
let total = 0;

const responseData = await request({
operation,
fields,
variables: {
sortField,
sortOrder,
page: page - 1, // start page: 0
perPage,
},
});
data = responseData["allPosts"];

const query = {
_start: (current - 1) * pageSize,
_end: current * pageSize,
_sort: _sort.join(","),
_order: _order.join(","),
q,
};

const { data, headers } = await axios.get(
`${url}?${stringify(query)}&${filters}`,
);

const total = +headers["x-total-count"];
const responseMetaData = await request({
operation: `_${operation}Meta`,
fields: ["count"],
});
total = responseMetaData["_allPostsMeta"].count;

return {
data,
Expand Down
40 changes: 40 additions & 0 deletions src/definitions/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const generateFieldsArray = (obj: string[]) => {
const result: string[] = [];

// For each object path (property key) in the object
for (const objectPath of obj) {
// Split path into component parts
const parts = objectPath.split(".");

// Create sub-objects along path as needed
let target = result;
while (parts.length > 1) {
const part = parts.shift();

if (part) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
target = target[part] = target[part] || [];
}
}

// Set value at end of path
if (target.push) {
target.push(parts[0]);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
target[parts[0]] = undefined;
}
}

const response = [];
for (const iterator of Object.entries(result)) {
if (Number(iterator[0]) >= 0) {
response.push(iterator[1]);
} else {
response.push({ [iterator[0]]: iterator[1] });
}
}
return response;
};
4 changes: 4 additions & 0 deletions src/definitions/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const getDefaultSortOrder = (children: any): Sort => {
}
});

if (cols.length === 1) {
return cols[0];
}

return cols;
};

Expand Down
11 changes: 6 additions & 5 deletions src/hooks/data/useList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface UseListConfig {
search?: string;
sort?: Sort;
filters?: Filters;
fields?: string[];
}

const defaultConfig: UseListConfig = {
Expand All @@ -24,16 +25,16 @@ const defaultConfig: UseListConfig = {
},
};

export const useList = <TData extends BaseRecord = BaseRecord>(
export const useList = (
resource: string,
config?: UseListConfig,
queryOptions?: UseQueryOptions<GetListResponse<TData>>,
): QueryObserverResult<GetListResponse<TData>, unknown> => {
queryOptions?: UseQueryOptions<GetListResponse>,
): QueryObserverResult<GetListResponse, unknown> => {
const { getList } = useContext<IDataContext>(DataContext);

const queryResponse = useQuery<GetListResponse<TData>>(
const queryResponse = useQuery<GetListResponse>(
[`resource/list/${resource}`, { ...(config ?? defaultConfig) }],
() => getList<TData>(resource, config ?? defaultConfig),
() => getList(resource, config ?? defaultConfig),
queryOptions ?? { keepPreviousData: true },
);

Expand Down