Skip to content

Commit

Permalink
[admin] fix table filter and sort to work with prisma v5
Browse files Browse the repository at this point in the history
  • Loading branch information
AhmedElywa committed Aug 25, 2023
1 parent e4943f7 commit 3f7d1df
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 327 deletions.
5 changes: 2 additions & 3 deletions packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"build:es": "node ./scripts/build es",
"build:copy-files": "node ./scripts/copyFiles",
"build:types": "tsc -p tsconfig.build.json",
"build:css": "cross-env NODE_ENV=production tailwindcss-cli build -o ./dist/style.css"
"build:css": "cross-env NODE_ENV=production tailwindcss build -o ./dist/style.css"
},
"dependencies": {
"@headlessui/react": "^1.5.0",
Expand Down Expand Up @@ -55,8 +55,7 @@
"postcss": "^8.4.6",
"react": "17.0.2",
"react-dom": "17.0.2",
"tailwindcss": "3.0.23",
"tailwindcss-cli": "^0.1.2",
"tailwindcss": "3.3.3",
"typescript": "5.1.6",
"yargs": "^17.3.1"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/admin/src/PrismaTable/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const Form: React.FC<FormProps> = ({ action, model: modelName, data, onCancel, o

return (
<div
className="flex flex-col bg-white rounded shadow-lg text-gray-800 text-base mb-5"
style={action === 'create' ? { maxWidth: '800px', maxHeight: '100vh' } : {}}
className="flex flex-col bg-white rounded shadow-lg text-gray-800 text-base mb-5 mx-auto"
style={action === 'create' ? { maxWidth: '1000px', maxHeight: '100vh' } : {}}
>
<header className="py-4 px-5 rounded-t border-b border-gray-100 font-bold text-2xl">
{lang[action] + ' ' + model.name}
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/PrismaTable/Form/useActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ const useActions = (model: AdminSchemaModel, data: any, action: FormProps['actio
if (field?.update) {
if (field.kind === 'object') {
const fieldModel = models.find((item) => item.id === field.type)!;
console.log(newData);
if ((newData[key] && !data[key]) || (newData[key] && newData[key] !== data[key][fieldModel.idField])) {
const editField = fieldModel.fields.find((item) => item.name === fieldModel.idField)!;
updateData[key] = {
Expand Down
52 changes: 36 additions & 16 deletions packages/admin/src/PrismaTable/Table/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { TableContext } from '../Context';
import { SearchCircleIcon, TrashIcon } from '@heroicons/react/solid';
import { buttonClasses, classNames, inputClasses } from '../../components/css';
import { randString } from './utils';
import { getDate } from '../Form/getDate';
import { data } from 'autoprefixer';

interface Option {
id: any;
Expand All @@ -27,13 +29,19 @@ interface FilterProps {
filters: { id: string; value: any }[];
}

const removeByIndexWithSplice = (array: any[], index: number) => {
const newArr = array.slice(); // create a copy of the original array
newArr.splice(index, 1); // remove the item at the specified index
return newArr; // return the modified array
};

export const Filter: React.FC<FilterProps> = ({ model, setAllFilters, filters }) => {
const [state, setState] = useState(filters.map(() => randString(10)));
const { lang } = useContext(TableContext);

const deleteFilter = (index: number) => {
setState([...state.filter((_, i) => i !== index)]);
setAllFilters([...filters.filter((_, i) => i !== index)]);
setState(removeByIndexWithSplice(state, index));
setAllFilters(removeByIndexWithSplice(filters, index));
};

return (
Expand All @@ -46,13 +54,18 @@ export const Filter: React.FC<FilterProps> = ({ model, setAllFilters, filters })
deleteFilter={() => deleteFilter(index)}
filter={filters[index]}
setFilter={({ id, value }) => {
// Deep clone filters to break references
const newFilters = JSON.parse(JSON.stringify(filters));

if (!value) {
setAllFilters([...filters.filter((item) => item.id !== id)]);
} else {
const newFilters = [...filters];
newFilters[index] = { id, value };
setAllFilters(newFilters);
setAllFilters(removeByIndexWithSplice(newFilters, index));
return;
}

newFilters[index] = { id, value: { ...value } };

// Update the filters state
setAllFilters(newFilters);
}}
/>
))}
Expand Down Expand Up @@ -99,9 +112,9 @@ const FilterRow: React.FC<FilterRowProps> = ({ model, filter, setFilter, index,
};
let filterComponent;
if (getField.kind === 'enum') {
filterComponent = <EnumFilter key={getField.name} {...props} />;
filterComponent = <EnumFilter key={getField.name + index} {...props} />;
} else if (getField.kind === 'object') {
filterComponent = <ObjectFilter key={getField.name} {...props} />;
filterComponent = <ObjectFilter key={getField.name + index} {...props} />;
} else {
switch (getField.type) {
case 'Int':
Expand All @@ -110,10 +123,10 @@ const FilterRow: React.FC<FilterRowProps> = ({ model, filter, setFilter, index,
case 'Float':
case 'DateTime':
case 'String':
filterComponent = <DefaultFilter key={getField.name} {...props} />;
filterComponent = <DefaultFilter key={getField.name + index} {...props} />;
break;
case 'Boolean':
filterComponent = <BooleanFilter key={getField.name} {...props} />;
filterComponent = <BooleanFilter key={getField.name + index} {...props} />;
break;
}
}
Expand Down Expand Up @@ -168,6 +181,12 @@ const DefaultFilter: React.FC<FilterComponentsProps> = ({ filterValue, setFilter
const [option, setOption] = useState<Option>(
filterValue ? options.find((item) => !!filterValue[item.id])! : options[0],
);

const inputProps =
field.type === 'DateTime'
? { type: 'datetime-local', defaultValue: value[option.id] ? getDate(new Date(value[option.id])) : undefined }
: { type: 'text', value: value[option.id] || '' };

return (
<>
<Select
Expand All @@ -181,12 +200,11 @@ const DefaultFilter: React.FC<FilterComponentsProps> = ({ filterValue, setFilter
style={{ maxWidth: '13rem', lineHeight: 'inherit' }}
className={inputClasses.replace('py-2 px-4', 'py-2 px-3 text-sm')}
placeholder={lang[option.id as 'gt']}
type={field.type === 'DateTime' ? 'date' : 'text'}
value={value ? value[option.id] || '' : ''}
{...inputProps}
onChange={(event) =>
onChange({
name: option.id,
value: field.type === 'DateTime' ? event.target.valueAsDate : event.target.value,
value: field.type === 'DateTime' ? new Date(event.target.value).toISOString() : event.target.value,
wait: true,
})
}
Expand Down Expand Up @@ -234,7 +252,7 @@ export const EnumFilter: React.FC<FilterComponentsProps> = ({ field, filterValue
const ObjectFilter: React.FC<FilterComponentsProps> = ({ field, filterValue, setFilter }) => {
const { dir } = useContext(TableContext);
const model = useModel(field.type)!;
const filter = filterValue ? (field.list ? filterValue.some : filterValue) : {};
const filter = filterValue ? (field.list ? filterValue.some : filterValue.is) : {};
const options = model.fields
.filter((item) => item.filter && item.kind !== 'object' && !item.list && item.type !== 'Json')
.sort((a, b) => a.order - b.order)
Expand Down Expand Up @@ -265,7 +283,9 @@ const ObjectFilter: React.FC<FilterComponentsProps> = ({ field, filterValue, set
} else {
delete newValue[getField.name];
}
setFilter(Object.keys(newValue).length > 0 ? (field.list ? { some: newValue } : newValue) : undefined);
setFilter(
Object.keys(newValue).length > 0 ? (field.list ? { some: newValue } : { is: newValue }) : undefined,
);
},
}
: null;
Expand Down
37 changes: 9 additions & 28 deletions packages/admin/src/PrismaTable/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,7 @@ export const Table: React.FC<TableProps> = ({
<>
<div className="flex flex-col rounded-lg shadow bg-white">
{headerActions}
<div
className={classNames('w-full inline-flex space-x-4 my-4', dir === 'rtl' ? 'pr-4 space-x-reverse' : 'pl-4')}
>
<div className="w-full inline-flex space-x-4 my-4 rtl:pr-4 rtl:space-x-reverse ltr:pl-4">
{actions.create && !connect && (
<div>
<ActionButtons.Add />
Expand All @@ -222,9 +220,7 @@ export const Table: React.FC<TableProps> = ({
>
{lang.filter}
{!!filters.length && (
<span className={classNames('rounded-full bg-yellow-400 px-2', dir === 'rtl' ? 'mr-2' : 'ml-2')}>
{filters.length}
</span>
<span className="rounded-full bg-yellow-400 px-2 rtl:mr-2 ltr:ml-2">{filters.length}</span>
)}
</Popover.Button>
<Transition
Expand Down Expand Up @@ -402,20 +398,14 @@ export const Table: React.FC<TableProps> = ({
</div>
<div className={classNames('flex flex-wrap md:justify-between justify-center w-full', tdClasses)}>
<nav
className={classNames(
'w-full md:w-auto mb-4 md:mb-0 inline-flex -space-x-px',
dir === 'rtl' ? 'space-x-reverse' : '',
)}
className="w-full md:w-auto mb-4 md:mb-0 inline-flex -space-x-px rtl:space-x-reverse"
aria-label="Pagination"
>
<button
type="button"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
className={classNames(
'relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50',
dir === 'rtl' ? 'rounded-r-md' : 'rounded-l-md',
)}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 rtl:rounded-r-md ltr:rounded-l-md"
>
<ChevronDoubleRightIcon className={classNames('h-4 w-4', dir === 'rtl' ? '' : 'transform rotate-180')} />
</button>
Expand All @@ -425,7 +415,7 @@ export const Table: React.FC<TableProps> = ({
disabled={!canPreviousPage}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
>
<ChevronRightIcon className={classNames('h-4 w-4', dir === 'rtl' ? '' : 'transform rotate-180')} />
<ChevronRightIcon className="h-4 w-4 ltr:transform ltr:rotate-180" />
</button>
{initPages(pageCount, pageIndex + 1, paginationOptions).map((item) => (
<button
Expand All @@ -448,7 +438,7 @@ export const Table: React.FC<TableProps> = ({
disabled={!canNextPage}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
>
<ChevronLeftIcon className={classNames('h-4 w-4', dir === 'rtl' ? '' : 'transform rotate-180')} />
<ChevronLeftIcon className="h-4 w-4 ltr:transform ltr:rotate-180" />
</button>
<button
type="button"
Expand All @@ -462,25 +452,16 @@ export const Table: React.FC<TableProps> = ({
<ChevronDoubleLeftIcon className={classNames('h-4 w-4', dir === 'rtl' ? '' : 'transform rotate-180')} />
</button>
</nav>
<div
className={classNames(
'inline-flex justify-center -space-x-px w-full md:w-auto',
dir === 'rtl' ? 'space-x-reverse' : '',
)}
>
<div className="inline-flex justify-center -space-x-px w-full md:w-auto rtl:space-x-reverse">
{pageSizeOptions.map((item, index) => (
<button
type="button"
key={index}
className={classNames(
index === 0
? dir === 'rtl'
? 'rounded-r-md'
: 'rounded-l-md'
? 'rtl:rounded-r-md ltr:rounded-l-md'
: index === pageSizeOptions.length - 1
? dir === 'rtl'
? 'rounded-l-md'
: 'rounded-r-md'
? 'rtl:rounded-l-md ltr:rounded-r-md'
: '',
item === pageSize
? 'bg-blue-500 text-white hover:bg-blue-700'
Expand Down
57 changes: 51 additions & 6 deletions packages/admin/src/PrismaTable/Table/useFilterAndSort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,77 @@ const filterMemo = (modelName: string, filter?: any) => {
}, [filter, models]);
};

function isObject(item: any) {
return item && typeof item === 'object' && !Array.isArray(item);
}

function mergeDeep(target: any, ...sources: any[]): any {
if (!sources.length) return target;
const source: any = sources.shift();

if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}

return mergeDeep(target, ...sources);
}

const handleFilter = (filters: { id: string; value: any }[]) => {
if (filters.length) {
const newWhere: { [key: string]: { [key: string]: any } } = {};
filters.forEach((item) => {
newWhere[item.id] = item.value;
// Check if an entry with the same id already exists
if (newWhere[item.id]) {
// If the value for the existing entry is an object, and the new value is also an object, merge them
if (isObject(newWhere[item.id]) && isObject(item.value)) {
newWhere[item.id] = mergeDeep(newWhere[item.id], item.value);
} else {
// Otherwise, just overwrite it (or you can handle it differently if needed)
newWhere[item.id] = item.value;
}
} else {
newWhere[item.id] = item.value;
}
});
return newWhere;
}
return undefined;
};

export const useFilterAndSort = (model: string, filter?: any, defaultOrder?: Record<string, 'asc' | 'desc'>[]) => {
type OrderBy = Record<string, 'asc' | 'desc' | { sort: 'asc' | 'desc'; nulls: 'last' | 'first' }>;

export const useFilterAndSort = (model: string, filter?: any, defaultOrder?: OrderBy[]) => {
const initialFilter = filterMemo(model, filter);
const {
schema: { models },
} = useContext(TableContext);
const [where, setWhere] = useState<any>(handleFilter(initialFilter));
const [orderBy, setOrderBy] = useState<Record<string, 'asc' | 'desc'>[] | undefined>(defaultOrder);
const [orderBy, setOrderBy] = useState<OrderBy[] | undefined>(defaultOrder);

const filterHandler = (filters: { id: string; value: any }[]) => {
setWhere(handleFilter(filters));
setWhere(handleFilter(JSON.parse(JSON.stringify(filters))));
};

const sortByHandler = (sortBy: { id: string; desc: boolean }[]) => {
if (sortBy.length > 0) {
const newOrderBy: { [key: string]: 'asc' | 'desc' }[] = [];
const newOrderBy: OrderBy[] = [];
sortBy.forEach((item) => {
const field = item.id.split('.')[0];
const modelObject = models.find((item) => item.id === model);
const fieldModel = modelObject?.fields.find((item) => item.name === field);
newOrderBy.push({
[item.id.split('.')[0]]: item.desc ? 'desc' : 'asc',
[field]: fieldModel?.required
? item.desc
? 'desc'
: 'asc'
: { sort: item.desc ? 'desc' : 'asc', nulls: 'last' },
});
});
setOrderBy(newOrderBy);
Expand Down
13 changes: 13 additions & 0 deletions packages/admin/src/PrismaTable/dynamicTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ const DynamicTable: React.FC<DynamicTableProps> = ({
variables,
fetchPolicy: 'no-cache',
});
const whereRef = React.useRef(where);

useEffect(() => {
if (
where &&
whereRef.current &&
where !== whereRef.current &&
Object.keys(whereRef.current).length === Object.keys(where).length
) {
getData();
}
whereRef.current = where;
}, [where, getData]);

const [deleteOne] = useMutation(mutationDocument(models, model, 'delete'));

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ interface SameProps {
Update?: React.FC<{ id: any }>;
Delete?: React.FC<{ id: any }>;
};
defaultOrderBy?: Record<string, Record<string, 'asc' | 'desc'>[]>;
defaultOrderBy?: Record<string, Record<string, 'asc' | 'desc' | { sort: 'asc' | 'desc'; nulls: 'last' | 'first' }>[]>;
}

export interface ModelTableProps extends Partial<Omit<RequireContextProps, 'lang'>>, SameProps {
Expand Down
1 change: 0 additions & 1 deletion packages/admin/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
Expand Down
Loading

0 comments on commit 3f7d1df

Please sign in to comment.