feat: updates block field to use new collapsible

This commit is contained in:
James
2022-07-14 14:00:14 -07:00
parent aa89251a3b
commit 49d09a349f
22 changed files with 299 additions and 205 deletions

View File

@@ -24,38 +24,26 @@ const config = buildConfig({
})
```
### Overriding SCSS variables
### Overriding built-in styles
You can specify your own SCSS variable stylesheet that will allow for the override of Payload's base theme. This unlocks a ton of powerful theming and design options such as:
To make it as easy as possible for you to override our styles, Payload uses [BEM naming conventions](http://getbem.com/) for all CSS within the Admin UI. If you provide your own CSS, you can override any built-in styles easily.
- Changing dashboard font families
- Modifying color palette
- Creating a dark theme
- Etc.
In addition to adding your own style definitions, you can also override Payload's built-in CSS variables. We use as much as possible behind the scenes, and you can override any of them that you'd like to.
To do so, provide your base Payload config with a path to your own SCSS variable sheet.
You can find the built-in Payload CSS variables within [`./src/admin/scss/app.scss`](https://github.com/payloadcms/payload/blob/master/src/admin/scss/app.scss). The following variables are defined and can be overridden:
**Example in payload.config.js:**
```js
import { buildConfig } from 'payload/config';
import path from 'path';
- Breakpoints
- Base color shades (white to black by default)
- Success / warning / error color shades
- Theme-specific colors (background, input background, text color, etc.)
- Elevation colors (used to determine how "bright" something should be when compared to the background)
- Fonts
- Horizontal gutter
const config = buildConfig({
admin: {
scss: path.resolve(__dirname, 'relative/path/to/vars.scss'),
},
})
```
#### Dark mode
**Example stylesheet override:**
```scss
$font-body: 'Papyrus';
$style-radius-m: 10px;
```
To reference all Sass variables that you can override, look at the default [SCSS variable stylesheet](https://github.com/payloadcms/payload/blob/master/src/admin/scss/vars.scss) within the Payload source code.
<Banner type="error">
<strong>Warning:</strong><br />
Only SCSS variables, mixins, functions, and extends are allowed in <strong>your SCSS overrides</strong>. Do not attempt to add any CSS declarations to this file, as this variable stylesheet is imported by many components throughout the Payload Admin panel and will result in your CSS definition(s) being duplicated many times. If you need to add real CSS definitions, see "Adding your own CSS / SCSS" the top of this page.
<Banner type="warning">
If you're overriding colors or theme elevations, make sure to consider how your changes will affect dark mode.
</Banner>
By default, Payload automatically overrides all `--theme-elevation`s and inverts all success / warning / error shades to suit dark mode. We also update some base theme variables like `--theme-bg`, `--theme-text`, etc.

View File

@@ -1,12 +1,5 @@
@import '../dist/admin/scss/vars';
@import '../dist/admin/scss/z-index';
//////////////////////////////
// IMPORT OVERRIDES
//////////////////////////////
@import '~payload-scss-overrides';
@import '../dist/admin/scss/type';
@import '../dist/admin/scss/queries';
@import '../dist/admin/scss/resets';

View File

@@ -1,5 +1,5 @@
export type Props = {
addRow: (current: number) => void
addRow: (current: number, blockType?: string) => void
duplicateRow: (current: number) => void
removeRow: (index: number) => void
moveRow: (from: number, to: number) => void

View File

@@ -87,5 +87,9 @@
header {
padding: base(.75) var(--gutter-h);
}
&__content {
padding: var(--gutter-h);
}
}
}

View File

@@ -102,7 +102,7 @@ $cal-icon-width: 18px;
background: var(--theme-input-bg);
display: inline-flex;
border: none;
font-family: $font-body;
font-family: var(--font-body);
font-weight: 100;
border-radius: 0;
color: var(--theme-elevation-800);

View File

@@ -42,6 +42,10 @@
////////////////////////////////
&--size-small {
.popup__scroll {
padding: base(.75);
}
.popup__content {
@include shadow-m;
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useWindowInfo } from '@faceless-ui/window-info';
import { useScrollInfo } from '@faceless-ui/scroll-info';
import { Props } from './types';
@@ -40,13 +40,13 @@ const Popup: React.FC<Props> = (props) => {
const { y: scrollY } = useScrollInfo();
const { height: windowHeight, width: windowWidth } = useWindowInfo();
const handleClickOutside = (e) => {
const handleClickOutside = useCallback((e) => {
if (contentRef.current.contains(e.target)) {
return;
}
setActive(false);
};
}, []);
useThrottledEffect(() => {
if (contentRef.current && buttonRef.current) {
@@ -99,7 +99,7 @@ const Popup: React.FC<Props> = (props) => {
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [active, onToggleOpen]);
}, [active, handleClickOutside, onToggleOpen]);
useEffect(() => {
setActive(forceOpen);

View File

@@ -46,7 +46,7 @@ div.react-select {
color: var(--theme-elevation-1000);
input {
font-family: $font-body;
font-family: var(--font-body);
width: 100% !important;
}
}
@@ -65,7 +65,7 @@ div.react-select {
}
.rs__option {
font-family: $font-body;
font-family: var(--font-body);
font-size: $baseline-body-size;
padding: base(.375) base(.75);
color: var(--theme-elevation-800);

View File

@@ -9,7 +9,7 @@
flex-grow: 1;
color: var(--theme-elevation-800);
background-color: transparent;
font-family: $font-body;
font-family: var(--font-body);
font-weight: 600;
font-size: base(.75);
padding: base(.1) base(.2);

View File

@@ -44,6 +44,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
},
} = props;
const path = pathFromProps || name;
// Handle labeling for Arrays, Global Arrays, and Blocks
const getLabels = (p: Props) => {
if (p?.labels) return p.labels;
@@ -66,8 +68,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const { dispatchFields } = formContext;
const path = pathFromProps || name;
const memoizedValidate = useCallback((value, options) => {
return validate(value, { ...options, minRows, maxRows, required });
}, [maxRows, minRows, required, validate]);
@@ -91,6 +91,18 @@ const ArrayFieldType: React.FC<Props> = (props) => {
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
dispatchRows({ type: 'ADD', rowIndex });
setValue(value as number + 1);
setTimeout(() => {
const newRow = document.getElementById(`${path}-row-${rowIndex + 1}`);
if (newRow) {
const bounds = newRow.getBoundingClientRect();
window.scrollBy({
top: bounds.top - 100,
behavior: 'smooth',
});
}
}, 0);
}, [dispatchRows, dispatchFields, fields, path, setValue, value, operation, id, user, locale]);
const duplicateRow = useCallback(async (rowIndex: number) => {
@@ -233,7 +245,10 @@ const ArrayFieldType: React.FC<Props> = (props) => {
ref={provided.innerRef}
{...provided.droppableProps}
>
{rows.length > 0 && rows.map((row, i) => (
{rows.length > 0 && rows.map((row, i) => {
const rowNumber = i + 1;
return (
<Draggable
key={row.id}
draggableId={row.id}
@@ -242,6 +257,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
>
{(providedDrag) => (
<div
id={`${path}-row-${i}`}
ref={providedDrag.innerRef}
{...providedDrag.draggableProps}
>
@@ -251,7 +267,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
className={`${baseClass}__row`}
key={row.id}
dragHandleProps={providedDrag.dragHandleProps}
header={`${labels.singular} ${i + 1}`}
header={`${labels.singular} ${rowNumber >= 10 ? rowNumber : `0${rowNumber}`}`}
actions={!readOnly ? (
<ArrayAction
rowCount={rows.length}
@@ -278,7 +294,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
</div>
)}
</Draggable>
))}
);
})}
{(rows.length < minRows || (required && rows.length === 0)) && (
<Banner type="error">
This field requires at least
@@ -300,7 +317,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
</div>
)}
</Droppable>
{(!readOnly && (!hasMaxRows)) && (
{(!readOnly && !hasMaxRows) && (
<div className={`${baseClass}__add-button-wrap`}>
<Button
onClick={() => addRow(value as number)}

View File

@@ -1,16 +1,15 @@
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useAuth } from '../../../utilities/Auth';
import { usePreferences } from '../../../utilities/Preferences';
import { useLocale } from '../../../utilities/Locale';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import reducer from '../rowReducer';
import reducer, { Row } from '../rowReducer';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import { useForm } from '../../Form/context';
import buildStateFromSchema from '../../Form/buildStateFromSchema';
import DraggableSection from '../../DraggableSection';
import Error from '../../Error';
import useField from '../../useField';
import Popup from '../../../elements/Popup';
@@ -20,10 +19,15 @@ import Banner from '../../../elements/Banner';
import FieldDescription from '../../FieldDescription';
import { Props } from './types';
import { useOperation } from '../../../utilities/OperationProvider';
import { Collapsible } from '../../../elements/Collapsible';
import { ArrayAction } from '../../../elements/ArrayAction';
import RenderFields from '../../RenderFields';
import { fieldAffectsData } from '../../../../../fields/config/types';
import './index.scss';
import Pill from '../../../elements/Pill';
const baseClass = 'field-type blocks';
const baseClass = 'blocks-field';
const labelDefaults = {
singular: 'Block',
@@ -68,6 +72,7 @@ const Blocks: React.FC<Props> = (props) => {
}, [maxRows, minRows, required, validate]);
const [disableFormData, setDisableFormData] = useState(false);
const [selectorIndexOpen, setSelectorIndexOpen] = useState<number>();
const {
showError,
@@ -81,27 +86,56 @@ const Blocks: React.FC<Props> = (props) => {
condition,
});
const addRow = useCallback(async (rowIndex, blockType) => {
const onAddPopupToggle = useCallback((open) => {
if (!open) {
setSelectorIndexOpen(undefined);
}
}, []);
const addRow = useCallback(async (rowIndex: number, blockType: string) => {
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale });
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
dispatchRows({ type: 'ADD', rowIndex, blockType });
setValue(value as number + 1);
setTimeout(() => {
const newRow = document.getElementById(`${path}-row-${rowIndex + 1}`);
if (newRow) {
const bounds = newRow.getBoundingClientRect();
window.scrollBy({
top: bounds.top - 100,
behavior: 'smooth',
});
}
}, 0);
}, [path, setValue, value, blocks, dispatchFields, operation, id, user, locale]);
const removeRow = useCallback((rowIndex) => {
const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => {
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
dispatchRows({ type: 'ADD', rowIndex, blockType });
setValue(value as number + 1);
}, [dispatchRows, dispatchFields, path, setValue, value]);
const removeRow = useCallback((rowIndex: number) => {
dispatchRows({ type: 'REMOVE', rowIndex });
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
setValue(value as number - 1);
}, [path, setValue, value, dispatchFields]);
const moveRow = useCallback((moveFromIndex, moveToIndex) => {
const moveRow = useCallback((moveFromIndex: number, moveToIndex: number) => {
dispatchRows({ type: 'MOVE', moveFromIndex, moveToIndex });
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path });
}, [dispatchRows, dispatchFields, path]);
const onDragEnd = useCallback((result) => {
if (!result.destination) return;
const sourceIndex = result.source.index;
const destinationIndex = result.destination.index;
moveRow(sourceIndex, destinationIndex);
}, [moveRow]);
const setCollapse = useCallback(async (rowID: string, collapsed: boolean) => {
dispatchRows({ type: 'SET_COLLAPSE', id: rowID, collapsed });
@@ -130,26 +164,28 @@ const Blocks: React.FC<Props> = (props) => {
}
}, [preferencesKey, preferences, path, setPreference, rows]);
const onDragEnd = useCallback((result) => {
if (!result.destination) return;
const sourceIndex = result.source.index;
const destinationIndex = result.destination.index;
moveRow(sourceIndex, destinationIndex);
}, [moveRow]);
const toggleCollapseAll = useCallback(async (collapse: boolean) => {
dispatchRows({ type: 'SET_ALL_COLLAPSED', collapse });
// Get preferences, and once retrieved,
// Reset rows with preferences included
useEffect(() => {
const data = formContext.getDataByPath(path);
if (preferencesKey) {
const preferencesToSet = preferences || { fields: {} };
if (Array.isArray(data) && preferences) {
dispatchRows({ type: 'SET_ALL', data: data || [], collapsedState: preferences?.fields?.[path]?.collapsed });
setPreference(preferencesKey, {
...preferencesToSet,
fields: {
...preferencesToSet?.fields || {},
[path]: {
...preferencesToSet?.fields?.[path],
collapsed: collapse ? rows.map(({ id: rowID }) => rowID) : [],
},
},
});
}
}, [formContext, path, preferencesKey, preferences]);
}, [path, preferences, preferencesKey, rows, setPreference]);
// Set row count on mount and when form context is reset
useEffect(() => {
const data = formContext.getDataByPath(path);
const data = formContext.getDataByPath<Row[]>(path);
dispatchRows({ type: 'SET_ALL', data: data || [] });
}, [formContext, path]);
@@ -166,6 +202,7 @@ const Blocks: React.FC<Props> = (props) => {
const hasMaxRows = maxRows && rows.length >= maxRows;
const classes = [
'field-type',
baseClass,
className,
].filter(Boolean).join(' ');
@@ -182,7 +219,29 @@ const Blocks: React.FC<Props> = (props) => {
/>
</div>
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<h3>{label}</h3>
<ul className={`${baseClass}__header-actions`}>
<li>
<button
type="button"
onClick={() => toggleCollapseAll(true)}
className={`${baseClass}__header-action`}
>
Collapse All
</button>
</li>
<li>
<button
type="button"
onClick={() => toggleCollapseAll(false)}
className={`${baseClass}__header-action`}
>
Show All
</button>
</li>
</ul>
</div>
<FieldDescription
value={value}
description={description}
@@ -202,30 +261,82 @@ const Blocks: React.FC<Props> = (props) => {
const { blockType } = row;
const blockToRender = blocks.find((block) => block.slug === blockType);
const rowNumber = i + 1;
if (blockToRender) {
return (
<DraggableSection
readOnly={readOnly}
<Draggable
key={row.id}
id={row.id}
blockType="blocks"
draggableId={row.id}
index={i}
isDragDisabled={readOnly}
>
{(providedDrag) => (
<div
id={`${path}-row-${i}`}
ref={providedDrag.innerRef}
{...providedDrag.draggableProps}
>
<Collapsible
collapsed={row.collapsed}
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
className={`${baseClass}__row`}
key={row.id}
dragHandleProps={providedDrag.dragHandleProps}
header={(
<div className={`${baseClass}__block-header`}>
<span className={`${baseClass}__block-number`}>
{rowNumber >= 10 ? rowNumber : `0${rowNumber}`}
</span>
<Pill className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}>
{blockToRender.labels.singular}
</Pill>
</div>
)}
actions={!readOnly ? (
<React.Fragment>
<Popup
key={`${blockType}-${i}`}
forceOpen={selectorIndexOpen === i}
onToggleOpen={onAddPopupToggle}
buttonType="none"
size="large"
horizontalAlign="right"
render={({ close }) => (
<BlockSelector
blocks={blocks}
label={blockToRender?.labels?.singular}
isCollapsed={row.collapsed}
rowCount={rows.length}
rowIndex={i}
addRow={addRow}
removeRow={removeRow}
moveRow={moveRow}
setRowCollapse={setCollapse}
parentPath={path}
fieldTypes={fieldTypes}
permissions={permissions}
hasMaxRows={hasMaxRows}
fieldSchema={[
...blockToRender.fields,
]}
addRowIndex={i}
close={close}
/>
)}
/>
<ArrayAction
rowCount={rows.length}
duplicateRow={() => duplicateRow(i, blockType)}
addRow={() => setSelectorIndexOpen(i)}
moveRow={moveRow}
removeRow={removeRow}
index={i}
/>
</React.Fragment>
) : undefined}
>
<RenderFields
forceRender
readOnly={readOnly}
fieldTypes={fieldTypes}
permissions={permissions.fields}
fieldSchema={blockToRender.fields.map((field) => ({
...field,
path: `${path}.${i}${fieldAffectsData(field) ? `.${field.name}` : ''}`,
}))}
/>
</Collapsible>
</div>
)}
</Draggable>
);
}
@@ -251,7 +362,7 @@ const Blocks: React.FC<Props> = (props) => {
)}
</Droppable>
{(!readOnly && (rows.length < maxRows || maxRows === undefined)) && (
{(!readOnly && !hasMaxRows) && (
<div className={`${baseClass}__add-button-wrap`}>
<Popup
buttonType="custom"

View File

@@ -1,8 +1,7 @@
@import '../../../../scss/styles.scss';
.field-type.blocks {
.blocks-field {
margin: base(2) 0;
min-width: base(15);
&__header {
h3 {
@@ -12,6 +11,42 @@
margin-bottom: base(1);
}
&__header-wrap {
display: flex;
align-items: flex-end;
width: 100%;
}
&__header-actions {
list-style: none;
margin: 0 0 0 auto;
padding: 0;
display: flex;
}
&__header-action {
@extend %btn-reset;
cursor: pointer;
margin-left: base(.5);
font-weight: 600;
&:hover {
text-decoration: underline;
}
}
&__block-header {
display: flex;
}
&__block-number {
margin-right: base(.375)
}
&__row {
margin-bottom: base(.5);
}
&__error-wrap {
position: relative;
}
@@ -28,27 +63,7 @@
}
}
.section .section {
margin-top: 0;
}
.section__content {
>div>div {
width: 100%;
}
}
@include mid-break {
min-width: calc(100vw - #{base(2)});
}
}
.field-type.group,
.field-type.array,
.field-type.blocks {
.field-type.blocks {
.field-type.blocks__add-button-wrap {
margin-left: base(3);
}
.field-type:last-child {
margin-bottom: 0;
}
}

View File

@@ -133,7 +133,7 @@
}
@include small-break {
--gutter-h: #{base(.75)};
--gutter-h: #{base(.5)};
}
}
@@ -250,14 +250,8 @@ html {
}
}
html,
body,
#app {
height: 100%;
}
body {
font-family: $font-body;
font-family: var(--font-body);
font-weight: 400;
color: var(--theme-text);
margin: 0;

View File

@@ -1 +0,0 @@
/* Used as a placeholder for when the Payload app does not have custom SCSS */

View File

@@ -5,8 +5,6 @@
// IMPORT OVERRIDES
//////////////////////////////
@import '~payload-scss-overrides';
@import 'type';
@import 'queries';
@import 'resets';

View File

@@ -11,7 +11,7 @@
%h4,
%h5,
%h6 {
font-family: $font-body;
font-family: var(--font-body);
font-weight: 500;
}

View File

@@ -1,10 +1,5 @@
@use 'sass:math';
///////////////////////////////////////////////////////////////////////////////////////
// All variables and mixins within this file are able to be overridden by
// developers using Payload. No CSS definitions should be placed in this file.
///////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////
// BREAKPOINTS
/////////////////////////////
@@ -26,26 +21,6 @@ $baseline : math.div($baseline-px, $baseline-body-size)+rem;
@return math.div($baseline-px, $baseline-body-size) * $multiplier +rem;
}
//////////////////////////////
// FONTS
//////////////////////////////
$font-body : 'Suisse Intl' !default;
$font-mono : monospace !default;
//////////////////////////////
// COLORS (LEGACY - DO NOT USE. PREFER CSS VARIABLES)
//////////////////////////////
$color-dark-gray : #333333 !default;
$color-gray : #9A9A9A !default;
$color-light-gray : #DADADA !default;
$color-background-gray : #F3F3F3 !default;
$color-red : #ff6f76 !default;
$color-yellow : #FDFFA4 !default;
$color-green : #B2FFD6 !default;
$color-purple : #F3DDF3 !default;
//////////////////////////////
// STYLES
//////////////////////////////
@@ -144,7 +119,7 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--color-success-500);
@mixin formInput () {
@include inputShadow;
font-family: $font-body;
font-family: var(--font-body);
width: 100%;
border: 1px solid var(--theme-elevation-150);
background: var(--theme-input-bg);

View File

@@ -1,4 +1,4 @@
export default (element: HTMLElement): number => {
export const getOffsetTop = (element: HTMLElement): number => {
let el = element;
// Set our distance placeholder
let distance = 0;

View File

@@ -18,7 +18,6 @@ export const defaults: Config = {
indexHTML: path.resolve(__dirname, '../admin/index.html'),
components: {},
css: path.resolve(__dirname, '../admin/scss/custom.css'),
scss: path.resolve(__dirname, '../admin/scss/overrides.scss'),
dateFormat: 'MMMM do yyyy, h:mm a',
},
typescript: {

View File

@@ -45,7 +45,6 @@ export default joi.object({
disable: joi.bool(),
indexHTML: joi.string(),
css: joi.string(),
scss: joi.string(),
dateFormat: joi.string(),
components: joi.object()
.keys({

View File

@@ -112,7 +112,6 @@ export type Config = {
disable?: boolean;
indexHTML?: string;
css?: string
scss?: string
dateFormat?: string
components?: {
routes?: AdminRoute[]

View File

@@ -54,7 +54,6 @@ export default (config: SanitizedConfig): Configuration => ({
'payload-config': config.paths.config,
payload$: mockModulePath,
'payload-user-css': config.admin.css,
'payload-scss-overrides': config.admin.scss,
dotenv: mockDotENVPath,
},
extensions: ['.ts', '.tsx', '.js', '.json'],