feat: drag-and-drop columns (#2142)
This commit is contained in:
@@ -14,6 +14,7 @@ import Version from './views/Version';
|
||||
import { DocumentInfoProvider } from './utilities/DocumentInfo';
|
||||
import { useLocale } from './utilities/Locale';
|
||||
import { LoadingOverlayToggle } from './elements/Loading';
|
||||
import { TableColumnsProvider } from './elements/TableColumns';
|
||||
|
||||
const Dashboard = lazy(() => import('./views/Dashboard'));
|
||||
const ForgotPassword = lazy(() => import('./views/ForgotPassword'));
|
||||
@@ -188,10 +189,12 @@ const Routes = () => {
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.read?.permission) {
|
||||
return (
|
||||
<List
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
<TableColumnsProvider collection={collection}>
|
||||
<List
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
</TableColumnsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ export const Collapsible: React.FC<Props> = ({
|
||||
{dragHandleProps && (
|
||||
<div
|
||||
className={`${baseClass}__drag`}
|
||||
{...dragHandleProps}
|
||||
{...dragHandleProps.attributes}
|
||||
{...dragHandleProps.listeners}
|
||||
>
|
||||
<DragHandle />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd';
|
||||
import { DragHandleProps } from '../DraggableSortable/DraggableSortableItem/types';
|
||||
|
||||
export type Props = {
|
||||
collapsed?: boolean
|
||||
@@ -9,5 +9,5 @@ export type Props = {
|
||||
children: React.ReactNode
|
||||
onToggle?: (collapsed: boolean) => void
|
||||
initCollapsed?: boolean
|
||||
dragHandleProps?: DraggableProvidedDragHandleProps
|
||||
dragHandleProps?: DragHandleProps
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.column-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: var(--theme-elevation-50);
|
||||
padding: base(1) base(1) base(.5);
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useEffect, useId, useState } from 'react';
|
||||
import React, { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
|
||||
import Pill from '../Pill';
|
||||
import Plus from '../../icons/Plus';
|
||||
import X from '../../icons/X';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { useEditDepth } from '../../utilities/EditDepth';
|
||||
import DraggableSortable from '../DraggableSortable';
|
||||
import { useTableColumns } from '../TableColumns';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'column-selector';
|
||||
@@ -14,49 +16,58 @@ const baseClass = 'column-selector';
|
||||
const ColumnSelector: React.FC<Props> = (props) => {
|
||||
const {
|
||||
collection,
|
||||
columns,
|
||||
setColumns,
|
||||
} = props;
|
||||
|
||||
const [fields, setFields] = useState(() => flattenTopLevelFields(collection.fields, true));
|
||||
|
||||
useEffect(() => {
|
||||
setFields(flattenTopLevelFields(collection.fields, true));
|
||||
}, [collection.fields]);
|
||||
const {
|
||||
columns,
|
||||
toggleColumn,
|
||||
moveColumn,
|
||||
} = useTableColumns();
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const uuid = useId();
|
||||
const editDepth = useEditDepth();
|
||||
if (!columns) { return null; }
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{fields && fields.map((field, i) => {
|
||||
const isEnabled = columns.find((column) => column === field.name);
|
||||
<DraggableSortable
|
||||
className={baseClass}
|
||||
ids={columns.map((col) => col.accessor)}
|
||||
onDragEnd={({ moveFromIndex, moveToIndex }) => {
|
||||
moveColumn({
|
||||
fromIndex: moveFromIndex,
|
||||
toIndex: moveToIndex,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{columns.map((col, i) => {
|
||||
const {
|
||||
accessor,
|
||||
active,
|
||||
label,
|
||||
name,
|
||||
} = col;
|
||||
|
||||
return (
|
||||
<Pill
|
||||
draggable
|
||||
id={accessor}
|
||||
onClick={() => {
|
||||
let newState = [...columns];
|
||||
if (isEnabled) {
|
||||
newState = newState.filter((remainingColumn) => remainingColumn !== field.name);
|
||||
} else {
|
||||
newState.unshift(field.name);
|
||||
}
|
||||
|
||||
setColumns(newState);
|
||||
toggleColumn(accessor);
|
||||
}}
|
||||
alignIcon="left"
|
||||
key={`${collection.slug}-${field.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`}
|
||||
icon={isEnabled ? <X /> : <Plus />}
|
||||
key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`}
|
||||
icon={active ? <X /> : <Plus />}
|
||||
className={[
|
||||
`${baseClass}__column`,
|
||||
isEnabled && `${baseClass}__column--active`,
|
||||
active && `${baseClass}__column--active`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{getTranslation(field.label || field.name, i18n)}
|
||||
{getTranslation(label || name, i18n)}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DraggableSortable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,4 @@ import { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
|
||||
export type Props = {
|
||||
collection: SanitizedCollectionConfig,
|
||||
columns: string[]
|
||||
setColumns: (columns: string[]) => void,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { UseDraggableArguments } from '@dnd-kit/core';
|
||||
import React, { Fragment } from 'react';
|
||||
import { useDraggableSortable } from '../useDraggableSortable';
|
||||
import { ChildFunction } from './types';
|
||||
|
||||
export const DraggableSortableItem: React.FC<UseDraggableArguments & {
|
||||
children: ChildFunction
|
||||
}> = (props) => {
|
||||
const {
|
||||
id,
|
||||
disabled,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggableSortable({
|
||||
id,
|
||||
disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children({
|
||||
attributes: {
|
||||
...attributes,
|
||||
style: {
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
},
|
||||
},
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableSortableItem;
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
import React from 'react';
|
||||
import { UseDraggableArguments } from '@dnd-kit/core';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import { UseDraggableSortableReturn } from '../useDraggableSortable/types';
|
||||
|
||||
export type DragHandleProps = UseDraggableArguments & {
|
||||
attributes: UseDraggableArguments['attributes']
|
||||
listeners: SyntheticListenerMap
|
||||
}
|
||||
|
||||
export type ChildFunction = (args: UseDraggableSortableReturn) => React.ReactNode;
|
||||
|
||||
export type Props = UseDraggableArguments & {
|
||||
children: ChildFunction
|
||||
}
|
||||
75
src/admin/components/elements/DraggableSortable/index.tsx
Normal file
75
src/admin/components/elements/DraggableSortable/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useCallback, useId } from 'react';
|
||||
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||
import {
|
||||
DragEndEvent,
|
||||
useDroppable,
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
|
||||
import { Props } from './types';
|
||||
|
||||
const DraggableSortable: React.FC<Props> = (props) => {
|
||||
const {
|
||||
onDragEnd,
|
||||
ids,
|
||||
className,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const id = useId();
|
||||
|
||||
const { setNodeRef } = useDroppable({
|
||||
id,
|
||||
});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 5,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) return;
|
||||
|
||||
if (typeof onDragEnd === 'function') {
|
||||
onDragEnd({
|
||||
event,
|
||||
moveFromIndex: ids.findIndex((_id) => _id === active.id),
|
||||
moveToIndex: ids.findIndex((_id) => _id === over.id),
|
||||
});
|
||||
}
|
||||
}, [onDragEnd, ids]);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={ids}>
|
||||
<div
|
||||
className={className}
|
||||
ref={setNodeRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableSortable;
|
||||
15
src/admin/components/elements/DraggableSortable/types.ts
Normal file
15
src/admin/components/elements/DraggableSortable/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { Ref } from 'react';
|
||||
|
||||
export type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
ids: string[];
|
||||
droppableRef?: Ref<HTMLElement>;
|
||||
onDragEnd: (e: {
|
||||
event: DragEndEvent,
|
||||
moveFromIndex: number,
|
||||
moveToIndex: number,
|
||||
}) => void;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { UseDraggableArguments } from '@dnd-kit/core';
|
||||
import { UseDraggableSortableReturn } from './types';
|
||||
|
||||
export const useDraggableSortable = (props: UseDraggableArguments): UseDraggableSortableReturn => {
|
||||
const {
|
||||
id,
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({
|
||||
id,
|
||||
disabled,
|
||||
});
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
...attributes,
|
||||
style: {
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
},
|
||||
},
|
||||
isDragging,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform: transform && `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
|
||||
export type UseDraggableSortableReturn = {
|
||||
attributes: HTMLAttributes<unknown>
|
||||
listeners: SyntheticListenerMap
|
||||
setNodeRef: (node: HTMLElement | null) => void
|
||||
transform: string
|
||||
isDragging?: boolean
|
||||
}
|
||||
@@ -22,8 +22,6 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
collection,
|
||||
enableColumns = true,
|
||||
enableSort = false,
|
||||
columns,
|
||||
setColumns,
|
||||
handleSortChange,
|
||||
handleWhereChange,
|
||||
modifySearchQuery = true,
|
||||
@@ -95,11 +93,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__columns`}
|
||||
height={visibleDrawer === 'columns' ? 'auto' : 0}
|
||||
>
|
||||
<ColumnSelector
|
||||
collection={collection}
|
||||
columns={columns}
|
||||
setColumns={setColumns}
|
||||
/>
|
||||
<ColumnSelector collection={collection} />
|
||||
</AnimateHeight>
|
||||
)}
|
||||
<AnimateHeight
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Where } from '../../../../types';
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
import { Column } from '../Table/types';
|
||||
|
||||
export type Props = {
|
||||
enableColumns?: boolean,
|
||||
enableSort?: boolean,
|
||||
enableColumns?: boolean
|
||||
enableSort?: boolean
|
||||
modifySearchQuery?: boolean
|
||||
handleSortChange?: (sort: string) => void
|
||||
handleWhereChange?: (where: Where) => void
|
||||
columns?: string[]
|
||||
setColumns?: (columns: string[]) => void,
|
||||
collection: SanitizedCollectionConfig,
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
export type ListControls = {
|
||||
where?: unknown
|
||||
columns?: string[]
|
||||
columns?: Partial<Column>[]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useCallback, useEffect, useReducer, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ListDrawerProps } from './types';
|
||||
@@ -16,37 +16,11 @@ import { useDocumentDrawer } from '../DocumentDrawer';
|
||||
import Pill from '../Pill';
|
||||
import X from '../../icons/X';
|
||||
import ViewDescription from '../ViewDescription';
|
||||
import { Column } from '../Table/types';
|
||||
import getInitialColumnState from '../../views/collections/List/getInitialColumns';
|
||||
import buildListColumns from '../../views/collections/List/buildColumns';
|
||||
import formatFields from '../../views/collections/List/formatFields';
|
||||
import { ListPreferences } from '../../views/collections/List/types';
|
||||
import { usePreferences } from '../../utilities/Preferences';
|
||||
import { Field } from '../../../../fields/config/types';
|
||||
import { baseClass } from '.';
|
||||
|
||||
const buildColumns = ({
|
||||
collectionConfig,
|
||||
columns,
|
||||
onSelect,
|
||||
t,
|
||||
}) => buildListColumns({
|
||||
collection: collectionConfig,
|
||||
columns,
|
||||
t,
|
||||
cellProps: [{
|
||||
link: false,
|
||||
onClick: ({ collection, rowData }) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
docID: rowData.id,
|
||||
collectionConfig: collection,
|
||||
});
|
||||
}
|
||||
},
|
||||
className: `${baseClass}__first-cell`,
|
||||
}],
|
||||
});
|
||||
import { TableColumnsProvider } from '../TableColumns';
|
||||
|
||||
export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
drawerSlug,
|
||||
@@ -58,7 +32,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation(['upload', 'general']);
|
||||
const { permissions } = useAuth();
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
const { setPreference } = usePreferences();
|
||||
const { isModalOpen, closeModal } = useModal();
|
||||
const [limit, setLimit] = useState<number>();
|
||||
const [sort, setSort] = useState(null);
|
||||
@@ -78,15 +52,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
|
||||
const [fields, setFields] = useState<Field[]>(() => formatFields(selectedCollectionConfig, t));
|
||||
|
||||
const [tableColumns, setTableColumns] = useState<Column[]>(() => {
|
||||
const initialColumns = getInitialColumnState(fields, selectedCollectionConfig.admin.useAsTitle, selectedCollectionConfig.admin.defaultColumns);
|
||||
return buildColumns({
|
||||
collectionConfig: selectedCollectionConfig,
|
||||
columns: initialColumns,
|
||||
t,
|
||||
onSelect,
|
||||
});
|
||||
});
|
||||
useEffect(() => {
|
||||
setFields(formatFields(selectedCollectionConfig, t));
|
||||
}, [selectedCollectionConfig, t]);
|
||||
|
||||
// allow external control of selected collection, same as the initial state logic above
|
||||
useEffect(() => {
|
||||
@@ -97,8 +65,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}
|
||||
}, [selectedCollection, enabledCollectionConfigs, onSelect, t]);
|
||||
|
||||
const activeColumnNames = tableColumns.map(({ accessor }) => accessor);
|
||||
const stringifiedActiveColumns = JSON.stringify(activeColumnNames);
|
||||
const preferenceKey = `${selectedCollectionConfig.slug}-list`;
|
||||
|
||||
// this is the 'create new' drawer
|
||||
@@ -153,41 +119,14 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, where, limit, cacheBust, filterOptions, selectedCollectionConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const syncColumnsFromPrefs = async () => {
|
||||
const currentPreferences = await getPreference<ListPreferences>(preferenceKey);
|
||||
const newFields = formatFields(selectedCollectionConfig, t);
|
||||
setFields(newFields);
|
||||
const initialColumns = getInitialColumnState(newFields, selectedCollectionConfig.admin.useAsTitle, selectedCollectionConfig.admin.defaultColumns);
|
||||
setTableColumns(buildColumns({
|
||||
collectionConfig: selectedCollectionConfig,
|
||||
columns: currentPreferences?.columns || initialColumns,
|
||||
t,
|
||||
onSelect,
|
||||
}));
|
||||
};
|
||||
|
||||
syncColumnsFromPrefs();
|
||||
}, [t, getPreference, preferenceKey, onSelect, selectedCollectionConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const newPreferences = {
|
||||
limit,
|
||||
sort,
|
||||
columns: JSON.parse(stringifiedActiveColumns),
|
||||
};
|
||||
|
||||
setPreference(preferenceKey, newPreferences);
|
||||
}, [sort, limit, stringifiedActiveColumns, setPreference, preferenceKey]);
|
||||
|
||||
const setActiveColumns = useCallback((columns: string[]) => {
|
||||
setTableColumns(buildColumns({
|
||||
collectionConfig: selectedCollectionConfig,
|
||||
columns,
|
||||
t,
|
||||
onSelect,
|
||||
}));
|
||||
}, [selectedCollectionConfig, t, onSelect]);
|
||||
}, [sort, limit, setPreference, preferenceKey]);
|
||||
|
||||
const onCreateNew = useCallback(({ doc }) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
@@ -206,7 +145,21 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TableColumnsProvider
|
||||
collection={selectedCollectionConfig}
|
||||
cellProps={[{
|
||||
link: false,
|
||||
onClick: ({ collection: rowColl, rowData }) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
docID: rowData.id,
|
||||
collectionConfig: rowColl,
|
||||
});
|
||||
}
|
||||
},
|
||||
className: `${baseClass}__first-cell`,
|
||||
}]}
|
||||
>
|
||||
<DocumentInfoProvider collection={selectedCollectionConfig}>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultList}
|
||||
@@ -264,12 +217,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
data,
|
||||
limit: limit || selectedCollectionConfig?.admin?.pagination?.defaultLimit,
|
||||
setLimit,
|
||||
tableColumns,
|
||||
setColumns: setActiveColumns,
|
||||
setSort,
|
||||
newDocumentURL: null,
|
||||
hasCreatePermission,
|
||||
columnNames: activeColumnNames,
|
||||
disableEyebrow: true,
|
||||
modifySearchParams: false,
|
||||
onCardClick: (doc) => {
|
||||
@@ -290,6 +240,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
<DocumentDrawer onSave={onCreateNew} />
|
||||
</Fragment>
|
||||
</TableColumnsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
.pill {
|
||||
font-size: 1rem;
|
||||
line-height: base(1);
|
||||
border: 0;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
background: var(--theme-elevation-150);
|
||||
color: var(--theme-elevation-800);
|
||||
border-radius: $style-radius-s;
|
||||
padding: 0 base(.25);
|
||||
padding-left: base(.0875 + .25);
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 0;
|
||||
padding: 0 base(.25);
|
||||
align-items: center;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
@@ -26,6 +25,10 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&--is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&--has-icon {
|
||||
svg {
|
||||
display: block;
|
||||
@@ -33,11 +36,11 @@
|
||||
}
|
||||
|
||||
&--align-icon-left {
|
||||
padding-left: base(.125);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&--align-icon-right {
|
||||
padding-right: base(.125);
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&--style-white {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, { ElementType } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Props, RenderedTypeProps } from './types';
|
||||
import { useDraggableSortable } from '../DraggableSortable/useDraggableSortable';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'pill';
|
||||
|
||||
const Pill: React.FC<Props> = ({
|
||||
children,
|
||||
className,
|
||||
to,
|
||||
icon,
|
||||
alignIcon = 'right',
|
||||
onClick,
|
||||
pillStyle = 'light',
|
||||
}) => {
|
||||
const Pill: React.FC<Props> = (props) => {
|
||||
const {
|
||||
id,
|
||||
className,
|
||||
to,
|
||||
icon,
|
||||
alignIcon = 'right',
|
||||
onClick,
|
||||
pillStyle = 'light',
|
||||
draggable,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const { attributes, listeners, transform, setNodeRef, isDragging } = useDraggableSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
`${baseClass}--style-${pillStyle}`,
|
||||
@@ -23,19 +32,29 @@ const Pill: React.FC<Props> = ({
|
||||
(to || onClick) && `${baseClass}--has-action`,
|
||||
icon && `${baseClass}--has-icon`,
|
||||
icon && `${baseClass}--align-icon-${alignIcon}`,
|
||||
draggable && `${baseClass}--draggable`,
|
||||
isDragging && `${baseClass}--is-dragging`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
let RenderedType: string | React.FC<RenderedTypeProps> = 'div';
|
||||
let Element: ElementType | React.FC<RenderedTypeProps> = 'div';
|
||||
|
||||
if (onClick && !to) RenderedType = 'button';
|
||||
if (to) RenderedType = Link;
|
||||
if (onClick && !to) Element = 'button';
|
||||
if (to) Element = Link;
|
||||
|
||||
return (
|
||||
<RenderedType
|
||||
<Element
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
type={RenderedType === 'button' ? 'button' : undefined}
|
||||
type={Element === 'button' ? 'button' : undefined}
|
||||
to={to || undefined}
|
||||
{...draggable ? {
|
||||
...listeners,
|
||||
...attributes,
|
||||
style: {
|
||||
transform,
|
||||
},
|
||||
ref: setNodeRef,
|
||||
} : {}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{(icon && alignIcon === 'left') && (
|
||||
<React.Fragment>
|
||||
@@ -48,7 +67,7 @@ const Pill: React.FC<Props> = ({
|
||||
{icon}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</RenderedType>
|
||||
</Element>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ export type Props = {
|
||||
alignIcon?: 'left' | 'right',
|
||||
onClick?: () => void,
|
||||
pillStyle?: 'white' | 'light' | 'dark' | 'light-gray' | 'warning' | 'success',
|
||||
draggable?: boolean,
|
||||
id?: string
|
||||
}
|
||||
|
||||
export type RenderedTypeProps = {
|
||||
|
||||
@@ -7,9 +7,5 @@
|
||||
border: $style-stroke-width-s solid var(--theme-elevation-800);
|
||||
line-height: calc(#{$baseline} - #{$style-stroke-width-s * 2});
|
||||
margin: base(.25) base(.5) base(.25) 0;
|
||||
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
MultiValueProps,
|
||||
components as SelectComponents,
|
||||
} from 'react-select';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { useDraggableSortable } from '../../DraggableSortable/useDraggableSortable';
|
||||
import { Option as OptionType } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
@@ -26,16 +26,21 @@ export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
|
||||
},
|
||||
} = props;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({
|
||||
id: value as string,
|
||||
});
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className,
|
||||
!isDisabled && 'draggable',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
} = useDraggableSortable({
|
||||
id: value.toString(),
|
||||
});
|
||||
|
||||
return (
|
||||
<SelectComponents.MultiValue
|
||||
{...props}
|
||||
@@ -51,9 +56,7 @@ export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
|
||||
}
|
||||
},
|
||||
style: {
|
||||
...transform ? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
} : {},
|
||||
transform,
|
||||
},
|
||||
}}
|
||||
selectProps={{
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
.react-select-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-select {
|
||||
.rs__control {
|
||||
@include formInput;
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
import React, { useCallback, useId } from 'react';
|
||||
import {
|
||||
DragEndEvent,
|
||||
useDroppable,
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { Props } from './types';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
@@ -26,6 +12,8 @@ import { ValueContainer } from './ValueContainer';
|
||||
import { ClearIndicator } from './ClearIndicator';
|
||||
import { MultiValueRemove } from './MultiValueRemove';
|
||||
import { Control } from './Control';
|
||||
import DraggableSortable from '../DraggableSortable';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const SelectAdapter: React.FC<Props> = (props) => {
|
||||
@@ -45,7 +33,6 @@ const SelectAdapter: React.FC<Props> = (props) => {
|
||||
isLoading,
|
||||
onMenuOpen,
|
||||
components,
|
||||
droppableRef,
|
||||
selectProps,
|
||||
} = props;
|
||||
|
||||
@@ -73,7 +60,6 @@ const SelectAdapter: React.FC<Props> = (props) => {
|
||||
onMenuOpen={onMenuOpen}
|
||||
selectProps={{
|
||||
...selectProps,
|
||||
droppableRef,
|
||||
}}
|
||||
components={{
|
||||
ValueContainer,
|
||||
@@ -96,51 +82,23 @@ const SortableSelect: React.FC<Props> = (props) => {
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const uuid = useId();
|
||||
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: uuid,
|
||||
});
|
||||
|
||||
const onDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) return;
|
||||
|
||||
let sorted = value;
|
||||
|
||||
if (value && Array.isArray(value)) {
|
||||
const oldIndex = value.findIndex((item) => item.value === active.id);
|
||||
const newIndex = value.findIndex((item) => item.value === over.id);
|
||||
sorted = arrayMove(value, oldIndex, newIndex);
|
||||
}
|
||||
|
||||
onChange(sorted);
|
||||
}, [onChange, value]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
let ids: string[] = [];
|
||||
if (value) ids = Array.isArray(value) ? value.map((item) => item?.value as string) : [value?.value as string]; // TODO: fix these types
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
<DraggableSortable
|
||||
ids={ids}
|
||||
className="react-select-container"
|
||||
onDragEnd={({ moveFromIndex, moveToIndex }) => {
|
||||
let sorted = value;
|
||||
if (value && Array.isArray(value)) {
|
||||
sorted = arrayMove(value, moveFromIndex, moveToIndex);
|
||||
}
|
||||
onChange(sorted);
|
||||
}}
|
||||
>
|
||||
<SortableContext items={ids}>
|
||||
<SelectAdapter
|
||||
{...props}
|
||||
droppableRef={setNodeRef}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<SelectAdapter {...props} />
|
||||
</DraggableSortable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ export type OptionGroup = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
droppableRef?: Ref<HTMLElement>
|
||||
className?: string
|
||||
value?: Option | Option[],
|
||||
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,52 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Props } from './types';
|
||||
import { useTableColumns } from '../TableColumns';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'table';
|
||||
|
||||
const Table: React.FC<Props> = ({ columns, data }) => {
|
||||
if (columns && columns.length > 0) {
|
||||
export const Table: React.FC<Props> = ({ data }) => {
|
||||
const {
|
||||
columns,
|
||||
} = useTableColumns();
|
||||
|
||||
const activeColumns = columns.filter((col) => col.active);
|
||||
|
||||
if (!activeColumns || activeColumns.length === 0) {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<table
|
||||
cellPadding="0"
|
||||
cellSpacing="0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((col, i) => (
|
||||
<th
|
||||
key={i}
|
||||
id={`heading-${col.accessor}`}
|
||||
>
|
||||
{col.components.Heading}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data && data.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className={`row-${rowIndex + 1}`}
|
||||
<div>
|
||||
No columns selected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<table
|
||||
cellPadding="0"
|
||||
cellSpacing="0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{activeColumns.map((col, i) => (
|
||||
<th
|
||||
key={i}
|
||||
id={`heading-${col.accessor}`}
|
||||
>
|
||||
{columns.map((col, colIndex) => (
|
||||
{col.components.Heading}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data && data.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className={`row-${rowIndex + 1}`}
|
||||
>
|
||||
{columns.map((col, colIndex) => {
|
||||
const { active } = col;
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`cell-${col.accessor}`}
|
||||
>
|
||||
{col.components.renderCell(row, row[col.accessor])}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { FieldBase } from '../../../../fields/config/types';
|
||||
|
||||
export type Column = {
|
||||
accessor: string,
|
||||
accessor: string
|
||||
label: FieldBase['label']
|
||||
name: FieldBase['name']
|
||||
active: boolean
|
||||
components: {
|
||||
Heading: React.ReactNode,
|
||||
renderCell: (row: any, data: any) => React.ReactNode,
|
||||
Heading: React.ReactNode
|
||||
renderCell: (row: any, data: any) => React.ReactNode
|
||||
},
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
columns: Column[],
|
||||
data: unknown[]
|
||||
}
|
||||
|
||||
91
src/admin/components/elements/TableColumns/buildColumns.tsx
Normal file
91
src/admin/components/elements/TableColumns/buildColumns.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import type { TFunction } from 'react-i18next';
|
||||
import Cell from '../../views/collections/List/Cell';
|
||||
import SortColumn from '../SortColumn';
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
import { Column } from '../Table/types';
|
||||
import { fieldIsPresentationalOnly } from '../../../../fields/config/types';
|
||||
import flattenFields from '../../../../utilities/flattenTopLevelFields';
|
||||
import { Props as CellProps } from '../../views/collections/List/Cell/types';
|
||||
|
||||
const buildColumns = ({
|
||||
collection,
|
||||
columns,
|
||||
t,
|
||||
cellProps,
|
||||
}: {
|
||||
collection: SanitizedCollectionConfig,
|
||||
columns: Pick<Column, 'accessor' | 'active'>[],
|
||||
t: TFunction,
|
||||
cellProps?: Partial<CellProps>[]
|
||||
}): Column[] => {
|
||||
const flattenedFields = flattenFields([
|
||||
...collection.fields,
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'date',
|
||||
label: t('updatedAt'),
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'date',
|
||||
label: t('createdAt'),
|
||||
},
|
||||
]);
|
||||
|
||||
// sort the fields to the order of activeColumns
|
||||
const sortedFields = flattenedFields.sort((a, b) => {
|
||||
const aIndex = columns.findIndex((column) => column.accessor === a.name);
|
||||
const bIndex = columns.findIndex((column) => column.accessor === b.name);
|
||||
if (aIndex === -1 && bIndex === -1) return 0;
|
||||
if (aIndex === -1) return 1;
|
||||
if (bIndex === -1) return -1;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
const firstActiveColumn = sortedFields.find((field) => columns.find((column) => column.accessor === field.name)?.active);
|
||||
|
||||
const cols: Column[] = sortedFields.map((field, colIndex) => {
|
||||
const isActive = columns.find((column) => column.accessor === field.name)?.active || false;
|
||||
const isFirstActive = firstActiveColumn?.name === field.name;
|
||||
|
||||
return {
|
||||
active: isActive,
|
||||
accessor: field.name,
|
||||
name: field.name,
|
||||
label: field.label,
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
label={field.label || field.name}
|
||||
name={field.name}
|
||||
disable={(('disableSort' in field && Boolean(field.disableSort)) || fieldIsPresentationalOnly(field)) || undefined}
|
||||
/>
|
||||
),
|
||||
renderCell: (rowData, cellData) => {
|
||||
return (
|
||||
<Cell
|
||||
key={JSON.stringify(cellData)}
|
||||
field={field}
|
||||
colIndex={colIndex}
|
||||
collection={collection}
|
||||
rowData={rowData}
|
||||
cellData={cellData}
|
||||
link={isFirstActive}
|
||||
{...cellProps?.[colIndex] || {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return cols;
|
||||
};
|
||||
|
||||
export default buildColumns;
|
||||
96
src/admin/components/elements/TableColumns/columnReducer.ts
Normal file
96
src/admin/components/elements/TableColumns/columnReducer.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { TFunction } from 'react-i18next';
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
import { Column } from '../Table/types';
|
||||
import buildColumns from './buildColumns';
|
||||
|
||||
type TOGGLE = {
|
||||
type: 'toggle',
|
||||
payload: {
|
||||
column: string
|
||||
t: TFunction
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
}
|
||||
|
||||
type SET = {
|
||||
type: 'set',
|
||||
payload: {
|
||||
columns: Pick<Column, 'accessor' | 'active'>[]
|
||||
t: TFunction
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
}
|
||||
|
||||
type MOVE = {
|
||||
type: 'move',
|
||||
payload: {
|
||||
fromIndex: number
|
||||
toIndex: number
|
||||
t: TFunction
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
}
|
||||
|
||||
export type Action = TOGGLE | SET | MOVE;
|
||||
|
||||
export const columnReducer = (state: Column[], action: Action): Column[] => {
|
||||
switch (action.type) {
|
||||
case 'toggle': {
|
||||
const {
|
||||
column,
|
||||
t,
|
||||
collection,
|
||||
} = action.payload;
|
||||
|
||||
const withToggledColumn = state.map((col) => {
|
||||
if (col.name === column) {
|
||||
return {
|
||||
...col,
|
||||
active: !col.active,
|
||||
};
|
||||
}
|
||||
|
||||
return col;
|
||||
});
|
||||
|
||||
return buildColumns({
|
||||
columns: withToggledColumn,
|
||||
collection,
|
||||
t,
|
||||
});
|
||||
}
|
||||
case 'move': {
|
||||
const {
|
||||
fromIndex,
|
||||
toIndex,
|
||||
t,
|
||||
collection,
|
||||
} = action.payload;
|
||||
|
||||
const withMovedColumn = [...state];
|
||||
const [columnToMove] = withMovedColumn.splice(fromIndex, 1);
|
||||
withMovedColumn.splice(toIndex, 0, columnToMove);
|
||||
|
||||
return buildColumns({
|
||||
columns: withMovedColumn,
|
||||
collection,
|
||||
t,
|
||||
});
|
||||
}
|
||||
case 'set': {
|
||||
const {
|
||||
columns,
|
||||
t,
|
||||
collection,
|
||||
} = action.payload;
|
||||
|
||||
return buildColumns({
|
||||
columns,
|
||||
collection,
|
||||
t,
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Field, fieldHasSubFields, fieldAffectsData, tabHasName } from '../../../../../fields/config/types';
|
||||
import { Field, fieldHasSubFields, fieldAffectsData, tabHasName } from '../../../../fields/config/types';
|
||||
|
||||
const getRemainingColumns = (fields: Field[], useAsTitle: string): string[] => fields.reduce((remaining, field) => {
|
||||
if (fieldAffectsData(field) && field.name === useAsTitle) {
|
||||
167
src/admin/components/elements/TableColumns/index.tsx
Normal file
167
src/admin/components/elements/TableColumns/index.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useCallback, useEffect, useReducer, createContext, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
import { usePreferences } from '../../utilities/Preferences';
|
||||
import { ListPreferences } from '../../views/collections/List/types';
|
||||
import { Column } from '../Table/types';
|
||||
import buildColumns from './buildColumns';
|
||||
import { Action, columnReducer } from './columnReducer';
|
||||
import getInitialColumnState from './getInitialColumns';
|
||||
import { Props as CellProps } from '../../views/collections/List/Cell/types';
|
||||
|
||||
export interface ITableColumns {
|
||||
columns: Column[]
|
||||
dispatchTableColumns: React.Dispatch<Action>
|
||||
setActiveColumns: (columns: string[]) => void
|
||||
moveColumn:(args: {
|
||||
fromIndex: number
|
||||
toIndex: number
|
||||
}) => void
|
||||
toggleColumn: (column: string) => void
|
||||
}
|
||||
|
||||
export const TableColumnContext = createContext<ITableColumns>({} as ITableColumns);
|
||||
|
||||
export const useTableColumns = (): ITableColumns => useContext(TableColumnContext);
|
||||
|
||||
export const TableColumnsProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
collection: SanitizedCollectionConfig
|
||||
cellProps?: Partial<CellProps>[]
|
||||
}> = ({
|
||||
children,
|
||||
cellProps,
|
||||
collection,
|
||||
collection: {
|
||||
fields,
|
||||
admin: {
|
||||
useAsTitle,
|
||||
defaultColumns,
|
||||
},
|
||||
},
|
||||
}) => {
|
||||
const { t } = useTranslation('general');
|
||||
const preferenceKey = `${collection.slug}-list`;
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
|
||||
const [tableColumns, dispatchTableColumns] = useReducer(columnReducer, {}, () => {
|
||||
const initialColumns = getInitialColumnState(fields, useAsTitle, defaultColumns);
|
||||
return buildColumns({
|
||||
collection,
|
||||
columns: initialColumns.map((column) => ({
|
||||
accessor: column,
|
||||
active: true,
|
||||
})),
|
||||
cellProps,
|
||||
t,
|
||||
});
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Fetch preferences on first load
|
||||
// /////////////////////////////////////
|
||||
|
||||
useEffect(() => {
|
||||
const makeRequest = async () => {
|
||||
const currentPreferences = await getPreference<ListPreferences>(preferenceKey);
|
||||
if (currentPreferences?.columns) {
|
||||
dispatchTableColumns({
|
||||
type: 'set',
|
||||
payload: {
|
||||
columns: currentPreferences.columns.map((column) => {
|
||||
// 'string' is for backwards compatibility
|
||||
// the preference used to be stored as an array of strings
|
||||
if (typeof column === 'string') {
|
||||
return {
|
||||
accessor: column,
|
||||
active: true,
|
||||
};
|
||||
}
|
||||
return column;
|
||||
}),
|
||||
t,
|
||||
collection,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
makeRequest();
|
||||
}, [collection, getPreference, preferenceKey, t]);
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Set preferences on change
|
||||
// /////////////////////////////////////
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const currentPreferences = await getPreference<ListPreferences>(preferenceKey);
|
||||
|
||||
const newPreferences = {
|
||||
...currentPreferences,
|
||||
columns: tableColumns.map((c) => ({
|
||||
accessor: c.accessor,
|
||||
active: c.active,
|
||||
})),
|
||||
};
|
||||
|
||||
setPreference(preferenceKey, newPreferences);
|
||||
})();
|
||||
}, [preferenceKey, setPreference, fields, tableColumns, getPreference]);
|
||||
|
||||
const setActiveColumns = useCallback((columns: string[]) => {
|
||||
dispatchTableColumns({
|
||||
type: 'set',
|
||||
payload: {
|
||||
collection,
|
||||
columns: columns.map((column) => ({
|
||||
accessor: column,
|
||||
active: true,
|
||||
})),
|
||||
t,
|
||||
// onSelect,
|
||||
},
|
||||
});
|
||||
}, [collection, t]);
|
||||
|
||||
const moveColumn = useCallback((args: {
|
||||
fromIndex: number
|
||||
toIndex: number
|
||||
}) => {
|
||||
const { fromIndex, toIndex } = args;
|
||||
|
||||
dispatchTableColumns({
|
||||
type: 'move',
|
||||
payload: {
|
||||
fromIndex,
|
||||
toIndex,
|
||||
collection,
|
||||
t,
|
||||
},
|
||||
});
|
||||
}, [collection, t]);
|
||||
|
||||
const toggleColumn = useCallback((column: string) => {
|
||||
dispatchTableColumns({
|
||||
type: 'toggle',
|
||||
payload: {
|
||||
column,
|
||||
collection,
|
||||
t,
|
||||
},
|
||||
});
|
||||
}, [collection, t]);
|
||||
|
||||
return (
|
||||
<TableColumnContext.Provider
|
||||
value={{
|
||||
columns: tableColumns,
|
||||
dispatchTableColumns,
|
||||
setActiveColumns,
|
||||
moveColumn,
|
||||
toggleColumn,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableColumnContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useCallback, useEffect, useReducer } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../utilities/Auth';
|
||||
import withCondition from '../../withCondition';
|
||||
@@ -27,6 +26,8 @@ import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { createNestedFieldPath } from '../../Form/createNestedFieldPath';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import { NullifyLocaleField } from '../../NullifyField';
|
||||
import DraggableSortable from '../../../elements/DraggableSortable';
|
||||
import DraggableSortableItem from '../../../elements/DraggableSortable/DraggableSortableItem';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -142,13 +143,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
setModified(true);
|
||||
}, [dispatchRows, dispatchFields, path, setModified]);
|
||||
|
||||
const onDragEnd = useCallback((result) => {
|
||||
if (!result.destination) return;
|
||||
const sourceIndex = result.source.index;
|
||||
const destinationIndex = result.destination.index;
|
||||
moveRow(sourceIndex, destinationIndex);
|
||||
}, [moveRow]);
|
||||
|
||||
const setCollapse = useCallback(
|
||||
async (rowID: string, collapsed: boolean) => {
|
||||
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed });
|
||||
@@ -224,161 +218,160 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
if (!rows) return null;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{getTranslation(label || name, i18n)}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('collapseAll')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('showAll')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./gi, '__')}`}
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<NullifyLocaleField
|
||||
path={path}
|
||||
fieldValue={value}
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
|
||||
<Droppable droppableId="array-drop">
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
const rowNumber = i + 1;
|
||||
const fallbackLabel = `${getTranslation(labels.singular, i18n)} ${String(rowNumber).padStart(2, '0')}`;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={row.id}
|
||||
draggableId={row.id}
|
||||
index={i}
|
||||
isDragDisabled={readOnly}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={providedDrag.innerRef}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
header={(
|
||||
<RowLabel
|
||||
path={`${path}.${i}`}
|
||||
label={CustomRowLabel || fallbackLabel}
|
||||
rowNumber={rowNumber}
|
||||
/>
|
||||
)}
|
||||
actions={!readOnly ? (
|
||||
<ArrayAction
|
||||
rowCount={rows.length}
|
||||
duplicateRow={duplicateRow}
|
||||
addRow={addRow}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
index={i}
|
||||
/>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
indexPath={indexPath}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(`${path}.${i}`, field),
|
||||
}))}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{!checkSkipValidation(value) && (
|
||||
<React.Fragment>
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
{t('validation:requiresAtLeast', {
|
||||
count: minRows,
|
||||
label: getTranslation(minRows ? labels.plural : labels.singular, i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'),
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Button
|
||||
onClick={() => addRow(value as number)}
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</DragDropContext>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{getTranslation(label || name, i18n)}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('collapseAll')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('showAll')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./gi, '__')}`}
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<NullifyLocaleField
|
||||
path={path}
|
||||
fieldValue={value}
|
||||
/>
|
||||
|
||||
<DraggableSortable
|
||||
ids={rows.map((row) => row.id)}
|
||||
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
const rowNumber = i + 1;
|
||||
const fallbackLabel = `${getTranslation(labels.singular, i18n)} ${String(rowNumber).padStart(2, '0')}`;
|
||||
|
||||
return (
|
||||
<DraggableSortableItem
|
||||
key={row.id}
|
||||
id={row.id}
|
||||
disabled={readOnly}
|
||||
>
|
||||
{({ setNodeRef, transform, attributes, listeners }) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
key={`${path}-row-${i}`}
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
transform,
|
||||
}}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={{
|
||||
id: row.id,
|
||||
attributes,
|
||||
listeners,
|
||||
}}
|
||||
header={(
|
||||
<RowLabel
|
||||
path={`${path}.${i}`}
|
||||
label={CustomRowLabel || fallbackLabel}
|
||||
rowNumber={rowNumber}
|
||||
/>
|
||||
)}
|
||||
actions={!readOnly ? (
|
||||
<ArrayAction
|
||||
rowCount={rows.length}
|
||||
duplicateRow={duplicateRow}
|
||||
addRow={addRow}
|
||||
moveRow={moveRow}
|
||||
removeRow={removeRow}
|
||||
index={i}
|
||||
/>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
indexPath={indexPath}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(`${path}.${i}`, field),
|
||||
}))}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</DraggableSortableItem>
|
||||
);
|
||||
})}
|
||||
{!checkSkipValidation(value) && (
|
||||
<React.Fragment>
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
{t('validation:requiresAtLeast', {
|
||||
count: minRows,
|
||||
label: getTranslation(minRows ? labels.plural : labels.singular, i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'),
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DraggableSortable>
|
||||
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<div className={`${baseClass}__add-button-wrap`}>
|
||||
<Button
|
||||
onClick={() => addRow(value as number)}
|
||||
buttonStyle="icon-label"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { Fragment, useCallback, useEffect, useReducer } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../utilities/Auth';
|
||||
import { usePreferences } from '../../../utilities/Preferences';
|
||||
@@ -27,10 +26,12 @@ import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { NullifyLocaleField } from '../../NullifyField';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import { createNestedFieldPath } from '../../Form/createNestedFieldPath';
|
||||
import { DrawerToggler } from '../../../elements/Drawer';
|
||||
import DraggableSortable from '../../../elements/DraggableSortable';
|
||||
import DraggableSortableItem from '../../../elements/DraggableSortable/DraggableSortableItem';
|
||||
import { useDrawerSlug } from '../../../elements/Drawer/useDrawerSlug';
|
||||
import Button from '../../../elements/Button';
|
||||
import { RowActions } from './RowActions';
|
||||
import { DrawerToggler } from '../../../elements/Drawer';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -141,13 +142,6 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
setModified(true);
|
||||
}, [dispatchRows, dispatchFields, path, setModified]);
|
||||
|
||||
const onDragEnd = useCallback((result) => {
|
||||
if (!result.destination) return;
|
||||
const sourceIndex = result.source.index;
|
||||
const destinationIndex = result.destination.index;
|
||||
moveRow(sourceIndex, destinationIndex);
|
||||
}, [moveRow]);
|
||||
|
||||
const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => {
|
||||
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed });
|
||||
|
||||
@@ -221,190 +215,187 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
if (!rows) return null;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{getTranslation(label || name, i18n)}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('collapseAll')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('showAll')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
<NullifyLocaleField
|
||||
path={path}
|
||||
fieldValue={value}
|
||||
<div
|
||||
id={`field-${path.replace(/\./gi, '__')}`}
|
||||
className={classes}
|
||||
>
|
||||
<div className={`${baseClass}__error-wrap`}>
|
||||
<Error
|
||||
showError={showError}
|
||||
message={errorMessage}
|
||||
/>
|
||||
<Droppable
|
||||
droppableId="blocks-drop"
|
||||
isDropDisabled={readOnly}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
const { blockType } = row;
|
||||
const blockToRender = blocks.find((block) => block.slug === blockType);
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{getTranslation(label || name, i18n)}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('collapseAll')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
{t('showAll')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
</header>
|
||||
|
||||
const rowNumber = i + 1;
|
||||
<NullifyLocaleField
|
||||
path={path}
|
||||
fieldValue={value}
|
||||
/>
|
||||
|
||||
if (blockToRender) {
|
||||
return (
|
||||
<Draggable
|
||||
<DraggableSortable
|
||||
ids={rows.map((row) => row.id)}
|
||||
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
|
||||
>
|
||||
{rows.length > 0 && rows.map((row, i) => {
|
||||
const { blockType } = row;
|
||||
const blockToRender = blocks.find((block) => block.slug === blockType);
|
||||
|
||||
const rowNumber = i + 1;
|
||||
|
||||
if (blockToRender) {
|
||||
return (
|
||||
<DraggableSortableItem
|
||||
key={row.id}
|
||||
id={row.id}
|
||||
disabled={readOnly}
|
||||
>
|
||||
{({ setNodeRef, transform, attributes, listeners }) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
transform,
|
||||
}}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
draggableId={row.id}
|
||||
index={i}
|
||||
isDragDisabled={readOnly}
|
||||
>
|
||||
{(providedDrag) => (
|
||||
<div
|
||||
id={`${path}-row-${i}`}
|
||||
ref={providedDrag.innerRef}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<Collapsible
|
||||
collapsed={row.collapsed}
|
||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||
className={`${baseClass}__row`}
|
||||
key={row.id}
|
||||
dragHandleProps={providedDrag.dragHandleProps}
|
||||
header={(
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{rowNumber >= 10 ? rowNumber : `0${rowNumber}`}
|
||||
</span>
|
||||
<Pill
|
||||
pillStyle="white"
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}
|
||||
>
|
||||
{getTranslation(blockToRender.labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle
|
||||
path={`${path}.${i}.blockName`}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
actions={!readOnly ? (
|
||||
<RowActions
|
||||
addRow={addRow}
|
||||
removeRow={removeRow}
|
||||
duplicateRow={duplicateRow}
|
||||
moveRow={moveRow}
|
||||
rows={rows}
|
||||
blockType={blockType}
|
||||
blocks={blocks}
|
||||
labels={labels}
|
||||
rowIndex={i}
|
||||
/>
|
||||
) : undefined}
|
||||
dragHandleProps={{
|
||||
id: row.id,
|
||||
attributes,
|
||||
listeners,
|
||||
}}
|
||||
header={(
|
||||
<div className={`${baseClass}__block-header`}>
|
||||
<span className={`${baseClass}__block-number`}>
|
||||
{rowNumber >= 10 ? rowNumber : `0${rowNumber}`}
|
||||
</span>
|
||||
<Pill
|
||||
pillStyle="white"
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
fieldSchema={blockToRender.fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(`${path}.${i}`, field),
|
||||
}))}
|
||||
indexPath={indexPath}
|
||||
/>
|
||||
|
||||
</Collapsible>
|
||||
{getTranslation(blockToRender.labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle
|
||||
path={`${path}.${i}.blockName`}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
actions={!readOnly ? (
|
||||
<RowActions
|
||||
addRow={addRow}
|
||||
removeRow={removeRow}
|
||||
duplicateRow={duplicateRow}
|
||||
moveRow={moveRow}
|
||||
rows={rows}
|
||||
blockType={blockType}
|
||||
blocks={blocks}
|
||||
labels={labels}
|
||||
rowIndex={i}
|
||||
/>
|
||||
) : undefined}
|
||||
>
|
||||
<HiddenInput
|
||||
name={`${path}.${i}.id`}
|
||||
value={row.id}
|
||||
/>
|
||||
<RenderFields
|
||||
className={`${baseClass}__fields`}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
fieldTypes={fieldTypes}
|
||||
permissions={permissions?.fields}
|
||||
fieldSchema={blockToRender.fields.map((field) => ({
|
||||
...field,
|
||||
path: createNestedFieldPath(`${path}.${i}`, field),
|
||||
}))}
|
||||
indexPath={indexPath}
|
||||
/>
|
||||
|
||||
return null;
|
||||
})}
|
||||
{!checkSkipValidation(value) && (
|
||||
<React.Fragment>
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
{t('validation:requiresAtLeast', {
|
||||
count: minRows,
|
||||
label: getTranslation(minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural, i18n),
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<Fragment>
|
||||
<DrawerToggler
|
||||
slug={drawerSlug}
|
||||
className={`${baseClass}__drawer-toggler`}
|
||||
>
|
||||
<Button
|
||||
el="span"
|
||||
icon="plus"
|
||||
buttonStyle="icon-label"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
>
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
</DrawerToggler>
|
||||
<BlocksDrawer
|
||||
drawerSlug={drawerSlug}
|
||||
blocks={blocks}
|
||||
addRow={addRow}
|
||||
addRowIndex={value}
|
||||
labels={labels}
|
||||
/>
|
||||
</Fragment>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</DraggableSortableItem>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{!checkSkipValidation(value) && (
|
||||
<React.Fragment>
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
{t('validation:requiresAtLeast', {
|
||||
count: minRows,
|
||||
label: getTranslation(minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural, i18n),
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</DraggableSortable>
|
||||
{(!readOnly && !hasMaxRows) && (
|
||||
<Fragment>
|
||||
<DrawerToggler
|
||||
slug={drawerSlug}
|
||||
className={`${baseClass}__drawer-toggler`}
|
||||
>
|
||||
<Button
|
||||
el="span"
|
||||
icon="plus"
|
||||
buttonStyle="icon-label"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
>
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
</DrawerToggler>
|
||||
<BlocksDrawer
|
||||
drawerSlug={drawerSlug}
|
||||
blocks={blocks}
|
||||
addRow={addRow}
|
||||
addRowIndex={value}
|
||||
labels={labels}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch
|
||||
}, [user]);
|
||||
|
||||
const getPreference = useCallback(async <T = any>(key: string): Promise<T> => {
|
||||
if (typeof preferencesRef.current[key] !== 'undefined') return preferencesRef.current[key];
|
||||
const prefs = preferencesRef.current;
|
||||
if (typeof prefs[key] !== 'undefined') return prefs[key];
|
||||
const promise = new Promise((resolve: (value: T) => void) => {
|
||||
(async () => {
|
||||
const request = await requests.get(`${serverURL}${api}/_preferences/${key}`, {
|
||||
@@ -53,7 +54,7 @@ export const PreferencesProvider: React.FC<{children?: React.ReactNode}> = ({ ch
|
||||
resolve(value);
|
||||
})();
|
||||
});
|
||||
preferencesRef.current[key] = promise;
|
||||
prefs[key] = promise;
|
||||
return promise;
|
||||
}, [i18n.language, api, preferencesRef, serverURL]);
|
||||
|
||||
|
||||
@@ -40,9 +40,16 @@ const TextCell: React.FC<{children?: React.ReactNode}> = ({ children }) => (
|
||||
</span>
|
||||
);
|
||||
|
||||
export const getColumns = (collection: SanitizedCollectionConfig, global: SanitizedGlobalConfig, t: TFunction): Column[] => [
|
||||
export const buildColumns = (
|
||||
collection: SanitizedCollectionConfig,
|
||||
global: SanitizedGlobalConfig,
|
||||
t: TFunction,
|
||||
): Column[] => [
|
||||
{
|
||||
accessor: 'updatedAt',
|
||||
active: true,
|
||||
label: '',
|
||||
name: '',
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
@@ -62,6 +69,9 @@ export const getColumns = (collection: SanitizedCollectionConfig, global: Saniti
|
||||
},
|
||||
{
|
||||
accessor: 'id',
|
||||
active: true,
|
||||
label: '',
|
||||
name: '',
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
@@ -75,6 +85,9 @@ export const getColumns = (collection: SanitizedCollectionConfig, global: Saniti
|
||||
},
|
||||
{
|
||||
accessor: 'autosave',
|
||||
active: true,
|
||||
label: '',
|
||||
name: '',
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
|
||||
@@ -10,8 +10,7 @@ import { StepNavItem } from '../../elements/StepNav/types';
|
||||
import Meta from '../../utilities/Meta';
|
||||
import { Props } from './types';
|
||||
import IDLabel from '../../elements/IDLabel';
|
||||
import { getColumns } from './columns';
|
||||
import Table from '../../elements/Table';
|
||||
import { Table } from '../../elements/Table';
|
||||
import Paginator from '../../elements/Paginator';
|
||||
import PerPage from '../../elements/PerPage';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
@@ -27,7 +26,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
const { setStepNav } = useStepNav();
|
||||
const { params: { id } } = useRouteMatch<{ id: string }>();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
const [tableColumns] = useState(() => getColumns(collection, global, t));
|
||||
const [fetchURL, setFetchURL] = useState('');
|
||||
const { page, sort, limit } = useSearchParams();
|
||||
|
||||
@@ -183,10 +181,7 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
|
||||
{versionsData?.totalDocs > 0 && (
|
||||
<React.Fragment>
|
||||
<Table
|
||||
data={versionsData?.docs}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
<Table data={versionsData?.docs} />
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Paginator
|
||||
limit={versionsData.limit}
|
||||
|
||||
@@ -52,7 +52,9 @@ const EditView: React.FC<IndexProps> = (props) => {
|
||||
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
||||
);
|
||||
|
||||
const onSave = useCallback(async (json: any) => {
|
||||
const onSave = useCallback(async (json: {
|
||||
doc
|
||||
}) => {
|
||||
getVersions();
|
||||
getDocPermissions();
|
||||
setUpdatedAt(json?.doc?.updatedAt);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Field } from '../../../../../../fields/config/types';
|
||||
import { FieldAffectingData, UIField } from '../../../../../../fields/config/types';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
|
||||
export type Props = {
|
||||
field: Field
|
||||
field: UIField | FieldAffectingData
|
||||
colIndex: number
|
||||
collection: SanitizedCollectionConfig
|
||||
cellData: unknown
|
||||
|
||||
@@ -8,7 +8,7 @@ import Paginator from '../../../elements/Paginator';
|
||||
import ListControls from '../../../elements/ListControls';
|
||||
import Pill from '../../../elements/Pill';
|
||||
import Button from '../../../elements/Button';
|
||||
import Table from '../../../elements/Table';
|
||||
import { Table } from '../../../elements/Table';
|
||||
import Meta from '../../../utilities/Meta';
|
||||
import { Props } from './types';
|
||||
import ViewDescription from '../../../elements/ViewDescription';
|
||||
@@ -39,9 +39,6 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
data,
|
||||
newDocumentURL,
|
||||
limit,
|
||||
tableColumns,
|
||||
columnNames,
|
||||
setColumns,
|
||||
hasCreatePermission,
|
||||
disableEyebrow,
|
||||
modifySearchParams,
|
||||
@@ -89,15 +86,12 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
</header>
|
||||
<ListControls
|
||||
collection={collection}
|
||||
columns={columnNames}
|
||||
setColumns={setColumns}
|
||||
enableColumns={Boolean(!upload)}
|
||||
enableSort={Boolean(upload)}
|
||||
modifySearchQuery={modifySearchParams}
|
||||
handleSortChange={handleSortChange}
|
||||
handleWhereChange={handleWhereChange}
|
||||
/>
|
||||
|
||||
{!data.docs && (
|
||||
<StaggeredShimmers
|
||||
className={[
|
||||
@@ -108,15 +102,11 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
width={upload ? 'unset' : '100%'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(data.docs && data.docs.length > 0) && (
|
||||
<React.Fragment>
|
||||
{!upload && (
|
||||
<RelationshipProvider>
|
||||
<Table
|
||||
data={data.docs}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
<Table data={data.docs} />
|
||||
</RelationshipProvider>
|
||||
)}
|
||||
{upload && (
|
||||
|
||||
@@ -15,7 +15,7 @@ const buildColumns = ({
|
||||
cellProps,
|
||||
}: {
|
||||
collection: SanitizedCollectionConfig,
|
||||
columns: string[],
|
||||
columns: Pick<Column, 'accessor' | 'active'>[],
|
||||
t: TFunction,
|
||||
cellProps?: Partial<CellProps>[]
|
||||
}): Column[] => {
|
||||
@@ -36,51 +36,56 @@ const buildColumns = ({
|
||||
type: 'date',
|
||||
label: t('createdAt'),
|
||||
},
|
||||
], true);
|
||||
]);
|
||||
|
||||
return (columns || []).reduce((cols, col, colIndex) => {
|
||||
let field = null;
|
||||
// sort the fields to the order of activeColumns
|
||||
const sortedFields = flattenedFields.sort((a, b) => {
|
||||
const aIndex = columns.findIndex((column) => column.accessor === a.name);
|
||||
const bIndex = columns.findIndex((column) => column.accessor === b.name);
|
||||
if (aIndex === -1 && bIndex === -1) return 0;
|
||||
if (aIndex === -1) return 1;
|
||||
if (bIndex === -1) return -1;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
|
||||
flattenedFields.forEach((fieldToCheck) => {
|
||||
if (fieldToCheck.name === col) {
|
||||
field = fieldToCheck;
|
||||
}
|
||||
});
|
||||
const firstActiveColumn = sortedFields.find((field) => columns.find((column) => column.accessor === field.name)?.active);
|
||||
|
||||
if (field) {
|
||||
return [
|
||||
...cols,
|
||||
{
|
||||
accessor: field.name,
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
label={field.label || field.name}
|
||||
name={field.name}
|
||||
disable={(field.disableSort || fieldIsPresentationalOnly(field)) || undefined}
|
||||
/>
|
||||
),
|
||||
renderCell: (rowData, cellData) => {
|
||||
return (
|
||||
<Cell
|
||||
key={JSON.stringify(cellData)}
|
||||
field={field}
|
||||
colIndex={colIndex}
|
||||
collection={collection}
|
||||
rowData={rowData}
|
||||
cellData={cellData}
|
||||
link={colIndex === 0}
|
||||
{...cellProps?.[colIndex] || {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
const cols: Column[] = sortedFields.map((field, colIndex) => {
|
||||
const isActive = columns.find((column) => column.accessor === field.name)?.active || false;
|
||||
const isFirstActive = firstActiveColumn?.name === field.name;
|
||||
|
||||
return {
|
||||
active: isActive,
|
||||
accessor: field.name,
|
||||
name: field.name,
|
||||
label: field.label,
|
||||
components: {
|
||||
Heading: (
|
||||
<SortColumn
|
||||
label={field.label || field.name}
|
||||
name={field.name}
|
||||
disable={(('disableSort' in field && Boolean(field.disableSort)) || fieldIsPresentationalOnly(field)) || undefined}
|
||||
/>
|
||||
),
|
||||
renderCell: (rowData, cellData) => {
|
||||
return (
|
||||
<Cell
|
||||
key={JSON.stringify(cellData)}
|
||||
field={field}
|
||||
colIndex={colIndex}
|
||||
collection={collection}
|
||||
rowData={rowData}
|
||||
cellData={cellData}
|
||||
link={isFirstActive}
|
||||
{...cellProps?.[colIndex] || {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return cols;
|
||||
}, []);
|
||||
return cols;
|
||||
};
|
||||
|
||||
export default buildColumns;
|
||||
|
||||
96
src/admin/components/views/collections/List/columnReducer.ts
Normal file
96
src/admin/components/views/collections/List/columnReducer.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { TFunction } from 'react-i18next';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { Column } from '../../../elements/Table/types';
|
||||
import buildColumns from './buildColumns';
|
||||
|
||||
type TOGGLE = {
|
||||
type: 'toggle',
|
||||
payload: {
|
||||
column: string
|
||||
t: TFunction
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
}
|
||||
|
||||
type SET = {
|
||||
type: 'set',
|
||||
payload: {
|
||||
columns: Pick<Column, 'accessor' | 'active'>[]
|
||||
t: TFunction
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
}
|
||||
|
||||
type MOVE = {
|
||||
type: 'move',
|
||||
payload: {
|
||||
fromIndex: number
|
||||
toIndex: number
|
||||
t: TFunction
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
}
|
||||
|
||||
export type Action = TOGGLE | SET | MOVE;
|
||||
|
||||
export const columnReducer = (state: Column[], action: Action): Column[] => {
|
||||
switch (action.type) {
|
||||
case 'toggle': {
|
||||
const {
|
||||
column,
|
||||
t,
|
||||
collection,
|
||||
} = action.payload;
|
||||
|
||||
const withToggledColumn = state.map((col) => {
|
||||
if (col.name === column) {
|
||||
return {
|
||||
...col,
|
||||
active: !col.active,
|
||||
};
|
||||
}
|
||||
|
||||
return col;
|
||||
});
|
||||
|
||||
return buildColumns({
|
||||
columns: withToggledColumn,
|
||||
collection,
|
||||
t,
|
||||
});
|
||||
}
|
||||
case 'move': {
|
||||
const {
|
||||
fromIndex,
|
||||
toIndex,
|
||||
t,
|
||||
collection,
|
||||
} = action.payload;
|
||||
|
||||
const withMovedColumn = [...state];
|
||||
const [columnToMove] = withMovedColumn.splice(fromIndex, 1);
|
||||
withMovedColumn.splice(toIndex, 0, columnToMove);
|
||||
|
||||
return buildColumns({
|
||||
columns: withMovedColumn,
|
||||
collection,
|
||||
t,
|
||||
});
|
||||
}
|
||||
case 'set': {
|
||||
const {
|
||||
columns,
|
||||
t,
|
||||
collection,
|
||||
} = action.payload;
|
||||
|
||||
return buildColumns({
|
||||
columns,
|
||||
collection,
|
||||
t,
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import queryString from 'qs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -9,13 +9,10 @@ import DefaultList from './Default';
|
||||
import RenderCustomComponent from '../../../utilities/RenderCustomComponent';
|
||||
import { useStepNav } from '../../../elements/StepNav';
|
||||
import formatFields from './formatFields';
|
||||
import buildColumns from './buildColumns';
|
||||
import { ListIndexProps, ListPreferences } from './types';
|
||||
import { usePreferences } from '../../../utilities/Preferences';
|
||||
import { useSearchParams } from '../../../utilities/SearchParams';
|
||||
import { Column } from '../../../elements/Table/types';
|
||||
import { Field } from '../../../../../fields/config/types';
|
||||
import getInitialColumns from './getInitialColumns';
|
||||
|
||||
const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
const {
|
||||
@@ -26,8 +23,6 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
plural,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle,
|
||||
defaultColumns,
|
||||
pagination: {
|
||||
defaultLimit,
|
||||
},
|
||||
@@ -48,22 +43,13 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
const { page, sort, limit, where } = useSearchParams();
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
const [fetchURL, setFetchURL] = useState<string>('');
|
||||
const [fields] = useState<Field[]>(() => formatFields(collection, t));
|
||||
const [tableColumns, setTableColumns] = useState<Column[]>(() => {
|
||||
const initialColumns = getInitialColumns(fields, useAsTitle, defaultColumns);
|
||||
return buildColumns({ collection, columns: initialColumns, t });
|
||||
});
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug];
|
||||
const hasCreatePermission = collectionPermissions?.create?.permission;
|
||||
const newDocumentURL = `${admin}/collections/${slug}/create`;
|
||||
const [{ data }, { setParams: setFetchParams }] = usePayloadAPI(fetchURL, { initialParams: { page: 1 } });
|
||||
|
||||
const activeColumnNames = tableColumns.map(({ accessor }) => accessor);
|
||||
const stringifiedActiveColumns = JSON.stringify(activeColumnNames);
|
||||
|
||||
useEffect(() => {
|
||||
setStepNav([
|
||||
{
|
||||
@@ -96,7 +82,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
setFetchURL(`${serverURL}${api}/${slug}`);
|
||||
|
||||
setFetchParams(params);
|
||||
}, [setFetchParams, page, sort, where, collection, getPreference, limit, serverURL, api, slug]);
|
||||
}, [setFetchParams, page, sort, where, collection, limit, serverURL, api, slug]);
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Fetch preferences on first load
|
||||
@@ -105,9 +91,6 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const currentPreferences = await getPreference<ListPreferences>(preferenceKey);
|
||||
if (currentPreferences?.columns) {
|
||||
setTableColumns(buildColumns({ collection, columns: currentPreferences?.columns, t }));
|
||||
}
|
||||
|
||||
const params = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 0 });
|
||||
|
||||
@@ -128,23 +111,22 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
}, [collection, getPreference, preferenceKey, history, t, defaultLimit]);
|
||||
|
||||
// /////////////////////////////////////
|
||||
// When any preference-enabled values are updated,
|
||||
// Set preferences
|
||||
// Set preferences on change
|
||||
// /////////////////////////////////////
|
||||
|
||||
useEffect(() => {
|
||||
const newPreferences = {
|
||||
limit,
|
||||
sort,
|
||||
columns: JSON.parse(stringifiedActiveColumns),
|
||||
};
|
||||
(async () => {
|
||||
const currentPreferences = await getPreference<ListPreferences>(preferenceKey);
|
||||
|
||||
setPreference(preferenceKey, newPreferences);
|
||||
}, [sort, limit, stringifiedActiveColumns, preferenceKey, setPreference]);
|
||||
const newPreferences = {
|
||||
...currentPreferences,
|
||||
limit,
|
||||
sort,
|
||||
};
|
||||
|
||||
const setActiveColumns = useCallback((columns: string[]) => {
|
||||
setTableColumns(buildColumns({ collection, columns, t }));
|
||||
}, [collection, t]);
|
||||
setPreference(preferenceKey, newPreferences);
|
||||
})();
|
||||
}, [sort, limit, preferenceKey, setPreference, getPreference]);
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
@@ -155,9 +137,6 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
newDocumentURL,
|
||||
hasCreatePermission,
|
||||
data,
|
||||
tableColumns,
|
||||
columnNames: activeColumnNames,
|
||||
setColumns: setActiveColumns,
|
||||
limit: limit || defaultLimit,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { PaginatedDocs } from '../../../../../mongoose/types';
|
||||
import { Column } from '../../../elements/Table/types';
|
||||
import { Props as ListControlsProps } from '../../../elements/ListControls/types';
|
||||
import { Props as PerPageProps } from '../../../elements/PerPage';
|
||||
import { Props as PaginatorProps } from '../../../elements/Paginator/types';
|
||||
@@ -11,9 +10,7 @@ export type Props = {
|
||||
newDocumentURL: string
|
||||
setListControls: (controls: unknown) => void
|
||||
setSort: (sort: string) => void
|
||||
tableColumns: Column[]
|
||||
columnNames: string[]
|
||||
setColumns: (columns: string[]) => void
|
||||
toggleColumn: (column: string) => void
|
||||
hasCreatePermission: boolean
|
||||
setLimit: (limit: number) => void
|
||||
limit: number
|
||||
@@ -34,7 +31,10 @@ export type ListIndexProps = {
|
||||
}
|
||||
|
||||
export type ListPreferences = {
|
||||
columns: string[]
|
||||
columns: {
|
||||
accessor: string
|
||||
active: boolean
|
||||
}[]
|
||||
limit: number
|
||||
sort: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user