From fc9a8f7d83e400d534305c978247a93a2d73174d Mon Sep 17 00:00:00 2001 From: atanasster Date: Sun, 3 May 2020 01:14:37 -0400 Subject: [PATCH] feat: props table featured controls with actions --- core/core/src/index.test.ts | 18 ++- core/core/src/randomize.test.ts | 19 ++- core/core/src/randomizeData.ts | 9 +- core/core/src/utils.ts | 45 ++---- .../__snapshots__/csf-props-info.test.ts.snap | 28 ++-- .../extract-props-info.test.ts.snap | 17 ++- core/specification/src/controls.ts | 18 +++ .../controls-editors-starter.stories.tsx | 17 ++- integrations/storybook/rollup.config.js | 1 + .../storybook/src/docs-page/testing-page.tsx | 16 ++ integrations/storybook/src/preset.ts | 5 +- .../test/__snapshots__/imports.test.ts.snap | 28 ++-- .../test/__snapshots__/jsx.test.ts.snap | 1 - ui/blocks/package.json | 3 - .../components/ComponentsBlockContainer.tsx | 10 +- ui/blocks/src/ControlsTable/ControlsTable.tsx | 79 ++-------- .../src/ControlsTable/controlsActions.ts | 65 +++++++++ ui/blocks/src/PropsTable/PropsTable.tsx | 137 ++++++++++++------ ui/blocks/src/StorySource/StorySource.tsx | 4 +- .../components/ComponentsContainer.tsx | 15 +- ui/blocks/src/typings.d.ts | 1 - ui/components/src/ActionBar/ActionBar.tsx | 2 +- .../src/ActionContainer/ActionContainer.tsx | 41 ++---- ui/components/src/Source/Source.tsx | 4 +- .../src/ThemeContext/ThemeContext.tsx | 4 +- ui/pages/src/ClassicPage/ClassicPage.tsx | 4 +- ui/pages/src/TestingPage/TestingPage.tsx | 22 +++ ui/pages/src/TestingPage/index.ts | 1 + ui/pages/src/index.ts | 1 + yarn.lock | 7 +- 30 files changed, 381 insertions(+), 241 deletions(-) create mode 100644 integrations/storybook/src/docs-page/testing-page.tsx create mode 100644 ui/blocks/src/ControlsTable/controlsActions.ts create mode 100644 ui/pages/src/TestingPage/TestingPage.tsx create mode 100644 ui/pages/src/TestingPage/index.ts diff --git a/core/core/src/index.test.ts b/core/core/src/index.test.ts index 358b569b5..f4ed0a8bc 100644 --- a/core/core/src/index.test.ts +++ b/core/core/src/index.test.ts @@ -1,20 +1,22 @@ -import { ControlTypes } from '@component-controls/specification'; +import { + ControlTypes, + ComponentControls, +} from '@component-controls/specification'; import { mergeControlValues, resetControlValues, getControlValues, - LoadedComponentControls, } from './index'; describe('Controls utils', () => { - const controls: LoadedComponentControls = { - name: { type: ControlTypes.TEXT, value: 'hello', defaultValue: 'hello' }, - age: { type: ControlTypes.NUMBER, value: 19, defaultValue: 19 }, + const controls: ComponentControls = { + name: { type: ControlTypes.TEXT, value: 'hello', resetValue: 'hello' }, + age: { type: ControlTypes.NUMBER, value: 19, resetValue: 19 }, }; - const modifiedControls: LoadedComponentControls = { - name: { type: ControlTypes.TEXT, value: 'today', defaultValue: 'hello' }, - age: { type: ControlTypes.NUMBER, value: 19, defaultValue: 19 }, + const modifiedControls: ComponentControls = { + name: { type: ControlTypes.TEXT, value: 'today', resetValue: 'hello' }, + age: { type: ControlTypes.NUMBER, value: 19, resetValue: 19 }, }; it('Should merge property value', () => { diff --git a/core/core/src/randomize.test.ts b/core/core/src/randomize.test.ts index f4cfcb4b8..319cebb7b 100644 --- a/core/core/src/randomize.test.ts +++ b/core/core/src/randomize.test.ts @@ -1,30 +1,33 @@ const faker = require('faker/locale/en_US'); -import { LoadedComponentControls, LoadedComponentControl } from './utils'; -import { ControlTypes } from '@component-controls/specification'; +import { + ControlTypes, + ComponentControls, + ComponentControl, +} from '@component-controls/specification'; import { randomizeData } from '@component-controls/core/src/randomizeData'; describe('Options utility functions', () => { - const name: LoadedComponentControl = { + const name: ComponentControl = { type: ControlTypes.TEXT, value: 'Tom', defaultValue: 'Tom', }; - const lastName: LoadedComponentControl = { + const lastName: ComponentControl = { type: ControlTypes.TEXT, value: 'Mark', defaultValue: 'Mark', }; - const age: LoadedComponentControl = { + const age: ComponentControl = { type: ControlTypes.NUMBER, value: 19, defaultValue: 19, }; - const male: LoadedComponentControl = { + const male: ComponentControl = { type: ControlTypes.BOOLEAN, value: true, defaultValue: true, }; - const object: LoadedComponentControl = { + const object: ComponentControl = { type: ControlTypes.OBJECT, value: { name, @@ -39,7 +42,7 @@ describe('Options utility functions', () => { male, }, }; - const controls: LoadedComponentControls = { + const controls: ComponentControl = { name, lastName, age, diff --git a/core/core/src/randomizeData.ts b/core/core/src/randomizeData.ts index 71d6976c3..e3456bd3d 100644 --- a/core/core/src/randomizeData.ts +++ b/core/core/src/randomizeData.ts @@ -2,11 +2,10 @@ import { ControlTypes, ComponentControlNumber, ComponentControlOptions, + ComponentControls, } from '@component-controls/specification'; const faker = require('faker/locale/en_US'); -import { LoadedComponentControls } from './utils'; - const arrayElements = (arr: any[], c?: number) => { const array = arr || ['a', 'b', 'c']; let count = 0; @@ -32,9 +31,7 @@ interface RandomizedData { [key: string]: any; } -export const randomizeData = ( - controls: LoadedComponentControls, -): RandomizedData => { +export const randomizeData = (controls: ComponentControls): RandomizedData => { return Object.keys(controls) .map(name => { const control = controls[name]; @@ -109,7 +106,7 @@ export const randomizeData = ( return { name, value: { - ...randomizeData(control.value as LoadedComponentControls), + ...randomizeData(control.value as ComponentControls), }, }; } diff --git a/core/core/src/utils.ts b/core/core/src/utils.ts index ab7faaf7d..cd4451211 100644 --- a/core/core/src/utils.ts +++ b/core/core/src/utils.ts @@ -6,31 +6,12 @@ import { ControlTypes, } from '@component-controls/specification'; -/** - * once controls are loaded, the value is saved into - * defaultValue, an additional field in Loaded... - */ - -export type LoadedComponentControl = ComponentControl & { defaultValue?: any }; -export interface LoadedComponentControls { - [name: string]: LoadedComponentControl; -} - -// save default value for 'reset' -export const loadControls = ( - controls: ComponentControls, -): LoadedComponentControls => - Object.keys(controls).reduce((v, key) => { - const prop = controls[key]; - return { ...v, [key]: { ...prop, defaultValue: prop.value } }; - }, {}); - const mergeValue = (control: ComponentControl, value: any): any => { if (control && control.type === ControlTypes.OBJECT) { return { ...control, value: mergeControlValues( - control.value as LoadedComponentControls, + control.value as ComponentControls, undefined, value, ), @@ -39,18 +20,16 @@ const mergeValue = (control: ComponentControl, value: any): any => { return { ...control, value, - defaultValue: - (control as LoadedComponentControl).defaultValue === undefined - ? control.value - : (control as LoadedComponentControl).defaultValue, + resetValue: + control.resetValue === undefined ? control.value : control.resetValue, }; }; export const mergeControlValues = ( - controls: LoadedComponentControls, + controls: ComponentControls, controlName: string | undefined, value: any, -): LoadedComponentControls => { +): ComponentControls => { return controlName ? { ...controls, @@ -69,15 +48,15 @@ export const mergeControlValues = ( }; export const resetControlValues = ( - controls: LoadedComponentControls, + controls: ComponentControls, controlName?: string, ) => { return controlName - ? controls[controlName].defaultValue + ? controls[controlName].resetValue : Object.keys(controls).reduce( (acc, key) => ({ ...acc, - [key]: controls[key].defaultValue, + [key]: controls[key].resetValue, }), {}, ); @@ -88,7 +67,7 @@ export interface ControlValues { } export const getControlValue = ( - controls: LoadedComponentControls, + controls: ComponentControls, propName: string, ): any => { const control: ComponentControl = controls[propName]; @@ -104,16 +83,14 @@ export const getControlValue = ( control.type === ControlTypes.OBJECT && typeof value === 'object' ) { - return getControlValues(value as LoadedComponentControls); + return getControlValues(value as ComponentControls); } return value; } return undefined; }; -export const getControlValues = ( - controls: LoadedComponentControls, -): ControlValues => +export const getControlValues = (controls: ComponentControls): ControlValues => controls ? Object.keys(controls).reduce((acc, key) => { const value = getControlValue(controls, key); diff --git a/core/instrument/test/__snapshots__/csf-props-info.test.ts.snap b/core/instrument/test/__snapshots__/csf-props-info.test.ts.snap index e7ff4b825..869ff1656 100644 --- a/core/instrument/test/__snapshots__/csf-props-info.test.ts.snap +++ b/core/instrument/test/__snapshots__/csf-props-info.test.ts.snap @@ -16,40 +16,48 @@ Can adapt to multiple groups of controls, displaying them in their own tabs.", "description": "if false, will nothave a collapsible frame.", "parentName": "BlockContainerProps", "type": Object { - "name": "boolean", - "raw": "boolean", + "name": "any", + "raw": "any", + }, + }, + "description": Object { + "description": "optional markdown description.", + "parentName": "BlockContainerProps", + "type": Object { + "name": "any", + "raw": "any", }, }, "id": Object { "description": "id of the story", "parentName": "StoryInputProps", "type": Object { - "name": "string", - "raw": "string", + "name": "any", + "raw": "any", }, }, "name": Object { "description": "alternatively you can use the name of a story to load from an external file", "parentName": "StoryInputProps", "type": Object { - "name": "string", - "raw": "string", + "name": "any", + "raw": "any", }, }, "sxStyle": Object { "description": "theme-ui styling object for Block Box", "parentName": "BlockContainerProps", "type": Object { - "name": "SystemStyleObject", - "raw": "SystemStyleObject", + "name": "any", + "raw": "any", }, }, "title": Object { "description": "optional section title for the block.", "parentName": "BlockContainerProps", "type": Object { - "name": "string", - "raw": "string", + "name": "any", + "raw": "any", }, }, }, diff --git a/core/instrument/test/__snapshots__/extract-props-info.test.ts.snap b/core/instrument/test/__snapshots__/extract-props-info.test.ts.snap index b925ea03e..5a5d71eb8 100644 --- a/core/instrument/test/__snapshots__/extract-props-info.test.ts.snap +++ b/core/instrument/test/__snapshots__/extract-props-info.test.ts.snap @@ -816,6 +816,14 @@ Array values are converted into responsive values. ], }, }, + "description": Object { + "description": "optional markdown description.", + "parentName": "BlockContainerProps", + "type": Object { + "name": "string", + "raw": "string", + }, + }, "dir": Object { "parentName": "HTMLAttributes", "type": Object { @@ -2246,7 +2254,7 @@ If an array of components is specified, each component will be displayed in a se if the function returns false, it can stop chabging to the new tab", "type": Object { "name": "function", - "raw": "((name: string, component: StoryComponent) => boolean | void) & ((event: SyntheticEvent) => void)", + "raw": "((event: SyntheticEvent) => void) | ((name: string, component: StoryComponent) => boolean | void)", }, }, "onSelectCapture": Object { @@ -2902,6 +2910,13 @@ import { jsx } from 'theme-ui' "raw": "string", }, }, + "visibleOnControlsOnly": Object { + "description": "set to true if you need the blockto be visible even if only controls are available", + "type": Object { + "name": "boolean", + "raw": "boolean", + }, + }, "vocab": Object { "parentName": "HTMLAttributes", "type": Object { diff --git a/core/specification/src/controls.ts b/core/specification/src/controls.ts index e926cfc70..c27afceb8 100644 --- a/core/specification/src/controls.ts +++ b/core/specification/src/controls.ts @@ -148,10 +148,28 @@ export interface ComponentControlBase { */ value?: T; + /** + * default value is usually set at run-time, from the value + */ + defaultValue?: T; + + /** + * reset value - this is automatically saved as the initial 'value' + * used when user wants to click rest and go back to the initial values + */ + resetValue?: T; + /** * hide the label from the property editor */ hideLabel?: boolean; + + /** + * full text property description. + * can use markdown. + */ + description?: string; + /** * hide the property editor for this property * will only use the value diff --git a/examples/stories/src/stories/controls-editors-starter.stories.tsx b/examples/stories/src/stories/controls-editors-starter.stories.tsx index aab00a8cc..9b2ff7216 100644 --- a/examples/stories/src/stories/controls-editors-starter.stories.tsx +++ b/examples/stories/src/stories/controls-editors-starter.stories.tsx @@ -25,11 +25,26 @@ overview.story = { 'Story with two dynamic control values: `name` and `age`. You can use the controls to edit the story properties at run-time.', controls: { - name: { type: ControlTypes.TEXT, label: 'Name', value: 'Mark' }, + name: { + type: ControlTypes.TEXT, + label: 'Name', + value: 'Mark', + description: ` +## name of the person + +any text is allowed +`, + }, age: { type: ControlTypes.NUMBER, + description: ` +## age of the person + +numeric, values between 10 and 75 allowed +`, label: 'Age', value: 19, + defaultValue: null, min: 10, max: 75, }, diff --git a/integrations/storybook/rollup.config.js b/integrations/storybook/rollup.config.js index 50503b46c..c643fd068 100644 --- a/integrations/storybook/rollup.config.js +++ b/integrations/storybook/rollup.config.js @@ -9,5 +9,6 @@ export default config({ './src/register-props-panel.tsx', './src/register-storysource-panel.tsx', './src//docs-page/full-page.tsx', + './src//docs-page/testing-page.tsx', ], }); diff --git a/integrations/storybook/src/docs-page/testing-page.tsx b/integrations/storybook/src/docs-page/testing-page.tsx new file mode 100644 index 000000000..33aa8f07f --- /dev/null +++ b/integrations/storybook/src/docs-page/testing-page.tsx @@ -0,0 +1,16 @@ +/* eslint-disable react/display-name */ +import React from 'react'; +import { TestingPage } from '@component-controls/pages'; +import { DocsContainer } from './DocsContainer'; + +export default { + key: 'test', + title: 'Testing', + render: ({ active }: { active: boolean }) => { + return ( + + + + ); + }, +}; diff --git a/integrations/storybook/src/preset.ts b/integrations/storybook/src/preset.ts index f7979e78c..2062b0d5c 100644 --- a/integrations/storybook/src/preset.ts +++ b/integrations/storybook/src/preset.ts @@ -10,7 +10,10 @@ module.exports = { }, addons: (entry: any = {}) => { const { pages: customPages } = entry; - const pages = customPages || [require.resolve('./full-page')]; + const pages = customPages || [ + require.resolve('./full-page'), + require.resolve('./testing-page'), + ]; if (pages.length) { return [ { diff --git a/props-info/react-docgen-typescript/test/__snapshots__/imports.test.ts.snap b/props-info/react-docgen-typescript/test/__snapshots__/imports.test.ts.snap index 7852a52db..9dfae3e33 100644 --- a/props-info/react-docgen-typescript/test/__snapshots__/imports.test.ts.snap +++ b/props-info/react-docgen-typescript/test/__snapshots__/imports.test.ts.snap @@ -10,40 +10,48 @@ Object { "description": "if false, will nothave a collapsible frame.", "parentName": "BlockContainerProps", "type": Object { - "name": "boolean", - "raw": "boolean", + "name": "any", + "raw": "any", + }, + }, + "description": Object { + "description": "optional markdown description.", + "parentName": "BlockContainerProps", + "type": Object { + "name": "any", + "raw": "any", }, }, "id": Object { "description": "id of the story", "parentName": "StoryInputProps", "type": Object { - "name": "string", - "raw": "string", + "name": "any", + "raw": "any", }, }, "name": Object { "description": "alternatively you can use the name of a story to load from an external file", "parentName": "StoryInputProps", "type": Object { - "name": "string", - "raw": "string", + "name": "any", + "raw": "any", }, }, "sxStyle": Object { "description": "theme-ui styling object for Block Box", "parentName": "BlockContainerProps", "type": Object { - "name": "SystemStyleObject", - "raw": "SystemStyleObject", + "name": "any", + "raw": "any", }, }, "title": Object { "description": "optional section title for the block.", "parentName": "BlockContainerProps", "type": Object { - "name": "string", - "raw": "string", + "name": "any", + "raw": "any", }, }, }, diff --git a/props-info/react-docgen-typescript/test/__snapshots__/jsx.test.ts.snap b/props-info/react-docgen-typescript/test/__snapshots__/jsx.test.ts.snap index 01a8fa020..7bade3b85 100644 --- a/props-info/react-docgen-typescript/test/__snapshots__/jsx.test.ts.snap +++ b/props-info/react-docgen-typescript/test/__snapshots__/jsx.test.ts.snap @@ -2198,7 +2198,6 @@ import { jsx } from 'theme-ui' "type": Object { "name": "string", "raw": "string", - "required": true, }, }, "translate": Object { diff --git a/ui/blocks/package.json b/ui/blocks/package.json index d4ba69790..af116342a 100644 --- a/ui/blocks/package.json +++ b/ui/blocks/package.json @@ -40,8 +40,6 @@ "@storybook/csf": "^0.0.1", "copy-to-clipboard": "^3.2.1", "global": "^4.3.2", - "js-string-escape": "^1.0.1", - "qs": "^6.9.1", "react": "^16.8.3", "react-dom": "^16.8.3", "react-table": "^7.0.0", @@ -51,7 +49,6 @@ "@theme-ui/presets": "^0.3.0", "@types/jest": "^25.1.2", "@types/mdx-js__react": "^1.5.1", - "@types/qs": "^6.9.1", "@types/theme-ui": "^0.3.0", "cross-env": "^5.2.1", "eslint": "^6.5.1", diff --git a/ui/blocks/src/BlockContainer/components/ComponentsBlockContainer.tsx b/ui/blocks/src/BlockContainer/components/ComponentsBlockContainer.tsx index 9008de887..a1d54b7be 100644 --- a/ui/blocks/src/BlockContainer/components/ComponentsBlockContainer.tsx +++ b/ui/blocks/src/BlockContainer/components/ComponentsBlockContainer.tsx @@ -21,10 +21,11 @@ export const ComponentsBlockContainer: FC = ({ of, children, sxStyle, + visibleOnControlsOnly, ...rest }) => { const [title, setTitle] = React.useState(); - const { components } = useComponentsContext({ of }); + const { components, story } = useComponentsContext({ of }); const componentNames = Object.keys(components); React.useEffect(() => { setTitle( @@ -33,7 +34,11 @@ export const ComponentsBlockContainer: FC = ({ : userTitle, ); }, [userTitle]); - if (!componentNames.length) { + + if ( + !componentNames.length && + (visibleOnControlsOnly !== true || !story?.controls) + ) { //no components to display return null; } @@ -43,6 +48,7 @@ export const ComponentsBlockContainer: FC = ({ onSelect={tabName => userTitle === CURRENT_STORY ? setTitle(tabName) : undefined } + visibleOnControlsOnly={visibleOnControlsOnly} {...rest} > {(component, props, otherProps) => children(component, props, otherProps)} diff --git a/ui/blocks/src/ControlsTable/ControlsTable.tsx b/ui/blocks/src/ControlsTable/ControlsTable.tsx index 0f5b31a6c..eaf3d3ffb 100644 --- a/ui/blocks/src/ControlsTable/ControlsTable.tsx +++ b/ui/blocks/src/ControlsTable/ControlsTable.tsx @@ -1,17 +1,10 @@ /** @jsx jsx */ import { jsx, Box } from 'theme-ui'; -import React, { FC, MouseEvent } from 'react'; -import { window, document } from 'global'; -import qs from 'qs'; -import copy from 'copy-to-clipboard'; -import { ComponentControls } from '@component-controls/specification'; +import React, { FC } from 'react'; import { - resetControlValues, - getControlValues, - LoadedComponentControls, - LoadedComponentControl, - randomizeData, -} from '@component-controls/core'; + ComponentControls, + ComponentControl, +} from '@component-controls/specification'; import { ActionContainer, Tab, @@ -23,6 +16,7 @@ import { StoryBlockContainer, StoryBlockContainerProps, } from '../BlockContainer/story'; +import { useControlsActions } from './controlsActions'; import { BlockControlsContext } from '../context'; import { SingleControlsTable } from './SingleControlsTable'; @@ -32,7 +26,7 @@ export type ControlsTableProps = StoryBlockContainerProps; const DEFAULT_GROUP_ID = 'Other'; interface GroupedControlsType { - [key: string]: LoadedComponentControls; + [key: string]: ComponentControls; } const createData = (controls: ComponentControls): any[] | undefined => @@ -59,7 +53,6 @@ const createData = (controls: ComponentControls): any[] | undefined => export const ControlsTable: FC = ( props: ControlsTableProps, ) => { - const [copied, setCopied] = React.useState(false); const { setControlValue, clickControl } = React.useContext( BlockControlsContext, ); @@ -68,36 +61,15 @@ export const ControlsTable: FC = ( {(context, rest) => { const { story, id: storyId } = context; const { controls } = story || {}; + const controlsActions = useControlsActions({ + controls, + storyId, + setControlValue, + }); if (controls && Object.keys(controls).length) { - const onReset = (e: MouseEvent) => { - e.preventDefault(); - if (setControlValue && storyId) { - const values = resetControlValues(controls); - setControlValue(storyId, undefined, values); - } - }; - const onCopy = (e: MouseEvent) => { - e.preventDefault(); - setCopied(true); - const { location } = document; - const query = qs.parse(location.search, { - ignoreQueryPrefix: true, - }); - const values = getControlValues(controls); - Object.keys(values).forEach(key => { - query[`controls-${key}`] = values[key]; - }); - - copy( - `${location.origin + location.pathname}?${qs.stringify(query, { - encode: false, - })}`, - ); - window.setTimeout(() => setCopied(false), 1500); - }; const groupped: GroupedControlsType = Object.keys(controls) .filter(k => { - const p: LoadedComponentControl = controls[k]; + const p: ComponentControl = controls[k]; return p.type && !p.hidden; }) .reduce((acc: GroupedControlsType, k: string) => { @@ -118,32 +90,9 @@ export const ControlsTable: FC = ( if (groupedItems.length === 0) { return null; } - const actionItems = [ - { - title: copied ? 'copied' : 'copy', - onClick: onCopy, - id: 'copy', - 'aria-label': 'copy control values', - }, - { - title: 'reset', - onClick: onReset, - id: 'reset', - 'aria-label': 'reset control values to their initial value', - }, - { - title: 'randomize', - onClick: () => { - if (setControlValue && controls && storyId) { - setControlValue(storyId, undefined, randomizeData(controls)); - } - }, - id: 'randomize', - 'aria-label': 'generate random values for the component controls', - }, - ]; + return ( - + { + const { controls, setControlValue, storyId } = props; + const [copied, setCopied] = React.useState(false); + if (!controls || !Object.keys(controls).length) { + return []; + } + const onReset = (e: MouseEvent) => { + e.preventDefault(); + if (setControlValue && storyId) { + const values = resetControlValues(controls); + setControlValue(storyId, undefined, values); + } + }; + const onCopy = (e: MouseEvent) => { + e.preventDefault(); + setCopied(true); + const values = getControlValues(controls); + + copy(JSON.stringify(values, null, 2)); + window.setTimeout(() => setCopied(false), 1500); + }; + return [ + { + title: copied ? 'copied' : 'copy', + onClick: onCopy, + id: 'copy', + 'aria-label': 'copy control values', + }, + { + title: 'reset', + onClick: onReset, + id: 'reset', + 'aria-label': 'reset control values to their initial value', + }, + { + title: 'randomize', + onClick: () => { + if (setControlValue && controls && storyId) { + setControlValue(storyId, undefined, randomizeData(controls)); + } + }, + id: 'randomize', + 'aria-label': 'generate random values for the component controls', + }, + ]; +}; diff --git a/ui/blocks/src/PropsTable/PropsTable.tsx b/ui/blocks/src/PropsTable/PropsTable.tsx index 18faf5d02..2dd8c7231 100644 --- a/ui/blocks/src/PropsTable/PropsTable.tsx +++ b/ui/blocks/src/PropsTable/PropsTable.tsx @@ -1,9 +1,15 @@ /* eslint-disable react/display-name */ /** @jsx jsx */ -import { jsx, Text, Flex, Styled } from 'theme-ui'; +import { jsx, Text, Flex, Styled, Box } from 'theme-ui'; import { FC, useMemo, useContext } from 'react'; +import { ComponentControl, PropType } from '@component-controls/specification'; import { getPropertyEditor, PropertyEditor } from '@component-controls/editors'; -import { Table, TableProps, Markdown } from '@component-controls/components'; +import { + Table, + TableProps, + Markdown, + ActionContainer, +} from '@component-controls/components'; import { Column } from 'react-table'; import { ComponentsBlockContainer, @@ -11,7 +17,7 @@ import { } from '../BlockContainer/components/ComponentsBlockContainer'; import { BlockControlsContext } from '../context'; import { InvalidType } from '../notifications'; - +import { useControlsActions } from '../ControlsTable/controlsActions'; export interface PropsTableOwnProps { /** * extra custom columns passed to the PropsTable. @@ -26,6 +32,11 @@ type GroupingProps = Partial< Pick >; +interface PropRow { + name: string; + prop: PropType; + control: ComponentControl; +} export const PropsTable: FC = ({ extraColumns = [], ...props @@ -33,45 +44,68 @@ export const PropsTable: FC = ({ const { setControlValue, clickControl } = useContext(BlockControlsContext); return ( - + {(component, { story }, rest) => { - const { info } = component || {}; - if (!info) { - return null; - } - const { controls } = story || {}; - const keys = Object.keys(info.props); - if (!keys.length) { - return null; - } - const parents = new Set(); - const rows = keys.map(key => { - const prop = info.props[key]; - const parentName = prop.parentName ?? '-'; - parents.add(parentName); - return { - name: key, - prop: { ...prop, parentName }, - control: controls ? controls[key] : undefined, - }; - }); - const groupProps: GroupingProps = {}; - if (parents.size > 1) { - groupProps.expanded = { - [`prop.parentName:${parents.values().next().value}`]: true, - }; - groupProps.groupBy = ['prop.parentName']; - } else { - groupProps.hiddenColumns = ['prop.parentName']; - } - - // check if we should display controls in the PrpsTable - // at least one control's name should exist as a property name - const hasControls = - controls && - Object.keys(controls).some(key => { - return rows.some(({ name }) => name === key); + const { info = { props: undefined } } = component || {}; + const { controls = {} } = story || {}; + const propControls = { ...controls }; + const { rows, hasControls, groupProps } = useMemo(() => { + // check if we should display controls in the PrpsTable + // at least one control's name should exist as a property name + const hasControls = !!Object.keys(propControls).length; + const keys = info.props ? Object.keys(info.props) : []; + const parents = new Set(); + let rows: PropRow[] = keys.map(key => { + //@ts-ignore + const prop = info.props[key]; + const control = propControls[key]; + const parentName = prop.parentName || control?.groupId || '-'; + const { description, label } = control || {}; + if (control) { + delete propControls[key]; + } + parents.add(parentName); + return { + name: key, + prop: { ...prop, description, label, parentName }, + control, + }; }); + if (propControls) { + const controlsRows: PropRow[] = []; + Object.keys(propControls).forEach(key => { + const control = propControls[key]; + if (!control.hidden) { + const parentName = control.groupId || '-'; + parents.add(parentName); + controlsRows.push({ + name: key, + prop: { + description: control.description, + parentName, + defaultValue: control.defaultValue, + type: { + name: control.type, + }, + }, + control, + }); + } + }); + rows = [...controlsRows, ...rows]; + } + const groupProps: GroupingProps = {}; + if (parents.size > 1) { + groupProps.expanded = { + [`prop.parentName:${parents.values().next().value}`]: true, + }; + groupProps.groupBy = ['prop.parentName']; + } else { + groupProps.hiddenColumns = ['prop.parentName']; + } + + return { hasControls, rows, groupProps }; + }, [story?.id, controls]); const columns = useMemo(() => { const cachedColumns = [ @@ -223,10 +257,29 @@ export const PropsTable: FC = ({ }); } return cachedColumns; - }, [extraColumns, hasControls]); - return ( + }, [story?.id, extraColumns, hasControls]); + const table = ( ); + if (!hasControls) { + return table; + } + const controlsActions = useControlsActions({ + controls, + setControlValue, + storyId: story?.id, + }); + return ( + + +
+ + + ); }} ); diff --git a/ui/blocks/src/StorySource/StorySource.tsx b/ui/blocks/src/StorySource/StorySource.tsx index 642dd4ab5..847f6023d 100644 --- a/ui/blocks/src/StorySource/StorySource.tsx +++ b/ui/blocks/src/StorySource/StorySource.tsx @@ -101,7 +101,7 @@ export const StorySource: FC = ({ return ( {tokens.map((line: any, i: number) => (
@@ -162,7 +162,7 @@ export const StorySource: FC = ({ ); }} > - {source} + {source.trim()} ); }} diff --git a/ui/blocks/src/context/components/ComponentsContainer.tsx b/ui/blocks/src/context/components/ComponentsContainer.tsx index 085318ce3..cf4e6018d 100644 --- a/ui/blocks/src/context/components/ComponentsContainer.tsx +++ b/ui/blocks/src/context/components/ComponentsContainer.tsx @@ -6,7 +6,7 @@ import { ComponentInputProps, useComponentsContext } from './ComponentsContext'; export type ComponentsContainerProps = { children: ( - component: StoryComponent, + component: StoryComponent | undefined, props: { story?: Story; tabName: string; @@ -18,27 +18,32 @@ export type ComponentsContainerProps = { * if the function returns false, it can stop chabging to the new tab */ onSelect?: (name: string, component: StoryComponent) => boolean | void; + /** + * set to true if you need the blockto be visible even if only controls are available + */ + visibleOnControlsOnly?: boolean; } & ComponentInputProps; export const ComponentsContainer: React.FC = ({ of, children, onSelect, + visibleOnControlsOnly, ...rest }) => { const { components, story } = useComponentsContext({ of, }); - if (!components) { - return null; + const keys = components ? Object.keys(components) : []; + if (keys.length === 0 && visibleOnControlsOnly === true && story?.controls) { + keys.push('Controls'); } - const keys = Object.keys(components); if (keys.length === 0) { return null; } if (keys.length === 1) { return children( - components[keys[0]], + components ? components[keys[0]] : undefined, { story, tabName: keys[0], diff --git a/ui/blocks/src/typings.d.ts b/ui/blocks/src/typings.d.ts index 2277cc286..0847c5858 100644 --- a/ui/blocks/src/typings.d.ts +++ b/ui/blocks/src/typings.d.ts @@ -1,3 +1,2 @@ declare module 'global'; declare module '@theme-ui/presets'; -declare module 'js-string-escape'; diff --git a/ui/components/src/ActionBar/ActionBar.tsx b/ui/components/src/ActionBar/ActionBar.tsx index 2bdd6a670..7c1ab4e35 100644 --- a/ui/components/src/ActionBar/ActionBar.tsx +++ b/ui/components/src/ActionBar/ActionBar.tsx @@ -8,7 +8,7 @@ export interface ActionBarProps { /** * collection of action items */ - actions: ActionItems; + actions?: ActionItems; } const ActionColors = ({ diff --git a/ui/components/src/ActionContainer/ActionContainer.tsx b/ui/components/src/ActionContainer/ActionContainer.tsx index b962230f3..deaf51982 100644 --- a/ui/components/src/ActionContainer/ActionContainer.tsx +++ b/ui/components/src/ActionContainer/ActionContainer.tsx @@ -1,20 +1,23 @@ /** @jsx jsx */ import { FC } from 'react'; -import { transparentize } from 'polished'; -import { jsx, Box, useThemeUI } from 'theme-ui'; +import styled from '@emotion/styled'; +import { jsx, Box, Theme } from 'theme-ui'; import { ActionBar, ActionItem } from '../ActionBar'; +const StyledContainer = styled(Box)` + ${({ theme }: { theme: Theme }) => ` + border-radius: 4px; + box-shadow: 0px 1px 3px 0px ${theme.colors?.shadow}; + border: 1px solid ${theme.colors?.shadow}; + `} +`; + export interface ActionContainerProps { /** * optional actions provided to the component */ actions?: ActionItem[]; - /** - * padding at the top, to account for the absolute position of the ActionBar - */ - paddingTop?: string | number; - /** * if plain, skip the border and spacing around the children */ @@ -27,31 +30,13 @@ export interface ActionContainerProps { export const ActionContainer: FC = ({ children, actions, - paddingTop, plain, }) => { - const { theme } = useThemeUI(); + const hasActions = actions && !!actions.length; return (
- {actions && } - {plain ? ( - children - ) : ( - - {children} - - )} + {hasActions && } + {plain ? children : {children}}
); }; diff --git a/ui/components/src/Source/Source.tsx b/ui/components/src/Source/Source.tsx index c8e235ab7..b376cf638 100644 --- a/ui/components/src/Source/Source.tsx +++ b/ui/components/src/Source/Source.tsx @@ -39,12 +39,12 @@ export const Source: FC = ({ ]; return ( - + diff --git a/ui/components/src/ThemeContext/ThemeContext.tsx b/ui/components/src/ThemeContext/ThemeContext.tsx index 3c91299e7..81e7ad1e2 100644 --- a/ui/components/src/ThemeContext/ThemeContext.tsx +++ b/ui/components/src/ThemeContext/ThemeContext.tsx @@ -5,7 +5,7 @@ import { merge } from '@theme-ui/core'; import { ThemeProvider as ThemeUIProvider, Theme } from 'theme-ui'; -import { lighten } from 'polished'; +import { lighten, transparentize } from 'polished'; export interface ThemeContextProps { theme?: Theme; @@ -96,6 +96,7 @@ export const ThemeProvider: React.FC = ({ highlight: '#339793', selected: '#1EA7FD', fadedText: lighten(0.25, defTheme.colors.text), + shadow: transparentize(0.9, defTheme.colors.text), modes: { dark: { ...(defTheme.colors.modes ? defTheme.colors.modes.dark : {}), @@ -103,6 +104,7 @@ export const ThemeProvider: React.FC = ({ text: '#d3d4db', header: '#111111', fadedText: '#b3b4ba', + shadow: transparentize(0.9, '#d3d4db'), }, }, }, diff --git a/ui/pages/src/ClassicPage/ClassicPage.tsx b/ui/pages/src/ClassicPage/ClassicPage.tsx index 01f9d40f4..678e06b3a 100644 --- a/ui/pages/src/ClassicPage/ClassicPage.tsx +++ b/ui/pages/src/ClassicPage/ClassicPage.tsx @@ -1,7 +1,6 @@ import React, { FC } from 'react'; import { EditPage, - ControlsTable, Title, Subtitle, Story, @@ -23,8 +22,7 @@ export const ClassicPage: FC = () => { - - + ); diff --git a/ui/pages/src/TestingPage/TestingPage.tsx b/ui/pages/src/TestingPage/TestingPage.tsx new file mode 100644 index 000000000..99299de25 --- /dev/null +++ b/ui/pages/src/TestingPage/TestingPage.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; +import { + EditPage, + ControlsTable, + Title, + Subtitle, + Story, + Description, +} from '@component-controls/blocks'; + +export const TestingPage: FC = () => { + return ( + <> + + + <Subtitle /> + <Description /> + <Story id="." /> + <ControlsTable /> + </> + ); +}; diff --git a/ui/pages/src/TestingPage/index.ts b/ui/pages/src/TestingPage/index.ts new file mode 100644 index 000000000..141e0aecb --- /dev/null +++ b/ui/pages/src/TestingPage/index.ts @@ -0,0 +1 @@ +export * from './TestingPage'; diff --git a/ui/pages/src/index.ts b/ui/pages/src/index.ts index ffbfbbf20..0df508841 100644 --- a/ui/pages/src/index.ts +++ b/ui/pages/src/index.ts @@ -1,3 +1,4 @@ export * from './CanvasPage'; export * from './ClassicPage'; export * from './CurrentStoryPage'; +export * from './TestingPage'; diff --git a/yarn.lock b/yarn.lock index d910161d5..ca7dbe6a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4099,11 +4099,6 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== -"@types/qs@^6.9.1": - version "6.9.1" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" - integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw== - "@types/reach__router@^1.2.3": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.5.tgz#14e1e981cccd3a5e50dc9e969a72de0b9d472f6d" @@ -13911,7 +13906,7 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.6.0, qs@^6.9.1: +qs@^6.6.0: version "6.9.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==