roadmap: rte and upload drawers (#1668)

This commit is contained in:
Jacob Fletcher
2022-12-23 12:41:06 -05:00
committed by GitHub
parent 794b6e8783
commit baf5b10d23
85 changed files with 2018 additions and 1766 deletions

View File

@@ -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`,

View 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>
);
};

View File

@@ -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;
}
}
}

View File

@@ -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,

View File

@@ -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
}
]

View File

@@ -26,6 +26,7 @@
z-index: 2;
width: 100%;
transition: all 300ms ease-out;
overflow: hidden;
}
&__content-children {

View File

@@ -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;
};

View File

@@ -12,4 +12,5 @@ export type TogglerProps = HTMLAttributes<HTMLButtonElement> & {
formatSlug?: boolean
children: React.ReactNode
className?: string
disabled?: boolean
}

View File

@@ -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 {

View 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>
);
};

View 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);
}
}
}

View 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,
];
};

View 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
}
]

View File

@@ -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

View File

@@ -11,5 +11,6 @@
&__text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}

View File

@@ -2,6 +2,7 @@
.step-nav {
display: flex;
overflow: auto;
* {
display: block;

View File

@@ -38,4 +38,8 @@ $caretSize: 6;
transition: opacity .2s ease-in-out;
cursor: default;
}
@include mid-break {
display: none;
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,6 +1,9 @@
@import '../../../../scss/styles.scss';
.rich-text__button {
position: relative;
cursor: pointer;
svg {
width: base(.75);
height: base(.75);

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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);
}

View File

@@ -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)}

View File

@@ -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;
}
}
}
}

View File

@@ -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>
);
};

View File

@@ -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[]

View File

@@ -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);
}
}
}

View File

@@ -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%;
}

View File

@@ -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>
);
};

View File

@@ -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;
}
}
}

View File

@@ -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>
);

View File

@@ -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
}

View File

@@ -1,7 +1,7 @@
@import '../../../../../../../scss/styles.scss';
.upload-rich-text-button {
.btn {
margin-right: base(1);
}
display: flex;
align-items: center;
height: 100%;
}

View File

@@ -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>
);
};

View File

@@ -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);
}
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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>
);
};

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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)});
}
}
}
}

View File

@@ -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(() => {

View File

@@ -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}

View File

@@ -9,4 +9,7 @@ export type Props = {
rowData: {
[path: string]: unknown
}
link?: boolean
onClick?: (Props) => void
className?: string
}

View File

@@ -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>
)}

View File

@@ -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] || {}}
/>
);
},
},
},
];

View File

@@ -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 (

View File

@@ -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 = {

View File

@@ -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",

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}} အတွက် ဗားရှင်းများကို ကြည့်ရှုနေသည်"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -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",

View File

@@ -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"
}
}
}

View File

@@ -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"
]
}
}

View File

@@ -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}}"
}
}
}

View File

@@ -35,7 +35,6 @@ const flattenFields = (fields: Field[], keepPresentationalFields?: boolean): (Fi
}, []),
];
}
return fieldsToUse;
}, []);
};

View File

@@ -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: [
{

View File

@@ -0,0 +1 @@
uploads2

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -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',

View File

@@ -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