diff --git a/integrations/storybook/src/blocks/BlockContext.tsx b/integrations/storybook/src/blocks/BlockContext.tsx index 545653d80..7fd7d134a 100644 --- a/integrations/storybook/src/blocks/BlockContext.tsx +++ b/integrations/storybook/src/blocks/BlockContext.tsx @@ -1,17 +1,56 @@ import React from 'react'; import { BlockContext } from '@component-controls/blocks'; +import storyStore from '@component-controls/loader/story-store-data'; import { DocsContext } from '@storybook/addon-docs/blocks'; +import { FORCE_RE_RENDER } from '@storybook/core-events'; +import { + SetControlValueFn, + ClickControlFn, + ComponentControlButton, +} from '@component-controls/specification'; +import { mergeControlValues } from '@component-controls/core'; +import { SET_DATA_MSG } from '../shared/shared'; export const BlockContextProvider: React.FC = ({ children }) => { const context = React.useContext(DocsContext); const { id: currentId, clientApi, channel } = context as any; - + const story = storyStore && storyStore[currentId]; + const { controls } = story || {}; + const setControlValue: SetControlValueFn = + clientApi && clientApi.setControlValue + ? clientApi.setControlValue + : (storyId: string, propName: string | undefined, propValue: any) => { + if (controls) { + const newValues = mergeControlValues(controls, propName, propValue); + Object.keys(controls).forEach(key => { + controls[key] = newValues[key]; + }); + channel.emit(FORCE_RE_RENDER); + channel.emit(SET_DATA_MSG, { + storyId, + controls: newValues, + }); + } + }; + const clickControl: ClickControlFn = + clientApi && clientApi.clickControl + ? clientApi.clickControl + : (storyId: string, propName: string) => { + if (controls && controls[propName]) { + const control: ComponentControlButton = controls[ + propName + ] as ComponentControlButton; + if (control && typeof control.onClick === 'function') { + control.onClick(control); + } + } + }; return ( {children} diff --git a/integrations/storybook/src/blocks/ControlsTable.tsx b/integrations/storybook/src/blocks/ControlsTable.tsx index ff1dd8dca..c437d2674 100644 --- a/integrations/storybook/src/blocks/ControlsTable.tsx +++ b/integrations/storybook/src/blocks/ControlsTable.tsx @@ -1,18 +1,10 @@ import React, { FC } from 'react'; -import { FORCE_RE_RENDER } from '@storybook/core-events'; -import { - SetControlValueFn, - ClickControlFn, - ComponentControlButton, -} from '@component-controls/specification'; -import { mergeControlValues } from '@component-controls/core'; import { BlockControlsTable, BlockControlsTableProps, useStoryContext, } from '@component-controls/blocks'; -import { SET_DATA_MSG } from '../shared/shared'; import { ThemeProvider } from '../shared/ThemeProvider'; export type ControlsTableProps = BlockControlsTableProps; @@ -21,52 +13,19 @@ export const ControlsTable: FC = ({ name, ...rest }) => { - const { id, story, api, channel } = useStoryContext({ + const { id, story } = useStoryContext({ id: propId, name, }); const { controls } = story || {}; - const setControlValue: SetControlValueFn = - api && api.setControlValue - ? api.setControlValue - : (storyId: string, propName: string | undefined, propValue: any) => { - if (controls) { - const newValues = mergeControlValues(controls, propName, propValue); - Object.keys(controls).forEach(key => { - controls[key] = newValues[key]; - }); - channel.emit(FORCE_RE_RENDER); - channel.emit(SET_DATA_MSG, { - storyId, - controls: newValues, - }); - } - }; - const clickControl: ClickControlFn = - api && api.clickControl - ? api.clickControl - : (storyId: string, propName: string) => { - if (controls && controls[propName]) { - const control: ComponentControlButton = controls[ - propName - ] as ComponentControlButton; - if (control && typeof control.onClick === 'function') { - control.onClick(control); - } - } - }; + if (!controls || controls.disable) { return null; } return id ? ( - + ) : null; }; diff --git a/integrations/storybook/src/manager/Panel.tsx b/integrations/storybook/src/manager/Panel.tsx index ee0102d22..1d995fea5 100644 --- a/integrations/storybook/src/manager/Panel.tsx +++ b/integrations/storybook/src/manager/Panel.tsx @@ -8,6 +8,7 @@ import { import { SetControlValueFn } from '@component-controls/specification'; import { SET_STORIES } from '@storybook/core-events'; import { API } from '@storybook/api'; +import { BlockContext } from '@component-controls/blocks'; import { ControlsTable as SharedControlsTable } from '@component-controls/blocks'; import { SET_DATA_MSG, @@ -69,11 +70,14 @@ const WrappedControlsTable: React.FC = ({ - + + + diff --git a/integrations/storybook/src/preview/PreviewPanel.tsx b/integrations/storybook/src/preview/PreviewPanel.tsx index d96cd1b2e..96cf5e29e 100644 --- a/integrations/storybook/src/preview/PreviewPanel.tsx +++ b/integrations/storybook/src/preview/PreviewPanel.tsx @@ -3,26 +3,16 @@ import { ControlsTable as SharedControlsTable } from '@component-controls/blocks export const createControlsPanel = ({ storyId, - context, }: { storyId: string; - context: any; }): any | null => { // @ts-ignore - const { clientApi: api } = context; const name = 'controls'; - const { setControlValue, clickControl } = api; return (expanded: any): any => { switch (true) { case expanded === name: { return { - node: ( - - ), + node: , title: `Hide ${name}`, }; } diff --git a/ui/blocks/README.md b/ui/blocks/README.md index de5850c41..4c76af697 100644 --- a/ui/blocks/README.md +++ b/ui/blocks/README.md @@ -15,6 +15,7 @@ - [StorySource](#insstorysourceins) - [Subtitle](#inssubtitleins) - [Title](#institleins) + - [InvalidType](#insinvalidtypeins) # Overview @@ -124,12 +125,6 @@ _BlockControlsTable [source code](https:/github.com/ccontrols/component-controls **Properties:** -- **setControlValue**? : _SetControlValueFn_ - - generic function to update the values of component controls. -- **clickControl**? : _ClickControlFn_ - - generic function to propagate a click event for component controls. - **id**? : _string_ id of the story @@ -150,12 +145,6 @@ _ControlsTable [source code](https:/github.com/ccontrols/component-controls/blob **Properties:** -- **setControlValue**? : _SetControlValueFn_ - - generic function to update the values of component controls. -- **clickControl**? : _ClickControlFn_ - - generic function to propagate a click event for component controls. - **id**? : _string_ id of the story @@ -175,13 +164,16 @@ _SingleControlsTable [source code](https:/github.com/ccontrols/component-control - **controls**? : _ComponentControls_ - componnet controls to display in the table. + component controls to display in the table. - **storyId**? : _string_ storyId, will be used to update the values of the controls - **setControlValue**? : _SetControlValueFn_ generic function to update the values of component controls. +- **clickControl**? : _ClickControlFn_ + + generic function to propagate a click event for component controls. ## Description @@ -207,6 +199,9 @@ _BlockPropsTable [source code](https:/github.com/ccontrols/component-controls/bl **Properties:** +- **extraColumns**? : _Column<{}>\[]_ + + extra custom columns passed to the PropsTable. - **of**? : _any_ Specify the component(s), for which to have information displayed. @@ -243,6 +238,9 @@ _PropsTable [source code](https:/github.com/ccontrols/component-controls/blob/ma **Properties:** +- **extraColumns**? : _Column<{}>\[]_ + + extra custom columns passed to the PropsTable. - **of**? : _any_ Specify the component(s), for which to have information displayed. @@ -407,4 +405,10 @@ _Title [source code](https:/github.com/ccontrols/component-controls/blob/master/ text to be displayed in the component. - **ref**? : _((instance: HTMLHeadingElement) => void) | RefObject<HTMLHeadingElement>_ +## InvalidType + +error message when the control type is not found. + +_InvalidType [source code](https:/github.com/ccontrols/component-controls/blob/master/ui/blocks/src/notifications/InvalidType.tsx)_ + diff --git a/ui/blocks/package.json b/ui/blocks/package.json index 9b3529077..633ca750d 100644 --- a/ui/blocks/package.json +++ b/ui/blocks/package.json @@ -41,8 +41,8 @@ "qs": "^6.9.1", "react": "^16.8.3", "react-dom": "^16.8.3", - "theme-ui": "^0.3.1", - "@theme-ui/prism": "^0.3.0" + "react-table": "^7.0.0", + "theme-ui": "^0.3.1" }, "devDependencies": { "@types/jest": "^25.1.2", @@ -56,7 +56,9 @@ }, "peerDependencies": { "react": "*", - "react-dom": "*" + "react-dom": "*", + "react-table": "*", + "theme-ui": "*" }, "publishConfig": { "access": "public" diff --git a/ui/blocks/src/ControlsTable/block/BlockControlsTable.stories.tsx b/ui/blocks/src/ControlsTable/block/BlockControlsTable.stories.tsx index 9d0febe11..5e398e81b 100644 --- a/ui/blocks/src/ControlsTable/block/BlockControlsTable.stories.tsx +++ b/ui/blocks/src/ControlsTable/block/BlockControlsTable.stories.tsx @@ -1,9 +1,5 @@ import React from 'react'; -import { ControlTypes } from '@component-controls/specification'; -import { - LoadedComponentControls, - mergeControlValues, -} from '@component-controls/core'; +import { StoryContextConsumer } from '../../context/story/StoryContext'; import { MockContext } from '../../test/MockContext'; import { BlockControlsTable } from './BlockControlsTable'; @@ -13,42 +9,14 @@ export default { }; export const overview = () => { - const [controls, setControls] = React.useState({ - name: { - type: ControlTypes.TEXT, - label: 'Name', - value: 'Mark', - defaultValue: 'Mark', - }, - age: { - type: ControlTypes.NUMBER, - label: 'Age', - value: 19, - defaultValue: 19, - }, - }); - return ( - -

{`Hello, my name is ${controls.name.value}, and I am ${controls.age.value} years old.`}

- - setControls(mergeControlValues(controls, name, value)) - } - clickControl={() => - setControls( - mergeControlValues( - controls, - 'age', - typeof controls.age.value === 'string' - ? parseInt(controls.age.value, 10) + 1 - : 19, - ), - ) - } - /> + + + {({ story: { controls } = {} }) => ( +

{`Hello, my name is ${controls?.name.value}, and I am ${controls?.age.value} years old.`}

+ )} +
+
); }; diff --git a/ui/blocks/src/ControlsTable/plain/ControlsTable.stories.tsx b/ui/blocks/src/ControlsTable/plain/ControlsTable.stories.tsx index 4ca6fc08b..549ea5f1d 100644 --- a/ui/blocks/src/ControlsTable/plain/ControlsTable.stories.tsx +++ b/ui/blocks/src/ControlsTable/plain/ControlsTable.stories.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { ControlTypes } from '@component-controls/specification'; -import { - LoadedComponentControls, - mergeControlValues, - getControlValues, - loadControls, -} from '@component-controls/core'; +import { StoryContextConsumer } from '../../context/story/StoryContext'; import { ControlsTable } from './ControlsTable'; import { MockContext } from '../../test/MockContext'; @@ -15,240 +9,14 @@ export default { }; export const overview = () => { - const [controls, setControls] = React.useState({ - name: { - type: ControlTypes.TEXT, - label: 'Name', - value: 'Mark', - defaultValue: 'Mark', - }, - age: { - type: ControlTypes.NUMBER, - label: 'Age', - value: 19, - defaultValue: 19, - }, - }); - - return ( - -

{`Hello, my name is ${controls.name.value}, and I am ${controls.age.value} years old.`}

- - setControls(mergeControlValues(controls, name, value)) - } - clickControl={() => - setControls( - mergeControlValues( - controls, - 'age', - typeof controls.age.value === 'string' - ? parseInt(controls.age.value, 10) + 1 - : 19, - ), - ) - } - /> -
- ); -}; - -const arrayOfObjects = [ - { - label: 'Sparky', - dogParent: 'Matthew', - location: 'Austin', - }, - { - label: 'Juniper', - dogParent: 'Joshua', - location: 'Austin', - }, -]; -const GROUP_IDS = { - DISPLAY: 'Display', - GENERAL: 'General', - FAVORITES: 'Favorites', -}; - -const advancedControls: LoadedComponentControls = loadControls({ - userName: { - type: ControlTypes.TEXT, - label: 'Name', - value: 'Storyteller', - groupId: GROUP_IDS.GENERAL, - }, - age: { - type: ControlTypes.NUMBER, - label: 'Age', - value: 78, - range: true, - min: 0, - max: 90, - step: 5, - groupId: GROUP_IDS.GENERAL, - }, - birthday: { - type: ControlTypes.DATE, - label: 'Birthday', - value: new Date(), - groupId: GROUP_IDS.GENERAL, - }, - dollars: { - type: ControlTypes.NUMBER, - label: 'Dollars', - value: 12.5, - min: 0, - max: 100, - step: 0.01, - groupId: GROUP_IDS.GENERAL, - }, - years: { - type: ControlTypes.NUMBER, - label: 'Years in NY', - value: 9, - groupId: GROUP_IDS.GENERAL, - }, - nice: { - type: ControlTypes.BOOLEAN, - label: 'Nice', - value: true, - groupId: GROUP_IDS.FAVORITES, - }, - items: { - type: ControlTypes.ARRAY, - label: 'Items', - value: ['Laptop', 'Book', 'Whiskey'], - groupId: GROUP_IDS.FAVORITES, - }, - - fruit: { - type: ControlTypes.OPTIONS, - label: 'Fruit', - value: 'apple', - options: { - Apple: 'apple', - Banana: 'banana', - Cherry: 'cherry', - }, - groupId: GROUP_IDS.FAVORITES, - }, - otherFruit: { - type: ControlTypes.OPTIONS, - label: 'Other Fruit', - value: 'watermelon', - options: { - Kiwi: 'kiwi', - Guava: 'guava', - Watermelon: 'watermelon', - }, - display: 'radio', - groupId: GROUP_IDS.FAVORITES, - }, - dog: { - type: ControlTypes.OPTIONS, - options: arrayOfObjects, - value: arrayOfObjects[0], - groupId: GROUP_IDS.FAVORITES, - }, - backgroundColor: { - type: ControlTypes.COLOR, - value: '#dedede', - groupId: GROUP_IDS.DISPLAY, - }, - - color: { - type: ControlTypes.COLOR, - value: '#000000', - groupId: GROUP_IDS.DISPLAY, - }, - otherStyles: { - type: ControlTypes.OBJECT, - label: 'Styles', - value: { - // do not randomize the border style - border: { - type: ControlTypes.TEXT, - value: '2px dashed silver', - data: null, - }, - borderRadius: { type: ControlTypes.NUMBER, value: 10 }, - padding: { type: ControlTypes.NUMBER, value: 10 }, - }, - groupId: GROUP_IDS.DISPLAY, - }, - images: { - type: ControlTypes.FILES, - label: 'Happy Picture', - accept: 'image/*', - value: [ - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfiARwMCyEWcOFPAAAAP0lEQVQoz8WQMQoAIAwDL/7/z3GwghSp4KDZyiUpBMCYUgd8rehtH16/l3XewgU2KAzapjXBbNFaPS6lDMlKB6OiDv3iAH1OAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE4LTAxLTI4VDEyOjExOjMzLTA3OjAwlAHQBgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOC0wMS0yOFQxMjoxMTozMy0wNzowMOVcaLoAAAAASUVORK5CYII=', - ], - groupId: GROUP_IDS.DISPLAY, - }, -}); - -export const advanced = () => { - const [controls, setControls] = React.useState( - advancedControls, - ); - const { - userName, - age, - fruit, - otherFruit, - dollars, - years, - backgroundColor, - color, - items, - otherStyles, - nice, - images, - dog, - birthday, - } = getControlValues(controls); - const intro = `My name is ${userName}, I'm ${age} years old, and my favorite fruit is ${fruit}. I also enjoy ${otherFruit}, and hanging out with my dog ${dog}`; - const style = { - backgroundColor, - color, - ...otherStyles, - }; - const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!'; - const dateOptions = { - year: 'numeric', - month: 'long', - day: 'numeric', - timeZone: 'UTC', - }; - return ( - -
-

{intro}

-

- My birthday is:{' '} - {new Date(birthday).toLocaleDateString('en-US', dateOptions)} at: - {new Date(birthday).toLocaleTimeString()} -

-

I live in NY for {years} years.

-

My wallet contains: ${dollars.toFixed(2)}

-

In my backpack, I have:

-
    - {items && items.map((item: string) =>
  • {item}
  • )} -
-

{salutation}

-

- When I am happy I look like this: happy -

-
- - setControls(mergeControlValues(controls, name, value)) - } - /> + + + {({ story: { controls } = {} }) => ( +

{`Hello, my name is ${controls?.name.value}, and I am ${controls?.age.value} years old.`}

+ )} +
+
); }; diff --git a/ui/blocks/src/ControlsTable/plain/ControlsTable.tsx b/ui/blocks/src/ControlsTable/plain/ControlsTable.tsx index 9b44c9cee..0808eea09 100644 --- a/ui/blocks/src/ControlsTable/plain/ControlsTable.tsx +++ b/ui/blocks/src/ControlsTable/plain/ControlsTable.tsx @@ -4,10 +4,6 @@ import React, { FC, MouseEvent } from 'react'; import { window, document } from 'global'; import qs from 'qs'; import copy from 'copy-to-clipboard'; -import { - SetControlValueFn, - ClickControlFn, -} from '@component-controls/specification'; import { resetControlValues, getControlValues, @@ -28,19 +24,7 @@ import { SingleControlsTable } from './SingleControlsTable'; import { StoryInputProps } from '../../context/story/StoryContext'; -export interface ControlsTableOwnProps { - /** - * generic function to update the values of component controls. - */ - setControlValue?: SetControlValueFn; - - /** - * generic function to propagate a click event for component controls. - */ - clickControl?: ClickControlFn; -} - -export type ControlsTableProps = ControlsTableOwnProps & StoryInputProps; +export type ControlsTableProps = StoryInputProps; const DEFAULT_GROUP_ID = 'Other'; @@ -58,12 +42,13 @@ export const ControlsTable: FC = ({ ...rest }) => { const [copied, setCopied] = React.useState(false); - const { story, id: storyId } = useStoryContext({ - id, - name, - }); + const { story, id: storyId, setControlValue, clickControl } = useStoryContext( + { + id, + name, + }, + ); const { controls } = story || {}; - const { setControlValue } = rest; if (controls && Object.keys(controls).length) { const onReset = (e: MouseEvent) => { e.preventDefault(); @@ -143,6 +128,8 @@ export const ControlsTable: FC = ({ {groupedItems.length === 1 ? ( @@ -157,6 +144,8 @@ export const ControlsTable: FC = ({ diff --git a/ui/blocks/src/ControlsTable/plain/SingleControlsTable.tsx b/ui/blocks/src/ControlsTable/plain/SingleControlsTable.tsx index 89f001f25..7c0ec28f7 100644 --- a/ui/blocks/src/ControlsTable/plain/SingleControlsTable.tsx +++ b/ui/blocks/src/ControlsTable/plain/SingleControlsTable.tsx @@ -8,12 +8,11 @@ import { import { getPropertyEditor, PropertyEditor } from '@component-controls/editors'; import { Table } from '@component-controls/components'; import { Flex } from 'theme-ui'; - -const InvalidType = () => Invalid Type; +import { InvalidType } from '../../notifications/InvalidType'; export interface SingleControlsTableProps { /** - * componnet controls to display in the table. + * component controls to display in the table. */ controls?: ComponentControls; /** @@ -28,6 +27,7 @@ export interface SingleControlsTableProps { /** * generic function to propagate a click event for component controls. */ + clickControl?: ClickControlFn; } /** diff --git a/ui/blocks/src/PropsTable/block/BlockPropsTable.stories.tsx b/ui/blocks/src/PropsTable/block/BlockPropsTable.stories.tsx index f4c351c50..911ea93c5 100644 --- a/ui/blocks/src/PropsTable/block/BlockPropsTable.stories.tsx +++ b/ui/blocks/src/PropsTable/block/BlockPropsTable.stories.tsx @@ -3,12 +3,12 @@ import { BlockPropsTable } from './BlockPropsTable'; import { MockContext } from '../../test/MockContext'; export default { - title: 'Blocks/Core/BlockPropsTable', + title: 'Blocks/Core/PropsTable/block', component: BlockPropsTable, }; -export const simple = () => ( - +export const overview = () => ( + ); diff --git a/ui/blocks/src/PropsTable/plain/PropsTable.stories.tsx b/ui/blocks/src/PropsTable/plain/PropsTable.stories.tsx index dafad00f2..227530266 100644 --- a/ui/blocks/src/PropsTable/plain/PropsTable.stories.tsx +++ b/ui/blocks/src/PropsTable/plain/PropsTable.stories.tsx @@ -1,14 +1,48 @@ import React from 'react'; import { PropsTable } from './PropsTable'; +import { StoryContextConsumer } from '../../context/story/StoryContext'; import { MockContext } from '../../test/MockContext'; export default { - title: 'Blocks/Core/PropsTable', + title: 'Blocks/Core/PropsTable/plain', component: PropsTable, }; -export const simple = () => ( - +export const overview = () => ( + + + +); + +export const subcomponents = () => ( + + + +); + +export const extraColumns = () => ( + + { + //@ts-ignore` + return row.original.name.toUpperCase(); + }, + }, + ]} + /> + +); + +export const controls = () => ( + + + {({ story: { controls } = {} }) => ( +

{`Hello, my name is ${controls?.name.value}, and I am ${controls?.age.value} years old.`}

+ )} +
); diff --git a/ui/blocks/src/PropsTable/plain/PropsTable.tsx b/ui/blocks/src/PropsTable/plain/PropsTable.tsx index 1a439f419..448e0cdd4 100644 --- a/ui/blocks/src/PropsTable/plain/PropsTable.tsx +++ b/ui/blocks/src/PropsTable/plain/PropsTable.tsx @@ -2,11 +2,20 @@ /** @jsx jsx */ import { jsx, Text, Flex, Styled } from 'theme-ui'; import { FC } from 'react'; +import { getPropertyEditor, PropertyEditor } from '@component-controls/editors'; import { Table, TableProps, Markdown } from '@component-controls/components'; +import { Column } from 'react-table'; import { ComponentsContainer } from '../../context/components/ComponentsContainer'; import { ComponentInputProps } from '../../context/components/ComponentsContext'; - -export type PropsTableProps = ComponentInputProps & +import { InvalidType } from '../../notifications'; +export interface PropsTableOwnProps { + /** + * extra custom columns passed to the PropsTable. + */ + extraColumns?: Column[]; +} +export type PropsTableProps = PropsTableOwnProps & + ComponentInputProps & Omit; type GroupingProps = Partial< @@ -15,11 +24,12 @@ type GroupingProps = Partial< export const PropsTable: FC = ({ of, + extraColumns = [], sorting = true, ...rest }) => ( - {component => { + {(component, { story, setControlValue, clickControl }) => { const { info } = component || {}; if (!info) { return null; @@ -45,116 +55,167 @@ export const PropsTable: FC = ({ } /* */ + const columns: Column[] = [ + { + Header: 'Parent', + accessor: 'prop.parentName', + }, + { + Header: 'Name', + accessor: 'name', + Cell: ({ row: { original } }: any) => { + if (!original) { + return null; + } + const { + name, + prop: { + type: { required }, + }, + } = original; - return ( - { - if (!original) { - return null; - } - const { - name, - prop: { - type: { required }, - }, - } = original; - - return ( - + {name} + {required ? '*' : ''} + + ); + }, + }, + { + Header: 'Description', + accessor: 'prop.description', + width: '60%', + Cell: ({ row: { original } }: any) => { + if (!original) { + return null; + } + const { + prop: { + description, + type: { raw, name }, + }, + } = original; + return ( + + {description && {description}} + {(raw ?? name) && ( + - {name} - {required ? '*' : ''} - - ); - }, - }, - { - Header: 'Description', - accessor: 'prop.description', - width: '60%', - Cell: ({ row: { original } }: any) => { - if (!original) { - return null; - } - const { - prop: { - description, - type: { raw, name }, - }, - } = original; + {raw ?? name} + + )} + + ); + }, + }, + { + Header: 'Default', + accessor: 'prop.defaultValue', + width: '20%', + Cell: ({ row: { original } }: any) => { + if (!original) { + return null; + } + const { + prop: { defaultValue }, + } = original; + let value = null; + switch (typeof defaultValue) { + case 'object': + value = JSON.stringify(defaultValue, null, 2); + break; + case 'undefined': + value = '-'; + break; + default: + value = defaultValue.toString(); + } + return ( + + {value} + + ); + }, + }, + ]; + const { controls } = story || {}; + // 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); + }); + if (hasControls) { + columns.push({ + Header: 'Controls', + width: '30%', + Cell: ({ row: { original } }: any) => { + if (controls) { + const control = controls[original.name]; + if (control && story) { + const InputType: PropertyEditor = + getPropertyEditor(control.type) || InvalidType; + const onChange = (propName: string, value: any) => { + if (setControlValue && story.id) { + setControlValue(story.id, propName, value); + } + }; + const onClick = () => { + if (clickControl && story.id) { + clickControl(story.id, name); + } + }; return ( - {description && {description}} - {(raw ?? name) && ( - - {raw ?? name} - - )} + ); - }, - }, - { - Header: 'Default', - accessor: 'prop.defaultValue', - width: '20%', - Cell: ({ row: { original } }: any) => { - if (!original) { - return null; - } - const { - prop: { defaultValue }, - } = original; - let value = null; - switch (typeof defaultValue) { - case 'object': - value = JSON.stringify(defaultValue, null, 2); - break; - case 'undefined': - value = '-'; - break; - default: - value = defaultValue.toString(); - } - return ( - - {value} - - ); - }, - }, - ]} + } + } + return null; + }, + }); + } + + return ( +
); diff --git a/ui/blocks/src/PropsTable/plain/Subcomponents.stories.tsx b/ui/blocks/src/PropsTable/plain/Subcomponents.stories.tsx deleted file mode 100644 index 7d2f6bdc0..000000000 --- a/ui/blocks/src/PropsTable/plain/Subcomponents.stories.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { PropsTable } from './PropsTable'; -import { Title } from '../../Title'; -import { Subtitle } from '../../Subtitle'; - -export default { - title: 'Blocks/Core/PropsTable', - component: Title, - subcompoents: { Subtitle }, -}; - -export const subcompoents = () => ; diff --git a/ui/blocks/src/context/components/ComponentsContainer.tsx b/ui/blocks/src/context/components/ComponentsContainer.tsx index 796202db1..f5224db7c 100644 --- a/ui/blocks/src/context/components/ComponentsContainer.tsx +++ b/ui/blocks/src/context/components/ComponentsContainer.tsx @@ -1,18 +1,35 @@ import React from 'react'; -import { StoryComponent } from '@component-controls/specification'; +import { + Story, + StoryComponent, + SetControlValueFn, + ClickControlFn, +} from '@component-controls/specification'; import { Tab, Tabs, TabList, TabPanel } from '@component-controls/components'; import { ComponentInputProps, useComponentsContext } from './ComponentsContext'; export type ComponentsContainerProps = { - children: (component: StoryComponent) => React.ReactElement | null; + children: ( + component: StoryComponent, + props: { + story?: Story; + setControlValue?: SetControlValueFn; + clickControl?: ClickControlFn; + }, + ) => React.ReactElement | null; } & ComponentInputProps; export const ComponentsContainer: React.FC = ({ of, children, }) => { - const { components } = useComponentsContext({ + const { + components, + story, + setControlValue, + clickControl, + } = useComponentsContext({ of, }); if (!components) { @@ -23,7 +40,11 @@ export const ComponentsContainer: React.FC = ({ return null; } if (keys.length === 1) { - return children(components[keys[0]]); + return children(components[keys[0]], { + story, + setControlValue, + clickControl, + }); } return ( @@ -34,7 +55,7 @@ export const ComponentsContainer: React.FC = ({ {keys.map(key => ( - {children(components[key])} + {children(components[key], { story, setControlValue, clickControl })} ))} diff --git a/ui/blocks/src/context/components/ComponentsContext.tsx b/ui/blocks/src/context/components/ComponentsContext.tsx index 67759bb9e..e7990203b 100644 --- a/ui/blocks/src/context/components/ComponentsContext.tsx +++ b/ui/blocks/src/context/components/ComponentsContext.tsx @@ -4,6 +4,8 @@ import { Story, StoriesKind, StoryComponent, + SetControlValueFn, + ClickControlFn, } from '@component-controls/specification'; import { BlockContext, CURRENT_SELECTION } from '../context'; @@ -18,21 +20,38 @@ export interface ComponentInputProps { of?: '.' | any; } -export interface ComponnetContextProps { +export interface ComponentContextProps { components: { [label: string]: StoryComponent; }; kind?: StoriesKind; + story?: Story; + /** + * generic function to update the values of component controls. + */ + setControlValue?: SetControlValueFn; + + /** + * generic function to propagate a click event for component controls. + */ + clickControl?: ClickControlFn; } export const useComponentsContext = ({ of = CURRENT_SELECTION, -}: ComponentInputProps): ComponnetContextProps => { - const { currentId, mockStore } = React.useContext(BlockContext); +}: ComponentInputProps): ComponentContextProps => { + const { + currentId, + mockStore, + setControlValue, + clickControl, + } = React.useContext(BlockContext); const store = mockStore || storyStore; if (!currentId) { return { components: {}, + setControlValue, + clickControl, }; } const story: Story = store && store.stories[currentId]; @@ -68,5 +87,8 @@ export const useComponentsContext = ({ return { components: { ...components, ...subComponents }, kind, + story, + setControlValue, + clickControl, }; }; diff --git a/ui/blocks/src/context/context.tsx b/ui/blocks/src/context/context.tsx index ec8dc34c7..2d1af9d17 100644 --- a/ui/blocks/src/context/context.tsx +++ b/ui/blocks/src/context/context.tsx @@ -1,12 +1,30 @@ import React from 'react'; -import { StoriesStore } from '@component-controls/specification'; +import { + StoriesStore, + SetControlValueFn, + ClickControlFn, +} from '@component-controls/specification'; import { toId, storyNameFromExport } from '@storybook/csf'; export const CURRENT_SELECTION = '.'; export interface BlockContextProps { - api?: any; - channel?: any; + /** + * current story id + */ currentId?: string; + + /** + * generic function to update the values of component controls. + */ + setControlValue?: SetControlValueFn; + + /** + * generic function to propagate a click event for component controls. + */ + clickControl?: ClickControlFn; + /** + * store mockup when running tests + */ mockStore?: StoriesStore; } diff --git a/ui/blocks/src/context/story/StoryContext.tsx b/ui/blocks/src/context/story/StoryContext.tsx index 4178428d7..7e9a6aee9 100644 --- a/ui/blocks/src/context/story/StoryContext.tsx +++ b/ui/blocks/src/context/story/StoryContext.tsx @@ -1,10 +1,13 @@ -import React from 'react'; +import React, { FC } from 'react'; import storyStore from '@component-controls/loader/story-store-data'; import { Story, StoriesKind, StoryComponent, + SetControlValueFn, + ClickControlFn, } from '@component-controls/specification'; + import { BlockContext, CURRENT_SELECTION, storyIdFromName } from '../context'; export interface StoryInputProps { @@ -18,12 +21,31 @@ export interface StoryInputProps { } export interface StoryContextProps { + /** + * story id + */ id?: string; - api?: any; - channel?: any; + /** + * the current story object + */ story?: Story; + /** + * the file/document of stories + */ kind?: StoriesKind; + /** + * current story's/document's component + */ component?: StoryComponent; + /** + * generic function to update the values of component controls. + */ + setControlValue?: SetControlValueFn; + + /** + * generic function to propagate a click event for component controls. + */ + clickControl?: ClickControlFn; } /** @@ -34,7 +56,12 @@ export const useStoryContext = ({ id, name, }: StoryInputProps): StoryContextProps => { - const { currentId, api, channel, mockStore } = React.useContext(BlockContext); + const { + currentId, + mockStore, + clickControl, + setControlValue, + } = React.useContext(BlockContext); const store = mockStore || storyStore; const inputId = id === CURRENT_SELECTION ? currentId : id; @@ -43,8 +70,8 @@ export const useStoryContext = ({ if (!storyId) { return { - api, - channel, + clickControl, + setControlValue, }; } const story: Story = store && store.stories[storyId]; @@ -61,10 +88,19 @@ export const useStoryContext = ({ : undefined; return { id: storyId, - api, - channel, kind, story, component, + clickControl, + setControlValue, }; }; + +export interface StoryContextConsumer { + children: (context: StoryContextProps) => React.ReactElement; +} +export const StoryContextConsumer: FC = ({ children, ...rest }) => { + const context = useStoryContext(rest); + return children(context); +}; diff --git a/ui/blocks/src/index.ts b/ui/blocks/src/index.ts index f8da45daa..854868f84 100644 --- a/ui/blocks/src/index.ts +++ b/ui/blocks/src/index.ts @@ -6,3 +6,4 @@ export * from './ComponentSource'; export * from './PropsTable'; export * from './Subtitle'; export * from './Title'; +export * from './notifications'; diff --git a/ui/blocks/src/notifications/InvalidType.tsx b/ui/blocks/src/notifications/InvalidType.tsx new file mode 100644 index 000000000..3e685cffe --- /dev/null +++ b/ui/blocks/src/notifications/InvalidType.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +/** + * error message when the control type is not found. + */ +export const InvalidType = () => Invalid Type; diff --git a/ui/blocks/src/notifications/index.ts b/ui/blocks/src/notifications/index.ts new file mode 100644 index 000000000..9e88738fb --- /dev/null +++ b/ui/blocks/src/notifications/index.ts @@ -0,0 +1 @@ +export * from './InvalidType'; diff --git a/ui/blocks/src/test/MockContext.tsx b/ui/blocks/src/test/MockContext.tsx index 0a4744e48..61ed772bf 100644 --- a/ui/blocks/src/test/MockContext.tsx +++ b/ui/blocks/src/test/MockContext.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import { mergeControlValues } from '@component-controls/core'; import { BlockContext } from '../context'; import { storyStore } from './storyStore'; -import { Story } from '@component-controls/specification'; export interface MockContexProps { storyId?: string; @@ -13,19 +13,32 @@ export const MockContext: React.FC = ({ children, storyId = 'story', ...rest -}) => ( - { + const story = storyStore.stories[storyId]; + const [controls, setControls] = React.useState(story.controls); + return ( + { + if (controls) { + setControls(mergeControlValues(controls, name, value)); + } }, - }, - }} - > - {children} - -); + mockStore: { + ...(storyStore as any), + stories: { + ...storyStore.stories, + [storyId]: { + ...(storyStore.stories[storyId] || {}), + controls, + ...rest, + }, + }, + }, + }} + > + {children} + + ); +}; diff --git a/ui/blocks/src/test/storyStore.ts b/ui/blocks/src/test/storyStore.ts index 6ea55f41a..4564a88e6 100644 --- a/ui/blocks/src/test/storyStore.ts +++ b/ui/blocks/src/test/storyStore.ts @@ -1,4 +1,4 @@ -import { StoriesStore } from '@component-controls/specification'; +import { StoriesStore, ControlTypes } from '@component-controls/specification'; export const storyStore: StoriesStore = { components: { @@ -195,6 +195,31 @@ and a [link](https://google.com) }, }, }, + '/Users/atanasster/component-controls/core/instrument/test/fixtures/components/custom-controls.js': { + from: '../../components/button-named-arrow-func', + importedName: 'Button', + name: 'Button', + request: + '/Users/atanasster/component-controls/core/instrument/test/fixtures/components/button-named-arrow-func.js', + info: { + description: '', + displayName: 'Control', + props: { + name: { + type: { + name: 'string', + }, + description: 'a name property', + }, + age: { + type: { + name: 'number', + }, + description: 'age is a number property', + }, + }, + }, + }, }, kinds: { Story: { @@ -203,12 +228,15 @@ and a [link](https://google.com) '/Users/atanasster/component-controls/core/instrument/test/fixtures/components/button-default-arrow-func.js', Button: '/Users/atanasster/component-controls/core/instrument/test/fixtures/components/button-named-arrow-func.js', + Control: + '/Users/atanasster/component-controls/core/instrument/test/fixtures/components/custom-controls.js', }, title: 'Story', }, }, stories: { story: { + id: 'story', arguments: [], kind: 'Story', component: 'ArrowButton', @@ -232,6 +260,7 @@ and a [link](https://google.com) }, }, single: { + id: 'single', arguments: [], kind: 'Story', component: 'ArrowButton', @@ -255,6 +284,7 @@ and a [link](https://google.com) }, }, button: { + id: 'button', arguments: [], kind: 'Story', component: 'Button', @@ -271,5 +301,24 @@ and a [link](https://google.com) name: 'button', source: "() => 'hello'", }, + controls: { + id: 'controls', + arguments: [], + kind: 'Story', + name: 'controls', + component: 'Control', + controls: { + name: { + type: ControlTypes.TEXT, + label: 'Name', + value: 'Mark', + }, + age: { + type: ControlTypes.NUMBER, + label: 'Age', + value: 19, + }, + }, + }, }, };