diff --git a/package.json b/package.json index 49a7c5c78d..35d64868f0 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,8 @@ "@babel/preset-typescript": "^7.12.1", "@babel/register": "^7.11.5", "@date-io/date-fns": "^2.10.6", + "@dnd-kit/core": "^6.0.5", + "@dnd-kit/sortable": "^7.0.1", "@faceless-ui/modal": "^2.0.1", "@faceless-ui/scroll-info": "^1.2.3", "@faceless-ui/window-info": "^2.0.2", @@ -155,7 +157,7 @@ "passport-local-mongoose": "^7.0.0", "path-browserify": "^1.0.1", "pino": "^6.4.1", - "pino-pretty": "^4.3.0", + "pino-pretty": "^9.1.1", "pluralize": "^8.0.0", "postcss": "^8.4.6", "postcss-loader": "^6.2.1", @@ -177,7 +179,6 @@ "react-router-navigation-prompt": "^1.9.6", "react-select": "^3.0.8", "react-simple-code-editor": "^0.11.0", - "react-sortable-hoc": "^2.0.0", "react-toastify": "^8.2.0", "sanitize-filename": "^1.6.3", "sass": "^1.55.0", diff --git a/src/admin/components/elements/Button/index.scss b/src/admin/components/elements/Button/index.scss index 4acd9ae871..5662fb3a27 100644 --- a/src/admin/components/elements/Button/index.scss +++ b/src/admin/components/elements/Button/index.scss @@ -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 { diff --git a/src/admin/components/elements/Button/index.tsx b/src/admin/components/elements/Button/index.tsx index 4ddfc5faa7..5fb02e76f6 100644 --- a/src/admin/components/elements/Button/index.tsx +++ b/src/admin/components/elements/Button/index.tsx @@ -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 ( - - {tooltip && ( - - {tooltip} - - )} - {children && ( - - {children} - - )} - {icon && ( - - {isValidElement(icon) && icon} - {BuiltInIcon && } - - )} - + + + {tooltip} + + + {children && ( + + {children} + + )} + {icon && ( + + {isValidElement(icon) && icon} + {BuiltInIcon && } + + )} + + ); }; @@ -53,7 +54,7 @@ const Button: React.FC = (props) => { className, id, type = 'button', - el, + el = 'button', to, url, children, @@ -69,6 +70,8 @@ const Button: React.FC = (props) => { tooltip, } = props; + const [showTooltip, setShowTooltip] = React.useState(false); + const classes = [ baseClass, className && className, @@ -84,6 +87,7 @@ const Button: React.FC = (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) => { 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) => { {children} @@ -123,6 +130,7 @@ const Button: React.FC = (props) => { {children} @@ -130,18 +138,21 @@ const Button: React.FC = (props) => { ); default: + const Tag = el; // eslint-disable-line no-case-declarations + return ( - + ); } }; diff --git a/src/admin/components/elements/Button/types.ts b/src/admin/components/elements/Button/types.ts index 2cdadc7e7b..48eaaa3afb 100644 --- a/src/admin/components/elements/Button/types.ts +++ b/src/admin/components/elements/Button/types.ts @@ -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, diff --git a/src/admin/components/elements/CopyToClipboard/index.scss b/src/admin/components/elements/CopyToClipboard/index.scss index 3adcc1d55b..227dd59722 100644 --- a/src/admin/components/elements/CopyToClipboard/index.scss +++ b/src/admin/components/elements/CopyToClipboard/index.scss @@ -14,22 +14,8 @@ width: 0px; } - .tooltip { - pointer-events: none; - opacity: 0; - visibility: hidden; - } - &:focus, &:active { outline: none; } - - &:hover { - .tooltip { - opacity: 1; - visibility: visible; - - } - } } diff --git a/src/admin/components/elements/CopyToClipboard/index.tsx b/src/admin/components/elements/CopyToClipboard/index.tsx index 85e210e75b..e108412222 100644 --- a/src/admin/components/elements/CopyToClipboard/index.tsx +++ b/src/admin/components/elements/CopyToClipboard/index.tsx @@ -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 = ({ const [hovered, setHovered] = useState(false); const { t } = useTranslation('general'); - useEffect(() => { - if (copied && !hovered) { - setTimeout(() => { - setCopied(false); - }, 1500); - } - }, [copied, hovered]); - if (value) { return ( + + {id && ( + + )} + + ), + }} + /> + + + ); + } + 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) => ( + + )); + }, [id, drawerSlug, collectionSlug]); + + const MemoizedDrawerToggler = useMemo(() => { + return ((props) => ( + + )); + }, [id, drawerSlug, collectionSlug]); + + const MemoizedDrawerState = useMemo(() => ({ + drawerSlug, + drawerDepth, + isDrawerOpen: isOpen, + toggleDrawer, + }), [drawerDepth, drawerSlug, isOpen, toggleDrawer]); + + return [ + MemoizedDrawer, + MemoizedDrawerToggler, + MemoizedDrawerState, + ]; +}; diff --git a/src/admin/components/elements/DocumentDrawer/types.ts b/src/admin/components/elements/DocumentDrawer/types.ts new file mode 100644 index 0000000000..0a5361cb9e --- /dev/null +++ b/src/admin/components/elements/DocumentDrawer/types.ts @@ -0,0 +1,31 @@ +import React, { HTMLAttributes } from 'react'; + +export type DocumentDrawerProps = { + collectionSlug: string + id?: string + onSave?: (json: Record) => void + customHeader?: React.ReactNode + drawerSlug?: string +} + +export type DocumentTogglerProps = HTMLAttributes & { + children?: React.ReactNode + className?: string + drawerSlug?: string + id?: string + collectionSlug: string +} + +export type UseDocumentDrawer = (args: { + id?: string + collectionSlug: string +}) => [ + React.FC>, // drawer + React.FC>, // toggler + { + drawerSlug: string, + drawerDepth: number + isDrawerOpen: boolean + toggleDrawer: () => void + } +] diff --git a/src/admin/components/elements/Drawer/index.scss b/src/admin/components/elements/Drawer/index.scss new file mode 100644 index 0000000000..3a564e8fd8 --- /dev/null +++ b/src/admin/components/elements/Drawer/index.scss @@ -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); + } +} diff --git a/src/admin/components/elements/Drawer/index.tsx b/src/admin/components/elements/Drawer/index.tsx new file mode 100644 index 0000000000..af487a1632 --- /dev/null +++ b/src/admin/components/elements/Drawer/index.tsx @@ -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 = ({ + 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 ( + + ); +}; + +export const Drawer: React.FC = ({ + 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 ( + + {drawerDepth === 1 && ( +
+ )} + + ); +}; diff --git a/src/admin/components/elements/ReactSelect/ValueContainer/index.scss b/src/admin/components/elements/ReactSelect/ValueContainer/index.scss new file mode 100644 index 0000000000..41bc946078 --- /dev/null +++ b/src/admin/components/elements/ReactSelect/ValueContainer/index.scss @@ -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; + } + } + } +} diff --git a/src/admin/components/elements/ReactSelect/ValueContainer/index.tsx b/src/admin/components/elements/ReactSelect/ValueContainer/index.tsx new file mode 100644 index 0000000000..82aea89190 --- /dev/null +++ b/src/admin/components/elements/ReactSelect/ValueContainer/index.tsx @@ -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> = (props) => { + const { + selectProps, + } = props; + + return ( +
+ +
+ ); +}; diff --git a/src/admin/components/elements/ReactSelect/index.scss b/src/admin/components/elements/ReactSelect/index.scss index 97fd1bb174..2fb544d262 100644 --- a/src/admin/components/elements/ReactSelect/index.scss +++ b/src/admin/components/elements/ReactSelect/index.scss @@ -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); } -} \ No newline at end of file +} diff --git a/src/admin/components/elements/ReactSelect/index.tsx b/src/admin/components/elements/ReactSelect/index.tsx index 700980bf45..63816e1de7 100644 --- a/src/admin/components/elements/ReactSelect/index.tsx +++ b/src/admin/components/elements/ReactSelect/index.tsx @@ -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