Merge branch 'master' of https://github.com/payloadcms/payload into fix/select-field-clear-value
This commit is contained in:
@@ -22,22 +22,6 @@
|
||||
|
||||
&--has-tooltip {
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.btn__tooltip {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translate(-50%, -10px);
|
||||
}
|
||||
|
||||
.btn__content {
|
||||
&:hover {
|
||||
.btn__tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--icon-style-without-border {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { isValidElement } from 'react';
|
||||
import React, { Fragment, isValidElement } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Props } from './types';
|
||||
|
||||
@@ -21,30 +21,31 @@ const icons = {
|
||||
|
||||
const baseClass = 'btn';
|
||||
|
||||
const ButtonContents = ({ children, icon, tooltip }) => {
|
||||
const ButtonContents = ({ children, icon, tooltip, showTooltip }) => {
|
||||
const BuiltInIcon = icons[icon];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`${baseClass}__content`}
|
||||
>
|
||||
{tooltip && (
|
||||
<Tooltip className={`${baseClass}__tooltip`}>
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
)}
|
||||
{children && (
|
||||
<span className={`${baseClass}__label`}>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
{icon && (
|
||||
<span className={`${baseClass}__icon`}>
|
||||
{isValidElement(icon) && icon}
|
||||
{BuiltInIcon && <BuiltInIcon />}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<Fragment>
|
||||
<Tooltip
|
||||
className={`${baseClass}__tooltip`}
|
||||
show={showTooltip}
|
||||
>
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
<span className={`${baseClass}__content`}>
|
||||
{children && (
|
||||
<span className={`${baseClass}__label`}>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
{icon && (
|
||||
<span className={`${baseClass}__icon`}>
|
||||
{isValidElement(icon) && icon}
|
||||
{BuiltInIcon && <BuiltInIcon />}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,7 +54,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
className,
|
||||
id,
|
||||
type = 'button',
|
||||
el,
|
||||
el = 'button',
|
||||
to,
|
||||
url,
|
||||
children,
|
||||
@@ -69,6 +70,8 @@ const Button: React.FC<Props> = (props) => {
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className && className,
|
||||
@@ -84,6 +87,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
function handleClick(event) {
|
||||
setShowTooltip(false);
|
||||
if (type !== 'submit' && onClick) event.preventDefault();
|
||||
if (onClick) onClick(event);
|
||||
}
|
||||
@@ -93,6 +97,8 @@ const Button: React.FC<Props> = (props) => {
|
||||
type,
|
||||
className: classes,
|
||||
disabled,
|
||||
onMouseEnter: tooltip ? () => setShowTooltip(true) : undefined,
|
||||
onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined,
|
||||
onClick: !disabled ? handleClick : undefined,
|
||||
rel: newTab ? 'noopener noreferrer' : undefined,
|
||||
target: newTab ? '_blank' : undefined,
|
||||
@@ -108,6 +114,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
<ButtonContents
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
showTooltip={showTooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonContents>
|
||||
@@ -123,6 +130,7 @@ const Button: React.FC<Props> = (props) => {
|
||||
<ButtonContents
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
showTooltip={showTooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonContents>
|
||||
@@ -130,18 +138,21 @@ const Button: React.FC<Props> = (props) => {
|
||||
);
|
||||
|
||||
default:
|
||||
const Tag = el; // eslint-disable-line no-case-declarations
|
||||
|
||||
return (
|
||||
<button
|
||||
<Tag
|
||||
type="submit"
|
||||
{...buttonProps}
|
||||
>
|
||||
<ButtonContents
|
||||
icon={icon}
|
||||
tooltip={tooltip}
|
||||
showTooltip={showTooltip}
|
||||
>
|
||||
{children}
|
||||
</ButtonContents>
|
||||
</button>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { MouseEvent } from 'react';
|
||||
import React, { ElementType, MouseEvent } from 'react';
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
id?: string,
|
||||
type?: 'submit' | 'button',
|
||||
el?: 'link' | 'anchor' | undefined,
|
||||
el?: 'link' | 'anchor' | ElementType,
|
||||
to?: string,
|
||||
url?: string,
|
||||
children?: React.ReactNode,
|
||||
|
||||
@@ -14,22 +14,8 @@
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Copy from '../../icons/Copy';
|
||||
import Tooltip from '../Tooltip';
|
||||
@@ -18,14 +18,6 @@ const CopyToClipboard: React.FC<Props> = ({
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
useEffect(() => {
|
||||
if (copied && !hovered) {
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1500);
|
||||
}
|
||||
}, [copied, hovered]);
|
||||
|
||||
if (value) {
|
||||
return (
|
||||
<button
|
||||
@@ -44,13 +36,15 @@ const CopyToClipboard: React.FC<Props> = ({
|
||||
ref.current.select();
|
||||
ref.current.setSelectionRange(0, value.length + 1);
|
||||
document.execCommand('copy');
|
||||
|
||||
setCopied(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Copy />
|
||||
<Tooltip>
|
||||
<Tooltip
|
||||
show={hovered || copied}
|
||||
delay={copied ? 0 : undefined}
|
||||
>
|
||||
{copied && (successMessage ?? t('copied'))}
|
||||
{!copied && (defaultMessage ?? t('copy'))}
|
||||
</Tooltip>
|
||||
|
||||
@@ -102,7 +102,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="aboutToDelete"
|
||||
values={{ label: singular, title: titleToRender }}
|
||||
values={{ label: getTranslation(singular, i18n), title: titleToRender }}
|
||||
t={t}
|
||||
>
|
||||
aboutToDelete
|
||||
|
||||
36
src/admin/components/elements/DocumentDrawer/index.scss
Normal file
36
src/admin/components/elements/DocumentDrawer/index.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.doc-drawer {
|
||||
&__header {
|
||||
margin-bottom: base(1);
|
||||
}
|
||||
|
||||
&__header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: base(2.5);
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
svg {
|
||||
width: base(2.5);
|
||||
height: base(2.5);
|
||||
position: relative;
|
||||
top: base(-.5);
|
||||
right: base(-.75);
|
||||
|
||||
.stroke {
|
||||
stroke-width: .5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__header-close {
|
||||
svg {
|
||||
top: base(-.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/admin/components/elements/DocumentDrawer/index.tsx
Normal file
239
src/admin/components/elements/DocumentDrawer/index.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React, { useCallback, useEffect, useId, useMemo, useRef, 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 './index.scss';
|
||||
|
||||
const baseClass = 'doc-drawer';
|
||||
|
||||
const formatDocumentDrawerSlug = ({
|
||||
collectionSlug,
|
||||
id,
|
||||
depth,
|
||||
uuid,
|
||||
}: {
|
||||
collectionSlug: string,
|
||||
id: string,
|
||||
depth: number,
|
||||
uuid?: string, // supply when creating a new document and no id is available
|
||||
}) => `doc-drawer_${collectionSlug}_${depth}_${id || uuid || '0'}`;
|
||||
|
||||
export const DocumentDrawerToggler: React.FC<DocumentTogglerProps> = ({
|
||||
children,
|
||||
className,
|
||||
drawerSlug,
|
||||
id,
|
||||
collectionSlug,
|
||||
...rest
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation(['fields', 'general']);
|
||||
const [collectionConfig] = useRelatedCollections(collectionSlug);
|
||||
|
||||
return (
|
||||
<DrawerToggler
|
||||
slug={drawerSlug}
|
||||
formatSlug={false}
|
||||
className={className}
|
||||
aria-label={t(!id ? 'fields:addNewLabel' : 'general:editLabel', { label: getTranslation(collectionConfig.labels.singular, i18n) })}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</DrawerToggler>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentDrawer: React.FC<DocumentDrawerProps> = ({
|
||||
collectionSlug,
|
||||
id,
|
||||
drawerSlug,
|
||||
onSave,
|
||||
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 || hasInitializedState.current === true) {
|
||||
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]);
|
||||
|
||||
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 [isOpen, setIsOpen] = useState(false);
|
||||
const drawerSlug = formatDocumentDrawerSlug({
|
||||
collectionSlug,
|
||||
id,
|
||||
depth: drawerDepth,
|
||||
uuid,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen));
|
||||
}, [modalState, drawerSlug]);
|
||||
|
||||
const toggleDrawer = useCallback(() => {
|
||||
toggleModal(drawerSlug);
|
||||
}, [toggleModal, drawerSlug]);
|
||||
|
||||
const MemoizedDrawer = useMemo(() => {
|
||||
return ((props) => (
|
||||
<DocumentDrawer
|
||||
{...props}
|
||||
collectionSlug={collectionSlug}
|
||||
id={id}
|
||||
drawerSlug={drawerSlug}
|
||||
key={drawerSlug}
|
||||
/>
|
||||
));
|
||||
}, [id, drawerSlug, collectionSlug]);
|
||||
|
||||
const MemoizedDrawerToggler = useMemo(() => {
|
||||
return ((props) => (
|
||||
<DocumentDrawerToggler
|
||||
{...props}
|
||||
id={id}
|
||||
collectionSlug={collectionSlug}
|
||||
drawerSlug={drawerSlug}
|
||||
/>
|
||||
));
|
||||
}, [id, drawerSlug, collectionSlug]);
|
||||
|
||||
const MemoizedDrawerState = useMemo(() => ({
|
||||
drawerSlug,
|
||||
drawerDepth,
|
||||
isDrawerOpen: isOpen,
|
||||
toggleDrawer,
|
||||
}), [drawerDepth, drawerSlug, isOpen, toggleDrawer]);
|
||||
|
||||
return [
|
||||
MemoizedDrawer,
|
||||
MemoizedDrawerToggler,
|
||||
MemoizedDrawerState,
|
||||
];
|
||||
};
|
||||
31
src/admin/components/elements/DocumentDrawer/types.ts
Normal file
31
src/admin/components/elements/DocumentDrawer/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
export type DocumentDrawerProps = {
|
||||
collectionSlug: string
|
||||
id?: string
|
||||
onSave?: (json: Record<string, unknown>) => void
|
||||
customHeader?: React.ReactNode
|
||||
drawerSlug?: string
|
||||
}
|
||||
|
||||
export type DocumentTogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
drawerSlug?: string
|
||||
id?: string
|
||||
collectionSlug: string
|
||||
}
|
||||
|
||||
export type UseDocumentDrawer = (args: {
|
||||
id?: string
|
||||
collectionSlug: string
|
||||
}) => [
|
||||
React.FC<Omit<DocumentDrawerProps, 'collectionSlug' | 'id'>>, // drawer
|
||||
React.FC<Omit<DocumentTogglerProps, 'collectionSlug' | 'id'>>, // toggler
|
||||
{
|
||||
drawerSlug: string,
|
||||
drawerDepth: number
|
||||
isDrawerOpen: boolean
|
||||
toggleDrawer: () => void
|
||||
}
|
||||
]
|
||||
86
src/admin/components/elements/Drawer/index.scss
Normal file
86
src/admin/components/elements/Drawer/index.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.drawer {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
|
||||
&__blur-bg {
|
||||
@include blur-bg();
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@include blur-bg();
|
||||
opacity: 0;
|
||||
transform: translateX(#{base(4)});
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
&__content-children {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--is-open {
|
||||
.drawer__content,
|
||||
.drawer__blur-bg,
|
||||
.drawer__close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.drawer__close {
|
||||
transition: opacity 300ms ease-in-out;
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
|
||||
.drawer__content {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
@extend %btn-reset;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
text-indent: -9999px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
transition: none;
|
||||
transition-delay: 0ms;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@include mid-break {
|
||||
&__close {
|
||||
width: base(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme=dark] {
|
||||
.drawer__close {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
100
src/admin/components/elements/Drawer/index.tsx
Normal file
100
src/admin/components/elements/Drawer/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useWindowInfo } from '@faceless-ui/window-info';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props, TogglerProps } from './types';
|
||||
import { EditDepthContext, useEditDepth } from '../../utilities/EditDepth';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'drawer';
|
||||
|
||||
const zBase = 100;
|
||||
|
||||
const formatDrawerSlug = ({
|
||||
slug,
|
||||
depth,
|
||||
}: {
|
||||
slug: string,
|
||||
depth: number,
|
||||
}) => `drawer_${depth}_${slug}`;
|
||||
|
||||
export const DrawerToggler: React.FC<TogglerProps> = ({
|
||||
slug,
|
||||
formatSlug,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
...rest
|
||||
}) => {
|
||||
const { openModal } = useModal();
|
||||
const drawerDepth = useEditDepth();
|
||||
|
||||
const handleClick = useCallback((e) => {
|
||||
openModal(formatSlug !== false ? formatDrawerSlug({ slug, depth: drawerDepth }) : slug);
|
||||
if (typeof onClick === 'function') onClick(e);
|
||||
}, [openModal, drawerDepth, slug, onClick, formatSlug]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const Drawer: React.FC<Props> = ({
|
||||
slug,
|
||||
formatSlug,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation('general');
|
||||
const { closeModal, modalState } = useModal();
|
||||
const { breakpoints: { m: midBreak } } = useWindowInfo();
|
||||
const drawerDepth = useEditDepth();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [modalSlug] = useState(() => (formatSlug !== false ? formatDrawerSlug({ slug, depth: drawerDepth }) : slug));
|
||||
|
||||
useEffect(() => {
|
||||
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)}
|
||||
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>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
15
src/admin/components/elements/Drawer/types.ts
Normal file
15
src/admin/components/elements/Drawer/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
export type Props = {
|
||||
slug: string
|
||||
formatSlug?: boolean
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type TogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
slug: string
|
||||
formatSlug?: boolean
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.clear-indicator {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IndicatorProps } from 'react-select';
|
||||
import X from '../../../icons/X';
|
||||
import { Option as OptionType } from '../types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'clear-indicator';
|
||||
|
||||
export const ClearIndicator: React.FC<IndicatorProps<OptionType, true>> = (props) => {
|
||||
const {
|
||||
innerProps: { ref, ...restInnerProps },
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={baseClass}
|
||||
ref={ref}
|
||||
{...restInnerProps}
|
||||
>
|
||||
<X className={`${baseClass}__icon`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
42
src/admin/components/elements/ReactSelect/Control/index.tsx
Normal file
42
src/admin/components/elements/ReactSelect/Control/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { components as SelectComponents, ControlProps } from 'react-select';
|
||||
import { Option } from '../../../forms/field-types/Relationship/types';
|
||||
|
||||
export const Control: React.FC<ControlProps<Option, any>> = (props) => {
|
||||
const {
|
||||
children,
|
||||
innerProps,
|
||||
selectProps: {
|
||||
selectProps: {
|
||||
disableMouseDown,
|
||||
disableKeyDown,
|
||||
},
|
||||
},
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<SelectComponents.Control
|
||||
{...props}
|
||||
innerProps={{
|
||||
...innerProps,
|
||||
onMouseDown: (e) => {
|
||||
// we need to prevent react-select from hijacking the 'onMouseDown' event while modals are open (i.e. the 'Relationship' field component)
|
||||
if (!disableMouseDown) {
|
||||
innerProps.onMouseDown(e);
|
||||
}
|
||||
},
|
||||
// react-select has this typed incorrectly so we disable the linting rule
|
||||
// we need to prevent react-select from hijacking the 'onKeyDown' event while modals are open (i.e. the 'Relationship' field component)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
onKeyDown: (e) => {
|
||||
if (disableKeyDown) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SelectComponents.Control>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.multi-value {
|
||||
&.rs__multi-value {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: $style-stroke-width-s solid var(--theme-elevation-800);
|
||||
line-height: calc(#{$baseline} - #{$style-stroke-width-s * 2});
|
||||
margin: base(.25) base(.5) base(.25) 0;
|
||||
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
MultiValueProps,
|
||||
components as SelectComponents,
|
||||
} from 'react-select';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { Option as OptionType } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value';
|
||||
|
||||
export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
|
||||
const {
|
||||
className,
|
||||
isDisabled,
|
||||
innerProps,
|
||||
data: {
|
||||
value,
|
||||
},
|
||||
selectProps: {
|
||||
selectProps,
|
||||
selectProps: {
|
||||
disableMouseDown,
|
||||
},
|
||||
},
|
||||
} = props;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useSortable({
|
||||
id: value as string,
|
||||
});
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className,
|
||||
!isDisabled && 'draggable',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<SelectComponents.MultiValue
|
||||
{...props}
|
||||
className={classes}
|
||||
innerProps={{
|
||||
...innerProps,
|
||||
ref: setNodeRef,
|
||||
onMouseDown: (e) => {
|
||||
if (!disableMouseDown) {
|
||||
// we need to prevent the dropdown from opening when clicking on the drag handle, but not when a modal is open (i.e. the 'Relationship' field component)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
style: {
|
||||
...transform ? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
} : {},
|
||||
},
|
||||
}}
|
||||
selectProps={{
|
||||
...selectProps,
|
||||
// pass the draggable props through to the label so it alone acts as the draggable handle
|
||||
draggableProps: {
|
||||
...attributes,
|
||||
...listeners,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.multi-value-label {
|
||||
@extend %small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 base(.125) 0 base(.25);
|
||||
max-width: 150px;
|
||||
color: currentColor;
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { components as SelectComponents, MultiValueProps } from 'react-select';
|
||||
import { Option } from '../../../forms/field-types/Relationship/types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-label';
|
||||
|
||||
export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
|
||||
const {
|
||||
selectProps: {
|
||||
draggableProps,
|
||||
},
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SelectComponents.MultiValueLabel
|
||||
{...props}
|
||||
innerProps={{
|
||||
className: `${baseClass}__text`,
|
||||
...draggableProps || {},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.multi-value-remove {
|
||||
cursor: pointer;
|
||||
width: base(.75);
|
||||
height: base(.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
background: var(--theme-error-150);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MultiValueRemoveProps } from 'react-select/src/components/MultiValue';
|
||||
import X from '../../../icons/X';
|
||||
import Tooltip from '../../Tooltip';
|
||||
import { Option as OptionType } from '../types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-remove';
|
||||
|
||||
export const MultiValueRemove: React.FC<MultiValueRemoveProps<OptionType>> = (props) => {
|
||||
const {
|
||||
innerProps,
|
||||
} = props;
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
return (
|
||||
<button
|
||||
{...innerProps}
|
||||
type="button"
|
||||
className={baseClass}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onClick={(e) => {
|
||||
setShowTooltip(false);
|
||||
innerProps.onClick(e);
|
||||
}}
|
||||
aria-label={t('remove')}
|
||||
>
|
||||
<Tooltip
|
||||
className={`${baseClass}__tooltip`}
|
||||
show={showTooltip}
|
||||
>
|
||||
{t('remove')}
|
||||
</Tooltip>
|
||||
<X className={`${baseClass}__icon`} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.value-container {
|
||||
flex-grow: 1;
|
||||
|
||||
.rs__value-container {
|
||||
padding: base(.25) 0;
|
||||
min-height: base(1.5);
|
||||
overflow: visible;
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&--is-multi {
|
||||
margin-left: - base(0.25);
|
||||
width: calc(100% + base(0.5));
|
||||
padding-top: base(0.25);
|
||||
padding-bottom: base(0.25);
|
||||
padding-left: base(0.25);
|
||||
|
||||
.rs__multi-value {
|
||||
margin: calc(#{base(.125)} - #{$style-stroke-width-s * 2});
|
||||
}
|
||||
|
||||
&.rs__value-container--has-value {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { components as SelectComponents, ValueContainerProps } from 'react-select';
|
||||
import { Option } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'value-container';
|
||||
|
||||
export const ValueContainer: React.FC<ValueContainerProps<Option, any>> = (props) => {
|
||||
const {
|
||||
selectProps,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={selectProps.selectProps.droppableRef}
|
||||
className={baseClass}
|
||||
>
|
||||
<SelectComponents.ValueContainer {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +1,17 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
div.react-select {
|
||||
div.rs__control {
|
||||
.react-select {
|
||||
.rs__control {
|
||||
@include formInput;
|
||||
height: auto;
|
||||
padding-top: base(.25);
|
||||
padding-bottom: base(.25);
|
||||
}
|
||||
|
||||
.rs__value-container {
|
||||
padding: base(.25) 0;
|
||||
min-height: base(1.5);
|
||||
|
||||
>* {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&--is-multi {
|
||||
margin-left: - base(.25);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rs__indicators {
|
||||
.arrow {
|
||||
margin-left: base(.5);
|
||||
transform: rotate(90deg);
|
||||
width: base(.3);
|
||||
}
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.rs__indicator {
|
||||
padding: 0px 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rs__indicator-separator {
|
||||
@@ -47,7 +23,7 @@ div.react-select {
|
||||
|
||||
input {
|
||||
font-family: var(--font-body);
|
||||
width: 100% !important;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,38 +55,6 @@ div.react-select {
|
||||
}
|
||||
}
|
||||
|
||||
.rs__single-value {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.rs__multi-value {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: $style-stroke-width-s solid var(--theme-elevation-800);
|
||||
line-height: calc(#{$baseline} - #{$style-stroke-width-s * 2});
|
||||
margin: base(.25) base(.5) base(.25) 0;
|
||||
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
.rs__multi-value__label {
|
||||
padding: 0 base(.125) 0 base(.25);
|
||||
max-width: 150px;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.rs__multi-value__remove {
|
||||
padding: 0 base(.125);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
background: var(--theme-error-150);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
div.rs__control {
|
||||
background-color: var(--theme-error-200);
|
||||
@@ -120,4 +64,4 @@ div.react-select {
|
||||
&.rs--is-disabled .rs__control {
|
||||
background: var(--theme-elevation-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,39 @@
|
||||
import React, { MouseEventHandler, useCallback } from 'react';
|
||||
import Select, {
|
||||
components,
|
||||
MultiValueProps,
|
||||
Props as SelectProps,
|
||||
} from 'react-select';
|
||||
import React, { useCallback, useId } from 'react';
|
||||
import {
|
||||
SortableContainer,
|
||||
SortableContainerProps,
|
||||
SortableElement,
|
||||
SortStartHandler,
|
||||
SortEndHandler,
|
||||
SortableHandle,
|
||||
} from 'react-sortable-hoc';
|
||||
DragEndEvent,
|
||||
useDroppable,
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { arrayMove } from '../../../../utilities/arrayMove';
|
||||
import { Props, Option } from './types';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Props } from './types';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import { MultiValueLabel } from './MultiValueLabel';
|
||||
import { MultiValue } from './MultiValue';
|
||||
import { SingleValue } from '../../forms/field-types/Relationship/select-components/SingleValue';
|
||||
import { ValueContainer } from './ValueContainer';
|
||||
import { ClearIndicator } from './ClearIndicator';
|
||||
import { MultiValueRemove } from './MultiValueRemove';
|
||||
import { Control } from './Control';
|
||||
import './index.scss';
|
||||
|
||||
const SortableMultiValue = SortableElement(
|
||||
(props: MultiValueProps<Option>) => {
|
||||
// this prevents the menu from being opened/closed when the user clicks
|
||||
// on a value to begin dragging it. ideally, detecting a click (instead of
|
||||
// a drag) would still focus the control and toggle the menu, but that
|
||||
// requires some magic with refs that are out of scope for this example
|
||||
const onMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const classes = [
|
||||
props.className,
|
||||
!props.isDisabled && 'draggable',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<components.MultiValue
|
||||
{...props}
|
||||
className={classes}
|
||||
innerProps={{ ...props.innerProps, onMouseDown }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
const SortableMultiValueLabel = SortableHandle((props) => <components.MultiValueLabel {...props} />);
|
||||
|
||||
const SortableSelect = SortableContainer(Select) as React.ComponentClass<SelectProps<Option, true> & SortableContainerProps>;
|
||||
|
||||
const ReactSelect: React.FC<Props> = (props) => {
|
||||
const SelectAdapter: React.FC<Props> = (props) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const {
|
||||
className,
|
||||
showError = false,
|
||||
showError,
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
@@ -63,11 +41,12 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
placeholder = t('general:selectValue'),
|
||||
isSearchable = true,
|
||||
isClearable = true,
|
||||
isMulti,
|
||||
isSortable,
|
||||
filterOption = undefined,
|
||||
isLoading,
|
||||
onMenuOpen,
|
||||
components,
|
||||
droppableRef,
|
||||
selectProps,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
@@ -76,53 +55,6 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
showError && 'react-select--error',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const onSortStart: SortStartHandler = useCallback(({ helper }) => {
|
||||
const portalNode = helper;
|
||||
if (portalNode && portalNode.style) {
|
||||
portalNode.style.cssText += 'pointer-events: auto; cursor: grabbing;';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSortEnd: SortEndHandler = useCallback(({ oldIndex, newIndex }) => {
|
||||
onChange(arrayMove(value as Option[], oldIndex, newIndex));
|
||||
}, [onChange, value]);
|
||||
|
||||
if (isMulti && isSortable) {
|
||||
return (
|
||||
<SortableSelect
|
||||
useDragHandle
|
||||
// react-sortable-hoc props:
|
||||
axis="xy"
|
||||
onSortStart={onSortStart}
|
||||
onSortEnd={onSortEnd}
|
||||
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
|
||||
getHelperDimensions={({ node }) => node.getBoundingClientRect()}
|
||||
// react-select props:
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
{...props}
|
||||
value={value as Option[]}
|
||||
onChange={onChange}
|
||||
disabled={disabled ? 'disabled' : undefined}
|
||||
className={classes}
|
||||
classNamePrefix="rs"
|
||||
captureMenuScroll
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
isClearable={isClearable}
|
||||
isLoading={isLoading}
|
||||
onMenuOpen={onMenuOpen}
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore We're failing to provide a required index prop to SortableElement
|
||||
MultiValue: SortableMultiValue,
|
||||
MultiValueLabel: SortableMultiValueLabel,
|
||||
DropdownIndicator: Chevron,
|
||||
}}
|
||||
filterOption={filterOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
isLoading={isLoading}
|
||||
@@ -132,7 +64,6 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled ? 'disabled' : undefined}
|
||||
components={{ DropdownIndicator: Chevron }}
|
||||
className={classes}
|
||||
classNamePrefix="rs"
|
||||
options={options}
|
||||
@@ -140,8 +71,94 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
isClearable={isClearable}
|
||||
filterOption={filterOption}
|
||||
onMenuOpen={onMenuOpen}
|
||||
selectProps={{
|
||||
...selectProps,
|
||||
droppableRef,
|
||||
}}
|
||||
components={{
|
||||
ValueContainer,
|
||||
SingleValue,
|
||||
MultiValue,
|
||||
MultiValueLabel,
|
||||
MultiValueRemove,
|
||||
DropdownIndicator: Chevron,
|
||||
ClearIndicator,
|
||||
Control,
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableSelect: React.FC<Props> = (props) => {
|
||||
const {
|
||||
onChange,
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const uuid = useId();
|
||||
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: uuid,
|
||||
});
|
||||
|
||||
const onDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) return;
|
||||
|
||||
let sorted = value;
|
||||
|
||||
if (value && Array.isArray(value)) {
|
||||
const oldIndex = value.findIndex((item) => item.value === active.id);
|
||||
const newIndex = value.findIndex((item) => item.value === over.id);
|
||||
sorted = arrayMove(value, oldIndex, newIndex);
|
||||
}
|
||||
|
||||
onChange(sorted);
|
||||
}, [onChange, value]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
let ids: string[] = [];
|
||||
if (value) ids = Array.isArray(value) ? value.map((item) => item?.value as string) : [value?.value as string]; // TODO: fix these types
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={ids}>
|
||||
<SelectAdapter
|
||||
{...props}
|
||||
droppableRef={setNodeRef}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
const ReactSelect: React.FC<Props> = (props) => {
|
||||
const {
|
||||
isMulti,
|
||||
isSortable,
|
||||
} = props;
|
||||
|
||||
if (isMulti && isSortable) {
|
||||
return (
|
||||
<SortableSelect {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectAdapter {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactSelect;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Ref } from 'react';
|
||||
|
||||
export type Option = {
|
||||
[key: string]: unknown
|
||||
value: unknown
|
||||
@@ -9,6 +11,7 @@ export type OptionGroup = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
droppableRef?: Ref<HTMLElement>
|
||||
className?: string
|
||||
value?: Option | Option[],
|
||||
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
@@ -26,7 +29,16 @@ export type Props = {
|
||||
placeholder?: string
|
||||
isSearchable?: boolean
|
||||
isClearable?: boolean
|
||||
blurInputOnSelect?: boolean
|
||||
filterOption?:
|
||||
| (({ label, value, data }: { label: string, value: string, data: Option }, search: string) => boolean)
|
||||
| undefined,
|
||||
components?: {
|
||||
[key: string]: React.FC<any>
|
||||
}
|
||||
selectProps?: {
|
||||
disableMouseDown?: boolean
|
||||
disableKeyDown?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
$caretSize: 6;
|
||||
|
||||
.tooltip {
|
||||
opacity: 0;
|
||||
background-color: var(--theme-elevation-800);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -20%, 0);
|
||||
transform: translate3d(-50%, calc(#{$caretSize}px * -1), 0);
|
||||
padding: base(.2) base(.4);
|
||||
color: var(--theme-elevation-0);
|
||||
line-height: base(.75);
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
border-radius: 2px;
|
||||
visibility: hidden;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
top: calc(100% - #{base(.0625)});
|
||||
left: 50%;
|
||||
height: 0;
|
||||
width: 0;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: var(--theme-elevation-800);
|
||||
}
|
||||
&::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, 100%, 0);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: #{$caretSize}px solid transparent;
|
||||
border-right: #{$caretSize}px solid transparent;
|
||||
border-top: #{$caretSize}px solid var(--theme-elevation-800);
|
||||
}
|
||||
|
||||
&--show {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .2s ease-in-out;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,44 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const Tooltip: React.FC<Props> = (props) => {
|
||||
const { className, children } = props;
|
||||
|
||||
const classes = [
|
||||
'tooltip',
|
||||
const {
|
||||
className,
|
||||
].filter(Boolean).join(' ');
|
||||
children,
|
||||
show: showFromProps = true,
|
||||
delay = 350,
|
||||
} = props;
|
||||
|
||||
const [show, setShow] = React.useState(showFromProps);
|
||||
|
||||
useEffect(() => {
|
||||
let timerId: NodeJS.Timeout;
|
||||
|
||||
// do not use the delay on transition-out
|
||||
if (delay && showFromProps) {
|
||||
timerId = setTimeout(() => {
|
||||
setShow(showFromProps);
|
||||
}, delay);
|
||||
} else {
|
||||
setShow(showFromProps);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timerId) clearTimeout(timerId);
|
||||
};
|
||||
}, [showFromProps, delay]);
|
||||
|
||||
return (
|
||||
<aside className={classes}>
|
||||
<aside
|
||||
className={[
|
||||
'tooltip',
|
||||
className,
|
||||
show && 'tooltip--show',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{children}
|
||||
<span />
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export type Props = {
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
show?: boolean
|
||||
delay?: number
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ const Error: React.FC<Props> = (props) => {
|
||||
|
||||
if (showError) {
|
||||
return (
|
||||
<Tooltip className={baseClass}>
|
||||
<Tooltip
|
||||
className={baseClass}
|
||||
delay={0}
|
||||
>
|
||||
{message}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.relationship-add-new-modal {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
|
||||
&__blur-bg {
|
||||
@include blur-bg();
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
.collection-edit {
|
||||
@include blur-bg();
|
||||
transform: translateX(#{base(4)});
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-out;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.collection-edit__form {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.collection-edit__document-actions {
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--animated {
|
||||
|
||||
.collection-edit,
|
||||
.relationship-add-new-modal__blur-bg,
|
||||
.relationship-add-new-modal__close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.collection-edit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.collection-edit__document-actions {
|
||||
margin-top: base(2.75);
|
||||
}
|
||||
|
||||
&__close {
|
||||
@extend %btn-reset;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
text-indent: -9999px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease-in-out;
|
||||
transition-delay: 100ms;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: base(2.5);
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
svg {
|
||||
width: base(2.5);
|
||||
height: base(2.5);
|
||||
position: relative;
|
||||
top: base(-.5);
|
||||
right: base(-.75);
|
||||
|
||||
.stroke {
|
||||
stroke-width: .5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__header-close {
|
||||
svg {
|
||||
top: base(-.75);
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
width: base(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme=dark] {
|
||||
.relationship-add-new-modal__close {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useWindowInfo } from '@faceless-ui/window-info';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../../../../elements/Button';
|
||||
import { Props } from './types';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import RenderCustomComponent from '../../../../../utilities/RenderCustomComponent';
|
||||
import { useLocale } from '../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../utilities/Config';
|
||||
import DefaultEdit from '../../../../../views/collections/Edit/Default';
|
||||
import X from '../../../../../icons/X';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { EditDepthContext, useEditDepth } from '../../../../../utilities/EditDepth';
|
||||
import { getTranslation } from '../../../../../../../utilities/getTranslation';
|
||||
import { DocumentInfoProvider } from '../../../../../utilities/DocumentInfo';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'relationship-add-new-modal';
|
||||
|
||||
export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave, modalSlug }) => {
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal } = useModal();
|
||||
const { breakpoints: { m: midBreak } } = useWindowInfo();
|
||||
const locale = useLocale();
|
||||
const { permissions, user } = useAuth();
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
const [isAnimated, setIsAnimated] = useState(false);
|
||||
const editDepth = useEditDepth();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const modalAction = `${serverURL}${api}/${modalCollection.slug}?locale=${locale}&depth=0&fallback-locale=null`;
|
||||
|
||||
useEffect(() => {
|
||||
const buildState = async () => {
|
||||
const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale, t });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
buildState();
|
||||
}, [modalCollection, locale, user, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimated(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={[
|
||||
baseClass,
|
||||
isAnimated && `${baseClass}--animated`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{editDepth === 1 && (
|
||||
<div className={`${baseClass}__blur-bg`} />
|
||||
)}
|
||||
<DocumentInfoProvider collection={modalCollection}>
|
||||
<EditDepthContext.Provider value={editDepth + 1}>
|
||||
<button
|
||||
className={`${baseClass}__close`}
|
||||
type="button"
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
style={{
|
||||
width: `calc(${midBreak ? 'var(--gutter-h)' : 'var(--nav-width)'} + ${editDepth - 1} * 25px)`,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultEdit}
|
||||
CustomComponent={modalCollection.admin?.components?.views?.Edit}
|
||||
componentProps={{
|
||||
isLoading: !initialState,
|
||||
data: {},
|
||||
collection: modalCollection,
|
||||
permissions: permissions.collections[modalCollection.slug],
|
||||
isEditing: false,
|
||||
onSave,
|
||||
initialState,
|
||||
hasSavePermission: true,
|
||||
action: modalAction,
|
||||
disableEyebrow: true,
|
||||
disableActions: true,
|
||||
disableLeaveWithoutSaving: true,
|
||||
customHeader: (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<h2>
|
||||
{t('addNewLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
|
||||
</h2>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
className={`${baseClass}__header-close`}
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EditDepthContext.Provider>
|
||||
</DocumentInfoProvider>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../collections/config/types';
|
||||
|
||||
export type Props = {
|
||||
modalSlug: string
|
||||
modalCollection: SanitizedCollectionConfig
|
||||
onSave: (json: Record<string, unknown>) => void
|
||||
}
|
||||
@@ -12,28 +12,16 @@
|
||||
|
||||
&__add-button {
|
||||
@include formInput;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin-left: -1px;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
|
||||
.btn__content,
|
||||
.btn__label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn__content,
|
||||
.btn__label {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn__label {
|
||||
padding: 0 base(.5);
|
||||
align-items: center;
|
||||
}
|
||||
padding: 0 base(0.5);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
&__relations {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -57,4 +45,4 @@
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../../../elements/Button';
|
||||
import { Props } from './types';
|
||||
@@ -7,10 +6,10 @@ import { SanitizedCollectionConfig } from '../../../../../../collections/config/
|
||||
import Popup from '../../../../elements/Popup';
|
||||
import { useRelatedCollections } from './useRelatedCollections';
|
||||
import { useAuth } from '../../../../utilities/Auth';
|
||||
import { AddNewRelationModal } from './Modal';
|
||||
import { useEditDepth } from '../../../../utilities/EditDepth';
|
||||
import Plus from '../../../../icons/Plus';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
import Tooltip from '../../../../elements/Tooltip';
|
||||
import { useDocumentDrawer } from '../../../../elements/DocumentDrawer';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -18,30 +17,31 @@ const baseClass = 'relationship-add-new';
|
||||
|
||||
export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, value, setValue, dispatchOptions }) => {
|
||||
const relatedCollections = useRelatedCollections(relationTo);
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const { permissions } = useAuth();
|
||||
const [hasPermission, setHasPermission] = useState(false);
|
||||
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>();
|
||||
const [show, setShow] = useState(false);
|
||||
const [selectedCollection, setSelectedCollection] = useState<string>();
|
||||
const relatedToMany = relatedCollections.length > 1;
|
||||
const [collectionConfig, setCollectionConfig] = useState<SanitizedCollectionConfig>(() => (!relatedToMany ? relatedCollections[0] : undefined));
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const editDepth = useEditDepth();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const modalSlug = `${path}-add-modal-depth-${editDepth}`;
|
||||
|
||||
const openModal = useCallback(async (collection: SanitizedCollectionConfig) => {
|
||||
setModalCollection(collection);
|
||||
toggleModal(modalSlug);
|
||||
}, [toggleModal, modalSlug]);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [
|
||||
DocumentDrawer,
|
||||
DocumentDrawerToggler,
|
||||
{ toggleDrawer, isDrawerOpen },
|
||||
] = useDocumentDrawer({
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
});
|
||||
|
||||
const onSave = useCallback((json) => {
|
||||
const newValue = Array.isArray(relationTo) ? {
|
||||
relationTo: modalCollection.slug,
|
||||
relationTo: collectionConfig.slug,
|
||||
value: json.doc.id,
|
||||
} : json.doc.id;
|
||||
|
||||
dispatchOptions({
|
||||
type: 'ADD',
|
||||
collection: modalCollection,
|
||||
collection: collectionConfig,
|
||||
docs: [
|
||||
json.doc,
|
||||
],
|
||||
@@ -55,9 +55,8 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
setModalCollection(undefined);
|
||||
toggleModal(modalSlug);
|
||||
}, [relationTo, modalCollection, dispatchOptions, i18n, hasMany, toggleModal, modalSlug, setValue, value]);
|
||||
setSelectedCollection(undefined);
|
||||
}, [relationTo, collectionConfig, dispatchOptions, i18n, hasMany, setValue, value]);
|
||||
|
||||
const onPopopToggle = useCallback((state) => {
|
||||
setPopupOpen(state);
|
||||
@@ -66,76 +65,108 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
useEffect(() => {
|
||||
if (permissions) {
|
||||
if (relatedCollections.length === 1) {
|
||||
setHasPermission(permissions.collections[relatedCollections[0].slug].create.permission);
|
||||
setShow(permissions.collections[relatedCollections[0].slug].create.permission);
|
||||
} else {
|
||||
setHasPermission(relatedCollections.some((collection) => permissions.collections[collection.slug].create.permission));
|
||||
setShow(relatedCollections.some((collection) => permissions.collections[collection.slug].create.permission));
|
||||
}
|
||||
}
|
||||
}, [permissions, relatedCollections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen(modalSlug)) {
|
||||
setModalCollection(undefined);
|
||||
if (relatedToMany && selectedCollection) {
|
||||
setCollectionConfig(relatedCollections.find((collection) => collection.slug === selectedCollection));
|
||||
}
|
||||
}, [isModalOpen, modalSlug]);
|
||||
}, [selectedCollection, relatedToMany, relatedCollections]);
|
||||
|
||||
return hasPermission ? (
|
||||
<div
|
||||
className={baseClass}
|
||||
id={`${path}-add-new`}
|
||||
>
|
||||
{relatedCollections.length === 1 && (
|
||||
<Button
|
||||
className={`${baseClass}__add-button`}
|
||||
onClick={() => openModal(relatedCollections[0])}
|
||||
buttonStyle="none"
|
||||
tooltip={t('addNewLabel', { label: relatedCollections[0].labels.singular })}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
{relatedCollections.length > 1 && (
|
||||
<Popup
|
||||
buttonType="custom"
|
||||
horizontalAlign="center"
|
||||
onToggleOpen={onPopopToggle}
|
||||
button={(
|
||||
<Button
|
||||
useEffect(() => {
|
||||
if (relatedToMany && collectionConfig) {
|
||||
// the drawer must be rendered on the page before before opening it
|
||||
// this is why 'selectedCollection' is different from 'collectionConfig'
|
||||
toggleDrawer();
|
||||
setSelectedCollection(undefined);
|
||||
}
|
||||
}, [toggleDrawer, relatedToMany, collectionConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (relatedToMany && !isDrawerOpen) {
|
||||
setCollectionConfig(undefined);
|
||||
}
|
||||
}, [isDrawerOpen, relatedToMany]);
|
||||
|
||||
if (show) {
|
||||
return (
|
||||
<div
|
||||
className={baseClass}
|
||||
id={`${path}-add-new`}
|
||||
>
|
||||
{relatedCollections.length === 1 && (
|
||||
<Fragment>
|
||||
<DocumentDrawerToggler
|
||||
className={`${baseClass}__add-button`}
|
||||
buttonStyle="none"
|
||||
tooltip={popupOpen ? undefined : t('addNew')}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onClick={() => setShowTooltip(false)}
|
||||
>
|
||||
<Tooltip
|
||||
className={`${baseClass}__tooltip`}
|
||||
show={showTooltip}
|
||||
>
|
||||
{t('addNewLabel', { label: relatedCollections[0].labels.singular })}
|
||||
</Tooltip>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
render={({ close: closePopup }) => (
|
||||
<ul className={`${baseClass}__relations`}>
|
||||
{relatedCollections.map((relatedCollection) => {
|
||||
if (permissions.collections[relatedCollection.slug].create.permission) {
|
||||
return (
|
||||
<li key={relatedCollection.slug}>
|
||||
<button
|
||||
className={`${baseClass}__relation-button ${baseClass}__relation-button--${relatedCollection.slug}`}
|
||||
type="button"
|
||||
onClick={() => { closePopup(); openModal(relatedCollection); }}
|
||||
>
|
||||
{getTranslation(relatedCollection.labels.singular, i18n)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
</DocumentDrawerToggler>
|
||||
<DocumentDrawer onSave={onSave} />
|
||||
</Fragment>
|
||||
)}
|
||||
{relatedCollections.length > 1 && (
|
||||
<Fragment>
|
||||
<Popup
|
||||
buttonType="custom"
|
||||
horizontalAlign="center"
|
||||
onToggleOpen={onPopopToggle}
|
||||
button={(
|
||||
<Button
|
||||
className={`${baseClass}__add-button`}
|
||||
buttonStyle="none"
|
||||
tooltip={popupOpen ? undefined : t('addNew')}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
)}
|
||||
render={({ close: closePopup }) => (
|
||||
<ul className={`${baseClass}__relations`}>
|
||||
{relatedCollections.map((relatedCollection) => {
|
||||
if (permissions.collections[relatedCollection.slug].create.permission) {
|
||||
return (
|
||||
<li key={relatedCollection.slug}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${baseClass}__relation-button ${baseClass}__relation-button--${relatedCollection.slug}`}
|
||||
onClick={() => {
|
||||
closePopup();
|
||||
setSelectedCollection(relatedCollection.slug);
|
||||
}}
|
||||
>
|
||||
{getTranslation(relatedCollection.labels.singular, i18n)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{modalCollection && (
|
||||
<AddNewRelationModal
|
||||
{...{ onSave, modalSlug, modalCollection }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
return null;
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
/>
|
||||
{collectionConfig && permissions.collections[collectionConfig.slug].create.permission && (
|
||||
<DocumentDrawer
|
||||
onSave={onSave}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,11 @@ import { useConfig } from '../../../../utilities/Config';
|
||||
export const useRelatedCollections = (relationTo: string | string[]): SanitizedCollectionConfig[] => {
|
||||
const config = useConfig();
|
||||
const [relatedCollections] = useState(() => {
|
||||
const relations = typeof relationTo === 'string' ? [relationTo] : relationTo;
|
||||
return relations.map((relation) => config.collections.find((collection) => collection.slug === relation));
|
||||
if (relationTo) {
|
||||
const relations = typeof relationTo === 'string' ? [relationTo] : relationTo;
|
||||
return relations.map((relation) => config.collections.find((collection) => collection.slug === relation));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return relatedCollections;
|
||||
|
||||
@@ -23,6 +23,8 @@ import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
|
||||
import { AddNewRelation } from './AddNew';
|
||||
import { findOptionsByValue } from './findOptionsByValue';
|
||||
import { GetFilterOptions } from './GetFilterOptions';
|
||||
import { SingleValue } from './select-components/SingleValue';
|
||||
import { MultiValueLabel } from './select-components/MultiValueLabel';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -73,7 +75,6 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
const [hasLoadedFirstPage, setHasLoadedFirstPage] = useState(false);
|
||||
const [enableWordBoundarySearch, setEnableWordBoundarySearch] = useState(false);
|
||||
const firstRun = useRef(true);
|
||||
|
||||
const pathOrName = path || name;
|
||||
|
||||
const memoizedValidate = useCallback((value, validationOptions) => {
|
||||
@@ -92,6 +93,8 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
condition,
|
||||
});
|
||||
|
||||
const [drawerIsOpen, setDrawerIsOpen] = useState(false);
|
||||
|
||||
const getResults: GetResults = useCallback(async ({
|
||||
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
|
||||
lastLoadedPage: lastLoadedPageArg,
|
||||
@@ -370,6 +373,15 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
isMulti={hasMany}
|
||||
isSortable={isSortable}
|
||||
isLoading={isLoading}
|
||||
components={{
|
||||
SingleValue,
|
||||
MultiValueLabel,
|
||||
}}
|
||||
selectProps={{
|
||||
disableMouseDown: drawerIsOpen,
|
||||
disableKeyDown: drawerIsOpen,
|
||||
setDrawerIsOpen,
|
||||
}}
|
||||
onMenuOpen={() => {
|
||||
if (!hasLoadedFirstPage) {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.relationship--multi-value-label {
|
||||
display: flex;
|
||||
|
||||
&__content {
|
||||
@extend %small;
|
||||
padding: 0 base(.125) 0 base(.25);
|
||||
max-width: 150px;
|
||||
color: currentColor;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__drawer-toggler {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: base(0.25);
|
||||
|
||||
.icon {
|
||||
width: base(0.75);
|
||||
height: base(0.75);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { components, MultiValueProps } from 'react-select';
|
||||
import { useDocumentDrawer } from '../../../../../elements/DocumentDrawer';
|
||||
import Tooltip from '../../../../../elements/Tooltip';
|
||||
import Edit from '../../../../../icons/Edit';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { Option } from '../../types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'relationship--multi-value-label';
|
||||
|
||||
export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
|
||||
const {
|
||||
data: {
|
||||
value,
|
||||
relationTo,
|
||||
label,
|
||||
},
|
||||
selectProps: {
|
||||
setDrawerIsOpen,
|
||||
draggableProps,
|
||||
},
|
||||
} = props;
|
||||
|
||||
const { permissions } = useAuth();
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const { t } = useTranslation('general');
|
||||
const hasReadPermission = Boolean(permissions?.collections?.[relationTo]?.read?.permission);
|
||||
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { isDrawerOpen }] = useDocumentDrawer({
|
||||
id: value?.toString(),
|
||||
collectionSlug: relationTo,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof setDrawerIsOpen === 'function') setDrawerIsOpen(isDrawerOpen);
|
||||
}, [isDrawerOpen, setDrawerIsOpen]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<components.MultiValueLabel
|
||||
{...props}
|
||||
innerProps={{
|
||||
className: `${baseClass}__text`,
|
||||
...draggableProps || {},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{relationTo && hasReadPermission && (
|
||||
<Fragment>
|
||||
<DocumentDrawerToggler
|
||||
className={`${baseClass}__drawer-toggler`}
|
||||
aria-label={`Edit ${label}`}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onClick={() => setShowTooltip(false)}
|
||||
>
|
||||
<Tooltip
|
||||
className={`${baseClass}__tooltip`}
|
||||
show={showTooltip}
|
||||
>
|
||||
{t('editLabel', { label: '' })}
|
||||
</Tooltip>
|
||||
<Edit />
|
||||
</DocumentDrawerToggler>
|
||||
<DocumentDrawer />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
@import '../../../../../../scss/styles.scss';
|
||||
|
||||
.relationship--single-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.rs__single-value {
|
||||
color: currentColor;
|
||||
max-width: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&__drawer-toggler {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: base(0.25);
|
||||
|
||||
.icon {
|
||||
width: base(0.75);
|
||||
height: base(0.75);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { components as SelectComponents, SingleValueProps } from 'react-select';
|
||||
import { useDocumentDrawer } from '../../../../../elements/DocumentDrawer';
|
||||
import Tooltip from '../../../../../elements/Tooltip';
|
||||
import Edit from '../../../../../icons/Edit';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { Option } from '../../types';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'relationship--single-value';
|
||||
|
||||
export const SingleValue: React.FC<SingleValueProps<Option>> = (props) => {
|
||||
const {
|
||||
data: {
|
||||
value,
|
||||
relationTo,
|
||||
label,
|
||||
},
|
||||
children,
|
||||
selectProps: {
|
||||
selectProps: {
|
||||
setDrawerIsOpen,
|
||||
},
|
||||
},
|
||||
} = props;
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const { t } = useTranslation('general');
|
||||
const { permissions } = useAuth();
|
||||
const hasReadPermission = Boolean(permissions?.collections?.[relationTo]?.read?.permission);
|
||||
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { isDrawerOpen }] = useDocumentDrawer({
|
||||
id: value.toString(),
|
||||
collectionSlug: relationTo,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof setDrawerIsOpen === 'function') setDrawerIsOpen(isDrawerOpen);
|
||||
}, [isDrawerOpen, setDrawerIsOpen]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__label`}>
|
||||
<SelectComponents.SingleValue {...props}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
{children}
|
||||
</div>
|
||||
{relationTo && hasReadPermission && (
|
||||
<Fragment>
|
||||
<DocumentDrawerToggler
|
||||
className={`${baseClass}__drawer-toggler`}
|
||||
aria-label={t('editLabel', { label })}
|
||||
onMouseDown={(e) => e.stopPropagation()} // prevents react-select dropdown from opening
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
onClick={() => setShowTooltip(false)}
|
||||
>
|
||||
<Tooltip
|
||||
className={`${baseClass}__tooltip`}
|
||||
show={showTooltip}
|
||||
>
|
||||
{t('edit')}
|
||||
</Tooltip>
|
||||
<Edit />
|
||||
</DocumentDrawerToggler>
|
||||
</Fragment>
|
||||
)}
|
||||
</SelectComponents.SingleValue>
|
||||
</div>
|
||||
{relationTo && hasReadPermission && (
|
||||
<DocumentDrawer />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -70,16 +70,19 @@
|
||||
}
|
||||
|
||||
&__document-actions {
|
||||
@include blur-bg;
|
||||
padding-right: $baseline;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--z-nav);
|
||||
|
||||
>* {
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
@include blur-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&__document-actions--has-2 {
|
||||
@@ -233,4 +236,4 @@
|
||||
padding-bottom: base(3.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
%small {
|
||||
margin: 0;
|
||||
font-size: base(.4);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export function arrayMove<T>(array: readonly T[], from: number, to: number) {
|
||||
const slicedArray = array.slice();
|
||||
slicedArray.splice(
|
||||
to < 0 ? array.length + to : to,
|
||||
0,
|
||||
slicedArray.splice(from, 1)[0],
|
||||
);
|
||||
return slicedArray;
|
||||
}
|
||||
Reference in New Issue
Block a user