roadmap: rte and upload drawers (#1668)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useId, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
|
||||
import Pill from '../Pill';
|
||||
@@ -6,7 +6,7 @@ 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 './index.scss';
|
||||
|
||||
const baseClass = 'column-selector';
|
||||
@@ -18,8 +18,15 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
setColumns,
|
||||
} = props;
|
||||
|
||||
const [fields] = useState(() => flattenTopLevelFields(collection.fields, true));
|
||||
const [fields, setFields] = useState(() => flattenTopLevelFields(collection.fields, true));
|
||||
|
||||
useEffect(() => {
|
||||
setFields(flattenTopLevelFields(collection.fields, true));
|
||||
}, [collection.fields]);
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const uuid = useId();
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -38,7 +45,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
setColumns(newState);
|
||||
}}
|
||||
alignIcon="left"
|
||||
key={field.name || i}
|
||||
key={`${field.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`}
|
||||
icon={isEnabled ? <X /> : <Plus />}
|
||||
className={[
|
||||
`${baseClass}__column`,
|
||||
|
||||
140
src/admin/components/elements/DocumentDrawer/DrawerContent.tsx
Normal file
140
src/admin/components/elements/DocumentDrawer/DrawerContent.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify';
|
||||
import { DocumentDrawerProps } from './types';
|
||||
import DefaultEdit from '../../views/collections/Edit/Default';
|
||||
import X from '../../icons/X';
|
||||
import { Fields } from '../../forms/Form/types';
|
||||
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import Button from '../Button';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { DocumentInfoProvider } from '../../utilities/DocumentInfo';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import formatFields from '../../views/collections/Edit/formatFields';
|
||||
import { useRelatedCollections } from '../../forms/field-types/Relationship/AddNew/useRelatedCollections';
|
||||
import IDLabel from '../IDLabel';
|
||||
import { baseClass } from '.';
|
||||
|
||||
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
||||
collectionSlug,
|
||||
id,
|
||||
drawerSlug,
|
||||
onSave: onSaveFromProps,
|
||||
customHeader,
|
||||
}) => {
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal, modalState, closeModal } = useModal();
|
||||
const locale = useLocale();
|
||||
const { permissions, user } = useAuth();
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
const { t, i18n } = useTranslation(['fields', 'general']);
|
||||
const hasInitializedState = useRef(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [collectionConfig] = useRelatedCollections(collectionSlug);
|
||||
|
||||
const [fields, setFields] = useState(() => formatFields(collectionConfig, true));
|
||||
|
||||
useEffect(() => {
|
||||
setFields(formatFields(collectionConfig, true));
|
||||
}, [collectionSlug, collectionConfig]);
|
||||
|
||||
const [{ data, isLoading: isLoadingDocument, isError }] = usePayloadAPI(
|
||||
(id ? `${serverURL}${api}/${collectionSlug}/${id}` : null),
|
||||
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema({
|
||||
fieldSchema: fields,
|
||||
data,
|
||||
user,
|
||||
operation: id ? 'update' : 'create',
|
||||
id,
|
||||
locale,
|
||||
t,
|
||||
});
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
hasInitializedState.current = true;
|
||||
}, [data, fields, id, user, locale, isLoadingDocument, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen));
|
||||
}, [modalState, drawerSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !isLoadingDocument && isError) {
|
||||
closeModal(drawerSlug);
|
||||
toast.error(data.errors?.[0].message || t('error:unspecific'));
|
||||
}
|
||||
}, [isError, t, isOpen, data, drawerSlug, closeModal, isLoadingDocument]);
|
||||
|
||||
const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => {
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
onSaveFromProps({
|
||||
...args,
|
||||
collectionConfig,
|
||||
});
|
||||
}
|
||||
}, [collectionConfig, onSaveFromProps]);
|
||||
|
||||
if (isError) return null;
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider collection={collectionConfig}>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={collectionConfig.admin?.components?.views?.Edit}
|
||||
componentProps={{
|
||||
isLoading: !initialState,
|
||||
data,
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
permissions: permissions.collections[collectionConfig.slug],
|
||||
isEditing: Boolean(id),
|
||||
apiURL: id ? `${serverURL}${api}/${collectionSlug}/${id}` : null,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission: true,
|
||||
action: `${serverURL}${api}/${collectionSlug}${id ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`,
|
||||
disableEyebrow: true,
|
||||
disableActions: true,
|
||||
me: true,
|
||||
disableLeaveWithoutSaving: true,
|
||||
customHeader: (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-content`}>
|
||||
<h2 className={`${baseClass}__header-text`}>
|
||||
{!customHeader ? t(!id ? 'fields:addNewLabel' : 'general:editLabel', { label: getTranslation(collectionConfig.labels.singular, i18n) }) : customHeader}
|
||||
</h2>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
className={`${baseClass}__header-close`}
|
||||
onClick={() => toggleModal(drawerSlug)}
|
||||
aria-label={t('general:close')}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
{id && (
|
||||
<IDLabel id={id} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
};
|
||||
@@ -2,34 +2,60 @@
|
||||
|
||||
.doc-drawer {
|
||||
&__header {
|
||||
margin-top: base(2.5);
|
||||
margin-bottom: base(1);
|
||||
|
||||
@include mid-break {
|
||||
margin-top: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: base(2.5);
|
||||
margin-bottom: base(1);
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
svg {
|
||||
width: base(2.5);
|
||||
height: base(2.5);
|
||||
position: relative;
|
||||
top: base(-.5);
|
||||
right: base(-.75);
|
||||
&__header-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stroke {
|
||||
stroke-width: .5px;
|
||||
}
|
||||
&__toggler {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__header-close {
|
||||
svg {
|
||||
top: base(-.75);
|
||||
&__header-close {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
|
||||
svg {
|
||||
width: base(2.75);
|
||||
height: base(2.75);
|
||||
position: relative;
|
||||
left: base(-.825);
|
||||
top: base(-.825);
|
||||
|
||||
.stroke {
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify';
|
||||
import { DocumentDrawerProps, DocumentTogglerProps, UseDocumentDrawer } from './types';
|
||||
import DefaultEdit from '../../views/collections/Edit/Default';
|
||||
import X from '../../icons/X';
|
||||
import { Fields } from '../../forms/Form/types';
|
||||
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { Drawer, DrawerToggler } from '../Drawer';
|
||||
import Button from '../Button';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { DocumentInfoProvider } from '../../utilities/DocumentInfo';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import formatFields from '../../views/collections/Edit/formatFields';
|
||||
import { useRelatedCollections } from '../../forms/field-types/Relationship/AddNew/useRelatedCollections';
|
||||
import IDLabel from '../IDLabel';
|
||||
import { useEditDepth } from '../../utilities/EditDepth';
|
||||
import { DocumentDrawerContent } from './DrawerContent';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'doc-drawer';
|
||||
export const baseClass = 'doc-drawer';
|
||||
|
||||
const formatDocumentDrawerSlug = ({
|
||||
collectionSlug,
|
||||
@@ -43,6 +30,7 @@ export const DocumentDrawerToggler: React.FC<DocumentTogglerProps> = ({
|
||||
drawerSlug,
|
||||
id,
|
||||
collectionSlug,
|
||||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation(['fields', 'general']);
|
||||
@@ -52,7 +40,11 @@ export const DocumentDrawerToggler: React.FC<DocumentTogglerProps> = ({
|
||||
<DrawerToggler
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={className}
|
||||
className={[
|
||||
className,
|
||||
`${baseClass}__toggler`,
|
||||
].filter(Boolean).join(' ')}
|
||||
disabled={disabled}
|
||||
aria-label={t(!id ? 'fields:addNewLabel' : 'general:editLabel', { label: getTranslation(collectionConfig.labels.singular, i18n) })}
|
||||
{...rest}
|
||||
>
|
||||
@@ -61,138 +53,24 @@ export const DocumentDrawerToggler: React.FC<DocumentTogglerProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentDrawer: React.FC<DocumentDrawerProps> = ({
|
||||
collectionSlug,
|
||||
id,
|
||||
drawerSlug,
|
||||
onSave: onSaveFromProps,
|
||||
customHeader,
|
||||
}) => {
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal, modalState, closeModal } = useModal();
|
||||
const locale = useLocale();
|
||||
const { permissions, user } = useAuth();
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
const { t, i18n } = useTranslation(['fields', 'general']);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [collectionConfig] = useRelatedCollections(collectionSlug);
|
||||
export const DocumentDrawer: React.FC<DocumentDrawerProps> = (props) => {
|
||||
const { drawerSlug } = props;
|
||||
|
||||
const [fields, setFields] = useState(() => formatFields(collectionConfig, true));
|
||||
|
||||
useEffect(() => {
|
||||
setFields(formatFields(collectionConfig, true));
|
||||
}, [collectionSlug, collectionConfig]);
|
||||
|
||||
const [{ data, isLoading: isLoadingDocument, isError }] = usePayloadAPI(
|
||||
(id ? `${serverURL}${api}/${collectionSlug}/${id}` : null),
|
||||
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
|
||||
return (
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={baseClass}
|
||||
>
|
||||
<DocumentDrawerContent {...props} />
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema({
|
||||
fieldSchema: fields,
|
||||
data,
|
||||
user,
|
||||
operation: id ? 'update' : 'create',
|
||||
id,
|
||||
locale,
|
||||
t,
|
||||
});
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [data, fields, id, user, locale, isLoadingDocument, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen));
|
||||
}, [modalState, drawerSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !isLoadingDocument && isError) {
|
||||
closeModal(drawerSlug);
|
||||
toast.error(data.errors?.[0].message || t('error:unspecific'));
|
||||
}
|
||||
}, [isError, t, isOpen, data, drawerSlug, closeModal, isLoadingDocument]);
|
||||
|
||||
const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => {
|
||||
if (typeof onSaveFromProps === 'function') {
|
||||
onSaveFromProps({
|
||||
...args,
|
||||
collectionConfig,
|
||||
});
|
||||
}
|
||||
}, [collectionConfig, onSaveFromProps]);
|
||||
|
||||
if (isError) return null;
|
||||
|
||||
if (isOpen) {
|
||||
// IMPORTANT: we must ensure that modals are not recursively rendered
|
||||
// to do this, do not render the drawer until it is open
|
||||
return (
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={baseClass}
|
||||
>
|
||||
<DocumentInfoProvider collection={collectionConfig}>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={collectionConfig.admin?.components?.views?.Edit}
|
||||
componentProps={{
|
||||
isLoading: !initialState,
|
||||
data,
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
permissions: permissions.collections[collectionConfig.slug],
|
||||
isEditing: Boolean(id),
|
||||
apiURL: id ? `${serverURL}${api}/${collectionSlug}/${id}` : null,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission: true,
|
||||
action: `${serverURL}${api}/${collectionSlug}${id ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`,
|
||||
disableEyebrow: true,
|
||||
disableActions: true,
|
||||
me: true,
|
||||
disableLeaveWithoutSaving: true,
|
||||
customHeader: (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-content`}>
|
||||
<h2>
|
||||
{!customHeader ? t(!id ? 'fields:addNewLabel' : 'general:editLabel', { label: getTranslation(collectionConfig.labels.singular, i18n) }) : customHeader}
|
||||
</h2>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
className={`${baseClass}__header-close`}
|
||||
onClick={() => toggleModal(drawerSlug)}
|
||||
aria-label={t('general:close')}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
{id && (
|
||||
<IDLabel id={id} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useDocumentDrawer: UseDocumentDrawer = ({ id, collectionSlug }) => {
|
||||
const drawerDepth = useEditDepth();
|
||||
const uuid = useId();
|
||||
const { modalState, toggleModal } = useModal();
|
||||
const { modalState, toggleModal, closeModal, openModal } = useModal();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const drawerSlug = formatDocumentDrawerSlug({
|
||||
collectionSlug,
|
||||
@@ -209,6 +87,14 @@ export const useDocumentDrawer: UseDocumentDrawer = ({ id, collectionSlug }) =>
|
||||
toggleModal(drawerSlug);
|
||||
}, [toggleModal, drawerSlug]);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
closeModal(drawerSlug);
|
||||
}, [closeModal, drawerSlug]);
|
||||
|
||||
const openDrawer = useCallback(() => {
|
||||
openModal(drawerSlug);
|
||||
}, [openModal, drawerSlug]);
|
||||
|
||||
const MemoizedDrawer = useMemo(() => {
|
||||
return ((props) => (
|
||||
<DocumentDrawer
|
||||
@@ -237,7 +123,9 @@ export const useDocumentDrawer: UseDocumentDrawer = ({ id, collectionSlug }) =>
|
||||
drawerDepth,
|
||||
isDrawerOpen: isOpen,
|
||||
toggleDrawer,
|
||||
}), [drawerDepth, drawerSlug, isOpen, toggleDrawer]);
|
||||
closeDrawer,
|
||||
openDrawer,
|
||||
}), [drawerDepth, drawerSlug, isOpen, toggleDrawer, closeDrawer, openDrawer]);
|
||||
|
||||
return [
|
||||
MemoizedDrawer,
|
||||
|
||||
@@ -4,10 +4,10 @@ import { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
export type DocumentDrawerProps = {
|
||||
collectionSlug: string
|
||||
id?: string
|
||||
onSave?: (args: {
|
||||
onSave?: (json: {
|
||||
doc: Record<string, any>
|
||||
message: string
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
message: string,
|
||||
}) => void
|
||||
customHeader?: React.ReactNode
|
||||
drawerSlug?: string
|
||||
@@ -19,6 +19,7 @@ export type DocumentTogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
drawerSlug?: string
|
||||
id?: string
|
||||
collectionSlug: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type UseDocumentDrawer = (args: {
|
||||
@@ -32,5 +33,7 @@ export type UseDocumentDrawer = (args: {
|
||||
drawerDepth: number
|
||||
isDrawerOpen: boolean
|
||||
toggleDrawer: () => void
|
||||
closeDrawer: () => void
|
||||
openDrawer: () => void
|
||||
}
|
||||
]
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
transition: all 300ms ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__content-children {
|
||||
|
||||
@@ -10,13 +10,13 @@ const baseClass = 'drawer';
|
||||
|
||||
const zBase = 100;
|
||||
|
||||
const formatDrawerSlug = ({
|
||||
export const formatDrawerSlug = ({
|
||||
slug,
|
||||
depth,
|
||||
}: {
|
||||
slug: string,
|
||||
depth: number,
|
||||
}) => `drawer_${depth}_${slug}`;
|
||||
}): string => `drawer_${depth}_${slug}`;
|
||||
|
||||
export const DrawerToggler: React.FC<TogglerProps> = ({
|
||||
slug,
|
||||
@@ -24,6 +24,7 @@ export const DrawerToggler: React.FC<TogglerProps> = ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const { openModal } = useModal();
|
||||
@@ -39,6 +40,7 @@ export const DrawerToggler: React.FC<TogglerProps> = ({
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
@@ -57,44 +59,55 @@ export const Drawer: React.FC<Props> = ({
|
||||
const { breakpoints: { m: midBreak } } = useWindowInfo();
|
||||
const drawerDepth = useEditDepth();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [animateIn, setAnimateIn] = useState(false);
|
||||
const [modalSlug] = useState(() => (formatSlug !== false ? formatDrawerSlug({ slug, depth: drawerDepth }) : slug));
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(modalState[modalSlug].isOpen);
|
||||
setIsOpen(modalState[modalSlug]?.isOpen);
|
||||
}, [modalSlug, modalState]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={[
|
||||
className,
|
||||
baseClass,
|
||||
isOpen && `${baseClass}--is-open`,
|
||||
].filter(Boolean).join(' ')}
|
||||
style={{
|
||||
zIndex: zBase + drawerDepth,
|
||||
}}
|
||||
>
|
||||
{drawerDepth === 1 && (
|
||||
<div className={`${baseClass}__blur-bg`} />
|
||||
)}
|
||||
<button
|
||||
className={`${baseClass}__close`}
|
||||
id={`close-drawer__${modalSlug}`}
|
||||
type="button"
|
||||
onClick={() => closeModal(modalSlug)}
|
||||
useEffect(() => {
|
||||
setAnimateIn(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
if (isOpen) {
|
||||
// IMPORTANT: do not render the drawer until it is explicitly open, this is to avoid large html trees especially when nesting drawers
|
||||
|
||||
return (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={[
|
||||
className,
|
||||
baseClass,
|
||||
animateIn && `${baseClass}--is-open`,
|
||||
].filter(Boolean).join(' ')}
|
||||
style={{
|
||||
width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${drawerDepth - 1} * 25px)`,
|
||||
zIndex: zBase + drawerDepth,
|
||||
}}
|
||||
aria-label={t('close')}
|
||||
/>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<div className={`${baseClass}__content-children`}>
|
||||
<EditDepthContext.Provider value={drawerDepth + 1}>
|
||||
{children}
|
||||
</EditDepthContext.Provider>
|
||||
>
|
||||
{drawerDepth === 1 && (
|
||||
<div className={`${baseClass}__blur-bg`} />
|
||||
)}
|
||||
<button
|
||||
className={`${baseClass}__close`}
|
||||
id={`close-drawer__${modalSlug}`}
|
||||
type="button"
|
||||
onClick={() => closeModal(modalSlug)}
|
||||
style={{
|
||||
width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${drawerDepth - 1} * 25px)`,
|
||||
}}
|
||||
aria-label={t('close')}
|
||||
/>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<div className={`${baseClass}__content-children`}>
|
||||
<EditDepthContext.Provider value={drawerDepth + 1}>
|
||||
{children}
|
||||
</EditDepthContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -12,4 +12,5 @@ export type TogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
formatSlug?: boolean
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
display: flex;
|
||||
margin-left: - base(.5);
|
||||
margin-right: - base(.5);
|
||||
width: calc(100% + #{$baseline});
|
||||
width: calc(100% + #{base(1)});
|
||||
|
||||
.btn {
|
||||
margin: 0 base(.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle-columns,
|
||||
@@ -31,10 +35,6 @@
|
||||
&__toggle-sort {
|
||||
min-width: 140px;
|
||||
|
||||
&.btn {
|
||||
margin: 0 base(.5);
|
||||
}
|
||||
|
||||
&.btn--style-primary {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
@@ -52,6 +52,16 @@
|
||||
&__buttons {
|
||||
margin-left: base(.5);
|
||||
}
|
||||
|
||||
&__buttons-wrap {
|
||||
margin-left: - base(.25);
|
||||
margin-right: - base(.25);
|
||||
width: calc(100% + #{base(0.5)});
|
||||
|
||||
.btn {
|
||||
margin: 0 base(.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
|
||||
310
src/admin/components/elements/ListDrawer/DrawerContent.tsx
Normal file
310
src/admin/components/elements/ListDrawer/DrawerContent.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import React, { Fragment, useCallback, useEffect, useReducer, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ListDrawerProps } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { DocumentInfoProvider } from '../../utilities/DocumentInfo';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
import DefaultList from '../../views/collections/List/Default';
|
||||
import Label from '../../forms/Label';
|
||||
import ReactSelect from '../ReactSelect';
|
||||
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`,
|
||||
}],
|
||||
});
|
||||
|
||||
const shouldIncludeCollection = ({
|
||||
coll: {
|
||||
admin: { enableRichTextRelationship },
|
||||
upload,
|
||||
slug,
|
||||
},
|
||||
uploads,
|
||||
collectionSlugs,
|
||||
}) => (enableRichTextRelationship && ((uploads && Boolean(upload)) || collectionSlugs?.includes(slug)));
|
||||
|
||||
export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
drawerSlug,
|
||||
onSelect,
|
||||
customHeader,
|
||||
collectionSlugs,
|
||||
uploads,
|
||||
selectedCollection,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation(['upload', 'general']);
|
||||
const { permissions } = useAuth();
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
const { isModalOpen, closeModal } = useModal();
|
||||
const [limit, setLimit] = useState<number>();
|
||||
const [sort, setSort] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [where, setWhere] = useState(null);
|
||||
const { serverURL, routes: { api }, collections } = useConfig();
|
||||
const [enabledCollectionConfigs] = useState(() => collections.filter((coll) => shouldIncludeCollection({ coll, uploads, collectionSlugs })));
|
||||
const [selectedCollectionConfig, setSelectedCollectionConfig] = useState<SanitizedCollectionConfig>(() => {
|
||||
let initialSelection: SanitizedCollectionConfig;
|
||||
if (selectedCollection) {
|
||||
// if passed a selection, find it and check if it's enabled
|
||||
const foundSelection = collections.find(({ slug }) => slug === selectedCollection);
|
||||
if (foundSelection && shouldIncludeCollection({ coll: foundSelection, uploads, collectionSlugs })) {
|
||||
initialSelection = foundSelection;
|
||||
}
|
||||
} else {
|
||||
// return the first one that is enabled
|
||||
initialSelection = collections.find((coll) => shouldIncludeCollection({ coll, uploads, collectionSlugs }));
|
||||
}
|
||||
return initialSelection;
|
||||
});
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<{ label: string, value: string }>(() => (selectedCollectionConfig ? { label: getTranslation(selectedCollectionConfig.labels.singular, i18n), value: selectedCollectionConfig.slug } : undefined));
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
// allow external control of selected collection, same as the initial state logic above
|
||||
useEffect(() => {
|
||||
let newSelection: SanitizedCollectionConfig;
|
||||
if (selectedCollection) {
|
||||
// if passed a selection, find it and check if it's enabled
|
||||
const foundSelection = collections.find(({ slug }) => slug === selectedCollection);
|
||||
if (foundSelection && shouldIncludeCollection({ coll: foundSelection, uploads, collectionSlugs })) {
|
||||
newSelection = foundSelection;
|
||||
}
|
||||
} else {
|
||||
// return the first one that is enabled
|
||||
newSelection = collections.find((coll) => shouldIncludeCollection({ coll, uploads, collectionSlugs }));
|
||||
}
|
||||
setSelectedCollectionConfig(newSelection);
|
||||
}, [selectedCollection, collectionSlugs, uploads, collections, onSelect, t]);
|
||||
|
||||
const activeColumnNames = tableColumns.map(({ accessor }) => accessor);
|
||||
const stringifiedActiveColumns = JSON.stringify(activeColumnNames);
|
||||
const preferenceKey = `${selectedCollectionConfig.slug}-list`;
|
||||
|
||||
// this is the 'create new' drawer
|
||||
const [
|
||||
DocumentDrawer,
|
||||
DocumentDrawerToggler,
|
||||
{
|
||||
drawerSlug: documentDrawerSlug,
|
||||
},
|
||||
] = useDocumentDrawer({
|
||||
collectionSlug: selectedCollectionConfig.slug,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOption) {
|
||||
setSelectedCollectionConfig(collections.find(({ slug }) => selectedOption.value === slug));
|
||||
}
|
||||
}, [selectedOption, collections]);
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[selectedCollectionConfig?.slug];
|
||||
const hasCreatePermission = collectionPermissions?.create?.permission;
|
||||
|
||||
// If modal is open, get active page of upload gallery
|
||||
const isOpen = isModalOpen(drawerSlug);
|
||||
const apiURL = isOpen ? `${serverURL}${api}/${selectedCollectionConfig.slug}` : null;
|
||||
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0); // used to force a re-fetch even when apiURL is unchanged
|
||||
const [{ data, isError }, { setParams }] = usePayloadAPI(apiURL, {});
|
||||
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1;
|
||||
|
||||
useEffect(() => {
|
||||
const params: {
|
||||
page?: number
|
||||
sort?: string
|
||||
where?: unknown
|
||||
limit?: number
|
||||
cacheBust?: number
|
||||
} = {};
|
||||
|
||||
if (page) params.page = page;
|
||||
if (where) params.where = where;
|
||||
if (sort) params.sort = sort;
|
||||
if (limit) params.limit = limit;
|
||||
if (cacheBust) params.cacheBust = cacheBust;
|
||||
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, where, limit, cacheBust]);
|
||||
|
||||
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]);
|
||||
|
||||
const onCreateNew = useCallback(({ doc }) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
docID: doc.id,
|
||||
collectionConfig: selectedCollectionConfig,
|
||||
});
|
||||
}
|
||||
dispatchCacheBust();
|
||||
closeModal(documentDrawerSlug);
|
||||
closeModal(drawerSlug);
|
||||
}, [closeModal, documentDrawerSlug, drawerSlug, onSelect, selectedCollectionConfig]);
|
||||
|
||||
if (!selectedCollectionConfig || isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DocumentInfoProvider collection={selectedCollectionConfig}>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultList}
|
||||
CustomComponent={selectedCollectionConfig?.admin?.components?.views?.List}
|
||||
componentProps={{
|
||||
collection: {
|
||||
...selectedCollectionConfig,
|
||||
fields,
|
||||
},
|
||||
customHeader: (
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header-content`}>
|
||||
<h2 className={`${baseClass}__header-text`}>
|
||||
{!customHeader ? getTranslation(selectedCollectionConfig?.labels?.plural, i18n) : customHeader}
|
||||
</h2>
|
||||
{hasCreatePermission && (
|
||||
<DocumentDrawerToggler
|
||||
className={`${baseClass}__create-new-button`}
|
||||
>
|
||||
<Pill>
|
||||
{t('general:createNew')}
|
||||
</Pill>
|
||||
</DocumentDrawerToggler>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
closeModal(drawerSlug);
|
||||
}}
|
||||
className={`${baseClass}__header-close`}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
{selectedCollectionConfig?.admin?.description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={selectedCollectionConfig.admin.description} />
|
||||
</div>
|
||||
)}
|
||||
{moreThanOneAvailableCollection && (
|
||||
<div className={`${baseClass}__select-collection-wrap`}>
|
||||
<Label label={t('selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
value={selectedOption}
|
||||
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
|
||||
options={enabledCollectionConfigs.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
),
|
||||
data,
|
||||
limit: limit || selectedCollectionConfig?.admin?.pagination?.defaultLimit,
|
||||
setLimit,
|
||||
tableColumns,
|
||||
setColumns: setActiveColumns,
|
||||
setSort,
|
||||
newDocumentURL: null,
|
||||
hasCreatePermission,
|
||||
columnNames: activeColumnNames,
|
||||
disableEyebrow: true,
|
||||
modifySearchParams: false,
|
||||
onCardClick: (doc) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
docID: doc.id,
|
||||
collectionConfig: selectedCollectionConfig,
|
||||
});
|
||||
}
|
||||
closeModal(drawerSlug);
|
||||
},
|
||||
disableCardLink: true,
|
||||
handleSortChange: setSort,
|
||||
handleWhereChange: setWhere,
|
||||
handlePageChange: setPage,
|
||||
handlePerPageChange: setLimit,
|
||||
}}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
<DocumentDrawer onSave={onCreateNew} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
97
src/admin/components/elements/ListDrawer/index.scss
Normal file
97
src/admin/components/elements/ListDrawer/index.scss
Normal file
@@ -0,0 +1,97 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.list-drawer {
|
||||
&__header {
|
||||
margin-top: base(2.5);
|
||||
width: 100%;
|
||||
|
||||
@include mid-break {
|
||||
margin-top: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__header-wrap {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__header-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
|
||||
.pill {
|
||||
pointer-events: none;
|
||||
margin: 0;
|
||||
margin-top: base(0.25);
|
||||
margin-left: base(0.5);
|
||||
|
||||
@include mid-break {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__header-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
|
||||
svg {
|
||||
width: base(2.75);
|
||||
height: base(2.75);
|
||||
position: relative;
|
||||
left: base(-.825);
|
||||
top: base(-.825);
|
||||
|
||||
.stroke {
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__select-collection-wrap {
|
||||
margin-top: base(1);
|
||||
}
|
||||
|
||||
&__first-cell {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
.collection-list__header {
|
||||
margin-bottom: base(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/admin/components/elements/ListDrawer/index.tsx
Normal file
120
src/admin/components/elements/ListDrawer/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { ListDrawerProps, ListTogglerProps, UseListDrawer } from './types';
|
||||
import { Drawer, DrawerToggler } from '../Drawer';
|
||||
import { useEditDepth } from '../../utilities/EditDepth';
|
||||
import { ListDrawerContent } from './DrawerContent';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const baseClass = 'list-drawer';
|
||||
|
||||
const formatListDrawerSlug = ({
|
||||
depth,
|
||||
uuid,
|
||||
}: {
|
||||
depth: number,
|
||||
uuid: string, // supply when creating a new document and no id is available
|
||||
}) => `list-drawer_${depth}_${uuid}`;
|
||||
|
||||
export const ListDrawerToggler: React.FC<ListTogglerProps> = ({
|
||||
children,
|
||||
className,
|
||||
drawerSlug,
|
||||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<DrawerToggler
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={[
|
||||
className,
|
||||
`${baseClass}__toggler`,
|
||||
].filter(Boolean).join(' ')}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</DrawerToggler>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListDrawer: React.FC<ListDrawerProps> = (props) => {
|
||||
const { drawerSlug } = props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={baseClass}
|
||||
>
|
||||
<ListDrawerContent {...props} />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export const useListDrawer: UseListDrawer = ({ collectionSlugs, uploads, selectedCollection }) => {
|
||||
const drawerDepth = useEditDepth();
|
||||
const uuid = useId();
|
||||
const { modalState, toggleModal, closeModal, openModal } = useModal();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const drawerSlug = formatListDrawerSlug({
|
||||
depth: drawerDepth,
|
||||
uuid,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen));
|
||||
}, [modalState, drawerSlug]);
|
||||
|
||||
const toggleDrawer = useCallback(() => {
|
||||
toggleModal(drawerSlug);
|
||||
}, [toggleModal, drawerSlug]);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
closeModal(drawerSlug);
|
||||
}, [drawerSlug, closeModal]);
|
||||
|
||||
const openDrawer = useCallback(() => {
|
||||
openModal(drawerSlug);
|
||||
}, [drawerSlug, openModal]);
|
||||
|
||||
const MemoizedDrawer = useMemo(() => {
|
||||
return ((props) => (
|
||||
<ListDrawer
|
||||
{...props}
|
||||
drawerSlug={drawerSlug}
|
||||
collectionSlugs={collectionSlugs}
|
||||
uploads={uploads}
|
||||
closeDrawer={closeDrawer}
|
||||
key={drawerSlug}
|
||||
selectedCollection={selectedCollection}
|
||||
/>
|
||||
));
|
||||
}, [drawerSlug, collectionSlugs, uploads, closeDrawer, selectedCollection]);
|
||||
|
||||
const MemoizedDrawerToggler = useMemo(() => {
|
||||
return ((props) => (
|
||||
<ListDrawerToggler
|
||||
{...props}
|
||||
drawerSlug={drawerSlug}
|
||||
/>
|
||||
));
|
||||
}, [drawerSlug]);
|
||||
|
||||
const MemoizedDrawerState = useMemo(() => ({
|
||||
drawerSlug,
|
||||
drawerDepth,
|
||||
isDrawerOpen: isOpen,
|
||||
toggleDrawer,
|
||||
closeDrawer,
|
||||
openDrawer,
|
||||
}), [drawerDepth, drawerSlug, isOpen, toggleDrawer, closeDrawer, openDrawer]);
|
||||
|
||||
return [
|
||||
MemoizedDrawer,
|
||||
MemoizedDrawerToggler,
|
||||
MemoizedDrawerState,
|
||||
];
|
||||
};
|
||||
38
src/admin/components/elements/ListDrawer/types.ts
Normal file
38
src/admin/components/elements/ListDrawer/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
|
||||
export type ListDrawerProps = {
|
||||
onSelect?: (args: {
|
||||
docID: string
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
}) => void
|
||||
customHeader?: React.ReactNode
|
||||
drawerSlug?: string
|
||||
collectionSlugs?: string[]
|
||||
uploads?: boolean
|
||||
selectedCollection?: string
|
||||
}
|
||||
|
||||
export type ListTogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
drawerSlug?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type UseListDrawer = (args: {
|
||||
collectionSlugs?: string[]
|
||||
selectedCollection?: string
|
||||
uploads?: boolean // finds all collections with upload: true
|
||||
}) => [
|
||||
React.FC<Omit<ListDrawerProps, 'collectionSlug' | 'id'>>, // drawer
|
||||
React.FC<Omit<ListTogglerProps, 'collectionSlug' | 'id'>>, // toggler
|
||||
{
|
||||
drawerSlug: string,
|
||||
drawerDepth: number
|
||||
isDrawerOpen: boolean
|
||||
toggleDrawer: () => void
|
||||
closeDrawer: () => void
|
||||
openDrawer: () => void
|
||||
}
|
||||
]
|
||||
@@ -13,7 +13,7 @@ const baseClass = 'per-page';
|
||||
|
||||
const defaultLimits = defaults.admin.pagination.limits;
|
||||
|
||||
type Props = {
|
||||
export type Props = {
|
||||
limits: number[]
|
||||
limit: number
|
||||
handleChange?: (limit: number) => void
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.step-nav {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
|
||||
* {
|
||||
display: block;
|
||||
|
||||
@@ -38,4 +38,8 @@ $caretSize: 6;
|
||||
transition: opacity .2s ease-in-out;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,9 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
buttonStyle="icon-label"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })}
|
||||
onClick={() => {
|
||||
if (reducedFields.length > 0) dispatchConditions({ type: 'add', field: reducedFields[0].value });
|
||||
}}
|
||||
>
|
||||
{t('or')}
|
||||
</Button>
|
||||
@@ -160,7 +162,9 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
buttonStyle="icon-label"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })}
|
||||
onClick={() => {
|
||||
if (reducedFields.length > 0) dispatchConditions({ type: 'add', field: reducedFields[0].value });
|
||||
}}
|
||||
>
|
||||
{t('addFilter')}
|
||||
</Button>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__add-button {
|
||||
&__add-button,
|
||||
&__add-button.doc-drawer__toggler {
|
||||
@include formInput;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@@ -35,7 +36,6 @@
|
||||
&__relation-button {
|
||||
@extend %btn-reset;
|
||||
cursor: pointer;
|
||||
@extend %btn-reset;
|
||||
display: block;
|
||||
padding: base(.125) 0;
|
||||
text-align: center;
|
||||
|
||||
@@ -15,14 +15,11 @@
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__drawer-toggler {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
&__text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__drawer-toggler {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.rich-text__button {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: base(.75);
|
||||
height: base(.75);
|
||||
|
||||
@@ -1,33 +1,54 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { ElementType, useCallback, useState } from 'react';
|
||||
import { useSlate } from 'slate-react';
|
||||
import isElementActive from './isActive';
|
||||
import toggleElement from './toggle';
|
||||
import { ButtonProps } from './types';
|
||||
import Tooltip from '../../../../elements/Tooltip';
|
||||
|
||||
import '../buttons.scss';
|
||||
|
||||
export const baseClass = 'rich-text__button';
|
||||
|
||||
const ElementButton: React.FC<ButtonProps> = ({ format, children, onClick, className }) => {
|
||||
const ElementButton: React.FC<ButtonProps> = (props) => {
|
||||
const {
|
||||
format,
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
tooltip,
|
||||
el = 'button',
|
||||
} = props;
|
||||
|
||||
const editor = useSlate();
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const defaultOnClick = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
setShowTooltip(false);
|
||||
toggleElement(editor, format);
|
||||
}, [editor, format]);
|
||||
|
||||
const Tag: ElementType = el;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<Tag
|
||||
{...el === 'button' && { type: 'button' }}
|
||||
className={[
|
||||
baseClass,
|
||||
className,
|
||||
isElementActive(editor, format) && `${baseClass}__button--active`,
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={onClick || defaultOnClick}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
{tooltip && (
|
||||
<Tooltip show={showTooltip}>
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Editor, Range } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ElementButton from '../Button';
|
||||
import { unwrapLink } from './utilities';
|
||||
import LinkIcon from '../../../../../icons/Link';
|
||||
import { EditModal } from './Modal';
|
||||
import { modalSlug as baseModalSlug } from './shared';
|
||||
import isElementActive from '../isActive';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { useLocale } from '../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../utilities/Config';
|
||||
import { getBaseFields } from './Modal/baseFields';
|
||||
import { Field } from '../../../../../../../fields/config/types';
|
||||
import reduceFieldsToValues from '../../../../Form/reduceFieldsToValues';
|
||||
|
||||
export const LinkButton = ({ fieldProps }) => {
|
||||
const customFieldSchema = fieldProps?.admin?.link?.fields;
|
||||
|
||||
const modalSlug = `${baseModalSlug}-${fieldProps.path}`;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const config = useConfig();
|
||||
const editor = useSlate();
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { toggleModal } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [initialState, setInitialState] = useState<Fields>({});
|
||||
const [fieldSchema] = useState(() => {
|
||||
const fields: Field[] = [
|
||||
...getBaseFields(config),
|
||||
];
|
||||
|
||||
if (customFieldSchema) {
|
||||
fields.push({
|
||||
name: 'fields',
|
||||
type: 'group',
|
||||
admin: {
|
||||
style: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 0,
|
||||
},
|
||||
},
|
||||
fields: customFieldSchema,
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ElementButton
|
||||
format="link"
|
||||
onClick={async () => {
|
||||
if (isElementActive(editor, 'link')) {
|
||||
unwrapLink(editor);
|
||||
} else {
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(true);
|
||||
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
if (!isCollapsed) {
|
||||
const data = {
|
||||
text: editor.selection ? Editor.string(editor, editor.selection) : '',
|
||||
};
|
||||
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'create', locale, t });
|
||||
setInitialState(state);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</ElementButton>
|
||||
{renderModal && (
|
||||
<EditModal
|
||||
modalSlug={modalSlug}
|
||||
fieldSchema={fieldSchema}
|
||||
initialState={initialState}
|
||||
close={() => {
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
}}
|
||||
handleModalSubmit={(fields) => {
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
const data = reduceFieldsToValues(fields, true);
|
||||
|
||||
const newLink = {
|
||||
type: 'link',
|
||||
linkType: data.linkType,
|
||||
url: data.url,
|
||||
doc: data.doc,
|
||||
newTab: data.newTab,
|
||||
fields: data.fields,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (isCollapsed || !editor.selection) {
|
||||
// If selection anchor and focus are the same,
|
||||
// Just inject a new node with children already set
|
||||
Transforms.insertNodes(editor, {
|
||||
...newLink,
|
||||
children: [{ text: String(data.text) }],
|
||||
});
|
||||
} else if (editor.selection) {
|
||||
// Otherwise we need to wrap the selected node in a link,
|
||||
// Delete its old text,
|
||||
// Move the selection one position forward into the link,
|
||||
// And insert the text back into the new link
|
||||
Transforms.wrapNodes(editor, newLink, { split: true });
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'word' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
}
|
||||
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { Fragment, useId, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Range } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import ElementButton from '../../Button';
|
||||
import LinkIcon from '../../../../../../icons/Link';
|
||||
import reduceFieldsToValues from '../../../../../Form/reduceFieldsToValues';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import isElementActive from '../../isActive';
|
||||
import { unwrapLink } from '../utilities';
|
||||
import { useEditDepth } from '../../../../../../utilities/EditDepth';
|
||||
import { formatDrawerSlug } from '../../../../../../elements/Drawer';
|
||||
import { getBaseFields } from '../LinkDrawer/baseFields';
|
||||
import { LinkDrawer } from '../LinkDrawer';
|
||||
import { Field } from '../../../../../../../../fields/config/types';
|
||||
import { Props as RichTextFieldProps } from '../../../types';
|
||||
|
||||
const insertLink = (editor, fields) => {
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
const data = reduceFieldsToValues(fields, true);
|
||||
|
||||
const newLink = {
|
||||
type: 'link',
|
||||
linkType: data.linkType,
|
||||
url: data.url,
|
||||
doc: data.doc,
|
||||
newTab: data.newTab,
|
||||
fields: data.fields,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (isCollapsed || !editor.selection) {
|
||||
// If selection anchor and focus are the same,
|
||||
// Just inject a new node with children already set
|
||||
Transforms.insertNodes(editor, {
|
||||
...newLink,
|
||||
children: [{ text: String(data.text) }],
|
||||
});
|
||||
} else if (editor.selection) {
|
||||
// Otherwise we need to wrap the selected node in a link,
|
||||
// Delete its old text,
|
||||
// Move the selection one position forward into the link,
|
||||
// And insert the text back into the new link
|
||||
Transforms.wrapNodes(editor, newLink, { split: true });
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'word' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
}
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
export const LinkButton: React.FC<{
|
||||
path: string
|
||||
fieldProps: RichTextFieldProps
|
||||
}> = ({ fieldProps }) => {
|
||||
const customFieldSchema = fieldProps?.admin?.link?.fields;
|
||||
|
||||
const { t } = useTranslation(['upload', 'general']);
|
||||
const editor = useSlate();
|
||||
const config = useConfig();
|
||||
|
||||
const [fieldSchema] = useState(() => {
|
||||
const fields: Field[] = [
|
||||
...getBaseFields(config),
|
||||
];
|
||||
|
||||
if (customFieldSchema) {
|
||||
fields.push({
|
||||
name: 'fields',
|
||||
type: 'group',
|
||||
admin: {
|
||||
style: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 0,
|
||||
},
|
||||
},
|
||||
fields: customFieldSchema,
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
const { openModal, closeModal } = useModal();
|
||||
const uuid = useId();
|
||||
const editDepth = useEditDepth();
|
||||
const drawerSlug = formatDrawerSlug({
|
||||
slug: `rich-text-link-${uuid}`,
|
||||
depth: editDepth,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ElementButton
|
||||
format="link"
|
||||
tooltip={t('fields:addLink')}
|
||||
className="link"
|
||||
onClick={() => {
|
||||
if (isElementActive(editor, 'link')) {
|
||||
unwrapLink(editor);
|
||||
} else {
|
||||
openModal(drawerSlug);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</ElementButton>
|
||||
<LinkDrawer
|
||||
drawerSlug={drawerSlug}
|
||||
handleModalSubmit={(fields) => {
|
||||
insertLink(editor, fields);
|
||||
closeModal(drawerSlug);
|
||||
}}
|
||||
fieldSchema={fieldSchema}
|
||||
handleClose={() => {
|
||||
closeModal(drawerSlug);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
@import '../../../../../../../scss/styles.scss';
|
||||
|
||||
.rich-text-link {
|
||||
position: relative;
|
||||
@@ -11,7 +11,6 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
|
||||
.popup__scroll,
|
||||
.popup__wrap {
|
||||
overflow: visible;
|
||||
@@ -52,18 +51,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text-link__button {
|
||||
@extend %btn-reset;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
letter-spacing: inherit;
|
||||
line-height: inherit;
|
||||
.rich-text-link__popup-toggler {
|
||||
position: relative;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
cursor: text;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&--open {
|
||||
z-index: var(--z-popup);
|
||||
}
|
||||
@@ -1,31 +1,73 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { HTMLAttributes, useCallback, useEffect, useId, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Node, Editor } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { unwrapLink } from './utilities';
|
||||
import Popup from '../../../../../elements/Popup';
|
||||
import { EditModal } from './Modal';
|
||||
import { modalSlug as baseModalSlug } from './shared';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { useLocale } from '../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../utilities/Config';
|
||||
import { getBaseFields } from './Modal/baseFields';
|
||||
import { Field } from '../../../../../../../fields/config/types';
|
||||
import reduceFieldsToValues from '../../../../Form/reduceFieldsToValues';
|
||||
import deepCopyObject from '../../../../../../../utilities/deepCopyObject';
|
||||
import Button from '../../../../../elements/Button';
|
||||
import { getTranslation } from '../../../../../../../utilities/getTranslation';
|
||||
|
||||
import { unwrapLink } from '../utilities';
|
||||
import Popup from '../../../../../../elements/Popup';
|
||||
import { LinkDrawer } from '../LinkDrawer';
|
||||
import { Fields } from '../../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../../Form/buildStateFromSchema';
|
||||
import { useAuth } from '../../../../../../utilities/Auth';
|
||||
import { useLocale } from '../../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import { getBaseFields } from '../LinkDrawer/baseFields';
|
||||
import { Field } from '../../../../../../../../fields/config/types';
|
||||
import reduceFieldsToValues from '../../../../../Form/reduceFieldsToValues';
|
||||
import deepCopyObject from '../../../../../../../../utilities/deepCopyObject';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
|
||||
import { useEditDepth } from '../../../../../../utilities/EditDepth';
|
||||
import { formatDrawerSlug } from '../../../../../../elements/Drawer';
|
||||
import { Props as RichTextFieldProps } from '../../../types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'rich-text-link';
|
||||
|
||||
// TODO: Multiple modal windows stacked go boom (rip). Edit Upload in fields -> rich text
|
||||
const insertChange = (editor, fields, customFieldSchema) => {
|
||||
const data = reduceFieldsToValues(fields, true);
|
||||
|
||||
const [, parentPath] = Editor.above(editor);
|
||||
|
||||
const newNode: Record<string, unknown> = {
|
||||
newTab: data.newTab,
|
||||
url: data.url,
|
||||
linkType: data.linkType,
|
||||
doc: data.doc,
|
||||
};
|
||||
|
||||
if (customFieldSchema) {
|
||||
newNode.fields = data.fields;
|
||||
}
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: parentPath },
|
||||
);
|
||||
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'block' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
export const LinkElement: React.FC<{
|
||||
attributes: HTMLAttributes<HTMLDivElement>
|
||||
children: React.ReactNode
|
||||
element: any
|
||||
fieldProps: RichTextFieldProps
|
||||
editorRef: React.RefObject<HTMLDivElement>
|
||||
}> = (props) => {
|
||||
const {
|
||||
attributes,
|
||||
children,
|
||||
element,
|
||||
editorRef,
|
||||
fieldProps,
|
||||
} = props;
|
||||
|
||||
export const LinkElement = ({ attributes, children, element, editorRef, fieldProps }) => {
|
||||
const customFieldSchema = fieldProps?.admin?.link?.fields;
|
||||
|
||||
const editor = useSlate();
|
||||
@@ -33,7 +75,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const { openModal, toggleModal } = useModal();
|
||||
const { openModal, toggleModal, closeModal } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [renderPopup, setRenderPopup] = useState(false);
|
||||
const [initialState, setInitialState] = useState<Fields>({});
|
||||
@@ -61,7 +103,13 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
return fields;
|
||||
});
|
||||
|
||||
const modalSlug = `${baseModalSlug}-${fieldProps.path}`;
|
||||
const uuid = useId();
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
const drawerSlug = formatDrawerSlug({
|
||||
slug: `rich-text-link-${uuid}`,
|
||||
depth: editDepth,
|
||||
});
|
||||
|
||||
const handleTogglePopup = useCallback((render) => {
|
||||
if (!render) {
|
||||
@@ -97,43 +145,16 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
contentEditable={false}
|
||||
>
|
||||
{renderModal && (
|
||||
<EditModal
|
||||
modalSlug={modalSlug}
|
||||
<LinkDrawer
|
||||
drawerSlug={drawerSlug}
|
||||
fieldSchema={fieldSchema}
|
||||
close={() => {
|
||||
toggleModal(modalSlug);
|
||||
handleClose={() => {
|
||||
toggleModal(drawerSlug);
|
||||
setRenderModal(false);
|
||||
}}
|
||||
handleModalSubmit={(fields) => {
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
|
||||
const data = reduceFieldsToValues(fields, true);
|
||||
|
||||
const [, parentPath] = Editor.above(editor);
|
||||
|
||||
const newNode: Record<string, unknown> = {
|
||||
newTab: data.newTab,
|
||||
url: data.url,
|
||||
linkType: data.linkType,
|
||||
doc: data.doc,
|
||||
};
|
||||
|
||||
if (customFieldSchema) {
|
||||
newNode.fields = data.fields;
|
||||
}
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: parentPath },
|
||||
);
|
||||
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'block' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
insertChange(editor, fields, customFieldSchema);
|
||||
closeModal(drawerSlug);
|
||||
}}
|
||||
initialState={initialState}
|
||||
/>
|
||||
@@ -181,7 +202,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setRenderPopup(false);
|
||||
openModal(modalSlug);
|
||||
openModal(drawerSlug);
|
||||
setRenderModal(true);
|
||||
}}
|
||||
tooltip={t('general:edit')}
|
||||
@@ -205,7 +226,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className={[
|
||||
`${baseClass}__button`,
|
||||
`${baseClass}__popup-toggler`,
|
||||
].filter(Boolean).join(' ')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') setRenderPopup(true); }}
|
||||
onClick={() => setRenderPopup(true)}
|
||||
@@ -0,0 +1,50 @@
|
||||
@import '../../../../../../../scss/styles.scss';
|
||||
|
||||
.rich-text-link-edit-modal {
|
||||
&__template {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: base(1);
|
||||
padding-bottom: base(2);
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
margin-bottom: $baseline;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: base(2.5);
|
||||
margin-bottom: base(1);
|
||||
|
||||
@include mid-break {
|
||||
margin-top: base(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__header-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
|
||||
svg {
|
||||
width: base(2.75);
|
||||
height: base(2.75);
|
||||
position: relative;
|
||||
left: base(-.825);
|
||||
top: base(-.825);
|
||||
|
||||
.stroke {
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Modal } from '@faceless-ui/modal';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MinimalTemplate } from '../../../../../..';
|
||||
import { Drawer } from '../../../../../../elements/Drawer';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import X from '../../../../../../icons/X';
|
||||
import Form from '../../../../../Form';
|
||||
@@ -11,29 +10,34 @@ import fieldTypes from '../../../..';
|
||||
import RenderFields from '../../../../../RenderFields';
|
||||
|
||||
import './index.scss';
|
||||
import { Gutter } from '../../../../../../elements/Gutter';
|
||||
|
||||
const baseClass = 'rich-text-link-edit-modal';
|
||||
|
||||
export const EditModal: React.FC<Props> = ({
|
||||
close,
|
||||
export const LinkDrawer: React.FC<Props> = ({
|
||||
handleClose,
|
||||
handleModalSubmit,
|
||||
initialState,
|
||||
fieldSchema,
|
||||
modalSlug,
|
||||
drawerSlug,
|
||||
}) => {
|
||||
const { t } = useTranslation('fields');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={baseClass}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__template`}>
|
||||
<Gutter className={`${baseClass}__template`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h3>{t('editLink')}</h3>
|
||||
<h2 className={`${baseClass}__header-text`}>
|
||||
{t('editLink')}
|
||||
</h2>
|
||||
<Button
|
||||
className={`${baseClass}__header-close`}
|
||||
buttonStyle="none"
|
||||
onClick={close}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
@@ -52,7 +56,7 @@ export const EditModal: React.FC<Props> = ({
|
||||
{t('general:submit')}
|
||||
</FormSubmit>
|
||||
</Form>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
</Gutter>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { Field } from '../../../../../../../../fields/config/types';
|
||||
import { Fields } from '../../../../../Form/types';
|
||||
|
||||
export type Props = {
|
||||
modalSlug: string
|
||||
close: () => void
|
||||
drawerSlug: string
|
||||
handleClose: () => void
|
||||
handleModalSubmit: (fields: Fields, data: Record<string, unknown>) => void
|
||||
initialState?: Fields
|
||||
fieldSchema: Field[]
|
||||
@@ -1,29 +0,0 @@
|
||||
@import '../../../../../../../scss/styles.scss';
|
||||
|
||||
.rich-text-link-edit-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
&__template {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
margin-bottom: $baseline;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: base(1.5);
|
||||
height: base(1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,7 @@
|
||||
@import '../../../../../../../scss/styles.scss';
|
||||
|
||||
.relationship-rich-text-button {
|
||||
.btn {
|
||||
margin-right: base(1);
|
||||
}
|
||||
|
||||
&__modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__modal-template {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
margin-bottom: $baseline;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: base(1.5);
|
||||
height: base(1.5);
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import ElementButton from '../../Button';
|
||||
import RelationshipIcon from '../../../../../../icons/Relationship';
|
||||
import Form from '../../../../../Form';
|
||||
import MinimalTemplate from '../../../../../../templates/Minimal';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import Submit from '../../../../../Submit';
|
||||
import X from '../../../../../../icons/X';
|
||||
import Fields from './Fields';
|
||||
import { requests } from '../../../../../../../api';
|
||||
import { injectVoidElement } from '../../injectVoid';
|
||||
import { useListDrawer } from '../../../../../../elements/ListDrawer';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const initialFormData = {};
|
||||
|
||||
const baseClass = 'relationship-rich-text-button';
|
||||
|
||||
const insertRelationship = (editor, { value, relationTo }) => {
|
||||
@@ -37,80 +28,58 @@ const insertRelationship = (editor, { value, relationTo }) => {
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
const { toggleModal } = useModal();
|
||||
const RelationshipButton: React.FC<{ path: string }> = () => {
|
||||
const { collections } = useConfig();
|
||||
const { t } = useTranslation('fields');
|
||||
const editor = useSlate();
|
||||
const { serverURL, routes: { api }, collections } = useConfig();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const [hasEnabledCollections] = useState(() => collections.find(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship));
|
||||
const modalSlug = `${path}-add-relationship`;
|
||||
const [enabledCollectionSlugs] = useState(() => collections.filter(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship).map(({ slug }) => slug));
|
||||
const [selectedCollectionSlug, setSelectedCollectionSlug] = useState(() => enabledCollectionSlugs[0]);
|
||||
const [
|
||||
ListDrawer,
|
||||
ListDrawerToggler,
|
||||
{
|
||||
closeDrawer,
|
||||
isDrawerOpen,
|
||||
},
|
||||
] = useListDrawer({
|
||||
collectionSlugs: enabledCollectionSlugs,
|
||||
selectedCollection: selectedCollectionSlug,
|
||||
});
|
||||
|
||||
const handleAddRelationship = useCallback(async (_, { relationTo, value }) => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const onSelect = useCallback(({ docID, collectionConfig }) => {
|
||||
insertRelationship(editor, {
|
||||
value: {
|
||||
id: docID,
|
||||
},
|
||||
relationTo: collectionConfig.slug,
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
insertRelationship(editor, { value: { id: json.id }, relationTo });
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
setLoading(false);
|
||||
}, [i18n.language, editor, toggleModal, modalSlug, api, serverURL]);
|
||||
closeDrawer();
|
||||
}, [editor, closeDrawer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderModal) {
|
||||
toggleModal(modalSlug);
|
||||
}
|
||||
}, [renderModal, toggleModal, modalSlug]);
|
||||
// always reset back to first option
|
||||
// TODO: this is not working, see the ListDrawer component
|
||||
setSelectedCollectionSlug(enabledCollectionSlugs[0]);
|
||||
}, [isDrawerOpen, enabledCollectionSlugs]);
|
||||
|
||||
if (!hasEnabledCollections) return null;
|
||||
if (!enabledCollectionSlugs || enabledCollectionSlugs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ElementButton
|
||||
className={baseClass}
|
||||
format="relationship"
|
||||
onClick={() => setRenderModal(true)}
|
||||
>
|
||||
<RelationshipIcon />
|
||||
</ElementButton>
|
||||
{renderModal && (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={`${baseClass}__modal`}
|
||||
<ListDrawerToggler>
|
||||
<ElementButton
|
||||
className={baseClass}
|
||||
format="relationship"
|
||||
tooltip={t('addRelationship')}
|
||||
el="div"
|
||||
onClick={() => {
|
||||
// do nothing
|
||||
}}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__modal-template`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h3>{t('addRelationship')}</h3>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
onClick={() => {
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</header>
|
||||
<Form
|
||||
onSubmit={handleAddRelationship}
|
||||
initialData={initialFormData}
|
||||
disabled={loading}
|
||||
>
|
||||
<Fields />
|
||||
<Submit>
|
||||
{t('addRelationship')}
|
||||
</Submit>
|
||||
</Form>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
)}
|
||||
<RelationshipIcon />
|
||||
</ElementButton>
|
||||
</ListDrawerToggler>
|
||||
<ListDrawer onSelect={onSelect} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
.rich-text-relationship {
|
||||
@extend %body;
|
||||
@include shadow-sm;
|
||||
padding: base(.5);
|
||||
padding: base(.75);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
background: var(--theme-input-bg);
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
max-width: base(15);
|
||||
@@ -19,20 +19,77 @@
|
||||
margin: base(.625) 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: base(1.25);
|
||||
height: base(1.25);
|
||||
margin-right: base(.5);
|
||||
&__label {
|
||||
margin-bottom: base(0.25);
|
||||
}
|
||||
|
||||
h5 {
|
||||
&__title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__label,
|
||||
&__title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
box-shadow: $focus-box-shadow;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: base(0.5);
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
}
|
||||
|
||||
&__actionButton {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
width: base(1);
|
||||
height: base(1);
|
||||
|
||||
line {
|
||||
stroke-width: $style-stroke-width-m;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useFocused, useSelected } from 'slate-react';
|
||||
import React, { HTMLAttributes, useCallback, useReducer, useState } from 'react';
|
||||
import { ReactEditor, useFocused, useSelected, useSlateStatic } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Transforms } from 'slate';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import RelationshipIcon from '../../../../../../icons/Relationship';
|
||||
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
|
||||
import { useDocumentDrawer } from '../../../../../../elements/DocumentDrawer';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import { useListDrawer } from '../../../../../../elements/ListDrawer';
|
||||
import { Props as RichTextProps } from '../../../types';
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
@@ -14,20 +18,115 @@ const initialParams = {
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
const Element = (props) => {
|
||||
const { attributes, children, element } = props;
|
||||
const Element: React.FC<{
|
||||
attributes: HTMLAttributes<HTMLDivElement>
|
||||
children: React.ReactNode
|
||||
element: any
|
||||
fieldProps: RichTextProps
|
||||
}> = (props) => {
|
||||
const {
|
||||
attributes,
|
||||
children,
|
||||
element,
|
||||
fieldProps,
|
||||
} = props;
|
||||
const { relationTo, value } = element;
|
||||
const { collections, serverURL, routes: { api } } = useConfig();
|
||||
const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
|
||||
const [enabledCollectionSlugs] = useState(() => collections.filter(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship).map(({ slug }) => slug));
|
||||
const [relatedCollection, setRelatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const [{ data }] = usePayloadAPI(
|
||||
const { t, i18n } = useTranslation(['fields', 'general']);
|
||||
const editor = useSlateStatic();
|
||||
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0);
|
||||
const [{ data }, { setParams }] = usePayloadAPI(
|
||||
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
|
||||
{ initialParams },
|
||||
);
|
||||
|
||||
const [
|
||||
DocumentDrawer,
|
||||
DocumentDrawerToggler,
|
||||
{
|
||||
closeDrawer,
|
||||
},
|
||||
] = useDocumentDrawer({
|
||||
collectionSlug: relatedCollection.slug,
|
||||
id: value?.id,
|
||||
});
|
||||
|
||||
const [
|
||||
ListDrawer,
|
||||
ListDrawerToggler,
|
||||
{
|
||||
closeDrawer: closeListDrawer,
|
||||
},
|
||||
] = useListDrawer({
|
||||
collectionSlugs: enabledCollectionSlugs,
|
||||
selectedCollection: relatedCollection.slug,
|
||||
});
|
||||
|
||||
const removeRelationship = useCallback(() => {
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
Transforms.removeNodes(
|
||||
editor,
|
||||
{ at: elementPath },
|
||||
);
|
||||
}, [editor, element]);
|
||||
|
||||
const updateRelationship = React.useCallback(({ doc }) => {
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{
|
||||
type: 'relationship',
|
||||
value: { id: doc.id },
|
||||
relationTo: relatedCollection.slug,
|
||||
children: [
|
||||
{ text: ' ' },
|
||||
],
|
||||
},
|
||||
{ at: elementPath },
|
||||
);
|
||||
|
||||
setParams({
|
||||
...initialParams,
|
||||
cacheBust, // do this to get the usePayloadAPI to re-fetch the data even though the URL string hasn't changed
|
||||
});
|
||||
|
||||
closeDrawer();
|
||||
dispatchCacheBust();
|
||||
}, [editor, element, relatedCollection, cacheBust, setParams, closeDrawer]);
|
||||
|
||||
const swapRelationship = React.useCallback(({ docID, collectionConfig }) => {
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{
|
||||
type: 'relationship',
|
||||
value: { id: docID },
|
||||
relationTo: collectionConfig.slug,
|
||||
children: [
|
||||
{ text: ' ' },
|
||||
],
|
||||
},
|
||||
{ at: elementPath },
|
||||
);
|
||||
|
||||
setRelatedCollection(collections.find((coll) => coll.slug === collectionConfig.slug));
|
||||
|
||||
setParams({
|
||||
...initialParams,
|
||||
cacheBust, // do this to get the usePayloadAPI to re-fetch the data even though the URL string hasn't changed
|
||||
});
|
||||
|
||||
closeListDrawer();
|
||||
dispatchCacheBust();
|
||||
}, [closeListDrawer, editor, element, cacheBust, setParams, collections]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
@@ -37,13 +136,65 @@ const Element = (props) => {
|
||||
contentEditable={false}
|
||||
{...attributes}
|
||||
>
|
||||
<RelationshipIcon />
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<div className={`${baseClass}__label`}>
|
||||
<p className={`${baseClass}__label`}>
|
||||
{t('labelRelationship', { label: getTranslation(relatedCollection.labels.singular, i18n) })}
|
||||
</div>
|
||||
<h5>{data[relatedCollection?.admin?.useAsTitle || 'id']}</h5>
|
||||
</p>
|
||||
<p className={`${baseClass}__title`}>
|
||||
{data[relatedCollection?.admin?.useAsTitle || 'id']}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{value?.id && (
|
||||
<DocumentDrawerToggler
|
||||
className={`${baseClass}__toggler`}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
>
|
||||
<Button
|
||||
icon="edit"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
el="div"
|
||||
className={`${baseClass}__actionButton`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
tooltip={t('general:editLabel', { label: relatedCollection.labels.singular })}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
/>
|
||||
</DocumentDrawerToggler>
|
||||
)}
|
||||
<ListDrawerToggler disabled={fieldProps?.admin?.readOnly}>
|
||||
<Button
|
||||
icon="swap"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__actionButton`}
|
||||
onClick={() => {
|
||||
// do nothing
|
||||
}}
|
||||
el="div"
|
||||
tooltip={t('swapRelationship')}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
/>
|
||||
</ListDrawerToggler>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__actionButton`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeRelationship();
|
||||
}}
|
||||
tooltip={t('fields:removeRelationship')}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
/>
|
||||
</div>
|
||||
{value?.id && (
|
||||
<DocumentDrawer onSave={updateRelationship} />
|
||||
)}
|
||||
<ListDrawer onSelect={swapRelationship} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ElementType } from 'react';
|
||||
|
||||
export type ButtonProps = {
|
||||
format: string
|
||||
onClick?: (e: React.MouseEvent) => void
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
tooltip?: string
|
||||
el?: ElementType
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import '../../../../../../../scss/styles.scss';
|
||||
|
||||
.upload-rich-text-button {
|
||||
.btn {
|
||||
margin-right: base(1);
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import ElementButton from '../../Button';
|
||||
import UploadIcon from '../../../../../../icons/Upload';
|
||||
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
|
||||
import UploadGallery from '../../../../../../elements/UploadGallery';
|
||||
import ListControls from '../../../../../../elements/ListControls';
|
||||
import ReactSelect from '../../../../../../elements/ReactSelect';
|
||||
import Paginator from '../../../../../../elements/Paginator';
|
||||
import formatFields from '../../../../../../views/collections/List/formatFields';
|
||||
import Label from '../../../../../Label';
|
||||
import MinimalTemplate from '../../../../../../templates/Minimal';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
|
||||
import PerPage from '../../../../../../elements/PerPage';
|
||||
import { useListDrawer } from '../../../../../../elements/ListDrawer';
|
||||
import { injectVoidElement } from '../../injectVoid';
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
import '../addSwapModals.scss';
|
||||
|
||||
const baseClass = 'upload-rich-text-button';
|
||||
const baseModalClass = 'rich-text-upload-modal';
|
||||
|
||||
const insertUpload = (editor, { value, relationTo }) => {
|
||||
const text = { text: ' ' };
|
||||
@@ -42,178 +27,48 @@ const insertUpload = (editor, { value, relationTo }) => {
|
||||
ReactEditor.focus(editor);
|
||||
};
|
||||
|
||||
const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
const { t, i18n } = useTranslation('upload');
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const UploadButton: React.FC<{
|
||||
path: string
|
||||
}> = () => {
|
||||
const { t } = useTranslation(['upload', 'general']);
|
||||
const editor = useSlate();
|
||||
const { serverURL, routes: { api }, collections } = useConfig();
|
||||
const [availableCollections] = useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string }>(() => {
|
||||
const firstAvailableCollection = collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship));
|
||||
if (firstAvailableCollection) {
|
||||
return { label: getTranslation(firstAvailableCollection.labels.singular, i18n), value: firstAvailableCollection.slug };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const [
|
||||
ListDrawer,
|
||||
ListDrawerToggler,
|
||||
{
|
||||
closeDrawer,
|
||||
},
|
||||
] = useListDrawer({
|
||||
uploads: true,
|
||||
});
|
||||
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(() => collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
|
||||
const [fields, setFields] = useState(() => (modalCollection ? formatFields(modalCollection, t) : undefined));
|
||||
const [limit, setLimit] = useState<number>();
|
||||
const [sort, setSort] = useState(null);
|
||||
const [where, setWhere] = useState(null);
|
||||
const [page, setPage] = useState(null);
|
||||
|
||||
const modalSlug = `${path}-add-upload`;
|
||||
const moreThanOneAvailableCollection = availableCollections.length > 1;
|
||||
const isOpen = isModalOpen(modalSlug);
|
||||
|
||||
// If modal is open, get active page of upload gallery
|
||||
const apiURL = isOpen ? `${serverURL}${api}/${modalCollection.slug}` : null;
|
||||
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
|
||||
|
||||
useEffect(() => {
|
||||
if (modalCollection) {
|
||||
setFields(formatFields(modalCollection, t));
|
||||
}
|
||||
}, [modalCollection, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderModal) {
|
||||
toggleModal(modalSlug);
|
||||
}
|
||||
}, [renderModal, toggleModal, modalSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
const params: {
|
||||
page?: number
|
||||
sort?: string
|
||||
where?: unknown
|
||||
limit?: number
|
||||
} = {};
|
||||
|
||||
if (page) params.page = page;
|
||||
if (where) params.where = where;
|
||||
if (sort) params.sort = sort;
|
||||
if (limit) params.limit = limit;
|
||||
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, where, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalCollectionOption) {
|
||||
setModalCollection(collections.find(({ slug }) => modalCollectionOption.value === slug));
|
||||
}
|
||||
}, [modalCollectionOption, collections]);
|
||||
|
||||
if (!modalCollection) {
|
||||
return null;
|
||||
}
|
||||
const onSelect = useCallback(({ docID, collectionConfig }) => {
|
||||
insertUpload(editor, {
|
||||
value: {
|
||||
id: docID,
|
||||
},
|
||||
relationTo: collectionConfig.slug,
|
||||
});
|
||||
closeDrawer();
|
||||
}, [editor, closeDrawer]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ElementButton
|
||||
className={baseClass}
|
||||
format="upload"
|
||||
onClick={() => setRenderModal(true)}
|
||||
>
|
||||
<UploadIcon />
|
||||
</ElementButton>
|
||||
{renderModal && (
|
||||
<Modal
|
||||
className={baseModalClass}
|
||||
slug={modalSlug}
|
||||
<ListDrawerToggler>
|
||||
<ElementButton
|
||||
className={baseClass}
|
||||
format="upload"
|
||||
tooltip={t('fields:addUpload')}
|
||||
el="div"
|
||||
onClick={() => {
|
||||
// do nothing
|
||||
}}
|
||||
>
|
||||
{isOpen && (
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseModalClass}__header`}>
|
||||
<h1>
|
||||
{t('fields:addLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
onClick={() => {
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
{moreThanOneAvailableCollection && (
|
||||
<div className={`${baseModalClass}__select-collection-wrap`}>
|
||||
<Label label={t('selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
value={modalCollectionOption}
|
||||
onChange={setModalCollectionOption}
|
||||
options={availableCollections.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ListControls
|
||||
collection={{
|
||||
...modalCollection,
|
||||
fields,
|
||||
}}
|
||||
enableColumns={false}
|
||||
enableSort
|
||||
modifySearchQuery={false}
|
||||
handleSortChange={setSort}
|
||||
handleWhereChange={setWhere}
|
||||
/>
|
||||
<UploadGallery
|
||||
docs={data?.docs}
|
||||
collection={modalCollection}
|
||||
onCardClick={(doc) => {
|
||||
insertUpload(editor, {
|
||||
value: {
|
||||
id: doc.id,
|
||||
},
|
||||
relationTo: modalCollection.slug,
|
||||
});
|
||||
setRenderModal(false);
|
||||
toggleModal(modalSlug);
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseModalClass}__page-controls`}>
|
||||
<Paginator
|
||||
limit={data.limit}
|
||||
totalPages={data.totalPages}
|
||||
page={data.page}
|
||||
hasPrevPage={data.hasPrevPage}
|
||||
hasNextPage={data.hasNextPage}
|
||||
prevPage={data.prevPage}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
onChange={setPage}
|
||||
disableHistoryChange
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<Fragment>
|
||||
<div className={`${baseModalClass}__page-info`}>
|
||||
{data.page}
|
||||
-
|
||||
{data.totalPages > 1 ? data.limit : data.totalDocs}
|
||||
{' '}
|
||||
{t('general:of')}
|
||||
{' '}
|
||||
{data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
limits={modalCollection?.admin?.pagination?.limits}
|
||||
limit={limit}
|
||||
modifySearchParams={false}
|
||||
handleChange={setLimit}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</MinimalTemplate>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
<UploadIcon />
|
||||
</ElementButton>
|
||||
</ListDrawerToggler>
|
||||
<ListDrawer onSelect={onSelect} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
@import '../../../../../../../../scss/styles.scss';
|
||||
|
||||
.edit-upload-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.template-minimal {
|
||||
padding-top: base(4);
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: $baseline;
|
||||
display: flex;
|
||||
|
||||
h1 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 0 0 $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text__toolbar {
|
||||
top: base(1);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Transforms, Element } from 'slate';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
import { Modal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../../../../../utilities/Auth';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
|
||||
import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema';
|
||||
import MinimalTemplate from '../../../../../../../templates/Minimal';
|
||||
import Button from '../../../../../../../elements/Button';
|
||||
import RenderFields from '../../../../../../RenderFields';
|
||||
import fieldTypes from '../../../../..';
|
||||
import Form from '../../../../../../Form';
|
||||
import Submit from '../../../../../../Submit';
|
||||
import { Field } from '../../../../../../../../../fields/config/types';
|
||||
import { useLocale } from '../../../../../../../utilities/Locale';
|
||||
import { getTranslation } from '../../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'edit-upload-modal';
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
closeModal: () => void
|
||||
relatedCollectionConfig: SanitizedCollectionConfig
|
||||
fieldSchema: Field[]
|
||||
element: Element & {
|
||||
fields: Field[]
|
||||
}
|
||||
}
|
||||
export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollectionConfig, fieldSchema, element }) => {
|
||||
const editor = useSlateStatic();
|
||||
const [initialState, setInitialState] = useState({});
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const handleUpdateEditData = useCallback((_, data) => {
|
||||
const newNode = {
|
||||
fields: data,
|
||||
};
|
||||
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: elementPath },
|
||||
);
|
||||
closeModal();
|
||||
}, [closeModal, editor, element]);
|
||||
|
||||
useEffect(() => {
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema({ fieldSchema, data: { ...element?.fields || {} }, user, operation: 'update', locale, t });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [fieldSchema, element.fields, user, locale, t]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
slug={slug}
|
||||
className={baseClass}
|
||||
>
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
{ t('editLabelData', { label: getTranslation(relatedCollectionConfig.labels.singular, i18n) }) }
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
<Form
|
||||
onSubmit={handleUpdateEditData}
|
||||
initialState={initialState}
|
||||
>
|
||||
<RenderFields
|
||||
readOnly={false}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fieldSchema}
|
||||
/>
|
||||
<Submit>
|
||||
{t('saveChanges')}
|
||||
</Submit>
|
||||
</Form>
|
||||
</div>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,185 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Modal } from '@faceless-ui/modal';
|
||||
import { Element, Transforms } from 'slate';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../../utilities/Config';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
|
||||
import usePayloadAPI from '../../../../../../../../hooks/usePayloadAPI';
|
||||
import MinimalTemplate from '../../../../../../../templates/Minimal';
|
||||
import Button from '../../../../../../../elements/Button';
|
||||
import Label from '../../../../../../Label';
|
||||
import ReactSelect from '../../../../../../../elements/ReactSelect';
|
||||
import ListControls from '../../../../../../../elements/ListControls';
|
||||
import UploadGallery from '../../../../../../../elements/UploadGallery';
|
||||
import Paginator from '../../../../../../../elements/Paginator';
|
||||
import PerPage from '../../../../../../../elements/PerPage';
|
||||
import formatFields from '../../../../../../../views/collections/List/formatFields';
|
||||
import { getTranslation } from '../../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import '../../addSwapModals.scss';
|
||||
|
||||
const baseClass = 'rich-text-upload-modal';
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
element: Element
|
||||
closeModal: () => void
|
||||
setRelatedCollectionConfig: (collectionConfig: SanitizedCollectionConfig) => void
|
||||
relatedCollectionConfig: SanitizedCollectionConfig
|
||||
}
|
||||
export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelatedCollectionConfig, relatedCollectionConfig, slug }) => {
|
||||
const { collections, serverURL, routes: { api } } = useConfig();
|
||||
const editor = useSlateStatic();
|
||||
const { t, i18n } = useTranslation('upload');
|
||||
|
||||
const [modalCollection, setModalCollection] = React.useState(relatedCollectionConfig);
|
||||
const [modalCollectionOption, setModalCollectionOption] = React.useState<{ label: string, value: string }>({ label: getTranslation(relatedCollectionConfig.labels.singular, i18n), value: relatedCollectionConfig.slug });
|
||||
const [availableCollections] = React.useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
|
||||
const [fields, setFields] = React.useState(() => formatFields(modalCollection, t));
|
||||
|
||||
const [limit, setLimit] = React.useState<number>();
|
||||
const [sort, setSort] = React.useState(null);
|
||||
const [where, setWhere] = React.useState(null);
|
||||
const [page, setPage] = React.useState(null);
|
||||
|
||||
const moreThanOneAvailableCollection = availableCollections.length > 1;
|
||||
|
||||
const apiURL = `${serverURL}${api}/${modalCollection.slug}`;
|
||||
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
|
||||
|
||||
const handleUpdateUpload = React.useCallback((doc) => {
|
||||
const newNode = {
|
||||
type: 'upload',
|
||||
value: { id: doc.id },
|
||||
relationTo: modalCollection.slug,
|
||||
children: [
|
||||
{ text: ' ' },
|
||||
],
|
||||
};
|
||||
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: elementPath },
|
||||
);
|
||||
closeModal();
|
||||
}, [closeModal, editor, element, modalCollection]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const params: {
|
||||
page?: number
|
||||
sort?: string
|
||||
where?: unknown
|
||||
limit?: number
|
||||
} = {};
|
||||
|
||||
if (page) params.page = page;
|
||||
if (where) params.where = where;
|
||||
if (sort) params.sort = sort;
|
||||
if (limit) params.limit = limit;
|
||||
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, where, limit]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFields(formatFields(modalCollection, t));
|
||||
setLimit(modalCollection.admin.pagination.defaultLimit);
|
||||
}, [modalCollection, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setModalCollection(collections.find(({ slug: collectionSlug }) => modalCollectionOption.value === collectionSlug));
|
||||
}, [modalCollectionOption, collections]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={baseClass}
|
||||
slug={slug}
|
||||
>
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
{t('chooseLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</header>
|
||||
{
|
||||
moreThanOneAvailableCollection && (
|
||||
<div className={`${baseClass}__select-collection-wrap`}>
|
||||
<Label label={t('selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
value={modalCollectionOption}
|
||||
onChange={setModalCollectionOption}
|
||||
options={availableCollections.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ListControls
|
||||
collection={
|
||||
{
|
||||
...modalCollection,
|
||||
fields,
|
||||
}
|
||||
}
|
||||
enableColumns={false}
|
||||
enableSort
|
||||
modifySearchQuery={false}
|
||||
handleSortChange={setSort}
|
||||
handleWhereChange={setWhere}
|
||||
/>
|
||||
<UploadGallery
|
||||
docs={data?.docs}
|
||||
collection={modalCollection}
|
||||
onCardClick={(doc) => {
|
||||
handleUpdateUpload(doc);
|
||||
setRelatedCollectionConfig(modalCollection);
|
||||
closeModal();
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Paginator
|
||||
limit={data.limit}
|
||||
totalPages={data.totalPages}
|
||||
page={data.page}
|
||||
hasPrevPage={data.hasPrevPage}
|
||||
hasNextPage={data.hasNextPage}
|
||||
prevPage={data.prevPage}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
onChange={setPage}
|
||||
disableHistoryChange
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{data.page}
|
||||
-
|
||||
{data.totalPages > 1 ? data.limit : data.totalDocs}
|
||||
{' '}
|
||||
{t('general:of')}
|
||||
{' '}
|
||||
{data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
limits={modalCollection?.admin?.pagination?.limits}
|
||||
limit={limit}
|
||||
modifySearchParams={false}
|
||||
handleChange={setLimit}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -51,7 +51,7 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: base(.75) base(1);
|
||||
padding: base(.75);
|
||||
justify-content: space-between;
|
||||
max-width: calc(100% - #{base(3.25)});
|
||||
}
|
||||
@@ -60,19 +60,35 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: base(0.5);
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: base(.25);
|
||||
}
|
||||
}
|
||||
|
||||
&__actionButton {
|
||||
margin: 0;
|
||||
margin-right: base(.5);
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
line {
|
||||
stroke-width: $style-stroke-width-m;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import React, { HTMLAttributes, useCallback, useReducer, useState } from 'react';
|
||||
import { Transforms } from 'slate';
|
||||
import { ReactEditor, useSlateStatic, useFocused, useSelected } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,10 +7,11 @@ import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
|
||||
import FileGraphic from '../../../../../../graphics/File';
|
||||
import useThumbnail from '../../../../../../../hooks/useThumbnail';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
|
||||
import { SwapUploadModal } from './SwapUploadModal';
|
||||
import { EditModal } from './EditModal';
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
|
||||
import { useDocumentDrawer } from '../../../../../../elements/DocumentDrawer';
|
||||
import { useListDrawer } from '../../../../../../elements/ListDrawer';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
|
||||
import { Props as RichTextProps } from '../../../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -21,27 +21,56 @@ const initialParams = {
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
const { relationTo, value } = element;
|
||||
const { toggleModal } = useModal();
|
||||
const Element: React.FC<{
|
||||
attributes: HTMLAttributes<HTMLDivElement>
|
||||
children: React.ReactNode
|
||||
element: any
|
||||
fieldProps: RichTextProps
|
||||
}> = ({ attributes, children, element }) => {
|
||||
const {
|
||||
relationTo,
|
||||
value,
|
||||
fieldProps,
|
||||
} = element;
|
||||
|
||||
const { collections, serverURL, routes: { api } } = useConfig();
|
||||
const [modalToRender, setModalToRender] = useState(undefined);
|
||||
const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo));
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0);
|
||||
const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo));
|
||||
|
||||
const [
|
||||
ListDrawer,
|
||||
ListDrawerToggler,
|
||||
{
|
||||
closeDrawer: closeListDrawer,
|
||||
},
|
||||
] = useListDrawer({
|
||||
uploads: true,
|
||||
selectedCollection: relatedCollection.slug,
|
||||
});
|
||||
|
||||
const [
|
||||
DocumentDrawer,
|
||||
DocumentDrawerToggler,
|
||||
{
|
||||
closeDrawer,
|
||||
},
|
||||
] = useDocumentDrawer({
|
||||
collectionSlug: relatedCollection.slug,
|
||||
id: value?.id,
|
||||
});
|
||||
|
||||
const editor = useSlateStatic();
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
|
||||
const modalSlug = `${path}-edit-upload-${modalToRender}`;
|
||||
|
||||
// Get the referenced document
|
||||
const [{ data: upload }] = usePayloadAPI(
|
||||
const [{ data }, { setParams }] = usePayloadAPI(
|
||||
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
|
||||
{ initialParams },
|
||||
);
|
||||
|
||||
const thumbnailSRC = useThumbnail(relatedCollection, upload);
|
||||
const thumbnailSRC = useThumbnail(relatedCollection, data);
|
||||
|
||||
const removeUpload = useCallback(() => {
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
@@ -52,18 +81,56 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
);
|
||||
}, [editor, element]);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
toggleModal(modalSlug);
|
||||
setModalToRender(null);
|
||||
}, [toggleModal, modalSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalToRender) {
|
||||
toggleModal(modalSlug);
|
||||
}
|
||||
}, [modalToRender, toggleModal, modalSlug]);
|
||||
const updateUpload = useCallback((json) => {
|
||||
const { doc } = json;
|
||||
|
||||
const fieldSchema = fieldProps?.admin?.upload?.collections?.[relatedCollection.slug]?.fields;
|
||||
const newNode = {
|
||||
fields: doc,
|
||||
};
|
||||
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: elementPath },
|
||||
);
|
||||
|
||||
// setRelatedCollection(collections.find((coll) => coll.slug === collectionConfig.slug));
|
||||
|
||||
setParams({
|
||||
...initialParams,
|
||||
cacheBust, // do this to get the usePayloadAPI to re-fetch the data even though the URL string hasn't changed
|
||||
});
|
||||
|
||||
dispatchCacheBust();
|
||||
closeDrawer();
|
||||
}, [editor, element, setParams, cacheBust, closeDrawer]);
|
||||
|
||||
const swapUpload = React.useCallback(({ docID, collectionConfig }) => {
|
||||
const newNode = {
|
||||
type: 'upload',
|
||||
value: { id: docID },
|
||||
relationTo: collectionConfig.slug,
|
||||
children: [
|
||||
{ text: ' ' },
|
||||
],
|
||||
};
|
||||
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
|
||||
setRelatedCollection(collections.find((coll) => coll.slug === collectionConfig.slug));
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: elementPath },
|
||||
);
|
||||
|
||||
dispatchCacheBust();
|
||||
closeListDrawer();
|
||||
}, [closeListDrawer, editor, element, collections]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -80,7 +147,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
{thumbnailSRC ? (
|
||||
<img
|
||||
src={thumbnailSRC}
|
||||
alt={upload?.filename}
|
||||
alt={data?.filename}
|
||||
/>
|
||||
) : (
|
||||
<FileGraphic />
|
||||
@@ -91,30 +158,36 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
{getTranslation(relatedCollection.labels.singular, i18n)}
|
||||
</div>
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{fieldSchema && (
|
||||
{value?.id && (
|
||||
<DocumentDrawerToggler className={`${baseClass}__toggler`}>
|
||||
<Button
|
||||
icon="edit"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
el="div"
|
||||
className={`${baseClass}__actionButton`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
tooltip={t('general:editLabel', { label: relatedCollection.labels.singular })}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
/>
|
||||
</DocumentDrawerToggler>
|
||||
)}
|
||||
<ListDrawerToggler>
|
||||
<Button
|
||||
icon="edit"
|
||||
icon="swap"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__actionButton`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setModalToRender('edit');
|
||||
onClick={() => {
|
||||
// do nothing
|
||||
}}
|
||||
tooltip={t('general:edit')}
|
||||
el="div"
|
||||
tooltip={t('swapUpload')}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon="swap"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__actionButton`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setModalToRender('swap');
|
||||
}}
|
||||
tooltip={t('swapUpload')}
|
||||
/>
|
||||
</ListDrawerToggler>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
@@ -125,36 +198,22 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
removeUpload();
|
||||
}}
|
||||
tooltip={t('removeUpload')}
|
||||
disabled={fieldProps?.admin?.readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${baseClass}__bottomRow`}>
|
||||
<strong>{upload?.filename}</strong>
|
||||
<strong>
|
||||
{data?.filename}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
|
||||
{modalToRender === 'swap' && (
|
||||
<SwapUploadModal
|
||||
slug={modalSlug}
|
||||
element={element}
|
||||
closeModal={closeModal}
|
||||
setRelatedCollectionConfig={setRelatedCollection}
|
||||
relatedCollectionConfig={relatedCollection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(modalToRender === 'edit' && fieldSchema) && (
|
||||
<EditModal
|
||||
slug={modalSlug}
|
||||
closeModal={closeModal}
|
||||
relatedCollectionConfig={relatedCollection}
|
||||
fieldSchema={fieldSchema}
|
||||
element={element}
|
||||
/>
|
||||
{value?.id && (
|
||||
<DocumentDrawer onSave={updateUpload} />
|
||||
)}
|
||||
<ListDrawer onSelect={swapUpload} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.rich-text-upload-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.template-minimal {
|
||||
padding-top: base(4);
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: $baseline;
|
||||
display: flex;
|
||||
|
||||
h1 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 0 0 $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&__select-collection-wrap {
|
||||
margin-bottom: base(1);
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__page-controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
.paginator {
|
||||
width: 100%;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
&__page-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
@import '../../../../../scss/styles.scss';
|
||||
|
||||
.add-upload-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.template-minimal {
|
||||
padding-top: base(6);
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: $baseline;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 0 0 $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&__sub-header {
|
||||
margin-top: base(.25);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
import { useAuth } from '../../../../utilities/Auth';
|
||||
import MinimalTemplate from '../../../../templates/Minimal';
|
||||
import Form from '../../../Form';
|
||||
import Button from '../../../../elements/Button';
|
||||
import RenderFields from '../../../RenderFields';
|
||||
import FormSubmit from '../../../Submit';
|
||||
import Upload from '../../../../views/collections/Edit/Upload';
|
||||
import ViewDescription from '../../../../elements/ViewDescription';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'add-upload-modal';
|
||||
|
||||
const AddUploadModal: React.FC<Props> = (props) => {
|
||||
const {
|
||||
collection,
|
||||
collection: {
|
||||
admin: {
|
||||
description,
|
||||
} = {},
|
||||
} = {},
|
||||
slug,
|
||||
fieldTypes,
|
||||
setValue,
|
||||
} = props;
|
||||
|
||||
const { permissions } = useAuth();
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal } = useModal();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const onSuccess = useCallback((json) => {
|
||||
toggleModal(slug);
|
||||
setValue(json.doc);
|
||||
}, [toggleModal, slug, setValue]);
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[collection.slug]?.fields;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={classes}
|
||||
slug={slug}
|
||||
>
|
||||
<MinimalTemplate width="wide">
|
||||
<Form
|
||||
method="post"
|
||||
action={`${serverURL}${api}/${collection.slug}`}
|
||||
onSuccess={onSuccess}
|
||||
disableSuccessStatus
|
||||
validationOperation="create"
|
||||
>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div>
|
||||
<h1>
|
||||
{t('newLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<FormSubmit>{t('general:save')}</FormSubmit>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
onClick={() => toggleModal(slug)}
|
||||
/>
|
||||
</div>
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<Upload
|
||||
collection={collection}
|
||||
/>
|
||||
<RenderFields
|
||||
permissions={collectionPermissions}
|
||||
readOnly={false}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={collection.fields}
|
||||
/>
|
||||
</Form>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUploadModal;
|
||||
@@ -1,9 +0,0 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
import { FieldTypes } from '../..';
|
||||
|
||||
export type Props = {
|
||||
setValue: (val: { id: string } | null) => void
|
||||
collection: SanitizedCollectionConfig
|
||||
slug: string
|
||||
fieldTypes: FieldTypes
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../../elements/Button';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import FileDetails from '../../../elements/FileDetails';
|
||||
@@ -9,11 +7,13 @@ import FieldDescription from '../../FieldDescription';
|
||||
import { FilterOptions, UploadField } from '../../../../../fields/config/types';
|
||||
import { Description } from '../../FieldDescription/types';
|
||||
import { FieldTypes } from '..';
|
||||
import AddModal from './Add';
|
||||
import SelectExistingModal from './SelectExisting';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { useEditDepth, EditDepthContext } from '../../../utilities/EditDepth';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { useDocumentDrawer } from '../../../elements/DocumentDrawer';
|
||||
import { useListDrawer } from '../../../elements/ListDrawer';
|
||||
import Button from '../../../elements/Button';
|
||||
import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types';
|
||||
import { ListDrawerProps } from '../../../elements/ListDrawer/types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -50,7 +50,6 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
description,
|
||||
label,
|
||||
relationTo,
|
||||
fieldTypes,
|
||||
value,
|
||||
onChange,
|
||||
showError,
|
||||
@@ -58,19 +57,33 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
api = '/api',
|
||||
collection,
|
||||
errorMessage,
|
||||
filterOptions,
|
||||
} = props;
|
||||
|
||||
const { toggleModal, modalState } = useModal();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
const addModalSlug = `${path}-add-depth-${editDepth}`;
|
||||
const selectExistingModalSlug = `${path}-select-existing-depth-${editDepth}`;
|
||||
|
||||
const [file, setFile] = useState(undefined);
|
||||
const [missingFile, setMissingFile] = useState(false);
|
||||
const [modalToRender, setModalToRender] = useState<string>();
|
||||
const [collectionSlugs] = useState([collection?.slug]);
|
||||
|
||||
const [
|
||||
DocumentDrawer,
|
||||
DocumentDrawerToggler,
|
||||
{
|
||||
closeDrawer,
|
||||
},
|
||||
] = useDocumentDrawer({
|
||||
collectionSlug: collectionSlugs[0],
|
||||
});
|
||||
|
||||
const [
|
||||
ListDrawer,
|
||||
ListDrawerToggler,
|
||||
{
|
||||
closeDrawer: closeListDrawer,
|
||||
},
|
||||
] = useListDrawer({
|
||||
collectionSlugs,
|
||||
});
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
@@ -110,11 +123,19 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
i18n,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modalState[addModalSlug]?.isOpen && !modalState[selectExistingModalSlug]?.isOpen) {
|
||||
setModalToRender(undefined);
|
||||
}
|
||||
}, [modalState, addModalSlug, selectExistingModalSlug]);
|
||||
const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => {
|
||||
setMissingFile(false);
|
||||
onChange(args.doc);
|
||||
closeDrawer();
|
||||
}, [onChange, closeDrawer]);
|
||||
|
||||
const onSelect = useCallback<ListDrawerProps['onSelect']>((args) => {
|
||||
setMissingFile(false);
|
||||
onChange({
|
||||
id: args.docID,
|
||||
});
|
||||
closeListDrawer();
|
||||
}, [onChange, closeListDrawer]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -146,62 +167,38 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
)}
|
||||
{(!file || missingFile) && (
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
toggleModal(addModalSlug);
|
||||
setModalToRender(addModalSlug);
|
||||
}}
|
||||
>
|
||||
{t('uploadNewLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</Button>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
toggleModal(selectExistingModalSlug);
|
||||
setModalToRender(selectExistingModalSlug);
|
||||
}}
|
||||
>
|
||||
{t('chooseFromExisting')}
|
||||
</Button>
|
||||
<div className={`${baseClass}__buttons`}>
|
||||
<DocumentDrawerToggler
|
||||
className={`${baseClass}__toggler`}
|
||||
>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
el="div"
|
||||
>
|
||||
{t('uploadNewLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</Button>
|
||||
</DocumentDrawerToggler>
|
||||
<ListDrawerToggler
|
||||
className={`${baseClass}__toggler`}
|
||||
>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
el="div"
|
||||
>
|
||||
{t('chooseFromExisting')}
|
||||
</Button>
|
||||
</ListDrawerToggler>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<EditDepthContext.Provider value={editDepth + 1}>
|
||||
{modalToRender === addModalSlug && (
|
||||
<AddModal
|
||||
{...{
|
||||
collection,
|
||||
slug: addModalSlug,
|
||||
fieldTypes,
|
||||
setValue: (e) => {
|
||||
setMissingFile(false);
|
||||
onChange(e);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{modalToRender === selectExistingModalSlug && (
|
||||
<SelectExistingModal
|
||||
{...{
|
||||
collection,
|
||||
slug: selectExistingModalSlug,
|
||||
setValue: (e) => {
|
||||
setMissingFile(false);
|
||||
onChange(e);
|
||||
},
|
||||
addModalSlug,
|
||||
filterOptions,
|
||||
path,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EditDepthContext.Provider>
|
||||
<FieldDescription
|
||||
value={file}
|
||||
description={description}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<DocumentDrawer onSave={onSave} />
|
||||
<ListDrawer onSelect={onSelect} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
@import '../../../../../scss/styles.scss';
|
||||
|
||||
.select-existing-upload-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.template-minimal {
|
||||
padding-top: base(6);
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: $baseline;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 0 0 $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&__sub-header {
|
||||
margin-top: base(.25);
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-right: base(1);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__page-controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
.paginator {
|
||||
width: 100%;
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
&__page-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__page-info {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import equal from 'deep-equal';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
import { useAuth } from '../../../../utilities/Auth';
|
||||
import { Where } from '../../../../../../types';
|
||||
import MinimalTemplate from '../../../../templates/Minimal';
|
||||
import Button from '../../../../elements/Button';
|
||||
import usePayloadAPI from '../../../../../hooks/usePayloadAPI';
|
||||
import ListControls from '../../../../elements/ListControls';
|
||||
import Paginator from '../../../../elements/Paginator';
|
||||
import UploadGallery from '../../../../elements/UploadGallery';
|
||||
import { Props } from './types';
|
||||
import PerPage from '../../../../elements/PerPage';
|
||||
import formatFields from '../../../../views/collections/List/formatFields';
|
||||
import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
|
||||
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
|
||||
import { useForm } from '../../../Form/context';
|
||||
import ViewDescription from '../../../../elements/ViewDescription';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'select-existing-upload-modal';
|
||||
|
||||
const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
const {
|
||||
setValue,
|
||||
collection,
|
||||
collection: {
|
||||
slug: collectionSlug,
|
||||
admin: {
|
||||
description,
|
||||
pagination: {
|
||||
defaultLimit,
|
||||
},
|
||||
} = {},
|
||||
} = {},
|
||||
slug: modalSlug,
|
||||
path,
|
||||
filterOptions,
|
||||
} = props;
|
||||
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { id } = useDocumentInfo();
|
||||
const { user } = useAuth();
|
||||
const { getData, getSiblingData } = useForm();
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const [fields] = useState(() => formatFields(collection, t));
|
||||
const [limit, setLimit] = useState(defaultLimit);
|
||||
const [sort, setSort] = useState(null);
|
||||
const [where, setWhere] = useState(null);
|
||||
const [page, setPage] = useState(null);
|
||||
const [optionFilters, setOptionFilters] = useState<Where>();
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const isOpen = isModalOpen(modalSlug);
|
||||
|
||||
const apiURL = isOpen ? `${serverURL}${api}/${collectionSlug}` : null;
|
||||
|
||||
const [{ data }, { setParams }] = usePayloadAPI(apiURL, {});
|
||||
|
||||
useEffect(() => {
|
||||
const params: {
|
||||
page?: number
|
||||
sort?: string
|
||||
where?: unknown
|
||||
limit?: number
|
||||
} = {};
|
||||
|
||||
if (page) params.page = page;
|
||||
if (where) params.where = { and: [where, optionFilters] };
|
||||
if (sort) params.sort = sort;
|
||||
if (limit) params.limit = limit;
|
||||
|
||||
setParams(params);
|
||||
}, [setParams, page, sort, where, limit, optionFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterOptions || !isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
|
||||
id,
|
||||
relationTo: collectionSlug,
|
||||
data: getData(),
|
||||
siblingData: getSiblingData(path),
|
||||
user,
|
||||
})[collectionSlug];
|
||||
if (!equal(newOptionFilters, optionFilters)) {
|
||||
setOptionFilters(newOptionFilters);
|
||||
}
|
||||
}, [collectionSlug, filterOptions, optionFilters, id, getData, getSiblingData, path, user, isOpen]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={classes}
|
||||
slug={modalSlug}
|
||||
>
|
||||
{isOpen && (
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div>
|
||||
<h1>
|
||||
{t('selectExistingLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
/>
|
||||
</div>
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<ListControls
|
||||
collection={{
|
||||
...collection,
|
||||
fields,
|
||||
}}
|
||||
enableColumns={false}
|
||||
enableSort
|
||||
modifySearchQuery={false}
|
||||
handleSortChange={setSort}
|
||||
handleWhereChange={setWhere}
|
||||
/>
|
||||
<UploadGallery
|
||||
docs={data?.docs}
|
||||
collection={collection}
|
||||
onCardClick={(doc) => {
|
||||
setValue(doc);
|
||||
toggleModal(modalSlug);
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Paginator
|
||||
limit={data.limit}
|
||||
totalPages={data.totalPages}
|
||||
page={data.page}
|
||||
hasPrevPage={data.hasPrevPage}
|
||||
hasNextPage={data.hasNextPage}
|
||||
prevPage={data.prevPage}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
onChange={setPage}
|
||||
disableHistoryChange
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{data.page}
|
||||
-
|
||||
{data.totalPages > 1 ? data.limit : data.totalDocs}
|
||||
{' '}
|
||||
{t('general:of')}
|
||||
{' '}
|
||||
{data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
limits={collection?.admin?.pagination?.limits}
|
||||
limit={limit}
|
||||
modifySearchParams={false}
|
||||
handleChange={setLimit}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</MinimalTemplate>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectExistingUploadModal;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
import { FilterOptions } from '../../../../../../fields/config/types';
|
||||
|
||||
export type Props = {
|
||||
setValue: (val: { id: string } | null) => void
|
||||
collection: SanitizedCollectionConfig
|
||||
slug: string
|
||||
path
|
||||
filterOptions: FilterOptions
|
||||
}
|
||||
@@ -5,26 +5,38 @@
|
||||
margin-bottom: $baseline;
|
||||
|
||||
&__wrap {
|
||||
display: flex;
|
||||
padding: base(1.5) base(1.5) $baseline;
|
||||
background: var(--theme-elevation-50);
|
||||
padding: base(1);
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
margin: base(-0.25);
|
||||
width: calc(100% + #{base(0.5)});
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
margin: 0 $baseline base(.5) 0;
|
||||
min-width: base(7);
|
||||
margin: 0
|
||||
}
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
margin: base(0.25);
|
||||
min-width: base(7);
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__wrap {
|
||||
display: block;
|
||||
padding: $baseline $baseline base(.5);
|
||||
padding: base(0.75);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
min-width: initial;
|
||||
}
|
||||
&__buttons {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
width: calc(100% - #{base(0.5)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,11 +199,13 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
}, [getVersions]);
|
||||
|
||||
useEffect(() => {
|
||||
const getDocPreferences = async () => {
|
||||
await getPreference<DocumentPreferences>(preferencesKey);
|
||||
};
|
||||
if (preferencesKey) {
|
||||
const getDocPreferences = async () => {
|
||||
await getPreference<DocumentPreferences>(preferencesKey);
|
||||
};
|
||||
|
||||
getDocPreferences();
|
||||
getDocPreferences();
|
||||
}
|
||||
}, [getPreference, preferencesKey]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
const DefaultCell: React.FC<Props> = (props) => {
|
||||
const {
|
||||
field,
|
||||
colIndex,
|
||||
collection: {
|
||||
slug,
|
||||
},
|
||||
@@ -18,6 +17,9 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
rowData: {
|
||||
id,
|
||||
} = {},
|
||||
link = true,
|
||||
onClick,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const { routes: { admin } } = useConfig();
|
||||
@@ -27,13 +29,26 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
|
||||
const wrapElementProps: {
|
||||
to?: string
|
||||
} = {};
|
||||
onClick?: () => void
|
||||
type?: 'button'
|
||||
className?: string
|
||||
} = {
|
||||
className,
|
||||
};
|
||||
|
||||
if (colIndex === 0) {
|
||||
if (link) {
|
||||
WrapElement = Link;
|
||||
wrapElementProps.to = `${admin}/collections/${slug}/${id}`;
|
||||
}
|
||||
|
||||
if (typeof onClick === 'function') {
|
||||
WrapElement = 'button';
|
||||
wrapElementProps.type = 'button';
|
||||
wrapElementProps.onClick = () => {
|
||||
onClick(props);
|
||||
};
|
||||
}
|
||||
|
||||
const CellComponent = cellData && cellComponents[field.type];
|
||||
|
||||
if (!CellComponent) {
|
||||
@@ -71,6 +86,9 @@ const Cell: React.FC<Props> = (props) => {
|
||||
} = {},
|
||||
} = {},
|
||||
},
|
||||
link,
|
||||
onClick,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -81,6 +99,9 @@ const Cell: React.FC<Props> = (props) => {
|
||||
cellData,
|
||||
collection,
|
||||
field,
|
||||
link,
|
||||
onClick,
|
||||
className,
|
||||
}}
|
||||
CustomComponent={CustomCell}
|
||||
DefaultComponent={DefaultCell}
|
||||
|
||||
@@ -9,4 +9,7 @@ export type Props = {
|
||||
rowData: {
|
||||
[path: string]: unknown
|
||||
}
|
||||
link?: boolean
|
||||
onClick?: (Props) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
@@ -42,6 +42,15 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
columnNames,
|
||||
setColumns,
|
||||
hasCreatePermission,
|
||||
disableEyebrow,
|
||||
modifySearchParams,
|
||||
disableCardLink,
|
||||
onCardClick,
|
||||
handleSortChange,
|
||||
handleWhereChange,
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
customHeader,
|
||||
} = props;
|
||||
|
||||
const { routes: { admin } } = useConfig();
|
||||
@@ -53,19 +62,28 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
<Meta
|
||||
title={getTranslation(collection.labels.plural, i18n)}
|
||||
/>
|
||||
<Eyebrow />
|
||||
{!disableEyebrow && (
|
||||
<Eyebrow />
|
||||
)}
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>{getTranslation(pluralLabel, i18n)}</h1>
|
||||
{hasCreatePermission && (
|
||||
<Pill to={newDocumentURL}>
|
||||
{t('createNew')}
|
||||
</Pill>
|
||||
)}
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
{customHeader && customHeader}
|
||||
{!customHeader && (
|
||||
<Fragment>
|
||||
<h1>
|
||||
{getTranslation(pluralLabel, i18n)}
|
||||
</h1>
|
||||
{hasCreatePermission && (
|
||||
<Pill to={newDocumentURL}>
|
||||
{t('createNew')}
|
||||
</Pill>
|
||||
)}
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</header>
|
||||
<ListControls
|
||||
@@ -74,22 +92,28 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
setColumns={setColumns}
|
||||
enableColumns={Boolean(!upload)}
|
||||
enableSort={Boolean(upload)}
|
||||
modifySearchQuery={modifySearchParams}
|
||||
handleSortChange={handleSortChange}
|
||||
handleWhereChange={handleWhereChange}
|
||||
/>
|
||||
{(data.docs && data.docs.length > 0) && (
|
||||
<React.Fragment>
|
||||
{!upload && (
|
||||
<RelationshipProvider>
|
||||
<Table
|
||||
data={data.docs}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
</RelationshipProvider>
|
||||
<RelationshipProvider>
|
||||
<Table
|
||||
data={data.docs}
|
||||
columns={tableColumns}
|
||||
/>
|
||||
</RelationshipProvider>
|
||||
)}
|
||||
{upload && (
|
||||
<UploadGallery
|
||||
docs={data.docs}
|
||||
collection={collection}
|
||||
onCardClick={(doc) => history.push(`${admin}/collections/${slug}/${doc.id}`)}
|
||||
onCardClick={(doc) => {
|
||||
if (typeof onCardClick === 'function') onCardClick(doc);
|
||||
if (!disableCardLink) history.push(`${admin}/collections/${slug}/${doc.id}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
@@ -99,7 +123,7 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
<p>
|
||||
{t('noResults', { label: getTranslation(pluralLabel, i18n) })}
|
||||
</p>
|
||||
{hasCreatePermission && (
|
||||
{hasCreatePermission && newDocumentURL && (
|
||||
<Button
|
||||
el="link"
|
||||
to={newDocumentURL}
|
||||
@@ -119,6 +143,8 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
prevPage={data.prevPage}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
disableHistoryChange={modifySearchParams === false}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<Fragment>
|
||||
@@ -134,6 +160,8 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
<PerPage
|
||||
limits={collection?.admin?.pagination?.limits}
|
||||
limit={limit}
|
||||
modifySearchParams={modifySearchParams}
|
||||
handleChange={handlePerPageChange}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,19 @@ import { SanitizedCollectionConfig } from '../../../../../collections/config/typ
|
||||
import { Column } from '../../../elements/Table/types';
|
||||
import { fieldIsPresentationalOnly } from '../../../../../fields/config/types';
|
||||
import flattenFields from '../../../../../utilities/flattenTopLevelFields';
|
||||
import { Props as CellProps } from './Cell/types';
|
||||
|
||||
const buildColumns = (collection: SanitizedCollectionConfig, columns: string[], t: TFunction): Column[] => {
|
||||
const buildColumns = ({
|
||||
collection,
|
||||
columns,
|
||||
t,
|
||||
cellProps,
|
||||
}: {
|
||||
collection: SanitizedCollectionConfig,
|
||||
columns: string[],
|
||||
t: TFunction,
|
||||
cellProps?: Partial<CellProps>[]
|
||||
}): Column[] => {
|
||||
const flattenedFields = flattenFields([
|
||||
...collection.fields,
|
||||
{
|
||||
@@ -49,16 +60,20 @@ const buildColumns = (collection: SanitizedCollectionConfig, columns: string[],
|
||||
disable={(field.disableSort || fieldIsPresentationalOnly(field)) || undefined}
|
||||
/>
|
||||
),
|
||||
renderCell: (rowData, cellData) => (
|
||||
<Cell
|
||||
key={JSON.stringify(cellData)}
|
||||
field={field}
|
||||
colIndex={colIndex}
|
||||
collection={collection}
|
||||
rowData={rowData}
|
||||
cellData={cellData}
|
||||
/>
|
||||
),
|
||||
renderCell: (rowData, cellData) => {
|
||||
return (
|
||||
<Cell
|
||||
key={JSON.stringify(cellData)}
|
||||
field={field}
|
||||
colIndex={colIndex}
|
||||
collection={collection}
|
||||
rowData={rowData}
|
||||
cellData={cellData}
|
||||
link={colIndex === 0}
|
||||
{...cellProps?.[colIndex] || {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -53,7 +53,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
const [fields] = useState<Field[]>(() => formatFields(collection, t));
|
||||
const [tableColumns, setTableColumns] = useState<Column[]>(() => {
|
||||
const initialColumns = getInitialColumns(fields, useAsTitle, defaultColumns);
|
||||
return buildColumns(collection, initialColumns, t);
|
||||
return buildColumns({ collection, columns: initialColumns, t });
|
||||
});
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug];
|
||||
@@ -106,7 +106,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
(async () => {
|
||||
const currentPreferences = await getPreference<ListPreferences>(preferenceKey);
|
||||
if (currentPreferences?.columns) {
|
||||
setTableColumns(buildColumns(collection, currentPreferences?.columns, t));
|
||||
setTableColumns(buildColumns({ collection, columns: currentPreferences?.columns, t }));
|
||||
}
|
||||
|
||||
const params = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 0 });
|
||||
@@ -143,7 +143,7 @@ const ListView: React.FC<ListIndexProps> = (props) => {
|
||||
}, [sort, limit, stringifiedActiveColumns, preferenceKey, setPreference]);
|
||||
|
||||
const setActiveColumns = useCallback((columns: string[]) => {
|
||||
setTableColumns(buildColumns(collection, columns, t));
|
||||
setTableColumns(buildColumns({ collection, columns, t }));
|
||||
}, [collection, t]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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';
|
||||
|
||||
export type Props = {
|
||||
collection: SanitizedCollectionConfig
|
||||
@@ -14,6 +17,16 @@ export type Props = {
|
||||
hasCreatePermission: boolean
|
||||
setLimit: (limit: number) => void
|
||||
limit: number
|
||||
disableEyebrow?: boolean
|
||||
modifySearchParams?: boolean
|
||||
onCardClick?: (doc: any) => void
|
||||
disableCardLink?: boolean
|
||||
handleSortChange?: ListControlsProps['handleSortChange']
|
||||
handleWhereChange?: ListControlsProps['handleWhereChange']
|
||||
handlePageChange?: PaginatorProps['onChange']
|
||||
handlePerPageChange?: PerPageProps['handleChange']
|
||||
onCreateNewClick?: () => void
|
||||
customHeader?: React.ReactNode
|
||||
}
|
||||
|
||||
export type ListIndexProps = {
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"addLabel": "Přidat {{label}}",
|
||||
"addLink": "Přidat Odkaz",
|
||||
"addNew": "Přidat nový",
|
||||
"addNewLabel": "Přidat nový {{label}}",
|
||||
"addRelationship": "Přidat vztah",
|
||||
|
||||
@@ -92,9 +92,11 @@
|
||||
"block": "Block",
|
||||
"blocks": "Blöcke",
|
||||
"addLabel": "{{label}} hinzufügen",
|
||||
"addLink": "Link Hinzufügen",
|
||||
"addNew": "Neu erstellen",
|
||||
"addNewLabel": "{{label}} erstellen",
|
||||
"addRelationship": "Verknüpfung hinzufügen",
|
||||
"addRelationship": "Verknüpfung Hinzufügen",
|
||||
"addUpload": "Hochladen Hinzufügen",
|
||||
"blockType": "Block-Typ",
|
||||
"chooseBetweenCustomTextOrDocument": "Wähle zwischen einer eigenen Text-URL oder verlinke zu einem anderen Dokument.",
|
||||
"chooseDocumentToLink": "Wähle ein Dokument zum Verlinken",
|
||||
@@ -117,12 +119,14 @@
|
||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein.",
|
||||
"relatedDocument": "Verknüpftes Dokument",
|
||||
"relationTo": "Verknüpfung zu",
|
||||
"removeUpload": "Hochgeladene Datei löschen",
|
||||
"removeRelationship": "Beziehung Entfernen",
|
||||
"removeUpload": "Hochgeladene Datei Löschen",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"searchForBlock": "Nach Block suchen",
|
||||
"selectExistingLabel": "{{label}} auswählen (vorhandene)",
|
||||
"showAll": "Alle anzeigen",
|
||||
"swapUpload": "Datei austauschen",
|
||||
"swapRelationship": "Beziehung Tauschen",
|
||||
"swapUpload": "Datei Austauschen",
|
||||
"textToDisplay": "Angezeigter Text",
|
||||
"toggleBlock": "Block umschalten",
|
||||
"uploadNewLabel": "{{label}} neu hochladen"
|
||||
@@ -299,4 +303,4 @@
|
||||
"viewingVersions": "Betrachte Versionen für {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Betrachte Versionen für das Globale Dokument {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +91,11 @@
|
||||
},
|
||||
"fields": {
|
||||
"addLabel": "Add {{label}}",
|
||||
"addLink": "Add Link",
|
||||
"addNew": "Add new",
|
||||
"addNewLabel": "Add new {{label}}",
|
||||
"addRelationship": "Add relationship",
|
||||
"addRelationship": "Add Relationship",
|
||||
"addUpload": "Add Upload",
|
||||
"block": "block",
|
||||
"blockType": "Block Type",
|
||||
"blocks": "blocks",
|
||||
@@ -118,11 +120,13 @@
|
||||
"passwordsDoNotMatch": "Passwords do not match.",
|
||||
"relatedDocument": "Related Document",
|
||||
"relationTo": "Relation To",
|
||||
"removeRelationship": "Remove Relationship",
|
||||
"removeUpload": "Remove Upload",
|
||||
"saveChanges": "Save changes",
|
||||
"searchForBlock": "Search for a block",
|
||||
"selectExistingLabel": "Select existing {{label}}",
|
||||
"showAll": "Show All",
|
||||
"swapRelationship": "Swap Relationship",
|
||||
"swapUpload": "Swap Upload",
|
||||
"textToDisplay": "Text to display",
|
||||
"toggleBlock": "Toggle block",
|
||||
@@ -303,4 +307,4 @@
|
||||
"viewingVersions": "Viewing versions for the {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viewing versions for the global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,11 @@
|
||||
"block": "bloque",
|
||||
"blocks": "bloques",
|
||||
"addLabel": "Añadir {{label}}",
|
||||
"addLink": "Añadir Enlace",
|
||||
"addNew": "Añadir nuevo",
|
||||
"addNewLabel": "Añadir {{label}}",
|
||||
"addRelationship": "Añadir relación",
|
||||
"addRelationship": "Añadir Relación",
|
||||
"addUpload": "Añadir Carga",
|
||||
"blockType": "Tipo de bloque",
|
||||
"chooseFromExisting": "Elegir existente",
|
||||
"chooseLabel": "Elegir {{label}}",
|
||||
@@ -109,11 +111,13 @@
|
||||
"passwordsDoNotMatch": "Las contraseñas no coinciden.",
|
||||
"relatedDocument": "Documento Relacionado",
|
||||
"relationTo": "Relación con",
|
||||
"removeRelationship": "Eliminar relación",
|
||||
"removeUpload": "Quitar Carga",
|
||||
"saveChanges": "Guardar cambios",
|
||||
"searchForBlock": "Buscar bloque",
|
||||
"selectExistingLabel": "Seleccionar {{label}} existente",
|
||||
"showAll": "Mostrar todo",
|
||||
"showAll": "Mostrar Todo",
|
||||
"swapRelationship": "Cambiar Relación",
|
||||
"swapUpload": "Cambiar carga",
|
||||
"toggleBlock": "Alternar bloque",
|
||||
"uploadNewLabel": "Subir nuevo {{label}}"
|
||||
@@ -290,4 +294,4 @@
|
||||
"viewingVersions": "Viendo versiones para {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Viendo versiones para el global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@
|
||||
"block": "bloc",
|
||||
"blocks": "blocs",
|
||||
"addLabel": "Ajouter {{label}}",
|
||||
"addLink": "Ajouter un Lien",
|
||||
"addNew": "Ajouter nouveau ou nouvelle",
|
||||
"addNewLabel": "Ajouter nouveau ou nouvelle {{label}}",
|
||||
"addRelationship": "Ajouter une relation",
|
||||
"addUpload": "Ajouter le téléchargement",
|
||||
"blockType": "Type de bloc",
|
||||
"chooseFromExisting": "Choisir parmi les existant(e)s",
|
||||
"chooseLabel": "Choisir un(e) {{label}}",
|
||||
@@ -110,12 +112,14 @@
|
||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas.",
|
||||
"relatedDocument": "Document connexe",
|
||||
"relationTo": "Lié à",
|
||||
"removeUpload": "Supprimer le téléversement",
|
||||
"removeRelationship": "Supprimer la relation",
|
||||
"removeUpload": "Supprimer le Téléversement",
|
||||
"saveChanges": "Sauvegarder les modifications",
|
||||
"searchForBlock": "Rechercher un bloc",
|
||||
"selectExistingLabel": "Sélectionnez {{label}} existant",
|
||||
"showAll": "Afficher tout",
|
||||
"swapUpload": "Changer de fichier",
|
||||
"swapRelationship": "Relation D'échange",
|
||||
"swapUpload": "Changer de Fichier",
|
||||
"toggleBlock": "Bloc bascule",
|
||||
"uploadNewLabel": "Téléverser un(e) nouveau ou nouvelle {{label}}"
|
||||
},
|
||||
@@ -291,4 +295,4 @@
|
||||
"viewingVersions": "Affichage des versions de ou du {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Affichage des versions globales de ou du {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@
|
||||
"block": "blocco",
|
||||
"blocks": "blocchi",
|
||||
"addLabel": "Aggiungi {{label}}",
|
||||
"addLink": "Aggiungi Collegamento",
|
||||
"addNew": "Aggiungi nuovo",
|
||||
"addNewLabel": "Aggiungi nuovo {{label}}",
|
||||
"addRelationship": "Aggiungi relazione",
|
||||
"addRelationship": "Aggiungi Relazione",
|
||||
"addUpload": "aggiungi Carica",
|
||||
"blockType": "Tipo di Blocco",
|
||||
"chooseFromExisting": "Scegli tra esistente",
|
||||
"chooseLabel": "Scegli {{label}}",
|
||||
@@ -110,11 +112,13 @@
|
||||
"passwordsDoNotMatch": "Le password non corrispondono.",
|
||||
"relatedDocument": "Documento Correlato",
|
||||
"relationTo": "Correla a",
|
||||
"removeRelationship": "Rimuovi Relazione",
|
||||
"removeUpload": "Rimuovi Upload",
|
||||
"saveChanges": "Salva modifiche",
|
||||
"searchForBlock": "Cerca un blocco",
|
||||
"selectExistingLabel": "Seleziona {{label}} esistente",
|
||||
"showAll": "Mostra tutto",
|
||||
"swapRelationship": "Cambia Relationship",
|
||||
"swapUpload": "Cambia Upload",
|
||||
"toggleBlock": "Apri/chiudi blocco",
|
||||
"uploadNewLabel": "Carica nuovo {{label}}"
|
||||
@@ -293,4 +297,4 @@
|
||||
"viewingVersions": "Visualizzazione delle versioni per {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizzazione delle versioni per {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,11 @@
|
||||
"block": "ブロック",
|
||||
"blocks": "ブロック",
|
||||
"addLabel": "{{label}} を追加",
|
||||
"addLink": "リンクを追加",
|
||||
"addNew": "新規追加",
|
||||
"addNewLabel": "{{label}} を新規追加",
|
||||
"addRelationship": "リレーションシップを追加",
|
||||
"addUpload": "アップロードを追加",
|
||||
"blockType": "ブロックタイプ",
|
||||
"chooseFromExisting": "既存から選択",
|
||||
"chooseLabel": "{{label}} を選択",
|
||||
@@ -109,11 +111,13 @@
|
||||
"passwordsDoNotMatch": "パスワードが一致しません",
|
||||
"relatedDocument": "リレーションデータ",
|
||||
"relationTo": "リレーション",
|
||||
"removeRelationship": "関係を削除",
|
||||
"removeUpload": "削除",
|
||||
"saveChanges": "変更を保存",
|
||||
"searchForBlock": "ブロックを検索",
|
||||
"selectExistingLabel": "既存 {{label}} を選択",
|
||||
"showAll": "すべて開く",
|
||||
"swapRelationship": "スワップ関係",
|
||||
"swapUpload": "差し替え",
|
||||
"toggleBlock": "ブロックを切り替え",
|
||||
"uploadNewLabel": "新規 {{label}} アップロード"
|
||||
@@ -290,4 +294,4 @@
|
||||
"viewingVersions": "表示バージョン: {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "表示バージョン: グローバルな {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,11 @@
|
||||
},
|
||||
"fields": {
|
||||
"addLabel": "{{label}} ထည့်သွင်းမည်။",
|
||||
"addLink": "လင့်ခ်ထည့်ပါ။",
|
||||
"addNew": "အသစ် ထည့်သွင်းမည်။",
|
||||
"addNewLabel": "{{label}} အားအသစ် ထည့်သွင်းမည်။",
|
||||
"addRelationship": "Relationship အသစ်ထည့်သွင်းမည်။",
|
||||
"addUpload": "Upload ထည့်ပါ။",
|
||||
"block": "ဘလောက်",
|
||||
"blockType": "ဘလောက် အမျိုးအစား",
|
||||
"blocks": "ဘလောက်များ",
|
||||
@@ -109,11 +111,13 @@
|
||||
"passwordsDoNotMatch": "စကားဝှက်များနှင့် မကိုက်ညီပါ။",
|
||||
"relatedDocument": "ဆက်စပ် ဖိုင်",
|
||||
"relationTo": "ဆက်စပ်မှု",
|
||||
"removeRelationship": "ဆက်ဆံရေးကို ဖယ်ရှားပါ။",
|
||||
"removeUpload": "အပ်လုဒ်ကို ဖယ်ရှားပါ။",
|
||||
"saveChanges": "သိမ်းဆည်းမည်။",
|
||||
"searchForBlock": "ဘလောက်တစ်ခုရှာမည်။",
|
||||
"selectExistingLabel": "ရှိပြီးသား {{label}} ကို ရွေးပါ",
|
||||
"showAll": "အကုန် ကြည့်မည်။",
|
||||
"swapRelationship": "လဲလှယ်ဆက်ဆံရေး",
|
||||
"swapUpload": "အပ်လုဒ်ဖလှယ်ပါ။",
|
||||
"toggleBlock": "Toggle block",
|
||||
"uploadNewLabel": "{{label}} အသစ်တင်မည်။"
|
||||
@@ -290,4 +294,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} အတွက် ဗားရှင်းများကို ကြည့်ရှုခြင်း",
|
||||
"viewingVersionsGlobal": "`ဂလိုဘယ်ဆိုင်ရာ {{entityLabel}} အတွက် ဗားရှင်းများကို ကြည့်ရှုနေသည်"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +91,11 @@
|
||||
},
|
||||
"fields": {
|
||||
"addLabel": "Legg til {{label}}",
|
||||
"addLink": "Legg til Lenke",
|
||||
"addNew": "Legg til ny",
|
||||
"addNewLabel": "Legg til ny {{label}}",
|
||||
"addRelationship": "Legg til relasjon",
|
||||
"addRelationship": "Legg til Relasjon",
|
||||
"addUpload": "Legg til Opplasting",
|
||||
"block": "blokk",
|
||||
"blockType": "Blokktype",
|
||||
"blocks": "blokker",
|
||||
@@ -118,12 +120,14 @@
|
||||
"passwordsDoNotMatch": "Passordene er ikke like.",
|
||||
"relatedDocument": "Relatert dokument",
|
||||
"relationTo": "Relasjon til",
|
||||
"removeUpload": "Fjern opplasting",
|
||||
"removeRelationship": "Fjern Forhold",
|
||||
"removeUpload": "Fjern Opplasting",
|
||||
"saveChanges": "Lagre endringer",
|
||||
"searchForBlock": "Søk etter en blokk",
|
||||
"selectExistingLabel": "Velg eksisterende {{label}}",
|
||||
"showAll": "Vis alle",
|
||||
"swapUpload": "Bytt opplasting",
|
||||
"swapRelationship": "Bytte Forhold",
|
||||
"swapUpload": "Bytt Opplasting",
|
||||
"textToDisplay": "Tekst som skal vises",
|
||||
"toggleBlock": "Veksle blokk",
|
||||
"uploadNewLabel": "Last opp ny {{label}}"
|
||||
@@ -302,4 +306,4 @@
|
||||
"viewingVersions": "Viser versjoner for {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viser versjoner for den globale variabelen {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@
|
||||
"block": "blok",
|
||||
"blocks": "blokken",
|
||||
"addLabel": "Voeg {{label}} toe",
|
||||
"addLink": "Voeg een link tsoe",
|
||||
"addNew": "Nieuw(e)",
|
||||
"addNewLabel": "Nieuw(e) {{label}} toevoegen",
|
||||
"addRelationship": "Nieuwe relatie",
|
||||
"addRelationship": "Nieuwe Relatie",
|
||||
"addUpload": "Upload Toevoegen",
|
||||
"blockType": "Bloktype",
|
||||
"chooseFromExisting": "Kies uit bestaande",
|
||||
"chooseLabel": "Kies {{label}}",
|
||||
@@ -110,12 +112,14 @@
|
||||
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen.",
|
||||
"relatedDocument": "Gerelateerd document",
|
||||
"relationTo": "Relatie tot",
|
||||
"removeUpload": "Verwijder upload",
|
||||
"removeRelationship": "Relatie Vserwijderen",
|
||||
"removeUpload": "Verwijder Upload",
|
||||
"saveChanges": "Bewaar aanpassingen",
|
||||
"searchForBlock": "Zoeken naar een blok",
|
||||
"selectExistingLabel": "Selecteer bestaand(e) {{label}}",
|
||||
"showAll": "Alles tonen",
|
||||
"swapUpload": "Upload verwisselen",
|
||||
"swapRelationship": "Relatie Wisselen",
|
||||
"swapUpload": "Upload Verwisselen",
|
||||
"toggleBlock": "Blok togglen",
|
||||
"uploadNewLabel": "Upload nieuw(e) {{label}}"
|
||||
},
|
||||
@@ -291,4 +295,4 @@
|
||||
"viewingVersions": "Bekijk versies voor {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Bekijk versies voor global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,11 @@
|
||||
"block": "Blok",
|
||||
"blocks": "Bloki",
|
||||
"addLabel": "Dodaj {{label}}",
|
||||
"addLink": "Dodaj Link",
|
||||
"addNew": "Dodaj nowy",
|
||||
"addNewLabel": "Dodaj nowy {{label}}",
|
||||
"addRelationship": "Dodaj relacje",
|
||||
"addRelationship": "Dodaj Relacje",
|
||||
"addUpload": "Dodaj ładowanie",
|
||||
"blockType": "Typ Bloku",
|
||||
"chooseFromExisting": "Wybierz z istniejących",
|
||||
"chooseLabel": "Wybierz {{label}}",
|
||||
@@ -109,12 +111,14 @@
|
||||
"passwordsDoNotMatch": "Hasła nie pasują",
|
||||
"relatedDocument": "Powiązany dokument",
|
||||
"relationTo": "Powiązany z",
|
||||
"removeUpload": "Usuń wrzucone",
|
||||
"removeRelationship": "Usuń Związek",
|
||||
"removeUpload": "Usuń Wrzucone",
|
||||
"saveChanges": "Zapisz zmiany",
|
||||
"searchForBlock": "Szukaj bloku",
|
||||
"selectExistingLabel": "Wybierz istniejący {{label}}",
|
||||
"showAll": "Pokaż wszystkie",
|
||||
"swapUpload": "Zamień wrzucone",
|
||||
"swapRelationship": "Zamiana Relacji",
|
||||
"swapUpload": "Zamień Wrzucone",
|
||||
"toggleBlock": "Przełącz blok",
|
||||
"uploadNewLabel": "Wrzuć nowy {{label}}"
|
||||
},
|
||||
@@ -290,4 +294,4 @@
|
||||
"viewingVersions": "Przeglądanie wersji {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Przeglądanie wersji dla globalnej kolekcji {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,11 @@
|
||||
"block": "bloco",
|
||||
"blocks": "blocos",
|
||||
"addLabel": "Adicionar {{label}}",
|
||||
"addLink": "Adicionar Link",
|
||||
"addNew": "Adicionar novo",
|
||||
"addNewLabel": "Adicionar novo {{label}}",
|
||||
"addRelationship": "Adicionar relação",
|
||||
"addRelationship": "Adicionar Relação",
|
||||
"addUpload": "Adicionar Upload",
|
||||
"blockType": "Tipo de bloco",
|
||||
"chooseFromExisting": "Escolher entre os existentes",
|
||||
"chooseLabel": "Escolher {{label}}",
|
||||
@@ -109,11 +111,13 @@
|
||||
"passwordsDoNotMatch": "Senhas não coincidem.",
|
||||
"relatedDocument": "Documento Relacionado",
|
||||
"relationTo": "Relacionado a",
|
||||
"removeRelationship": "Remover Relacionamento",
|
||||
"removeUpload": "Remover Upload",
|
||||
"saveChanges": "Salvar alterações",
|
||||
"searchForBlock": "Procurar bloco",
|
||||
"selectExistingLabel": "Selecionar {{label}} existente",
|
||||
"showAll": "Mostrar Tudo",
|
||||
"swapRelationship": "Relação de Troca",
|
||||
"swapUpload": "Substituir Upload",
|
||||
"toggleBlock": "Alternar bloco",
|
||||
"uploadNewLabel": "Carregar novo(a) {{label}}"
|
||||
@@ -290,4 +294,4 @@
|
||||
"viewingVersions": "Visualizando versões para o/a {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizando versões para o global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +91,11 @@
|
||||
},
|
||||
"fields": {
|
||||
"addLabel": "Добавить {{label}}",
|
||||
"addLink": "Добавить ссылку",
|
||||
"addNew": "Добавить новый",
|
||||
"addNewLabel": "Добавить {{label}}",
|
||||
"addRelationship": "Добавить Отношения",
|
||||
"addUpload": "Добавить загрузку",
|
||||
"block": "Блок",
|
||||
"blockType": "Тип Блока",
|
||||
"blocks": "Блоки",
|
||||
@@ -118,11 +120,13 @@
|
||||
"passwordsDoNotMatch": "Пароли не совпадают.",
|
||||
"relatedDocument": "Связанный документ",
|
||||
"relationTo": "Отношение к",
|
||||
"removeRelationship": "Удалить связь",
|
||||
"removeUpload": "Удалить загруженное",
|
||||
"saveChanges": "Сохранить изменения",
|
||||
"searchForBlock": "Найти Блок",
|
||||
"selectExistingLabel": "Выберите существующий {{label}}",
|
||||
"showAll": "Показать все",
|
||||
"swapRelationship": "Поменять отношения",
|
||||
"swapUpload": "Заменить загруженное",
|
||||
"textToDisplay": "Текст для отображения",
|
||||
"toggleBlock": "Переключить Блок",
|
||||
@@ -302,4 +306,4 @@
|
||||
"viewingVersions": "Просмотр версий для {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Просмотр версии для глобальной Коллекции {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@
|
||||
"block": "block",
|
||||
"blocks": "block",
|
||||
"addLabel": "Lägg till {{label}}",
|
||||
"addLink": "Lägg till Länk",
|
||||
"addNew": "Lägg till ny",
|
||||
"addNewLabel": "Lägg till ny {{label}}",
|
||||
"addRelationship": "Lägg till relation",
|
||||
"addRelationship": "Lägg till Relation",
|
||||
"addUpload": "Lägg till Uppladdning",
|
||||
"blockType": "Block Typ",
|
||||
"chooseFromExisting": "Välj bland befintliga",
|
||||
"chooseLabel": "Välj {{label}}",
|
||||
@@ -110,11 +112,13 @@
|
||||
"passwordsDoNotMatch": "Lösenorden matchar inte.",
|
||||
"relatedDocument": "Relaterat Dokument",
|
||||
"relationTo": "Relation till",
|
||||
"removeRelationship": "Ta Bort Relation",
|
||||
"removeUpload": "Ta Bort Uppladdning",
|
||||
"saveChanges": "Spara ändringar",
|
||||
"searchForBlock": "Sök efter ett block",
|
||||
"selectExistingLabel": "Välj befintlig {{label}}",
|
||||
"showAll": "Visa Alla",
|
||||
"swapRelationship": "Byt Förhållande",
|
||||
"swapUpload": "Byt Uppladdning",
|
||||
"toggleBlock": "Växla block",
|
||||
"uploadNewLabel": "Ladda upp ny {{label}}"
|
||||
@@ -291,4 +295,4 @@
|
||||
"viewingVersions": "Visar versioner för {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Visa versioner för den globala {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,8 +92,10 @@
|
||||
"fields": {
|
||||
"addLabel": "เพิ่ม {{label}}",
|
||||
"addNew": "เพิ่ม",
|
||||
"addLink": "เพิ่มลิงค์",
|
||||
"addNewLabel": "เพิ่ม {{label}} ใหม่",
|
||||
"addRelationship": "เพิ่มความสัมพันธ์",
|
||||
"addUpload": "เพิ่มการอัปโหลด",
|
||||
"block": "Block",
|
||||
"blockType": "ประเภท Block",
|
||||
"blocks": "Blocks",
|
||||
@@ -118,11 +120,13 @@
|
||||
"passwordsDoNotMatch": "รหัสผ่านไม่ตรงกัน",
|
||||
"relatedDocument": "เอกสารที่เกี่ยวข้อง",
|
||||
"relationTo": "เชื่อมกับ",
|
||||
"removeRelationship": "ลบความสัมพันธ์",
|
||||
"removeUpload": "ลบอัปโหลด",
|
||||
"saveChanges": "บันทึก",
|
||||
"searchForBlock": "ค้นหา Block",
|
||||
"selectExistingLabel": "เลือก {{label}} ที่มีอยู่",
|
||||
"showAll": "แสดงทั้งหมด",
|
||||
"swapRelationship": "สลับความสัมพันธ์",
|
||||
"swapUpload": "สลับอัปโหลด",
|
||||
"textToDisplay": "ข้อความสำหรับแสดงผล",
|
||||
"toggleBlock": "เปิด/ปิด Block",
|
||||
|
||||
@@ -102,9 +102,11 @@
|
||||
"block": "blok",
|
||||
"blocks": "blok",
|
||||
"addLabel": "{{label}} ekle",
|
||||
"addLink": "Link Ekle",
|
||||
"addNew": "Yeni",
|
||||
"addNewLabel": "Yeni {{label}}",
|
||||
"addRelationship": "İlişki ekle",
|
||||
"addRelationship": "İlişki Ekle",
|
||||
"addUpload": "Yükleme Ekle",
|
||||
"blockType": "Blok tipi",
|
||||
"chooseFromExisting": "Varolanlardan seç",
|
||||
"chooseLabel": "{{label}} seç",
|
||||
@@ -119,12 +121,14 @@
|
||||
"passwordsDoNotMatch": "Parolalar eşleşmiyor.",
|
||||
"relatedDocument": "İlişkili döküman",
|
||||
"relationTo": "Relation To",
|
||||
"removeRelationship": "İlişkiyi Kaldır",
|
||||
"removeUpload": "Dosyayı Sil",
|
||||
"saveChanges": "Değişiklikleri kaydet",
|
||||
"searchForBlock": "Blok ara",
|
||||
"selectExistingLabel": "Varolan {{label}} seç",
|
||||
"showAll": "Tümünü göster",
|
||||
"swapUpload": "Karşıya yüklemeyi değiştir",
|
||||
"swapRelationship": "Takas Ilişkisi",
|
||||
"swapUpload": "Karşıya Yüklemeyi Değiştir",
|
||||
"toggleBlock": "Bloğu aç/kapat",
|
||||
"uploadNewLabel": "Karşıya {{label}} yükle"
|
||||
},
|
||||
@@ -301,4 +305,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} için sürümler gösteriliyor",
|
||||
"viewingVersionsGlobal": "`Global {{entityLabel}} için sürümler gösteriliyor"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +398,9 @@
|
||||
"addLabel": {
|
||||
"type": "string"
|
||||
},
|
||||
"addLink":{
|
||||
"type": "string"
|
||||
},
|
||||
"addNew": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -407,6 +410,9 @@
|
||||
"addRelationship": {
|
||||
"type": "string"
|
||||
},
|
||||
"addUpload": {
|
||||
"type": "string"
|
||||
},
|
||||
"blockType": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -449,6 +455,9 @@
|
||||
"relationTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"removeRelationship": {
|
||||
"type": "string"
|
||||
},
|
||||
"removeUpload": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -464,6 +473,9 @@
|
||||
"showAll": {
|
||||
"type": "string"
|
||||
},
|
||||
"swapRelationship": {
|
||||
"type": "string"
|
||||
},
|
||||
"swapUpload": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1222,4 +1234,4 @@
|
||||
"validation",
|
||||
"version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@
|
||||
"block": "block",
|
||||
"blocks": "blocks",
|
||||
"addLabel": "Thêm: {{label}}",
|
||||
"addLink": "Thêm liên kết",
|
||||
"addNew": "Thêm mới",
|
||||
"addNewLabel": "Thêm mới: {{label}}",
|
||||
"addRelationship": "Thêm mối quan hệ (relationship)",
|
||||
"addUpload": "Thêm tải lên (upload)",
|
||||
"blockType": "Block Type",
|
||||
"chooseFromExisting": "Chọn từ vật phẩm có sẵn",
|
||||
"chooseLabel": "Chọn: {{label}}",
|
||||
@@ -110,11 +112,13 @@
|
||||
"passwordsDoNotMatch": "Mật khẩu không trùng.",
|
||||
"relatedDocument": "bản tài liệu liên quan",
|
||||
"relationTo": "Có quan hệ với",
|
||||
"removeReelationship": "Xóa mối quan hệ",
|
||||
"removeUpload": "Xóa bản tải lên",
|
||||
"saveChanges": "Luu thay đổi",
|
||||
"searchForBlock": "Tìm block",
|
||||
"selectExistingLabel": "Chọn một {{label}} có sẵn",
|
||||
"showAll": "Hiển thị toàn bộ",
|
||||
"swapRelationship": "Hoán đổi quan hệ",
|
||||
"swapUpload": "Đổi bản tải lên",
|
||||
"toggleBlock": "Bật/tắt block",
|
||||
"uploadNewLabel": "Tải lên bản mới: {{label}}"
|
||||
@@ -291,4 +295,4 @@
|
||||
"viewingVersions": "Xem những phiên bản của {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Xem những phiên bản toàn thể (global) của {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ const flattenFields = (fields: Field[], keepPresentationalFields?: boolean): (Fi
|
||||
}, []),
|
||||
];
|
||||
}
|
||||
|
||||
return fieldsToUse;
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -51,6 +51,20 @@ const RichTextFields: CollectionConfig = {
|
||||
type: 'richText',
|
||||
required: true,
|
||||
admin: {
|
||||
elements: [
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'ul',
|
||||
'ol',
|
||||
'indent',
|
||||
'link',
|
||||
'relationship',
|
||||
'upload',
|
||||
],
|
||||
link: {
|
||||
fields: [
|
||||
{
|
||||
|
||||
1
test/fields/collections/Upload2/.gitignore
vendored
Normal file
1
test/fields/collections/Upload2/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads2
|
||||
26
test/fields/collections/Upload2/index.ts
Normal file
26
test/fields/collections/Upload2/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import path from 'path';
|
||||
import { CollectionConfig } from '../../../../src/collections/config/types';
|
||||
|
||||
const Uploads2: CollectionConfig = {
|
||||
slug: 'uploads2',
|
||||
upload: {
|
||||
staticDir: path.resolve(__dirname, './uploads2'),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
{
|
||||
type: 'upload',
|
||||
name: 'media',
|
||||
relationTo: 'uploads2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const uploadsDoc = {
|
||||
text: 'An upload here',
|
||||
};
|
||||
|
||||
export default Uploads2;
|
||||
BIN
test/fields/collections/Upload2/payload.jpg
Normal file
BIN
test/fields/collections/Upload2/payload.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -21,6 +21,7 @@ import NumberFields, { numberDoc } from './collections/Number';
|
||||
import CodeFields, { codeDoc } from './collections/Code';
|
||||
import RelationshipFields from './collections/Relationship';
|
||||
import RadioFields, { radiosDoc } from './collections/Radio';
|
||||
import Uploads2 from './collections/Upload2';
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
@@ -53,6 +54,7 @@ export default buildConfig({
|
||||
TabsFields,
|
||||
TextFields,
|
||||
Uploads,
|
||||
Uploads2,
|
||||
],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
@@ -296,10 +296,11 @@ describe('fields', () => {
|
||||
test('should create new url link', async () => {
|
||||
await navigateToRichTextFields();
|
||||
|
||||
// Open link popup
|
||||
// Open link drawer
|
||||
await page.locator('.rich-text__toolbar button:not([disabled]) .link').click();
|
||||
|
||||
const editLinkModal = page.locator('.rich-text-link-edit-modal__template');
|
||||
// find the drawer
|
||||
const editLinkModal = await page.locator('[id^=drawer_1_rich-text-link-]');
|
||||
await expect(editLinkModal).toBeVisible();
|
||||
|
||||
// Fill values and click Confirm
|
||||
|
||||
Reference in New Issue
Block a user