Skip to content

Commit

Permalink
feat(common): Implement the widget configuration mode which renders t…
Browse files Browse the repository at this point in the history
…he exported `ConfigControls` for every widget.

Closes #562
  • Loading branch information
fussel178 committed Jun 21, 2021
1 parent 8c1f4f7 commit 6da4c4a
Show file tree
Hide file tree
Showing 14 changed files with 714 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { StateSelector } from 'zustand';
import { useParams } from 'react-router-dom';
import { EventBusState, useEventBus } from '@wuespace/telestion-client-core';
Expand All @@ -8,11 +8,13 @@ import { useCurrentDashboards } from '../../../hooks';
import { Dashboard } from './dashboard/dashboard';
import { NoDashboardsMessage } from './no-dashboards-message';
import { MissingEventBus } from './missing-event-bus';
import { ClipboardContent, ClipboardContext } from './props-clipboard';

// eventbus selector
const selector: StateSelector<EventBusState, EventBusState['eventBus']> =
state => state.eventBus;


/**
* A Telestion Client page that renders a dashboard page.
* It displays the currently active dashboard of an authenticated user.
Expand Down Expand Up @@ -53,6 +55,8 @@ const selector: StateSelector<EventBusState, EventBusState['eventBus']> =
* ```
*/
export function DashboardPage() {
const clipState = useState<ClipboardContent>();

const { id } = useParams<{ id: string }>();
const eventBus = useEventBus(selector);
const [dashboards] = useCurrentDashboards();
Expand All @@ -67,7 +71,11 @@ export function DashboardPage() {
return <NoDashboardsMessage />;
}

return <Dashboard dashboard={dashboards[idAsNumber]} />;
return (
<ClipboardContext.Provider value={clipState}>
<Dashboard dashboard={dashboards[idAsNumber]} />
</ClipboardContext.Provider>
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Grid, View } from '@adobe/react-spectrum';
import { Dashboard as DashboardType } from '@wuespace/telestion-client-types';

import { OverflowFix } from '../../../widget-helper';
import { WidgetRenderer } from './widget-renderer/widget-renderer';
import { WidgetSelector } from './widget-renderer/widget-selector';

/**
* React Props of {@link OverflowFix}
Expand Down Expand Up @@ -90,7 +90,7 @@ export function Dashboard({ dashboard }: DashboardProps) {
backgroundColor="gray-100"
borderRadius="regular"
>
<WidgetRenderer widgetDefinition={widget} />
<WidgetSelector definition={widget} />
</OverflowFix>
))}
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { GenericProps, Widget } from '@wuespace/telestion-client-types';

import { OverflowFix } from '../../../../widget-helper';
import { usePropsClipboard } from '../../props-clipboard';
import {
ConfigContainer,
ConfigHeader,
ConfigFooter,
CopyPasteActions
} from './config';

/**
* Return type of the {@link useState} hook
* specified for the {@link GenericProps} state.
*/
export type PropsState = [GenericProps, Dispatch<SetStateAction<GenericProps>>];

/**
* React Props of {@link ConfigRenderer}
*
* For more information about React Props, please look here:
* {@link https://reactjs.org/docs/components-and-props.html}
*
* @see {@link ConfigRenderer}
* @see {@link https://reactjs.org/docs/components-and-props.html}
*/
export interface ConfigRendererProps {
/**
* The actual widget which contains the components and settings.
*/
widget: Widget;

/**
* The unique identifier of the widget in the dashboard configuration.
*/
id: string;

/**
* The entire props state from {@link useState}.
*/
propsState: PropsState;

/**
* Close the configuration mode.
*/
onClose: () => void;
}

/**
* Renders the widget configuration and wrapper with header and footer.
* It implements the copy and paste for widget properties.
*
* @see {@link ConfigRendererProps}
* @see {@link WidgetRenderer}
* @see {@link Widget}
*
* @throws Error - if the ConfigControls are not defined
*
* @example
* ```tsx
* // build up state
* const [inConfig, open, close] = useBooleanState();
* const propsState = useStoredState(`${id}-${version}`, initialProps || {});
*
* return inConfig ? (
* <ConfigRenderer
* widget={widget}
* id={id}
* propsState={propsState}
* onClose={close}
* />
* ) : (
* <Content {...propsState[0]} />
* )
* ```
*/
export function ConfigRenderer({
widget,
id,
propsState,
onClose
}: ConfigRendererProps) {
const { name, title, ConfigControls } = widget;
const [global, setGlobal] = propsState;
const [clipboard, setClipboard] = usePropsClipboard(name);
const [local, setLocal] = useState(global);

const copy = () => setClipboard(local);

const paste = useCallback(() => {
if (clipboard) setLocal(clipboard);
}, [clipboard]);

const abort = useCallback(() => {
setLocal(global);
onClose();
}, [global, onClose]);

const confirm = () => {
setGlobal(local);
onClose();
};

const update = useCallback((newProps: Partial<GenericProps>) => {
setLocal(prevState => ({ ...prevState, ...newProps }));
}, []);

if (!ConfigControls) throw new Error('Config Controls are not defined');

return (
<ConfigContainer>
<ConfigHeader title={title || name} id={id}>
<CopyPasteActions
isPasteDisabled={!!clipboard}
onCopy={copy}
onPaste={paste}
/>
</ConfigHeader>
<OverflowFix flexShrink={1} flexGrow={1}>
{/* @ts-ignore */}
<ConfigControls currentProps={local} onUpdate={update} />
</OverflowFix>
<ConfigFooter onAbort={abort} onConfirm={confirm} />
</ConfigContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ReactElement } from 'react';
import { Flex, View } from '@adobe/react-spectrum';

/**
* React Props of {@link ConfigContainer}
*
* For more information about React Props, please look here:
* {@link https://reactjs.org/docs/components-and-props.html}
*
* @see {@link ConfigContainer}
* @see {@link https://reactjs.org/docs/components-and-props.html}
*/
export interface ConfigContainerProps {
children: ReactElement | ReactElement[];
}

/**
* This is the container element for the widget configuration.
* @param children - the container elements
*
* @example
* ```tsx
* function WidgetRenderer() {
* return inConfig ? (
* <ConfigContainer>
* {...elements}
* </ConfigContainer>
* ) : (
* <Content {...props}>
* );
* }
* ```
*/
export function ConfigContainer({ children }: ConfigContainerProps) {
return (
<View width="100%" height="100%">
<Flex direction="column" width="100%" height="100%">
{children}
</Flex>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { View, ButtonGroup, Button } from '@adobe/react-spectrum';

/**
* React Props of {@link ConfigFooter}
*
* For more information about React Props, please look here:
* {@link https://reactjs.org/docs/components-and-props.html}
*
* @see {@link ConfigFooter}
* @see {@link https://reactjs.org/docs/components-and-props.html}
*/
export interface ConfigFooterProps {
/**
* Gets called, when the user presses the abort button.
*/
onAbort: () => void;

/**
* Gets called, when the user presses the confirm button.
*/
onConfirm: () => void;
}

/**
* Renders the widget configuration footer.
*
* It contains the confirm and cancel/abort actions
* the user can take to exit the widget configuration mode.
*
* @see {@link ConfigFooterProps}
* @see {@link ConfigContainer}
* @see {@link WidgetRenderer}
*
* @example
* ```tsx
* function WidgetRenderer() {
* return inConfig ? (
* <ConfigContainer>
* {...elements}
* <ConfigFooter />
* </ConfigContainer>
* ) : (
* <Content {...props}>
* );
* }
* ```
*/
export function ConfigFooter({ onAbort, onConfirm }: ConfigFooterProps) {
return (
<View flexShrink={0} width="100%" paddingX="size-200" paddingY="size-100">
<ButtonGroup align="end">
<Button variant="secondary" onPress={onAbort}>
Cancel
</Button>
<Button variant="cta" onPress={onConfirm} autoFocus>
Confirm
</Button>
</ButtonGroup>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { View, Divider, Flex, Heading } from '@adobe/react-spectrum';

/**
* React Props of {@link ConfigHeader}
*
* For more information about React Props, please look here:
* {@link https://reactjs.org/docs/components-and-props.html}
*
* @see {@link ConfigHeader}
* @see {@link https://reactjs.org/docs/components-and-props.html}
*/
export interface ConfigHeaderProps {
/**
* The title/widgetName of the widget.
*/
title: string;

/**
* The unique identifier of the widget in the dashboard configuration.
*/
id: string;

/**
* Additional elements that should be rendered next to the heading
* in a flexbox row.
*/
// Ask the react-spectrum team for bad typings. :/
children: any;
}

/**
* Renders the widget configuration header.
*
* Here are the widget name/title and the id displayed.
*
* The action controls allows the user
* to copy and paste the configuration from one widget to another.
*
* @see {@link ConfigHeaderProps}
* @see {@link ConfigContainer}
* @see {@link WidgetRenderer}
*
* @example
* ```tsx
* function WidgetRenderer() {
* return inConfig ? (
* <ConfigContainer>
* <ConfigHeader />
* {...elements}
* </ConfigContainer>
* ) : (
* <Content {...props}>
* );
* }
* ```
*/
export function ConfigHeader({ title, id, children }: ConfigHeaderProps) {
return (
<>
<View flexShrink={0} width="100%" paddingX="size-200" paddingY="size-100">
<Flex
direction="row"
width="100%"
justifyContent="space-between"
alignItems="center"
>
<Flex direction="row">
<Heading
flexGrow={0}
level={3}
margin="size-200"
marginBottom="size-100"
>
{title}
</Heading>
<div>{id}</div>
</Flex>

{children}
</Flex>
</View>

<Divider size="S" />
</>
);
}
Loading

0 comments on commit 6da4c4a

Please sign in to comment.