diff --git a/.all-contributorsrc b/.all-contributorsrc index 4cdd4875e..28eaca1af 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -75,7 +75,8 @@ "code", "doc", "ideas", - "infra" + "infra", + "example" ] }, { diff --git a/core/player/BUILD b/core/player/BUILD index b33c16f5a..ec8602119 100644 --- a/core/player/BUILD +++ b/core/player/BUILD @@ -16,7 +16,8 @@ javascript_pipeline( "@npm//arr-flatten", "@npm//ebnf", "@npm//timm", - "@npm//error-polyfill" + "@npm//error-polyfill", + "@npm//ts-nested-error" ], test_data = [ "//core/make-flow:@player-ui/make-flow", diff --git a/core/player/src/__tests__/validation.test.ts b/core/player/src/__tests__/validation.test.ts index 35704f985..0fc2399b5 100644 --- a/core/player/src/__tests__/validation.test.ts +++ b/core/player/src/__tests__/validation.test.ts @@ -462,6 +462,79 @@ const flowWithItemsInArray: Flow = { }, }; +const multipleWarningsFlow: Flow = { + id: 'input-validation-flow', + views: [ + { + type: 'view', + id: 'view', + loadWarning: { + asset: { + id: 'load-warning', + type: 'warning-asset', + binding: 'foo.load', + }, + }, + navigationWarning: { + asset: { + id: 'required-warning', + type: 'warning-asset', + binding: 'foo.navigation', + }, + }, + }, + ], + schema: { + ROOT: { + foo: { + type: 'FooType', + }, + }, + FooType: { + navigation: { + type: 'String', + validation: [ + { + type: 'required', + severity: 'warning', + blocking: 'once', + trigger: 'navigation', + }, + ], + }, + load: { + type: 'String', + validation: [ + { + type: 'required', + severity: 'warning', + blocking: 'once', + trigger: 'load', + }, + ], + }, + }, + }, + data: {}, + navigation: { + BEGIN: 'FLOW_1', + FLOW_1: { + startState: 'VIEW_1', + VIEW_1: { + state_type: 'VIEW', + ref: 'view', + transitions: { + '*': 'END_Done', + }, + }, + END_Done: { + state_type: 'END', + outcome: 'done', + }, + }, + }, +}; + test('alt APIs', async () => { const player = new Player(); @@ -1022,6 +1095,53 @@ describe('validation', () => { const result = await flowResult; expect(result.endState.outcome).toBe('test'); }); + + it('should auto-dismiss when dismissal is triggered', async () => { + player.start(multipleWarningsFlow); + const state = player.getState() as InProgressState; + const { flowResult } = state; + // Starts with one warning + expect( + state.controllers.view.currentView?.lastUpdate?.loadWarning.asset + .validation + ).toBeDefined(); + + expect( + state.controllers.view.currentView?.lastUpdate?.navigationWarning.asset + .validation + ).toBeUndefined(); + + // Try to transition + state.controllers.flow.transition('next'); + + // Stays on the same view + expect( + state.controllers.flow.current?.currentState?.value.state_type + ).toBe('VIEW'); + + // new warning appears + expect( + state.controllers.view.currentView?.lastUpdate?.loadWarning.asset + .validation + ).toBeDefined(); + + expect( + state.controllers.view.currentView?.lastUpdate?.navigationWarning.asset + .validation + ).toBeDefined(); + + // Try to transition + state.controllers.flow.transition('next'); + + // Since data change (setting "sam") already triggered validation next step is auto dismiss + expect( + state.controllers.flow.current?.currentState?.value.state_type + ).toBe('END'); + + // Should work now that there's no error + const result = await flowResult; + expect(result.endState.outcome).toBe('done'); + }); }); describe('introspection and filtering', () => { @@ -1270,6 +1390,62 @@ describe('errors', () => { ], }); + const oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow = + makeFlow({ + id: 'view-1', + type: 'view', + thing1: { + asset: { + id: 'thing-1', + binding: 'foo.data.thing1', + type: 'input', + }, + }, + validation: [ + { + type: 'required', + ref: 'foo.data.thing1', + severity: 'error', + trigger: 'load', + blocking: 'false', + }, + { + type: 'required', + ref: 'foo.data.thing1', + trigger: 'navigation', + severity: 'warning', + }, + ], + }); + + const oneInputWithErrorOnLoadBlockingFalseAndWarningChangeTriggerFlow = + makeFlow({ + id: 'view-1', + type: 'view', + thing1: { + asset: { + id: 'thing-1', + binding: 'foo.data.thing1', + type: 'input', + }, + }, + validation: [ + { + type: 'required', + ref: 'foo.data.thing1', + severity: 'error', + trigger: 'load', + blocking: 'false', + }, + { + type: 'required', + ref: 'foo.data.thing1', + trigger: 'change', + severity: 'warning', + }, + ], + }); + it('blocks navigation by default', async () => { const player = new Player({ plugins: [new TrackBindingPlugin()] }); player.start(errorFlow); @@ -1320,6 +1496,124 @@ describe('errors', () => { 'END' ); }); + + it('error on load blocking false then warning with change trigger on navigation attempt', async () => { + const player = new Player({ plugins: [new TrackBindingPlugin()] }); + player.start( + oneInputWithErrorOnLoadBlockingFalseAndWarningChangeTriggerFlow + ); + const state = player.getState() as InProgressState; + + expect( + state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation + ).toMatchObject({ + message: 'A value is required', + severity: 'error', + displayTarget: 'field', + }); + + // Try to navigate, should prevent the navigation and display the warning + state.controllers.flow.transition('next'); + expect(state.controllers.flow.current?.currentState?.value.state_type).toBe( + 'VIEW' + ); + + expect( + state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation + ).toMatchObject({ + message: 'A value is required', + severity: 'warning', + displayTarget: 'field', + }); + + // Navigate _again_ this should dismiss it + state.controllers.flow.transition('next'); + // We make it to the next state + + expect(state.controllers.flow.current?.currentState?.value.state_type).toBe( + 'END' + ); + }); + + it('error on load blocking false then warning on navigation attempt', async () => { + const player = new Player({ plugins: [new TrackBindingPlugin()] }); + player.start( + oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow + ); + const state = player.getState() as InProgressState; + + expect( + state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation + ).toMatchObject({ + message: 'A value is required', + severity: 'error', + displayTarget: 'field', + }); + + // Try to navigate, should prevent the navigation and display the warning + state.controllers.flow.transition('next'); + expect(state.controllers.flow.current?.currentState?.value.state_type).toBe( + 'VIEW' + ); + + expect( + state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation + ).toMatchObject({ + message: 'A value is required', + severity: 'warning', + displayTarget: 'field', + }); + + // Navigate _again_ this should dismiss it + state.controllers.flow.transition('next'); + // We make it to the next state + + expect(state.controllers.flow.current?.currentState?.value.state_type).toBe( + 'END' + ); + }); + + it('error on load blocking false then input active then warning on navigation attempt', async () => { + const player = new Player({ plugins: [new TrackBindingPlugin()] }); + player.start( + oneInputWithErrorOnLoadBlockingFalseAndWarningNavigationTriggerFlow + ); + const state = player.getState() as InProgressState; + + expect( + state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation + ).toMatchObject({ + message: 'A value is required', + severity: 'error', + displayTarget: 'field', + }); + + // Type something to dismiss the error, should be empty to see the warning + state.controllers.data.set([['foo.data.thing1', '']]); + + // Try to navigate, should prevent the navigation and display the warning + state.controllers.flow.transition('next'); + expect(state.controllers.flow.current?.currentState?.value.state_type).toBe( + 'VIEW' + ); + + expect( + state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation + ).toMatchObject({ + message: 'A value is required', + severity: 'warning', + displayTarget: 'field', + }); + + // Navigate _again_ this should dismiss it + state.controllers.flow.transition('next'); + // We make it to the next state + + expect(state.controllers.flow.current?.currentState?.value.state_type).toBe( + 'END' + ); + }); + it('blocking false allows navigation', async () => { const player = new Player({ plugins: [new TrackBindingPlugin()] }); player.start(nonBlockingErrorFlow); @@ -3009,15 +3303,24 @@ describe('Validation in subflow', () => { player.start(flow); + /** + * + */ const getControllers = () => { const state = player.getState() as InProgressState; return state.controllers; }; + /** + * + */ const getValidationMessage = () => { return getControllers().view.currentView?.lastUpdate?.validation; }; + /** + * + */ const attemptTransition = () => { getControllers().flow.transition('next'); }; diff --git a/core/player/src/binding-grammar/__tests__/parser.test.ts b/core/player/src/binding-grammar/__tests__/parser.test.ts index 4f7957da7..edccc4205 100644 --- a/core/player/src/binding-grammar/__tests__/parser.test.ts +++ b/core/player/src/binding-grammar/__tests__/parser.test.ts @@ -1,6 +1,7 @@ import { VALID_AST_PARSER_TESTS, INVALID_AST_PARSER_TESTS, + VALID_AST_PARSER_CUSTOM_TESTS, } from './test-utils/ast-cases'; import type { ParserSuccessResult, ParserFailureResult } from '../ast'; import { parse as parseParsimmon } from '../parsimmon'; @@ -49,4 +50,13 @@ describe('custom', () => { expect(result.status).toBe(false); expect((result as ParserFailureResult).error.length > 0).toBe(true); }); + + test.each(VALID_AST_PARSER_CUSTOM_TESTS)( + 'Custom Unicode Valid: %s', + (binding, AST) => { + const result = parseCustom(binding); + expect(result.status).toBe(true); + expect((result as ParserSuccessResult).path).toStrictEqual(AST); + } + ); }); diff --git a/core/player/src/binding-grammar/__tests__/test-utils/ast-cases.ts b/core/player/src/binding-grammar/__tests__/test-utils/ast-cases.ts index 9c2ba85a6..3a13b9bc0 100644 --- a/core/player/src/binding-grammar/__tests__/test-utils/ast-cases.ts +++ b/core/player/src/binding-grammar/__tests__/test-utils/ast-cases.ts @@ -186,3 +186,12 @@ export const INVALID_AST_PARSER_TESTS: Array = [ 'foo.bar.{{nested.}', 'foo.bar`not done()', ]; + +export const VALID_AST_PARSER_CUSTOM_TESTS: Array<[string, PathNode]> = [ + ['foo‑<>~¡¢£', toPath([toValue('foo‑<>~¡¢£')])], + ['foo.bar<>~¡¢£', toPath([toValue('foo'), toValue('bar<>~¡¢£')])], + [ + 'foo[{{b‑ar}}].baz', + toPath([toValue('foo'), toPath([toValue('b‑ar')]), toValue('baz')]), + ], +]; diff --git a/core/player/src/binding-grammar/custom/index.ts b/core/player/src/binding-grammar/custom/index.ts index bd10a3e4a..e5e2fbd2b 100644 --- a/core/player/src/binding-grammar/custom/index.ts +++ b/core/player/src/binding-grammar/custom/index.ts @@ -26,7 +26,7 @@ const DOUBLE_QUOTE = '"'; const BACK_TICK = '`'; // const IDENTIFIER_REGEX = /[\w\-@]+/; -/** A _faster_ way to match chars instead of a regex (/[\w\-@]+/) */ +/** A _faster_ way to match chars instead of a regex. */ const isIdentifierChar = (char?: string): boolean => { if (!char) { return false; @@ -34,14 +34,22 @@ const isIdentifierChar = (char?: string): boolean => { const charCode = char.charCodeAt(0); - return ( - (charCode >= 48 && charCode <= 57) || // 0 - 9 - (charCode >= 65 && charCode <= 90) || // A-Z - (charCode >= 97 && charCode <= 122) || // a-z - charCode === 95 || // _ - charCode === 45 || // - - charCode === 64 // @ - ); + const matches = + charCode === 32 || // ' ' + charCode === 34 || // " + charCode === 39 || // ' + charCode === 40 || // ( + charCode === 41 || // ) + charCode === 42 || // * + charCode === 46 || // . + charCode === 61 || // = + charCode === 91 || // [ + charCode === 93 || // ] + charCode === 96 || // ` + charCode === 123 || // { + charCode === 125; // } + + return !matches; }; /** Parse out a binding AST from a path */ diff --git a/core/player/src/binding/index.ts b/core/player/src/binding/index.ts index c2b4f4b07..d9d8efd9f 100644 --- a/core/player/src/binding/index.ts +++ b/core/player/src/binding/index.ts @@ -1,5 +1,5 @@ import { SyncBailHook, SyncWaterfallHook } from 'tapable-ts'; -import NestedError from 'nested-error-stacks'; +import { NestedError } from 'ts-nested-error'; import type { ParserResult, AnyNode } from '../binding-grammar'; import { // We can swap this with whichever parser we want to use @@ -15,6 +15,8 @@ export * from './utils'; export * from './binding'; export const SIMPLE_BINDING_REGEX = /^[\w\-@]+(\.[\w\-@]+)*$/; +export const BINDING_BRACKETS_REGEX = /[\s()*=`{}'"[\]]/; +const LAZY_BINDING_REGEX = /^[^.]+(\..+)*$/; const DEFAULT_OPTIONS: BindingParserOptions = { get: () => { @@ -28,6 +30,9 @@ const DEFAULT_OPTIONS: BindingParserOptions = { }, }; +type BeforeResolveNodeContext = Required & + ResolveBindingASTOptions; + /** A parser for creating bindings from a string */ export class BindingParser { private cache: Record; @@ -37,7 +42,7 @@ export class BindingParser { public hooks = { skipOptimization: new SyncBailHook<[string], boolean>(), beforeResolveNode: new SyncWaterfallHook< - [AnyNode, Required & ResolveBindingASTOptions] + [AnyNode, BeforeResolveNodeContext] >(), }; @@ -56,8 +61,13 @@ export class BindingParser { path: string, resolveOptions: ResolveBindingASTOptions ) { + /** + * Ensure no binding characters exist in path and the characters remaining + * look like a binding format. + */ if ( - path.match(SIMPLE_BINDING_REGEX) && + !BINDING_BRACKETS_REGEX.test(path) && + LAZY_BINDING_REGEX.test(path) && this.hooks.skipOptimization.call(path) !== true ) { return { path: path.split('.'), updates: undefined } as NormalizedResult; diff --git a/core/player/src/binding/resolver.ts b/core/player/src/binding/resolver.ts index 047320056..ff5b5ee23 100644 --- a/core/player/src/binding/resolver.ts +++ b/core/player/src/binding/resolver.ts @@ -1,4 +1,4 @@ -import NestedError from 'nested-error-stacks'; +import { NestedError } from 'ts-nested-error'; import type { SyncWaterfallHook } from 'tapable-ts'; import type { PathNode, AnyNode } from '../binding-grammar'; import { findInArray, maybeConvertToNum } from './utils'; diff --git a/core/player/src/controllers/validation/controller.ts b/core/player/src/controllers/validation/controller.ts index a164bf56c..7211b7cb6 100644 --- a/core/player/src/controllers/validation/controller.ts +++ b/core/player/src/controllers/validation/controller.ts @@ -1,5 +1,6 @@ import type { Validation } from '@player-ui/types'; import { SyncHook, SyncWaterfallHook } from 'tapable-ts'; +import { setIn } from 'timm'; import type { BindingInstance, BindingFactory } from '../../binding'; import { isBinding } from '../../binding'; @@ -104,6 +105,13 @@ type StatefulError = { export type StatefulValidationObject = StatefulWarning | StatefulError; +/** Helper function to determin if the subset is within the containingSet */ +function isSubset(subset: Set, containingSet: Set): boolean { + if (subset.size > containingSet.size) return false; + for (const entry of subset) if (!containingSet.has(entry)) return false; + return true; +} + /** Helper for initializing a validation object that tracks state */ function createStatefulValidationObject( obj: ValidationObjectWithSource @@ -207,67 +215,82 @@ class ValidatedBinding { canDismiss: boolean ) { // If the currentState is not load, skip those - this.applicableValidations = this.applicableValidations.map((obj) => { - if (obj.state === 'dismissed') { - // Don't rerun any dismissed warnings - return obj; - } + this.applicableValidations = this.applicableValidations.map( + (originalValue) => { + if (originalValue.state === 'dismissed') { + // Don't rerun any dismissed warnings + return originalValue; + } - const blocking = - obj.value.blocking ?? - ((obj.value.severity === 'warning' && 'once') || true); - - const isBlockingNavigation = - blocking === true || (blocking === 'once' && !canDismiss); - - const dismissable = canDismiss && blocking === 'once'; - - if ( - this.currentPhase === 'navigation' && - obj.state === 'active' && - dismissable - ) { - if (obj.value.severity === 'warning') { - const warn = obj as ActiveWarning; - if (warn.dismissable && warn.response.dismiss) { - warn.response.dismiss(); - } else { - warn.dismissable = true; + // treat all warnings the same and block it once (unless blocking is true) + const blocking = + originalValue.value.blocking ?? + ((originalValue.value.severity === 'warning' && 'once') || true); + + const obj = setIn( + originalValue, + ['value', 'blocking'], + blocking + ) as StatefulValidationObject; + + const isBlockingNavigation = + blocking === true || (blocking === 'once' && !canDismiss); + + if ( + this.currentPhase === 'navigation' && + obj.state === 'active' && + obj.value.blocking !== true + ) { + if (obj.value.severity === 'warning') { + const warn = obj as ActiveWarning; + if ( + warn.dismissable && + warn.response.dismiss && + (warn.response.blocking !== 'once' || !warn.response.blocking) + ) { + warn.response.dismiss(); + } else { + if (warn?.response.blocking === 'once') { + warn.response.blocking = false; + } + + warn.dismissable = true; + } + + return warn as StatefulValidationObject; } + } - return obj; + const response = runner(obj.value); + + const newState = { + type: obj.type, + value: obj.value, + state: response ? 'active' : 'none', + isBlockingNavigation, + dismissable: + obj.value.severity === 'warning' && + this.currentPhase === 'navigation', + response: response + ? { + ...obj.value, + message: response.message ?? 'Something is broken', + severity: obj.value.severity, + displayTarget: obj.value.displayTarget ?? 'field', + } + : undefined, + } as StatefulValidationObject; + + if (newState.state === 'active' && obj.value.severity === 'warning') { + (newState.response as WarningValidationResponse).dismiss = () => { + (newState as StatefulWarning).state = 'dismissed'; + this.onDismiss?.(); + }; } - } - const response = runner(obj.value); - - const newState = { - type: obj.type, - value: obj.value, - state: response ? 'active' : 'none', - isBlockingNavigation, - dismissable: - obj.value.severity === 'warning' && - this.currentPhase === 'navigation', - response: response - ? { - ...obj.value, - message: response.message ?? 'Something is broken', - severity: obj.value.severity, - displayTarget: obj.value.displayTarget ?? 'field', - } - : undefined, - } as StatefulValidationObject; - - if (newState.state === 'active' && obj.value.severity === 'warning') { - (newState.response as WarningValidationResponse).dismiss = () => { - (newState as StatefulWarning).state = 'dismissed'; - this.onDismiss?.(); - }; + return newState; } - - return newState; - }); + ); } public update( @@ -275,6 +298,8 @@ class ValidatedBinding { canDismiss: boolean, runner: ValidationRunner ) { + const newApplicableValidations: StatefulValidationObject[] = []; + if (phase === 'load' && this.currentPhase !== undefined) { // Tried to run the 'load' phase twice. Aborting return; @@ -301,8 +326,23 @@ class ValidatedBinding { (this.currentPhase === 'load' || this.currentPhase === 'change') ) { // Can transition to a nav state from a change or load + + // if there is an non-blocking error that is active then remove the error from applicable validations so it can no longer be shown + // which is needed if there are additional warnings to become active for that binding after the error is shown + this.applicableValidations.forEach((element) => { + if ( + !( + element.type === 'error' && + element.state === 'active' && + element.isBlockingNavigation === false + ) + ) { + newApplicableValidations.push(element); + } + }); + this.applicableValidations = [ - ...this.applicableValidations, + ...newApplicableValidations, ...(this.currentPhase === 'load' ? this.validationsByState.change : []), ...this.validationsByState.navigation, ]; @@ -712,18 +752,12 @@ export class ValidationController implements BindingTracker { if (isNavigationTrigger) { // If validations didn't change since last update, dismiss all dismissible validations. const { activeBindings } = this; - if (this.setCompare(lastActiveBindings, activeBindings)) { + if (isSubset(activeBindings, lastActiveBindings)) { updateValidations(true); } } } - private setCompare(set1: Set, set2: Set): boolean { - if (set1.size !== set2.size) return false; - for (const entry of set1) if (!set2.has(entry)) return false; - return true; - } - private get activeBindings(): Set { return new Set( Array.from(this.getBindings()).filter( diff --git a/docs/site/pages/content/switches.mdx b/docs/site/pages/content/switches.mdx index 157833eea..3fb6fb13c 100644 --- a/docs/site/pages/content/switches.mdx +++ b/docs/site/pages/content/switches.mdx @@ -39,7 +39,7 @@ Anywhere you can place an `asset` node, a `dynamicSwitch` or `staticSwitch` can } }, { - "case": "{{name.first}} == 'margie", + "case": "{{name.first}} == 'margie'", "asset": { "id": "name", "type": "text", diff --git a/docs/site/pages/plugins/data-change-listener.mdx b/docs/site/pages/plugins/data-change-listener.mdx index d79806cfb..bca06544b 100644 --- a/docs/site/pages/plugins/data-change-listener.mdx +++ b/docs/site/pages/plugins/data-change-listener.mdx @@ -12,7 +12,8 @@ This plugin enables users to subscribe to data-change events within a view, and "id": "example-view", "type": "info", "listeners": { - "dataChange.foo.bar": "helloWorld()" + "dataChange.foo.bar": "helloWorld()", + "dataChange.foo.baz": ["helloWorld()", "doSomethingElseToo()"] } } ``` @@ -38,7 +39,7 @@ const player = new Player({ ## Usage -The format of `dataChange.` will execute the value (any valid expression), anytime a value within the target binding's tree is updated (`foo.bar` in the example above). +The format of `dataChange.` will execute the _value_ (any valid expression or collection of expressions), anytime a value within the target binding's tree is updated (`foo.bar` and `foo.baz` in the example above). Registrations can be made for any partial binding path, and will be evaluated anytime that path, or any child path, is mutated. The above example registration of `dataChange.foo.bar` will be triggered by a change to `foo.bar`, `foo.bar.baz`, or any other child path. (it will not be triggered by a change to `foo.baz`). diff --git a/docs/storybook/.storybook/preview.js b/docs/storybook/.storybook/preview.js index 65305f9f0..6fd0ab647 100644 --- a/docs/storybook/.storybook/preview.js +++ b/docs/storybook/.storybook/preview.js @@ -3,13 +3,22 @@ import { ReferenceAssetsPlugin } from '@player-ui/reference-assets-plugin-react' import { CommonTypesPlugin } from '@player-ui/common-types-plugin'; import { DataChangeListenerPlugin } from '@player-ui/data-change-listener-plugin'; import { ComputedPropertiesPlugin } from '@player-ui/computed-properties-plugin' +import * as dslRefComponents from '@player-ui/reference-assets-components'; + +const reactPlayerPlugins = [ + new ReferenceAssetsPlugin(), + new CommonTypesPlugin(), + new DataChangeListenerPlugin(), + new ComputedPropertiesPlugin(), +] + export const parameters = { - reactPlayerPlugins: [ - new ReferenceAssetsPlugin(), - new CommonTypesPlugin(), - new DataChangeListenerPlugin(), - new ComputedPropertiesPlugin(), - ], + reactPlayerPlugins, + dslEditor: { + additionalModules: { + '@player-ui/reference-assets-components': dslRefComponents, + }, + }, options: { storySort: { order: [ diff --git a/docs/storybook/.storybook/webpack.config.js b/docs/storybook/.storybook/webpack.config.js index f6fc82043..4b12ce742 100644 --- a/docs/storybook/.storybook/webpack.config.js +++ b/docs/storybook/.storybook/webpack.config.js @@ -12,9 +12,12 @@ const webpackConfig = async (initialConfig) => { symlinks: false, cache: false, fallback: { + fs: false, util: require.resolve('util/'), assert: require.resolve('assert/'), path: require.resolve('path-browserify'), + stream: false, + constants: require.resolve("constants-browserify") }, }, plugins: [...config.plugins, new TimeFixPlugin()], diff --git a/docs/storybook/BUILD b/docs/storybook/BUILD index f1b9c9f00..4d1ab747e 100644 --- a/docs/storybook/BUILD +++ b/docs/storybook/BUILD @@ -8,6 +8,7 @@ data = [ "//plugins/reference-assets/mocks:@player-ui/reference-assets-plugin-mocks", "//plugins/data-change-listener/core:@player-ui/data-change-listener-plugin", "//plugins/computed-properties/core:@player-ui/computed-properties-plugin", + "//plugins/reference-assets/components:@player-ui/reference-assets-components", "//tools/storybook:@player-ui/storybook", "//react/player:@player-ui/react", "//:tsconfig.json", diff --git a/docs/storybook/src/Welcome.stories.mdx b/docs/storybook/src/Welcome.stories.mdx index 31cfe2b89..d6ad0538f 100644 --- a/docs/storybook/src/Welcome.stories.mdx +++ b/docs/storybook/src/Welcome.stories.mdx @@ -1,4 +1,4 @@ -import { Meta } from '@storybook/addon-docs'; +import { Meta } from '@storybook/addon-docs/blocks'; diff --git a/docs/storybook/src/components/StoryWrapper.tsx b/docs/storybook/src/components/StoryWrapper.tsx index 7793c6cf1..7f72444e5 100644 --- a/docs/storybook/src/components/StoryWrapper.tsx +++ b/docs/storybook/src/components/StoryWrapper.tsx @@ -1,4 +1,5 @@ -import React, { PropsWithChildren } from 'react'; +import type { PropsWithChildren } from 'react'; +import React from 'react'; import { ChakraProvider } from '@chakra-ui/react'; export const StoryWrapper = (props: PropsWithChildren) => { diff --git a/docs/storybook/src/reference-assets/Action.stories.mdx b/docs/storybook/src/reference-assets/Action.stories.mdx index d028db428..815c51bb0 100644 --- a/docs/storybook/src/reference-assets/Action.stories.mdx +++ b/docs/storybook/src/reference-assets/Action.stories.mdx @@ -1,10 +1,11 @@ import { Meta, Story, Canvas } from '@storybook/addon-docs'; import { Action } from '@player-ui/reference-assets-plugin-react' import { StoryWrapper } from '../components' -import { PlayerStory } from '@player-ui/storybook'; +import { PlayerStory, DSLPlayerStory, createDSLStory } from '@player-ui/storybook'; import actionCountFlow from '@player-ui/reference-assets-plugin-mocks/action/action-counter.json'; import actionNavigationFlow from '@player-ui/reference-assets-plugin-mocks/action/action-navigation.json' + # Action Asset @@ -13,6 +14,20 @@ The `action` asset is used when you want a user to perform some _action_. It typ Actions can advance Player's state machine using the `value` prop, and/or evaluate an expression using the `exp` prop. +## Basic Usecase + +The example below uses the `exp` property to evaluate an expression when the action is clicked. +This will increment the count, and update the label + + + + + import('!!raw-loader!@player-ui/reference-assets-plugin-mocks/action/action-basic.tsx'))}/> + + + + + ## Expression Evaluation The example below uses the `exp` property to evaluate an expression when the action is clicked. @@ -34,4 +49,14 @@ This will increment the count, and update the label + + +## Navigation Transition To End + + + + + import('!!raw-loader!@player-ui/reference-assets-plugin-mocks/action/action-transition-to-end.tsx'))}/> + + \ No newline at end of file diff --git a/docs/storybook/src/reference-assets/Intro.stories.mdx b/docs/storybook/src/reference-assets/Intro.stories.mdx index 96f135e09..bd133415d 100644 --- a/docs/storybook/src/reference-assets/Intro.stories.mdx +++ b/docs/storybook/src/reference-assets/Intro.stories.mdx @@ -1,4 +1,4 @@ -import { Meta } from '@storybook/addon-docs'; +import { Meta } from '@storybook/addon-docs/blocks'; diff --git a/package.json b/package.json index cf8d05440..93aafa144 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "player", "version": "0.0.0", + "private": true, "description": "", "repository": { "type": "git", "url": "https://github.com/player-ui/player.git" }, - "private": true, "scripts": { - "test": "bazel test -- $(bazel query \"kind(nodejs_test, //...)\" --output label 2>/dev/null | tr '\\n' ' ')", + "dev:docs": "ibazel run //docs/site:start", + "postinstall": "patch-package && node ./scripts/yarn-link-setup.js", "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx $(scripts/pkg-roots.sh)", "prepare": "is-ci || husky install", - "postinstall": "patch-package && node ./scripts/yarn-link-setup.js", - "dev:docs": "ibazel run //docs/site:start" + "test": "bazel test -- $(bazel query \"kind(nodejs_test, //...)\" --output label 2>/dev/null | tr '\\n' ' ')" }, "dependencies": { "@auto-it/upload-assets": "^10.37.2", @@ -38,9 +38,10 @@ "@emotion/styled": "^11", "@kendallgassner/eslint-plugin-package-json": "^0.2.1", "@mdx-js/loader": "^1.6.22", - "@monaco-editor/react": "^4.3.1", - "@player-tools/cli": "0.3.0", - "@player-tools/dsl": "0.3.0", + "@monaco-editor/react": "^4.6.0", + "@player-tools/cli": "0.4.0-next.3", + "@player-tools/dsl": "0.4.0-next.3", + "@reduxjs/toolkit": "^1.9.5", "@rollup/plugin-image": "^3.0.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.0.6", @@ -52,6 +53,7 @@ "@storybook/components": "^6.4.15", "@storybook/manager-webpack5": "^6.4.15", "@storybook/react": "^6.4.15", + "@swc/wasm-web": "^1.3.74", "@testing-library/dom": "^8.10.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.2", @@ -59,6 +61,7 @@ "@testing-library/user-event": "^13.5.0", "@types/babel__register": "^7.17.0", "@types/carbon-components-react": "^7.46.1", + "@types/deep-equal": "1.0.1", "@types/dlv": "^1.1.2", "@types/fs-extra": "^9.0.13", "@types/jest": "^27.0.2", @@ -72,6 +75,7 @@ "@types/pubsub-js": "^1.8.3", "@types/react": "^17.0.25", "@types/react-redux": "^7.1.22", + "@types/redux-state-sync": "^3.1.5", "@types/signale": "^1.4.2", "@types/std-mocks": "^1.0.1", "@types/uuid": "^8.3.4", @@ -92,6 +96,7 @@ "command-line-application": "^0.10.1", "cosmiconfig": "^7.0.1", "cross-fetch": "^3.1.5", + "deep-equal": "1.1.1", "dequal": "^2.0.2", "detect-indent": "^6.0.0", "dlv": "^1.1.3", @@ -100,6 +105,7 @@ "elegant-spinner": "^3.0.0", "error-polyfill": "^0.1.3", "esbuild": "^0.13.15", + "esbuild-wasm": "0.14.23", "eslint": "^8.0.1", "eslint-config-airbnb": "^18.2.1", "eslint-config-prettier": "^8.3.0", @@ -158,6 +164,7 @@ "react-redux": "^7.2.6", "react-syntax-highlighter": "^15.4.5", "redux": "^4.1.2", + "redux-state-sync": "^3.1.4", "rehype-autolink-headings": "^6.1.1", "rehype-slug": "^5.0.1", "remark": "^12.0.1", @@ -173,8 +180,8 @@ "rollup-plugin-esbuild": "^4.7.2", "rollup-plugin-string": "^3.0.0", "rollup-plugin-styles": "^4.0.0", - "signale": "^1.4.0", "seamless-scroll-polyfill": "2.3.3", + "signale": "^1.4.0", "sorted-array": "^2.0.4", "source-map-js": "^1.0.2", "std-mocks": "^1.0.1", @@ -186,6 +193,7 @@ "timm": "^1.6.2", "ts-debounce": "^4.0.0", "ts-loader": "8.2.0", + "ts-nested-error": "^1.2.1", "ts-node": "^10.4.0", "typescript": "4.4.4", "util": "^0.12.4", diff --git a/plugins/markdown/core/src/__tests__/__snapshots__/index.test.ts.snap b/plugins/markdown/core/src/__tests__/__snapshots__/index.test.ts.snap deleted file mode 100644 index 8364036c6..000000000 --- a/plugins/markdown/core/src/__tests__/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,241 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MarkdownPlugin parses the flow containing markdown into valid FRF, based on the given mappers 1`] = ` -Object { - "id": "markdown-view", - "primaryInfo": Object { - "asset": Object { - "id": "markdown-primaryInfo-collection", - "type": "collection", - "values": Array [ - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-bold-composite-7", - "type": "composite", - "values": Array [ - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-bold-text-4", - "type": "text", - "value": "some ", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-bold-text-5", - "modifiers": Array [ - Object { - "type": "tag", - "value": "important", - }, - ], - "type": "text", - "value": "bold text", - }, - }, - ], - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-italic-text-8", - "modifiers": Array [ - Object { - "type": "tag", - "value": "emphasis", - }, - ], - "type": "text", - "value": "italicized text", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-orderd-list-list-20", - "metaData": Object { - "listType": "ordered", - }, - "type": "list", - "values": Array [ - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-orderd-list-text-11", - "type": "text", - "value": "First", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-orderd-list-text-14", - "type": "text", - "value": "Second", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-orderd-list-text-17", - "type": "text", - "value": "Third", - }, - }, - ], - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unorderd-list-list-31", - "type": "list", - "values": Array [ - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unorderd-list-text-21", - "modifiers": Array [ - Object { - "metaData": Object { - "ref": "https://turbotax.intuit.ca", - }, - "type": "link", - }, - ], - "type": "text", - "value": "First", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unorderd-list-text-25", - "type": "text", - "value": "Second", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unorderd-list-text-28", - "type": "text", - "value": "Third", - }, - }, - ], - }, - }, - Object { - "asset": Object { - "accessibility": "alt text", - "id": "markdown-primaryInfo-collection-image-image-32", - "metaData": Object { - "ref": "image.png", - }, - "type": "image", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unsupported-text-34", - "type": "text", - "value": "Highlights are ==not supported==", - }, - }, - ], - }, - }, - "title": Object { - "asset": Object { - "id": "markdown-view-title-composite-3", - "type": "composite", - "values": Array [ - Object { - "asset": Object { - "id": "markdown-view-title-text-0", - "type": "text", - "value": "Learn more at ", - }, - }, - Object { - "asset": Object { - "id": "markdown-view-title-text-1", - "modifiers": Array [ - Object { - "metaData": Object { - "ref": "https://turbotax.intuit.ca", - }, - "type": "link", - }, - ], - "type": "text", - "value": "TurboTax Canada", - }, - }, - ], - }, - }, - "type": "questionAnswer", -} -`; - -exports[`MarkdownPlugin parses the flow, with only the required mappers 1`] = ` -Object { - "id": "markdown-view", - "primaryInfo": Object { - "asset": Object { - "id": "markdown-primaryInfo-collection", - "type": "collection", - "values": Array [ - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-bold", - "type": "text", - "value": "some **bold text**", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-italic", - "type": "text", - "value": "*italicized text*", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-orderd-list", - "type": "text", - "value": "1. First -2. Second -3. Third", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unorderd-list", - "type": "text", - "value": "- [First](https://turbotax.intuit.ca) -- Second -- Third", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-image", - "type": "text", - "value": "![alt text](image.png)", - }, - }, - Object { - "asset": Object { - "id": "markdown-primaryInfo-collection-unsupported-text-36", - "type": "text", - "value": "Highlights are ==not supported==", - }, - }, - ], - }, - }, - "title": Object { - "asset": Object { - "id": "markdown-view-title", - "type": "text", - "value": "Learn more at [TurboTax Canada](https://turbotax.intuit.ca)", - }, - }, - "type": "questionAnswer", -} -`; diff --git a/plugins/markdown/core/src/__tests__/helpers/index.ts b/plugins/markdown/core/src/__tests__/helpers/index.ts index eb2b40053..d061a7294 100644 --- a/plugins/markdown/core/src/__tests__/helpers/index.ts +++ b/plugins/markdown/core/src/__tests__/helpers/index.ts @@ -72,6 +72,11 @@ export const mockMappers: Mappers = { type: 'text', value, }), + collection: ({ originalAsset, value }) => ({ + id: `${originalAsset.id}-collection-${depth++}`, + type: 'collection', + values: value.map(wrapAsset), + }), strong: ({ originalAsset, value }) => flatSingleElementCompositeAsset({ id: `${originalAsset.id}-text-${depth++}`, diff --git a/plugins/markdown/core/src/__tests__/index.test.ts b/plugins/markdown/core/src/__tests__/index.test.ts index 3e6738367..e7d93188f 100644 --- a/plugins/markdown/core/src/__tests__/index.test.ts +++ b/plugins/markdown/core/src/__tests__/index.test.ts @@ -6,166 +6,89 @@ import type { Flow } from '@player-ui/types'; import { mockMappers } from './helpers'; import { MarkdownPlugin } from '..'; -const unparsedFlow: Flow = { - id: 'markdown-flow', - data: { - internal: { - locale: { - linkMarkdown: - 'Learn more at [TurboTax Canada](https://turbotax.intuit.ca)', - }, - }, - }, - views: [ - { - id: 'markdown-view', - type: 'questionAnswer', - title: { - asset: { - id: 'markdown-view-title', - type: 'markdown', - value: '{{internal.locale.linkMarkdown}}', - }, - }, - primaryInfo: { - asset: { - id: 'markdown-primaryInfo-collection', - type: 'collection', - values: [ - { - asset: { - id: 'markdown-primaryInfo-collection-bold', - type: 'markdown', - value: 'some **bold text**', - }, - }, - { - asset: { - id: 'markdown-primaryInfo-collection-italic', - type: 'markdown', - value: '*italicized text*', - }, - }, - { - asset: { - id: 'markdown-primaryInfo-collection-orderd-list', - type: 'markdown', - value: '1. First\n2. Second\n3. Third', - }, - }, - { - asset: { - id: 'markdown-primaryInfo-collection-unorderd-list', - type: 'markdown', - value: - '- [First](https://turbotax.intuit.ca)\n- Second\n- Third', - }, - }, - { - asset: { - id: 'markdown-primaryInfo-collection-image', - type: 'markdown', - value: '![alt text](image.png)', - }, - }, - { - asset: { - id: 'markdown-primaryInfo-collection-unsupported', - type: 'markdown', - value: 'Highlights are ==not supported==', - }, - }, - ], - }, - }, - }, - ], - navigation: { - BEGIN: 'FLOW_1', - FLOW_1: { - startState: 'VIEW_1', - VIEW_1: { - state_type: 'VIEW', - ref: 'markdown-view', - transitions: { - '*': 'END_Done', +describe('MarkdownPlugin', () => { + describe('Transform Operation', () => { + const unparsedFlow: Flow = { + id: 'markdown-flow', + data: { + internal: { + locale: { + linkMarkdown: + 'Learn more at [TurboTax Canada](https://turbotax.intuit.ca)', + }, }, }, - END_Done: { - state_type: 'END', - outcome: 'done', - }, - }, - }, -}; - -describe('MarkdownPlugin', () => { - it('parses the flow containing markdown into valid FRF, based on the given mappers', () => { - const player = new Player({ - plugins: [new MarkdownPlugin(mockMappers)], - }); - player.start(unparsedFlow); - - const view = (player.getState() as InProgressState).controllers.view - .currentView?.lastUpdate; - - expect(view).toMatchSnapshot(); - }); - - it('parses the flow, with only the required mappers', () => { - const player = new Player({ - plugins: [ - new MarkdownPlugin({ - text: mockMappers.text, - paragraph: mockMappers.paragraph, - }), - ], - }); - player.start(unparsedFlow); - - const view = (player.getState() as InProgressState).controllers.view - .currentView?.lastUpdate; - - expect(view).toMatchSnapshot(); - }); - - it('parses regular flow and maps assets', () => { - const fingerprint = new PartialMatchFingerprintPlugin(new Registry()); - - fingerprint.register({ type: 'action' }, 0); - fingerprint.register({ type: 'text' }, 1); - fingerprint.register({ type: 'composite' }, 2); - - const player = new Player({ - plugins: [fingerprint, new MarkdownPlugin(mockMappers)], - }); - - player.start({ - id: 'action-with-expression', views: [ { - id: 'action', - type: 'action', - exp: '{{count}} = {{count}} + 1', - label: { + id: 'markdown-view', + type: 'questionAnswer', + title: { asset: { - id: 'action-label', + id: 'markdown-view-title', type: 'markdown', - value: 'Clicked {{count}} *times*', + value: '{{internal.locale.linkMarkdown}}', + }, + }, + primaryInfo: { + asset: { + id: 'markdown-primaryInfo-collection', + type: 'collection', + values: [ + { + asset: { + id: 'markdown-primaryInfo-collection-bold', + type: 'markdown', + value: 'some **bold text**', + }, + }, + { + asset: { + id: 'markdown-primaryInfo-collection-italic', + type: 'markdown', + value: '*italicized text*', + }, + }, + { + asset: { + id: 'markdown-primaryInfo-collection-orderd-list', + type: 'markdown', + value: '1. First\n2. Second\n3. Third', + }, + }, + { + asset: { + id: 'markdown-primaryInfo-collection-unorderd-list', + type: 'markdown', + value: + '- [First](https://turbotax.intuit.ca)\n- Second\n- Third', + }, + }, + { + asset: { + id: 'markdown-primaryInfo-collection-image', + type: 'markdown', + value: '![alt text](image.png)', + }, + }, + { + asset: { + id: 'markdown-primaryInfo-collection-unsupported', + type: 'markdown', + value: 'Highlights are ==not supported==', + }, + }, + ], }, }, }, ], - data: { - count: 0, - }, navigation: { BEGIN: 'FLOW_1', FLOW_1: { startState: 'VIEW_1', VIEW_1: { state_type: 'VIEW', - ref: 'action', + ref: 'markdown-view', transitions: { '*': 'END_Done', }, @@ -176,10 +99,366 @@ describe('MarkdownPlugin', () => { }, }, }, + }; + + it('parses the flow containing markdown into valid FRF, based on the given mappers', () => { + const player = new Player({ + plugins: [new MarkdownPlugin(mockMappers)], + }); + player.start(unparsedFlow); + + const view = (player.getState() as InProgressState).controllers.view + .currentView?.lastUpdate; + + expect(view).toMatchInlineSnapshot(` + Object { + "id": "markdown-view", + "primaryInfo": Object { + "asset": Object { + "id": "markdown-primaryInfo-collection", + "type": "collection", + "values": Array [ + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-bold-composite-7", + "type": "composite", + "values": Array [ + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-bold-text-4", + "type": "text", + "value": "some ", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-bold-text-5", + "modifiers": Array [ + Object { + "type": "tag", + "value": "important", + }, + ], + "type": "text", + "value": "bold text", + }, + }, + ], + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-italic-text-8", + "modifiers": Array [ + Object { + "type": "tag", + "value": "emphasis", + }, + ], + "type": "text", + "value": "italicized text", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-orderd-list-list-20", + "metaData": Object { + "listType": "ordered", + }, + "type": "list", + "values": Array [ + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-orderd-list-text-11", + "type": "text", + "value": "First", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-orderd-list-text-14", + "type": "text", + "value": "Second", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-orderd-list-text-17", + "type": "text", + "value": "Third", + }, + }, + ], + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unorderd-list-list-31", + "type": "list", + "values": Array [ + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unorderd-list-text-21", + "modifiers": Array [ + Object { + "metaData": Object { + "ref": "https://turbotax.intuit.ca", + }, + "type": "link", + }, + ], + "type": "text", + "value": "First", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unorderd-list-text-25", + "type": "text", + "value": "Second", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unorderd-list-text-28", + "type": "text", + "value": "Third", + }, + }, + ], + }, + }, + Object { + "asset": Object { + "accessibility": "alt text", + "id": "markdown-primaryInfo-collection-image-image-32", + "metaData": Object { + "ref": "image.png", + }, + "type": "image", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unsupported-text-34", + "type": "text", + "value": "Highlights are ==not supported==", + }, + }, + ], + }, + }, + "title": Object { + "asset": Object { + "id": "markdown-view-title-composite-3", + "type": "composite", + "values": Array [ + Object { + "asset": Object { + "id": "markdown-view-title-text-0", + "type": "text", + "value": "Learn more at ", + }, + }, + Object { + "asset": Object { + "id": "markdown-view-title-text-1", + "modifiers": Array [ + Object { + "metaData": Object { + "ref": "https://turbotax.intuit.ca", + }, + "type": "link", + }, + ], + "type": "text", + "value": "TurboTax Canada", + }, + }, + ], + }, + }, + "type": "questionAnswer", + } + `); }); - // the parser should create 2 text assets: `Clicked {{count}}` and a italicized `times`: - expect(fingerprint.get('action-label-text-38')).toBe(1); - expect(fingerprint.get('action-label-text-39')).toBe(1); + it('parses the flow, with only the required mappers', () => { + const player = new Player({ + plugins: [ + new MarkdownPlugin({ + text: mockMappers.text, + paragraph: mockMappers.paragraph, + collection: mockMappers.collection, + }), + ], + }); + player.start(unparsedFlow); + + const view = (player.getState() as InProgressState).controllers.view + .currentView?.lastUpdate; + + expect(view).toMatchInlineSnapshot(` + Object { + "id": "markdown-view", + "primaryInfo": Object { + "asset": Object { + "id": "markdown-primaryInfo-collection", + "type": "collection", + "values": Array [ + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-bold", + "type": "text", + "value": "some **bold text**", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-italic", + "type": "text", + "value": "*italicized text*", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-orderd-list", + "type": "text", + "value": "1. First + 2. Second + 3. Third", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unorderd-list", + "type": "text", + "value": "- [First](https://turbotax.intuit.ca) + - Second + - Third", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-image", + "type": "text", + "value": "![alt text](image.png)", + }, + }, + Object { + "asset": Object { + "id": "markdown-primaryInfo-collection-unsupported-text-36", + "type": "text", + "value": "Highlights are ==not supported==", + }, + }, + ], + }, + }, + "title": Object { + "asset": Object { + "id": "markdown-view-title", + "type": "text", + "value": "Learn more at [TurboTax Canada](https://turbotax.intuit.ca)", + }, + }, + "type": "questionAnswer", + } + `); + }); + }); + + describe('Interactions with Asset Registry', () => { + it('parses regular flow and maps assets', () => { + const fingerprint = new PartialMatchFingerprintPlugin(new Registry()); + + fingerprint.register({ type: 'action' }, 0); + fingerprint.register({ type: 'text' }, 1); + fingerprint.register({ type: 'composite' }, 2); + + const player = new Player({ + plugins: [fingerprint, new MarkdownPlugin(mockMappers)], + }); + + player.start({ + id: 'action-with-expression', + views: [ + { + id: 'action', + type: 'action', + exp: '{{count}} = {{count}} + 1', + label: { + asset: { + id: 'action-label', + type: 'markdown', + value: 'Clicked {{count}} *times*', + }, + }, + }, + ], + data: { + count: 0, + }, + navigation: { + BEGIN: 'FLOW_1', + FLOW_1: { + startState: 'VIEW_1', + VIEW_1: { + state_type: 'VIEW', + ref: 'action', + transitions: { + '*': 'END_Done', + }, + }, + END_Done: { + state_type: 'END', + outcome: 'done', + }, + }, + }, + }); + + // the parser should create 2 text assets: `Clicked {{count}}` and a italicized `times`: + const view = (player.getState() as InProgressState).controllers.view + .currentView?.lastUpdate; + + expect(view).toMatchInlineSnapshot(` + Object { + "exp": "{{count}} = {{count}} + 1", + "id": "action", + "label": Object { + "asset": Object { + "id": "action-label-composite-41", + "type": "composite", + "values": Array [ + Object { + "asset": Object { + "id": "action-label-text-38", + "type": "text", + "value": "Clicked 0 ", + }, + }, + Object { + "asset": Object { + "id": "action-label-text-39", + "modifiers": Array [ + Object { + "type": "tag", + "value": "emphasis", + }, + ], + "type": "text", + "value": "times", + }, + }, + ], + }, + }, + "type": "action", + } + `); + expect(fingerprint.get('action-label-text-38')).toBe(1); + expect(fingerprint.get('action-label-text-39')).toBe(1); + }); }); }); diff --git a/plugins/markdown/core/src/index.ts b/plugins/markdown/core/src/index.ts index 503b43b66..75af36a9d 100644 --- a/plugins/markdown/core/src/index.ts +++ b/plugins/markdown/core/src/index.ts @@ -39,11 +39,7 @@ export class MarkdownPlugin implements PlayerPlugin { parser: options.parseNode, }); - if (parsed.length === 1) { - return parsed[0]; - } - - return { ...node, nodeType: NodeType.MultiNode, values: parsed }; + return parsed; } return node; diff --git a/plugins/markdown/core/src/types.ts b/plugins/markdown/core/src/types.ts index 58906b01b..d074889b1 100644 --- a/plugins/markdown/core/src/types.ts +++ b/plugins/markdown/core/src/types.ts @@ -45,6 +45,10 @@ export interface Mappers { * required paragraph (composite) Asset */ paragraph: CompositeMapper; + /** + * required collection Asset to wrap arrays of assets + */ + collection: CompositeMapper; /** * strong markdown (e.g. **bold**) */ diff --git a/plugins/markdown/core/src/utils/markdownParser.ts b/plugins/markdown/core/src/utils/markdownParser.ts index 827790fd7..172296405 100644 --- a/plugins/markdown/core/src/utils/markdownParser.ts +++ b/plugins/markdown/core/src/utils/markdownParser.ts @@ -31,23 +31,37 @@ export function parseAssetMarkdownContent({ type?: Node.ChildrenTypes, options?: ParseObjectOptions ) => Node.Node | null; -}) { +}): Node.Node | null { const { children } = fromMarkdown(asset.value as string); + const isMultiParagraph = children.length > 1; - return children.map((node) => { - const transformer = transformers[node.type as keyof typeof transformers]; - const content = transformer({ - astNode: node as unknown, - asset, - mappers, - transformers, + if (isMultiParagraph) { + const value = children.map((node) => { + const transformer = transformers[node.type as keyof typeof transformers]; + return transformer({ + astNode: node as any, + asset, + mappers, + transformers, + }); }); - return ( - parser?.( - content, - children.length > 1 ? NodeType.Value : NodeType.Asset - ) || null - ); + const collection = mappers.collection({ + originalAsset: asset, + value, + }); + + return parser?.(collection, NodeType.Asset) || null; + } + + const transformer = + transformers[children[0].type as keyof typeof transformers]; + const content = transformer({ + astNode: children[0] as any, + asset, + mappers, + transformers, }); + + return parser?.(content, NodeType.Asset) || null; } diff --git a/plugins/pubsub/core/src/plugin.ts b/plugins/pubsub/core/src/plugin.ts index 4edb4ecd5..55bb9eebc 100644 --- a/plugins/pubsub/core/src/plugin.ts +++ b/plugins/pubsub/core/src/plugin.ts @@ -3,7 +3,7 @@ import type { PlayerPlugin, ExpressionContext, } from '@player-ui/player'; -import type { SubscribeHandler } from './pubsub'; +import type { SubscribeHandler, TinyPubSub } from './pubsub'; import { pubsub } from './pubsub'; import { PubSubPluginSymbol } from './symbols'; @@ -27,13 +27,23 @@ export class PubSubPlugin implements PlayerPlugin { static Symbol = PubSubPluginSymbol; public readonly symbol = PubSubPlugin.Symbol; + protected pubsub: TinyPubSub; + private expressionName: string; constructor(config?: PubSubConfig) { this.expressionName = config?.expressionName ?? 'publish'; + this.pubsub = pubsub; } apply(player: Player) { + // if there is already a pubsub plugin, reuse its pubsub instance + // to maintain the singleton across bundles for iOS/Android + const existing = player.findPlugin(PubSubPluginSymbol); + if (existing !== undefined) { + this.pubsub = existing.pubsub; + } + player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => { const existingExpression = expEvaluator.operators.expressions.get( this.expressionName @@ -67,7 +77,7 @@ export class PubSubPlugin implements PlayerPlugin { * @param data - Any additional data to attach to the event */ publish(event: string, ...args: unknown[]) { - pubsub.publish(event, ...args); + this.pubsub.publish(event, ...args); } /** @@ -81,7 +91,7 @@ export class PubSubPlugin implements PlayerPlugin { event: T, handler: SubscribeHandler ) { - return pubsub.subscribe(event, handler); + return this.pubsub.subscribe(event, handler); } /** @@ -90,13 +100,13 @@ export class PubSubPlugin implements PlayerPlugin { * @param token - A token from a `subscribe` call */ unsubscribe(token: string) { - pubsub.unsubscribe(token); + this.pubsub.unsubscribe(token); } /** * Remove all subscriptions */ clear() { - pubsub.clear(); + this.pubsub.clear(); } } diff --git a/plugins/pubsub/core/src/pubsub.ts b/plugins/pubsub/core/src/pubsub.ts index 2ad70865d..47e5215d3 100644 --- a/plugins/pubsub/core/src/pubsub.ts +++ b/plugins/pubsub/core/src/pubsub.ts @@ -28,7 +28,7 @@ let count = 1; /** * Tiny pubsub maker */ -class TinyPubSub { +export class TinyPubSub { private events: Map>>; private tokens: Map; diff --git a/plugins/reference-assets/mocks/BUILD b/plugins/reference-assets/mocks/BUILD index c2f5e0798..4b0cf81bf 100644 --- a/plugins/reference-assets/mocks/BUILD +++ b/plugins/reference-assets/mocks/BUILD @@ -10,10 +10,14 @@ generate_manifest( javascript_pipeline( name = "@player-ui/reference-assets-plugin-mocks", entry = "index.ts", + dependencies = [ + "@npm//@player-tools/dsl", + "//plugins/reference-assets/components:@player-ui/reference-assets-components" + ], other_srcs = [ "index.ts", ":mocks", - ], + ] + glob(["**/*.tsx"]), out_dir = "", ) diff --git a/plugins/reference-assets/mocks/action/action-basic.tsx b/plugins/reference-assets/mocks/action/action-basic.tsx new file mode 100644 index 000000000..aa697635a --- /dev/null +++ b/plugins/reference-assets/mocks/action/action-basic.tsx @@ -0,0 +1,51 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import React from 'react'; +import { Action } from '@player-ui/reference-assets-components'; +import type { DSLFlow } from '@player-tools/dsl'; +import { + binding as b, + expression as e, + makeBindingsForObject, +} from '@player-tools/dsl'; + +const schema = { + count: { + type: 'NumberType', + }, +}; + +const data = makeBindingsForObject(schema); + +const view1 = ( + + Count: {b`count`} + +); + +const flow: DSLFlow = { + id: 'test-flow', + views: [view1], + data: { + count: 0, + }, + schema, + navigation: { + BEGIN: 'FLOW_1', + FLOW_1: { + startState: 'VIEW_1', + VIEW_1: { + state_type: 'VIEW', + ref: view1, + transitions: { + '*': 'END_Done', + }, + }, + END_Done: { + state_type: 'END', + outcome: 'DONE', + }, + }, + }, +}; + +export default flow; diff --git a/plugins/reference-assets/mocks/action/action-transition-to-end.tsx b/plugins/reference-assets/mocks/action/action-transition-to-end.tsx new file mode 100644 index 000000000..af5146fab --- /dev/null +++ b/plugins/reference-assets/mocks/action/action-transition-to-end.tsx @@ -0,0 +1,42 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import React from 'react'; +import { Action, Collection } from '@player-ui/reference-assets-components'; +import type { DSLFlow } from '@player-tools/dsl'; +import { expression as e } from '@player-tools/dsl'; + +const view1 = ( + + + + End the flow (success) + + + End the flow (error) + + + +); + +const flow: DSLFlow = { + id: 'test-flow', + views: [view1], + navigation: { + BEGIN: 'FLOW_1', + FLOW_1: { + startState: 'VIEW_1', + VIEW_1: { + state_type: 'VIEW', + ref: view1, + transitions: { + '*': 'END_Done', + }, + }, + END_Done: { + state_type: 'END', + outcome: 'DONE', + }, + }, + }, +}; + +export default flow; diff --git a/react/player/src/asset/__tests__/index.test.tsx b/react/player/src/asset/__tests__/index.test.tsx index bebadf661..b7d30aeab 100644 --- a/react/player/src/asset/__tests__/index.test.tsx +++ b/react/player/src/asset/__tests__/index.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { Registry } from '@player-ui/partial-match-registry'; +import type { Asset as AssetType } from '@player-ui/player'; import type { AssetRegistryType } from '..'; import { ReactAsset, AssetContext } from '..'; @@ -27,3 +28,101 @@ test('it prioritizes local type and id', () => { expect(asset.getByText('foo')).not.toBeUndefined(); }); + +test('throws an error for an asset missing implementation', () => { + const assetDef = { + asset: { + id: 'bar-id', + type: 'bar', + }, + } as unknown as AssetType; + + const registry: AssetRegistryType = new Registry([ + [{ type: 'foo' }, () =>
foo
], + ]); + + expect(() => + render( + + + + ) + ).toThrowError('No implementation found for id: bar-id type: bar'); +}); + +test('throws an error for an asset missing type', () => { + const assetDef = { + asset: { + id: 'bar-id', + }, + } as unknown as AssetType; + + const registry: AssetRegistryType = new Registry([ + [{ type: 'foo' }, () =>
foo
], + [{ type: 'bar' }, () =>
bar
], + ]); + + expect(() => + render( + + + + ) + ).toThrowError('Asset is missing type for id: bar-id'); +}); + +test('throws an error for an asset that isnt an object', () => { + const assetDef = { + asset: 'bar', + } as unknown as AssetType; + + const registry: AssetRegistryType = new Registry([ + [{ type: 'foo' }, () =>
foo
], + [{ type: 'bar' }, () =>
bar
], + ]); + + expect(() => + render( + + + + ) + ).toThrowError('Asset was not an object got (string) instead: bar'); +}); + +test('throws an error for an asset that is an object but not valid', () => { + const assetDef = { + asset: ['a'], + } as unknown as AssetType; + + const registry: AssetRegistryType = new Registry([ + [{ type: 'foo' }, () =>
foo
], + [{ type: 'bar' }, () =>
bar
], + ]); + + expect(() => + render( + + + + ) + ).toThrowError('Asset is missing type for {"asset":["a"]}'); +}); +test('throws an error for an asset that unwraps nothing', () => { + const assetDef = { + asset: undefined, + } as unknown as AssetType; + + const registry: AssetRegistryType = new Registry([ + [{ type: 'foo' }, () =>
foo
], + [{ type: 'bar' }, () =>
bar
], + ]); + + expect(() => + render( + + + + ) + ).toThrowError('Cannot determine asset type for props: {}'); +}); diff --git a/react/player/src/asset/index.tsx b/react/player/src/asset/index.tsx index c95e2bf68..d290746a5 100644 --- a/react/player/src/asset/index.tsx +++ b/react/player/src/asset/index.tsx @@ -29,12 +29,24 @@ export const ReactAsset = ( unwrapped = (props as unknown as AssetWrapper).asset; } - if ( - !unwrapped || - typeof unwrapped !== 'object' || - unwrapped?.type === undefined - ) { - throw Error(`Cannot determine asset type.`); + if (!unwrapped) { + throw Error( + `Cannot determine asset type for props: ${JSON.stringify(props)}` + ); + } + + if (typeof unwrapped !== 'object') { + throw Error( + `Asset was not an object got (${typeof unwrapped}) instead: ${unwrapped}` + ); + } + + if (unwrapped.type === undefined) { + const info = + unwrapped.id === undefined + ? JSON.stringify(props) + : `id: ${unwrapped.id}`; + throw Error(`Asset is missing type for ${info}`); } const Impl = registry?.get(unwrapped); @@ -45,5 +57,5 @@ export const ReactAsset = ( ); } - return ; + return ; }; diff --git a/tools/storybook/BUILD b/tools/storybook/BUILD index 6b10dc406..58c89564c 100644 --- a/tools/storybook/BUILD +++ b/tools/storybook/BUILD @@ -16,6 +16,14 @@ javascript_pipeline( "@npm//dequal", "@npm//ts-debounce", "@npm//uuid", + "@npm//@reduxjs/toolkit", + "@npm//@swc/wasm-web", + "@npm//@types/redux-state-sync", + "@npm//esbuild-wasm", + "@npm//redux-state-sync", + "@npm//deep-equal", + "@npm//@types/deep-equal", + "@npm//@player-tools/dsl", "//plugins/metrics/react:@player-ui/metrics-plugin-react", "//plugins/beacon/react:@player-ui/beacon-plugin-react" ], diff --git a/tools/storybook/src/addons/appetize/index.tsx b/tools/storybook/src/addons/appetize/index.tsx index 55aed03d7..ef2d81b24 100644 --- a/tools/storybook/src/addons/appetize/index.tsx +++ b/tools/storybook/src/addons/appetize/index.tsx @@ -8,8 +8,10 @@ import { WithTooltip, TooltipLinkList, } from '@storybook/components'; +import { useDispatch, useSelector } from 'react-redux'; +import type { StateType } from '../../redux'; +import { setPlatform } from '../../redux'; import type { RenderTarget } from '../../types'; -import { useStateActions } from '../../state'; interface RenderSelectionProps { /** storybook api */ @@ -19,14 +21,16 @@ interface RenderSelectionProps { /** Component to show the appetize dropdown */ export const RenderSelection = ({ api }: RenderSelectionProps) => { const params = useParameter('appetizeTokens', {}); - const actions = useStateActions(api.getChannel()); - const [selectedPlatform, setPlatform] = - React.useState('web'); + const dispatch = useDispatch(); + + const selectedPlatform = useSelector( + (state) => state.platform.platform ?? 'web' + ); React.useEffect(() => { /** callback for the subscribe listener */ const listener = () => { - setPlatform('web'); + dispatch(setPlatform({ platform: 'web' })); }; api.getChannel().addListener(STORY_CHANGED, listener); @@ -34,7 +38,7 @@ export const RenderSelection = ({ api }: RenderSelectionProps) => { return () => { api.getChannel().removeListener(STORY_CHANGED, listener); }; - }, [api]); + }, [api, dispatch]); const mobilePlatforms = Object.keys(params) as Array<'ios' | 'android'>; @@ -54,8 +58,8 @@ export const RenderSelection = ({ api }: RenderSelectionProps) => { id: platform, title: platform, onClick: () => { - setPlatform(platform); - actions.setPlatform(platform); + setPlatform(platform as any); + dispatch(setPlatform({ platform })); onHide(); }, value: platform, diff --git a/tools/storybook/src/addons/editor/index.tsx b/tools/storybook/src/addons/editor/index.tsx index 4f5bb8b58..e8596c512 100644 --- a/tools/storybook/src/addons/editor/index.tsx +++ b/tools/storybook/src/addons/editor/index.tsx @@ -1,88 +1,209 @@ import React from 'react'; -import type { API } from '@storybook/api'; import { useDarkMode } from 'storybook-dark-mode'; -import { dequal } from 'dequal'; -import Editor from '@monaco-editor/react'; -import { useFlowState, useStateActions } from '../../state'; +import deepEqual from 'deep-equal'; +import Editor, { loader as monaco } from '@monaco-editor/react'; +import { Tabs, Placeholder } from '@storybook/components'; +import { useDispatch } from 'react-redux'; +import type { CompilationErrorType } from '../../redux'; +import { + setDSLEditorValue, + setJSONEditorValue, + useContentKind, + useDSLEditorValue, + useJSONEditorValue, +} from '../../redux'; + +monaco.init().then((m) => { + m.languages.typescript.typescriptDefaults.setCompilerOptions({ + jsx: m.languages.typescript.JsxEmit.React, + }); +}); interface EditorPanelProps { /** if the panel is shown */ active: boolean; - /** storybook api */ - api: API; } /** the panel for the flow editor */ -export const EditorPanel = (props: EditorPanelProps) => { - const { active } = props; +export const JSONEditorPanel = () => { const darkMode = useDarkMode(); - const flow = useFlowState(props.api.getChannel()); - const actions = useStateActions(props.api.getChannel()); - const [editorValue, setEditorValue] = React.useState( - flow ? JSON.stringify(flow, null, 2) : '{}' - ); - const updateTimerRef = React.useRef(undefined); + const jsonEditorValue = useJSONEditorValue(); - /** remove any pending saves */ - function clearPending() { - if (updateTimerRef.current) { - clearTimeout(updateTimerRef.current); - updateTimerRef.current = undefined; - } - } + const jsonValueAsString = + jsonEditorValue?.state === 'loaded' + ? JSON.stringify(jsonEditorValue.value, null, 2) + : ''; + + const dispatch = useDispatch(); - React.useEffect(() => { - if (!active) { + /** Handle change events */ + const onChange = (val: string | undefined) => { + if (!val || jsonEditorValue?.state !== 'loaded') { return; } try { - if (editorValue) { - const parsed = JSON.parse(editorValue); - if (dequal(flow, parsed)) { - return; - } + const parsed = JSON.parse(val); + if (!deepEqual(parsed, jsonEditorValue.value)) { + dispatch( + setJSONEditorValue({ + value: parsed, + }) + ); } - } catch (e) {} + } catch (e) { + // Parsing errors for JSON are handled by the editor + } + }; - setEditorValue(JSON.stringify(flow, null, 2)); - }, [flow, active]); + return ( + { + onChange(val); + }} + /> + ); +}; - if (!active) { +/** simple comp to showcase dsl errors */ +const CompileErrors = ({ + errors, +}: { + /** The errors to display */ + errors: CompilationErrorType; +}) => { + if ( + (errors.compileErrors === undefined || errors.compileErrors.length === 0) && + (errors.transpileErrors === undefined || + errors.transpileErrors.length === 0) + ) { return null; } - /** handler for changes to the content */ - const onChange = (val: string | undefined) => { - clearPending(); - setEditorValue(val ?? ''); + return ( +
+

Errors

+ {errors.compileErrors?.map((e) => ( +
{e.message}
+ ))} + {errors.transpileErrors?.map((e) => ( +
{e.message}
+ ))} +
+ ); +}; - try { - if (val) { - const parsed = JSON.parse(val); - if (!dequal(parsed, flow)) { - updateTimerRef.current = setTimeout(() => { - if (active) { - actions.setFlow(parsed); - } - }, 1000); - } - } - } catch (e) {} +/** A panel with the TSX editor built-in */ +const DSLEditorPanel = () => { + const darkMode = useDarkMode(); + + const jsonEditorValue = useJSONEditorValue(); + const dslEditorValue = useDSLEditorValue(); + const dispatch = useDispatch(); + + const editorValue = + dslEditorValue?.state === 'loaded' ? dslEditorValue.value : ''; + const flow = jsonEditorValue?.state === 'loaded' ? jsonEditorValue.value : ''; + + const compilationErrors = + dslEditorValue?.state === 'loaded' + ? dslEditorValue.compilationErrors + : undefined; + + const [selected, setSelected] = React.useState('tsx'); + + /** Handle editor updates */ + const onChange = (val: string | undefined) => { + if (val) { + dispatch( + setDSLEditorValue({ + value: val, + }) + ); + } }; return (
- { + setSelected(id); + }, + }} + > +
+
+ +
+ > + {selected === 'tsx' && ( + <> + {compilationErrors && } + { + onChange(val); + }} + /> + + )} + {selected === 'json' && ( + + )} +
); }; + +/** The editor panel */ +export const EditorPanel = (props: EditorPanelProps) => { + const contentType = useContentKind(); + + if (!props.active) { + return null; + } + + if (contentType === 'dsl') { + return ; + } + + if (contentType === 'json') { + return ; + } + + return ( + This story is not configured to allow flow edits. + ); +}; diff --git a/tools/storybook/src/addons/events/index.tsx b/tools/storybook/src/addons/events/index.tsx index 4ea2f563f..77aa50346 100644 --- a/tools/storybook/src/addons/events/index.tsx +++ b/tools/storybook/src/addons/events/index.tsx @@ -2,16 +2,17 @@ import React from 'react'; import { Table, Head, HeadCell, Cell, Body, Row } from '@devtools-ds/table'; import makeClass from 'clsx'; import { useDarkMode } from 'storybook-dark-mode'; -import type { API } from '@storybook/api'; -import { useEventState } from '../../state/hooks'; +import { useSelector } from 'react-redux'; +import { Placeholder } from '@storybook/components'; import type { EventType } from '../../state'; +import type { StateType } from '../../redux'; +import { useContentKind } from '../../redux'; + import styles from './events.css'; interface EventsPanelProps { /** if the panel is shown */ active: boolean; - /** storybook api */ - api: API; } /** Pad the cells to give room */ @@ -63,13 +64,22 @@ const ExtraCells = (event: EventType) => { /** The panel to show events */ export const EventsPanel = (props: EventsPanelProps) => { - const events = useEventState(props.api.getChannel()); + const events = useSelector((state) => state.events); const darkMode = useDarkMode(); + const contentType = useContentKind(); if (!props.active) { return null; } + if (contentType === undefined) { + return ( + + This story is not configured to receive Player events. + + ); + } + return (
( - + + + ), }); addons.addPanel(FLOW_PANEL_ID, { title: 'Flow', render: ({ active, key }) => ( - + + + ), }); @@ -34,13 +39,21 @@ export function register() { addons.add(FLOW_REFRESH_TOOL_ID, { title: 'Refresh Flow', type: types.TOOL, - render: () => , + render: () => ( + + + + ), }); addons.add(RENDER_SELECT_TOOL_ID, { title: 'Render Selection', type: types.TOOL, - render: () => , + render: () => ( + + + + ), }); }); } diff --git a/tools/storybook/src/addons/refresh/index.tsx b/tools/storybook/src/addons/refresh/index.tsx index ac2110e4a..7efdbfd1e 100644 --- a/tools/storybook/src/addons/refresh/index.tsx +++ b/tools/storybook/src/addons/refresh/index.tsx @@ -1,16 +1,11 @@ import React from 'react'; -import type { API } from '@storybook/api'; import { IconButton, Icons, Separator } from '@storybook/components'; -import { useStateActions } from '../../state'; - -interface FlowRefreshProps { - /** storybook api */ - api: API; -} +import { useDispatch } from 'react-redux'; +import { resetEditor } from '../../redux'; /** BUtton to refresh the current player flow */ -export const FlowRefresh = ({ api }: FlowRefreshProps) => { - const actions = useStateActions(api.getChannel()); +export const FlowRefresh = () => { + const dispatch = useDispatch(); return ( <> @@ -18,7 +13,7 @@ export const FlowRefresh = ({ api }: FlowRefreshProps) => { { - actions.resetFlow(); + dispatch(resetEditor()); }} > diff --git a/tools/storybook/src/decorator/index.tsx b/tools/storybook/src/decorator/index.tsx index 3f2b9f3f9..84832d08e 100644 --- a/tools/storybook/src/decorator/index.tsx +++ b/tools/storybook/src/decorator/index.tsx @@ -1,46 +1,62 @@ import React from 'react'; import type { DecoratorFn } from '@storybook/react'; -import addons from '@storybook/addons'; -import type { PlatformSetType } from '../state/hooks'; -import { subscribe } from '../state/hooks'; -import { ReactPlayerPluginContext, PlayerRenderContext } from '../player'; +import { useSelector } from 'react-redux'; +import type { StateType } from '../redux'; +import { StateProvider } from '../redux'; +import { + ReactPlayerPluginContext, + PlayerRenderContext, + DSLPluginContext, +} from '../player'; import type { PlayerParametersType, RenderTarget } from '../types'; -/** - * A story decorator for rendering player content - */ -export const PlayerDecorator: DecoratorFn = (story, ctx) => { - const playerParams = ctx.parameters as PlayerParametersType; - const [selectedPlatform, setPlatform] = - React.useState('web'); +/** Wrap the component in a PlayerContext provider w/ proper platform attribution */ +const PlayerRenderContextWrapper = ( + props: React.PropsWithChildren<{ + /** Params for the story */ + playerParams: PlayerParametersType; + }> +) => { + const { playerParams } = props; - React.useEffect(() => { - return subscribe( - addons.getChannel(), - '@@player/platform/set', - (evt) => { - setPlatform(evt.platform); - } - ); - }, []); + const platform = useSelector( + (s) => s.platform.platform ?? 'web' + ); return ( - - {story()} - + {props.children} ); }; + +/** + * A story decorator for rendering player content + */ +export const PlayerDecorator: DecoratorFn = (story, ctx) => { + const playerParams = ctx.parameters as PlayerParametersType; + + return ( + + + + + {story()} + + + + + ); +}; diff --git a/tools/storybook/src/dsl/createDSLStory.tsx b/tools/storybook/src/dsl/createDSLStory.tsx new file mode 100644 index 000000000..feb427518 --- /dev/null +++ b/tools/storybook/src/dsl/createDSLStory.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { DSLPlayerStory } from '../player'; + +/** Create a story */ +export function createDSLStory( + loader: () => Promise< + | string + | { + /** for dynamic imports */ + default: string; + } + >, + options?: any +) { + /** The story to render */ + const Comp = () => ; + + if (options?.args) { + Comp.args = options.args; + } + + return Comp; +} diff --git a/tools/storybook/src/dsl/index.ts b/tools/storybook/src/dsl/index.ts new file mode 100644 index 000000000..9616965ea --- /dev/null +++ b/tools/storybook/src/dsl/index.ts @@ -0,0 +1,2 @@ +export * from './transpile'; +export * from './createDSLStory'; diff --git a/tools/storybook/src/dsl/transpile.ts b/tools/storybook/src/dsl/transpile.ts new file mode 100644 index 000000000..d3a5e5bf6 --- /dev/null +++ b/tools/storybook/src/dsl/transpile.ts @@ -0,0 +1,55 @@ +import { initialize, transform } from 'esbuild-wasm/lib/browser'; +import * as React from 'react'; +import * as PlayerDSL from '@player-tools/dsl'; + +const setup = (async () => { + try { + await initialize({ + worker: true, + wasmURL: 'https://unpkg.com/esbuild-wasm@0.14.23/esbuild.wasm', + }); + } catch (e: any) {} +})(); + +/** Eval the code and check imports */ +export const execute = async ( + code: string, + options?: { + /** Other modules to include in the compilation */ + additionalModules?: Record; + } +) => { + const { additionalModules = {} } = options ?? {}; + + await setup; + + const result = await transform(code, { + loader: 'tsx', + format: 'cjs', + tsconfigRaw: { + compilerOptions: {}, + }, + }); + + const mods = { + react: React, + '@player-tools/dsl': PlayerDSL, + ...additionalModules, + }; + + // eslint-disable-next-line no-eval + const mod = eval(`(function(require, module){ ${result.code}})`); + + const exp: { + /** Exports of the running module */ + exports?: any; + } = {}; + /** a patch for `require` */ + const req = (name: string) => { + return (mods as any)[name]; + }; + + mod(req, exp); + + return exp.exports; +}; diff --git a/tools/storybook/src/index.ts b/tools/storybook/src/index.ts index 3c1ef925d..13231ca33 100644 --- a/tools/storybook/src/index.ts +++ b/tools/storybook/src/index.ts @@ -1,3 +1,4 @@ export { register } from './addons'; export * from './decorator'; export * from './player'; +export * from './dsl'; diff --git a/tools/storybook/src/player/PlayerStory.tsx b/tools/storybook/src/player/PlayerStory.tsx index d20e7422c..c6099760e 100644 --- a/tools/storybook/src/player/PlayerStory.tsx +++ b/tools/storybook/src/player/PlayerStory.tsx @@ -1,35 +1,46 @@ import React from 'react'; -import type { - ReactPlayerPlugin, - PlayerFlowStatus, - Flow, - ReactPlayerOptions, -} from '@player-ui/react'; -import { BeaconPlugin } from '@player-ui/beacon-plugin-react'; +import type { ReactPlayerOptions, ReactPlayerPlugin } from '@player-ui/react'; import { ReactPlayer } from '@player-ui/react'; -import { ChakraProvider, Spinner } from '@chakra-ui/react'; +import { BeaconPlugin } from '@player-ui/beacon-plugin-react'; import { makeFlow } from '@player-ui/make-flow'; +import { Placeholder } from '@storybook/components'; +import { useDispatch, useSelector } from 'react-redux'; +import type { PlayerFlowStatus, Flow } from '@player-ui/player'; import addons from '@storybook/addons'; -import type { AsyncImportFactory, RenderTarget } from '../types'; -import { useEditorFlow } from './hooks'; +import type { + AsyncImportFactory, + PlayerParametersType, + RenderTarget, +} from '../types'; import { Appetize } from './Appetize'; import { StorybookPlayerPlugin } from './storybookReactPlayerPlugin'; -import { useStateActions, subscribe } from '../state/hooks'; import { PlayerFlowSummary } from './PlayerFlowSummary'; +import type { StateType } from '../redux'; +import { + useContentKind, + useCompiledEditorValue, + useInitialJsonEditorValue, + useJSONEditorValue, +} from '../redux'; +import { useFlowSetListener } from './useFlowSet'; interface LocalPlayerStory { /** the mock to load */ - flow: Flow; + mock: Flow; - /** Web plugins to load into Player */ + /** plugins to the player */ webPlugins?: Array; } export const ReactPlayerPluginContext = React.createContext<{ - /** Web plugins to load into Player */ - plugins?: Array; + /** Plugins to use for the player */ + plugins: Array; }>({ plugins: [] }); +export const DSLPluginContext = React.createContext< + PlayerParametersType['dslEditor'] +>({}); + export const PlayerRenderContext = React.createContext({ platform: 'web', }); @@ -46,45 +57,41 @@ export const PlayerOptionsContext = React.createContext<{ options?: ReactPlayerOptions; }>({ options: {} }); -/** A component to render a player + flow */ -const LocalPlayerStory = (props: LocalPlayerStory) => { - let flow = useEditorFlow(props.flow); +/** A Component to render the current JSON editor value inside of Player */ +const PlayerJsonEditorStory = () => { + const jsonEditorValue = useJSONEditorValue(); + useFlowSetListener(addons.getChannel()); + + const { plugins } = React.useContext(ReactPlayerPluginContext); + + const dispatch = useDispatch(); - const renderContext = React.useContext(PlayerRenderContext); - const pluginContext = React.useContext(ReactPlayerPluginContext); - const controlsContext = React.useContext(StorybookControlsContext); - const optionsContext = React.useContext(PlayerOptionsContext); - const options = { ...optionsContext?.options }; - const stateActions = useStateActions(addons.getChannel()); - const plugins = props.webPlugins ?? pluginContext?.plugins ?? []; const [playerState, setPlayerState] = React.useState('not-started'); const [trackedBeacons, setTrackedBeacons] = React.useState([]); - const rp = React.useMemo(() => { + const wp = React.useMemo(() => { const beaconPlugin = new BeaconPlugin({ callback: (beacon) => { setTrackedBeacons((t) => [...t, beacon]); }, }); - return new ReactPlayer({ - ...options, - plugins: [ - new StorybookPlayerPlugin(stateActions), - beaconPlugin, - ...plugins, - ...(options?.plugins ?? []), - ], + plugins: [new StorybookPlayerPlugin(dispatch), beaconPlugin, ...plugins], }); - }, [plugins, flow]); + }, [dispatch, plugins]); /** A callback to start the flow */ const startFlow = () => { + if (jsonEditorValue?.state !== 'loaded') { + return; + } + setPlayerState('in-progress'); setTrackedBeacons([]); - rp.start(flow) + + wp.start(jsonEditorValue.value) .then(() => { setPlayerState('completed'); }) @@ -96,41 +103,9 @@ const LocalPlayerStory = (props: LocalPlayerStory) => { React.useEffect(() => { startFlow(); - }, [rp]); + }, [wp, jsonEditorValue]); - React.useEffect(() => { - // merge new data from storybook controls - if (controlsContext) { - flow = { - ...flow, - data: { - ...(flow.data ?? {}), - ...controlsContext, - }, - }; - stateActions.setFlow(flow); - } - }, [controlsContext]); - - React.useEffect(() => { - return subscribe(addons.getChannel(), '@@player/flow/reset', () => { - startFlow(); - }); - }, [rp]); - - if (renderContext.platform !== 'web' && renderContext.token) { - return ( - - ); - } - - const currentState = rp.player.getState(); + const currentState = wp.player.getState(); if (playerState === 'completed' && currentState.status === 'completed') { return ( @@ -146,7 +121,55 @@ const LocalPlayerStory = (props: LocalPlayerStory) => { return ; } - return ; + return ; +}; + +/** A component to render a player + flow */ +const LocalPlayerStory = (props: LocalPlayerStory) => { + const flow = useInitialJsonEditorValue(props.mock); + const platform = useSelector((state) => state.platform.platform); + const renderContext = React.useContext(PlayerRenderContext); + const webPlayerContext = React.useContext(ReactPlayerPluginContext); + const { options } = React.useContext(PlayerOptionsContext); + + if (platform === 'web') { + return ( + + + + ); + } + + if ( + renderContext.platform !== 'web' && + renderContext.token && + flow?.state === 'loaded' + ) { + return ( + + ); + } + + return Unable to render flow; }; type Mock = Record; @@ -158,11 +181,11 @@ function wrapInLazy( /** The component to load */ Component: React.ComponentType<{ /** the flow */ - flow: Flow; + mock: Flow; }>, /** A mock or a promise that resolve to a mock */ - flowFactory: AsyncImportFactory | MockFactoryOrPromise, + mockFactory: AsyncImportFactory | MockFactoryOrPromise, /** Any other props to pass */ other?: any @@ -170,7 +193,7 @@ function wrapInLazy( /** an async loader to wrap the mock as a player component */ const asPlayer = async () => { const mock = - typeof flowFactory === 'function' ? await flowFactory() : flowFactory; + typeof mockFactory === 'function' ? await mockFactory() : mockFactory; /** The component to load */ const Comp = () => { @@ -178,7 +201,8 @@ function wrapInLazy( ...makeFlow('default' in mock ? mock.default : mock), ...(other ?? {}), }; - return ; + + return ; }; return { @@ -206,6 +230,7 @@ export interface PlayerStoryProps { */ export const PlayerStory = (props: PlayerStoryProps) => { const { flow, storybookControls, options, ...other } = props; + useContentKind('json'); const MockComp = React.useMemo( () => wrapInLazy(LocalPlayerStory, flow, other), @@ -214,23 +239,79 @@ export const PlayerStory = (props: PlayerStoryProps) => { return (
- - }> - + + - - - {' '} - - - + + + +
); }; + +/** A DSL story that compiles code */ +export const DSLLocalPlayerStory = ( + props: Omit & { + /** Initial state of the dsl content */ + dslContent: string; + } +) => { + const dslContext = React.useContext(DSLPluginContext); + useContentKind('dsl'); + useCompiledEditorValue(props.dslContent, { + additionalModules: dslContext?.additionalModules, + }); + + return ; +}; + +/** A DSL story that handles lazy-loaded content */ +export const DSLPlayerStory = ( + props: Omit & { + /** Initial state of the dsl content */ + dslContent: () => Promise< + | string + | { + /** The default export of the module */ + default: string; + } + >; + } +) => { + const { dslContent, ...other } = props; + const AsLazyComp = React.useMemo(() => { + /** A function to load the flow for use in a lazy component */ + const loadFlow = async () => { + let content = await dslContent(); + + if (typeof content === 'object') { + content = content.default; + } + + return { + default: () => { + return ( + + ); + }, + }; + }; + + return React.lazy(loadFlow); + }, [dslContent]); + + return ( + + + + ); +}; diff --git a/tools/storybook/src/player/hooks.ts b/tools/storybook/src/player/hooks.ts deleted file mode 100644 index c35641a25..000000000 --- a/tools/storybook/src/player/hooks.ts +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import type { Flow } from '@player-ui/player'; -import addons from '@storybook/addons'; -import { DocsContext } from '@storybook/addon-docs'; -import { useStateActions, useFlowState } from '../state/hooks'; - -/** Use the flow from the editor or the original one */ -export function useEditorFlow(initialFlow: Flow) { - const stateActions = useStateActions(addons.getChannel()); - const flow = useFlowState(addons.getChannel()); - - const docsContext = React.useContext(DocsContext); - - React.useEffect(() => { - stateActions.setFlow(initialFlow); - }, [initialFlow]); - - React.useEffect(() => { - if (!flow) { - stateActions.setFlow(initialFlow); - } - }, [flow]); - - if (docsContext.id) { - return initialFlow; - } - - return flow ?? initialFlow; -} diff --git a/tools/storybook/src/player/storybookReactPlayerPlugin.ts b/tools/storybook/src/player/storybookReactPlayerPlugin.ts index a8925d794..94674fab2 100644 --- a/tools/storybook/src/player/storybookReactPlayerPlugin.ts +++ b/tools/storybook/src/player/storybookReactPlayerPlugin.ts @@ -1,14 +1,15 @@ import type { ReactPlayer, Player, ReactPlayerPlugin } from '@player-ui/react'; import type { Timing } from '@player-ui/metrics-plugin-react'; import { MetricsPlugin } from '@player-ui/metrics-plugin-react'; +import type { Dispatch } from 'redux'; import type { - StateActions, DataChangeEventType, LogEventType, StateChangeEventType, MetricChangeEventType, } from '../state'; import { createEvent } from '../state'; +import { addEvents, clearEvents } from '../redux'; /** * @@ -17,14 +18,15 @@ import { createEvent } from '../state'; export class StorybookPlayerPlugin implements ReactPlayerPlugin { public readonly name = 'Storybook'; - private actions: StateActions; + private dispatch: Dispatch; private metricsPlugin: MetricsPlugin; - constructor(actions: StateActions) { - this.actions = actions; + constructor(dispatch: Dispatch) { + this.dispatch = dispatch; this.metricsPlugin = new MetricsPlugin({ onUpdate: (metrics) => { - actions.setMetrics(metrics); + // TODO: Add this in + // actions.setMetrics(metrics); }, onRenderEnd: (timing) => { this.onMetricChange(timing, 'render'); @@ -43,7 +45,7 @@ export class StorybookPlayerPlugin implements ReactPlayerPlugin { rp.registerPlugin(this.metricsPlugin); rp.player.hooks.dataController.tap(this.name, (dc) => { - this.actions.clearEvents(); + this.dispatch(clearEvents()); dc.hooks.onUpdate.tap(this.name, (dataUpdates) => { const events: Array = dataUpdates.map( @@ -55,44 +57,52 @@ export class StorybookPlayerPlugin implements ReactPlayerPlugin { to: dataUpdate.newValue, }) ); - this.actions.addEvents(events); + this.dispatch(addEvents(events)); }); }); rp.player.logger.hooks.log.tap(this.name, (severity, data) => { - this.actions.addEvents([ - createEvent({ - type: 'log', - message: data, - severity, - }), - ]); + this.dispatch( + addEvents([ + createEvent({ + type: 'log', + message: data, + severity, + }), + ]) + ); }); rp.player.hooks.state.tap(this.name, (newState) => { if ('error' in newState) { - this.actions.addEvents([ - createEvent({ - type: 'stateChange', - state: newState.status, - error: newState.error.message, - }), - ]); + this.dispatch( + addEvents([ + createEvent({ + type: 'stateChange', + state: newState.status, + error: newState.error.message, + }), + ]) + ); } else if (newState.status === 'completed') { - this.actions.addEvents([ - createEvent({ - type: 'stateChange', - state: newState.status, - outcome: newState.endState.outcome, - }), - ]); + this.dispatch( + addEvents([ + createEvent({ + type: 'stateChange', + state: newState.status, + outcome: newState.endState.outcome, + }), + ]) + ); } else { - this.actions.addEvents([ - createEvent({ - type: 'stateChange', - state: newState.status, - }), - ]); + this.dispatch( + addEvents([ + createEvent({ + type: 'stateChange', + state: newState.status, + }), + ]) + ); } }); } @@ -102,12 +112,14 @@ export class StorybookPlayerPlugin implements ReactPlayerPlugin { return; } - this.actions.addEvents([ - createEvent({ - type: 'metric', - metricType, - message: `Duration: ${timing.duration.toFixed(0)} ms`, - }), - ]); + this.dispatch( + addEvents([ + createEvent({ + type: 'metric', + metricType, + message: `Duration: ${timing.duration.toFixed(0)} ms`, + }), + ]) + ); } } diff --git a/tools/storybook/src/player/useFlowSet.tsx b/tools/storybook/src/player/useFlowSet.tsx new file mode 100644 index 000000000..d97b65a7a --- /dev/null +++ b/tools/storybook/src/player/useFlowSet.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import type { Channel } from '@storybook/channels'; +import { setJSONEditorValue } from '../redux'; + +/** A flow to listen for the global `@@player/flow/set` cmd and update the flow accordingly */ +export const useFlowSetListener = (chan: Channel) => { + const dispatch = useDispatch(); + + React.useEffect(() => { + /** Constant handler for subscribe/unsub */ + const handler = (payload: string) => { + try { + const { flow } = JSON.parse(payload); + dispatch(setJSONEditorValue({ value: flow })); + } catch (e) { + console.error('Unable to set JSON payload from storybook event', e); + } + }; + + const eventName = '@@player/flow/set'; + + chan.addListener(eventName, handler); + + return () => { + chan.removeListener(eventName, handler); + }; + }, [chan]); +}; diff --git a/tools/storybook/src/redux/index.tsx b/tools/storybook/src/redux/index.tsx new file mode 100644 index 000000000..d81b94fc4 --- /dev/null +++ b/tools/storybook/src/redux/index.tsx @@ -0,0 +1,394 @@ +/* eslint-disable no-param-reassign */ +import React from 'react'; +import type { Flow } from '@player-ui/player'; +import { DSLCompiler } from '@player-tools/dsl'; +import { + configureStore, + createAction, + createAsyncThunk, + createReducer, +} from '@reduxjs/toolkit'; +import { Provider, useDispatch, useSelector } from 'react-redux'; +import { + createStateSyncMiddleware, + initStateWithPrevTab, +} from 'redux-state-sync'; +import { execute } from '../dsl'; +import type { RenderTarget } from '../types'; +import type { EventType } from '../state'; + +const compiler = new DSLCompiler(); + +export type LoadingState = 'loading' | 'loaded' | 'error'; + +export const resetEditor = createAction('@@player/flow/reset'); + +export const setDSLEditorValue = createAction<{ + /** The state of the editor */ + value: string; +}>('setDSLEditorValue'); + +export const setCompiledEditorResult = createAction<{ + /** The state of the editor */ + result?: Flow; + + /** Any errors from the compilation */ + errors?: CompilationErrorType; +}>('setCompiledEditorResult'); + +export const setEditorContentType = createAction<{ + /** The state of the editor */ + contentType: 'dsl' | 'json' | undefined; +}>('setEditorContentType'); + +export const setJSONEditorValue = createAction<{ + /** The state of the editor */ + value: Flow; +}>('setJSONEditorValue'); + +export const updateAndCompileDSLFlow = createAsyncThunk< + void, + { + /** Other external modules to include */ + additionalModules?: Record; + } +>('editor/dsl/compile', async (context, thunkAPI) => { + const content = (thunkAPI.getState() as any).editor.dsl?.value; + + if (!content) { + throw new Error('No content to compile'); + } + + try { + const transpiledResult = await execute(content, { + additionalModules: context.additionalModules, + }); + + if (transpiledResult) { + const compiled = await compiler.serialize(transpiledResult.default); + + if (compiled.contentType === 'flow') { + thunkAPI.dispatch( + setCompiledEditorResult({ result: compiled.value as any }) + ); + } + } + } catch (e: any) { + thunkAPI.dispatch( + setCompiledEditorResult({ + errors: { + transpileErrors: [ + { + message: e.message, + }, + ], + }, + }) + ); + } +}); + +export const setPlatform = createAction<{ + /** The platform to render on */ + platform: RenderTarget['platform']; +}>('@@player/platform/set'); + +export const unsetPlatform = createAction('@@player/platform/unset'); + +const platformReducer = createReducer<{ + /** The platform to render on */ + platform?: RenderTarget['platform']; +}>( + { + platform: 'web', + }, + (builder) => { + builder.addCase(setPlatform, (state, action) => { + state.platform = action.payload.platform; + }); + + builder.addCase(unsetPlatform, (state) => { + state.platform = undefined; + }); + } +); + +export type CompilationErrorType = { + /** Errors running esbuild */ + transpileErrors?: Array<{ + /** The error message */ + message: string; + }>; + + /** Errors converting the JS into JSON */ + compileErrors?: Array<{ + /** The error message */ + message: string; + }>; +}; + +export const setCompilationErrors = createAction( + 'setCompilationErrors' +); + +const flowEditorReducer = createReducer<{ + /** The primary content type for the editor */ + contentType?: 'json' | 'dsl'; + + /** The state of the JSON portion of the editor */ + json?: + | { + /** The state of the editor */ + state: 'loading' | 'error' | 'initial'; + } + | { + /** The state of the editor */ + state: 'loaded'; + /** The value of the editor */ + value: Flow; + }; + + /** The state of the DSL portion of the editor */ + dsl?: + | { + /** The state of the editor */ + state: 'loading' | 'error' | 'initial'; + } + | { + /** The state of the editor */ + state: 'loaded'; + /** The value of the editor */ + value: string; + /** Set if this value needs to be converted into JSON */ + needsCompile: boolean; + + /** Problems with compilation */ + compilationErrors?: CompilationErrorType; + }; +}>( + { + json: { state: 'initial' }, + dsl: { state: 'initial' }, + contentType: undefined, + }, + (builder) => { + builder.addCase(setEditorContentType, (state, action) => { + state.contentType = action.payload.contentType; + + if (action.payload.contentType === 'json') { + state.dsl = { state: 'initial' }; + } + }); + + builder.addCase(setDSLEditorValue, (state, action) => { + state.dsl = { + state: 'loaded', + value: action.payload.value, + needsCompile: true, + compilationErrors: undefined, + }; + }); + + builder.addCase(setCompilationErrors, (state, action) => { + if (state.dsl?.state === 'loaded') { + state.dsl.compilationErrors = action.payload; + } + }); + + builder.addCase(updateAndCompileDSLFlow.pending, (state) => { + state.json = { state: 'loading' }; + }); + + builder.addCase(resetEditor, (state) => { + state.json = { state: 'initial' }; + state.dsl = { state: 'initial' }; + }); + + builder.addCase(updateAndCompileDSLFlow.rejected, (state) => { + state.dsl = { state: 'error' }; + }); + + builder.addCase(setCompiledEditorResult, (state, action) => { + if (state.dsl?.state === 'loaded') { + state.dsl.needsCompile = false; + state.dsl.compilationErrors = action.payload.errors; + } + + if (action.payload.result) { + state.json = { state: 'loaded', value: action.payload.result }; + } + }); + + builder.addCase(setJSONEditorValue, (state, action) => { + state.json = { state: 'loaded', value: action.payload.value }; + }); + } +); + +export const addEvents = createAction>('@@player/events/add'); +export const clearEvents = createAction('@@player/events/clear'); + +const eventsReducer = createReducer>([], (builder) => { + builder.addCase(addEvents, (state, action) => { + state.push(...action.payload); + }); + + builder.addCase(clearEvents, () => { + return []; + }); +}); + +const STATE_SYNC_CHANNEL_NAME = (() => { + if (sessionStorage.getItem('player:channel')) { + return sessionStorage.getItem('player:channel') as string; + } + + const channel = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + sessionStorage.setItem('player:channel', channel); + return channel; +})(); + +export const store = configureStore({ + reducer: { + editor: flowEditorReducer, + platform: platformReducer, + events: eventsReducer, + }, + middleware: (getDefaultMiddleware) => { + return [ + ...getDefaultMiddleware(), + createStateSyncMiddleware({ + channel: STATE_SYNC_CHANNEL_NAME, + blacklist: [ + 'editor/dsl/compile/pending', + 'editor/dsl/compile/fulfilled', + 'editor/dsl/compile/rejected', + ], + }), + ]; + }, + devTools: true, +}); + +initStateWithPrevTab(store); + +/** A State provider with the store pre-added */ +export const StateProvider = (props: React.PropsWithChildren) => { + return ; +}; + +/** Get the value of the JSON editor */ +export const useJSONEditorValue = () => { + return useSelector((state: ReturnType) => { + return state.editor.json; + }); +}; + +/** Get the value of the DSL editor */ +export const useDSLEditorValue = () => { + const dslEditorValue = useSelector((state: StateType) => { + return state.editor.dsl; + }); + + return dslEditorValue; +}; + +/** Grab the current editor type */ +export const useContentKind = (contentTypeToSet?: 'json' | 'dsl') => { + const dispatch = useDispatch(); + + const contentType = useSelector((state: StateType) => { + return state.editor.contentType; + }); + + React.useEffect(() => { + if (contentTypeToSet && contentTypeToSet !== contentType) { + dispatch( + setEditorContentType({ + contentType: contentTypeToSet, + }) + ); + } + }, [contentType, contentTypeToSet, dispatch]); + + // React.useEffect(() => { + // if (contentTypeToSet) { + // return () => { + // dispatch( + // setEditorContentType({ + // contentType: undefined, + // }) + // ); + // }; + // } + // }, [contentTypeToSet, dispatch]); + + return contentType; +}; + +/** A hook to handle initializing and updating the JSON value of the flow from the DSL content */ +export const useCompiledEditorValue = ( + initialValue: string, + options?: { + /** Things to add to the compilation */ + additionalModules?: Record; + } +) => { + useContentKind('dsl'); + + const dispatch = useDispatch(); + + const dslEditorValue = useSelector((s: StateType) => { + return s.editor.dsl; + }); + + /** Fire off the initial set to reset it on story change */ + React.useEffect(() => { + store.dispatch(setDSLEditorValue({ value: initialValue })); + }, []); + + React.useEffect(() => { + if (dslEditorValue?.state === 'initial') { + dispatch(setDSLEditorValue({ value: initialValue })); + } + }, [dslEditorValue, initialValue]); + + React.useEffect(() => { + if (dslEditorValue?.state === 'loaded' && dslEditorValue.needsCompile) { + dispatch( + updateAndCompileDSLFlow({ + additionalModules: options?.additionalModules, + }) + ); + } + }, [dslEditorValue, options?.additionalModules]); + + return dslEditorValue; +}; + +/** A hook to handle initializing and updating the JSON value of the flow editor (for use when _not_ using DSL content) */ +export const useInitialJsonEditorValue = (initialValue: Flow) => { + const dispatch = useDispatch(); + useContentKind('json'); + + const jsonEditorValue = useSelector((s: StateType) => { + return s.editor.json; + }); + + /** Fire off the initial set to reset it on story change */ + React.useEffect(() => { + store.dispatch(setJSONEditorValue({ value: initialValue })); + }, []); + + React.useEffect(() => { + if (jsonEditorValue?.state === 'initial') { + dispatch(setJSONEditorValue({ value: initialValue })); + } + }, [jsonEditorValue, initialValue]); + + return jsonEditorValue; +}; + +export type StateType = ReturnType; diff --git a/tools/storybook/src/state/hooks.ts b/tools/storybook/src/state/hooks.ts deleted file mode 100644 index eb0182b32..000000000 --- a/tools/storybook/src/state/hooks.ts +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react'; -import type { Channel } from '@storybook/channels'; -import type { Flow } from '@player-ui/react'; -import type { PlayerFlowMetrics } from '@player-ui/metrics-plugin-react'; -import type { EventType } from './events'; -import type { RenderTarget } from '../types'; - -export interface StateActions { - addEvents(events: Array): void; - clearEvents(): void; - setFlow(flow: Flow): void; - setMetrics(metrics: PlayerFlowMetrics): void; - resetFlow(): void; - setPlatform(platform: RenderTarget['platform']): void; -} - -interface BasePublishedEvent { - /** The base event type */ - type: T; -} - -type EventClearType = BasePublishedEvent<'@@player/event/clear'>; - -interface EventAddType extends BasePublishedEvent<'@@player/event/add'> { - /** The events to append */ - events: Array; -} - -interface FlowSetType extends BasePublishedEvent<'@@player/flow/set'> { - /** the flow to use */ - flow: Flow; -} - -type FlowResetType = BasePublishedEvent<'@@player/flow/reset'>; - -interface MetricsSetEventType - extends BasePublishedEvent<'@@player/metrics/set'> { - /** the metrics data */ - metrics: PlayerFlowMetrics; -} - -export interface PlatformSetType - extends BasePublishedEvent<'@@player/platform/set'> { - /** The platform to render on */ - platform: RenderTarget['platform']; -} - -export type PlayerEventType = - | EventClearType - | EventAddType - | FlowSetType - | MetricsSetEventType - | FlowResetType - | PlatformSetType; - -/** Subscribe to player events in storybook */ -export function subscribe( - chan: Channel, - eventName: T['type'], - callback: (evt: T) => void -): () => void { - /** The handler to call */ - const handler = (payload: string) => { - callback(JSON.parse(payload)); - }; - - chan.addListener(eventName, handler); - - return () => { - chan.removeListener(eventName, handler); - }; -} - -/** publish an event to storybook */ -export function publish(chan: Channel, event: PlayerEventType) { - chan.emit(event.type, JSON.stringify(event)); -} - -/** wrapper to emit events */ -export function useStateActions(chan: Channel): StateActions { - return React.useMemo( - () => ({ - addEvents: (events) => { - publish(chan, { - type: '@@player/event/add', - events, - }); - }, - setMetrics: (metrics) => { - publish(chan, { - type: '@@player/metrics/set', - metrics, - }); - }, - clearEvents: () => { - publish(chan, { - type: '@@player/event/clear', - }); - }, - setFlow: (flow) => { - publish(chan, { - type: '@@player/flow/set', - flow, - }); - }, - resetFlow: () => { - publish(chan, { type: '@@player/flow/reset' }); - }, - setPlatform: (platform) => { - publish(chan, { type: '@@player/platform/set', platform }); - }, - }), - [chan] - ); -} - -/** react hook to subscribe to events */ -export function useEventState(chan: Channel) { - const [events, setEvents] = React.useState>([]); - - React.useEffect(() => { - const unsubAdd = subscribe( - chan, - '@@player/event/add', - (evt) => setEvents((old) => [...old, ...evt.events]) - ); - - const unsubClear = subscribe(chan, '@@player/event/clear', () => { - setEvents([]); - }); - - return () => { - unsubAdd(); - unsubClear(); - }; - }, [chan]); - - return events; -} - -/** hook to subscribe to flow events */ -export function useFlowState(chan: Channel) { - const [flow, setFlow] = React.useState(); - - React.useEffect(() => { - return subscribe(chan, '@@player/flow/set', (evt) => { - setFlow(evt.flow); - }); - }, [chan]); - - return flow; -} diff --git a/tools/storybook/src/state/index.ts b/tools/storybook/src/state/index.ts index 2e88b5f83..7981d6b64 100644 --- a/tools/storybook/src/state/index.ts +++ b/tools/storybook/src/state/index.ts @@ -1,2 +1 @@ export * from './events'; -export * from './hooks'; diff --git a/tools/storybook/src/types.ts b/tools/storybook/src/types.ts index 8351d83b0..5bc6e4db3 100644 --- a/tools/storybook/src/types.ts +++ b/tools/storybook/src/types.ts @@ -2,6 +2,12 @@ import type { ReactPlayerPlugin } from '@player-ui/react'; import type { AppetizeVersions } from './player/Appetize'; export interface PlayerParametersType { + /** Options for the dsl editor */ + dslEditor?: { + /** Enable more imports */ + additionalModules?: Record; + }; + /** plugins to use for any loaded player */ reactPlayerPlugins?: Array; diff --git a/yarn.lock b/yarn.lock index e3d16cf2b..fa7abda7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1405,6 +1405,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.6.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@~7.5.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" @@ -2685,20 +2692,19 @@ resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@monaco-editor/loader@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.2.0.tgz#373fad69973384624e3d9b60eefd786461a76acd" - integrity sha512-cJVCG/T/KxXgzYnjKqyAgsKDbH9mGLjcXxN6AmwumBwa2rVFkwvGcUj1RJtD0ko4XqLqJxwqsN/Z/KURB5f1OQ== +"@monaco-editor/loader@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558" + integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg== dependencies: state-local "^1.0.6" -"@monaco-editor/react@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.3.1.tgz#d65bcbf174c39b6d4e7fec43d0cddda82b70a12a" - integrity sha512-f+0BK1PP/W5I50hHHmwf11+Ea92E5H1VZXs+wvKplWUWOfyMa1VVwqkJrXjRvbcqHL+XdIGYWhWNdi4McEvnZg== +"@monaco-editor/react@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119" + integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw== dependencies: - "@monaco-editor/loader" "^1.2.0" - prop-types "^15.7.2" + "@monaco-editor/loader" "^1.4.0" "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -3117,10 +3123,10 @@ dependencies: "@octokit/openapi-types" "^12.7.0" -"@player-tools/cli@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/cli/-/cli-0.3.0.tgz#eff2389fac4a999ea592216a30c8ecfab4a05adb" - integrity sha512-GIT09FxiBoraDnxqNO1h2UcNQ98jbMmC/uoJiVja9n95IPHJQoZx4ajC6JeyjkGYNJm/nYQiFse0umtOntLBmg== +"@player-tools/cli@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/cli/-/cli-0.4.0-next.3.tgz#60fea43ab70f93edb101bc9228d1ebee61f88b7d" + integrity sha512-g+2pFOU8WN9eouSz8Qc4JCvvlC48axnbj7mJfF3hvsU1cUZar2BYq2HvpHVuOUh74rqNYAyhqLSC+w/HpgX55A== dependencies: "@babel/core" "^7.15.5" "@babel/plugin-transform-react-jsx-source" "^7.18.6" @@ -3130,11 +3136,11 @@ "@babel/register" "^7.17.7" "@oclif/core" "1.9.0" "@oclif/plugin-legacy" "^1.2.7" - "@player-tools/dsl" "0.3.0" - "@player-tools/language-service" "0.3.0" - "@player-tools/xlr-converters" "0.3.0" - "@player-tools/xlr-sdk" "0.3.0" - "@player-tools/xlr-utils" "0.3.0" + "@player-tools/dsl" "0.4.0-next.3" + "@player-tools/json-language-service" "0.4.0-next.3" + "@player-tools/xlr-converters" "0.4.0-next.3" + "@player-tools/xlr-sdk" "0.4.0-next.3" + "@player-tools/xlr-utils" "0.4.0-next.3" "@types/babel__register" "^7.17.0" chalk "^4.0.1" cosmiconfig "^7.0.1" @@ -3149,15 +3155,15 @@ vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" -"@player-tools/dsl@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/dsl/-/dsl-0.3.0.tgz#837bbeff1223b97cd7ca67af3946659e62431b14" - integrity sha512-/YifyT6it9rKEjdK6Pw6cEnZeojPkYMyCLxIybmBv2I/JtbsB3Mxui8AHvk6YYBIF7f38mANXFrKM68wty8a7A== +"@player-tools/dsl@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/dsl/-/dsl-0.4.0-next.3.tgz#c136ac5f10abcb33e714fae507496cea5df1fe10" + integrity sha512-qjb9q1aYfxq/e9dOF7uLoqhyuNnCSj1kKcaNYLj7EFtrWZKopuFsL1IQcPvLGa4kF0nL5hfva6cQnyecIV41ZA== dependencies: "@babel/runtime" "7.15.4" - "@player-ui/types" "^0.2.0" + "@player-ui/player" "0.4.0-next.7" + "@player-ui/types" "0.4.0-next.7" "@types/mkdirp" "^1.0.2" - "@types/signale" "^1.4.2" chalk "^4.0.1" command-line-application "^0.10.1" dequal "^2.0.2" @@ -3168,21 +3174,20 @@ react-flatten-children "^1.1.2" react-json-reconciler "^2.0.0" react-merge-refs "^1.1.0" - signale "^1.4.0" source-map-js "^1.0.2" tapable-ts "^0.1.0" ts-node "^10.4.0" typescript "4.8.4" -"@player-tools/language-service@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/language-service/-/language-service-0.3.0.tgz#41df2166143a7f04ab9b65bee971153628ba1f77" - integrity sha512-NP5EEnMeVHTlTijKK1CrfipwQdKdggwrNjYMyy6+kIyfcE+ygQVeMABYhu7i2EGn0XZGCh9WfhzTi0LeEkE/nA== +"@player-tools/json-language-service@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/json-language-service/-/json-language-service-0.4.0-next.3.tgz#5f1611aaad586784850f1f5501b6d52cbfe4b69c" + integrity sha512-5ogbrMzluGC7ypj+Hib7pQD/flOFClptop2j78vAUYYdAKMOKRu+dQW47+raqt71XCMawnra+5H2zUv4gAy6Eg== dependencies: "@babel/runtime" "7.15.4" - "@player-tools/xlr" "0.3.0" - "@player-tools/xlr-sdk" "0.3.0" - "@player-tools/xlr-utils" "0.3.0" + "@player-tools/xlr" "0.4.0-next.3" + "@player-tools/xlr-sdk" "0.4.0-next.3" + "@player-tools/xlr-utils" "0.4.0-next.3" change-case "^4.1.1" cross-fetch "^3.0.5" detect-indent "^6.0.0" @@ -3193,49 +3198,80 @@ vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" -"@player-tools/xlr-converters@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/xlr-converters/-/xlr-converters-0.3.0.tgz#604453b4f58a55fc656bdbb394e327f5654edf47" - integrity sha512-S7YrQxSXfEofArolc10pcVR2ATlizuLYe3lUPWpsByN1uW/0fJt69gyELFu+ytM5zSFDn45tTtFJf0f3PuOs4g== +"@player-tools/xlr-converters@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/xlr-converters/-/xlr-converters-0.4.0-next.3.tgz#43bbe3a0e7c6bb4c87a0267b992c8dd8925cd2ff" + integrity sha512-R3CRVib48U55zWjW3OA5WdAMXx8WtaKlQCY0hjVAxf0yOm+KDTHpLlV0/I9upAyqQZMWhAz9wublzBqXEWX5Xw== dependencies: "@babel/runtime" "7.15.4" - "@player-tools/xlr" "0.3.0" - "@player-tools/xlr-utils" "0.3.0" + "@player-tools/xlr" "0.4.0-next.3" + "@player-tools/xlr-utils" "0.4.0-next.3" -"@player-tools/xlr-sdk@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/xlr-sdk/-/xlr-sdk-0.3.0.tgz#87daf19cd7167a0f410a1d06580c1a44b8f1b52b" - integrity sha512-pP9EZwjXz90nRPg6G9s708daUcOc33UoXb8ZcO1P0JUgZ0OjX6Gr2O8PdmPclWsqwkJmS8KDNiqmUnLFa5kVYw== +"@player-tools/xlr-sdk@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/xlr-sdk/-/xlr-sdk-0.4.0-next.3.tgz#815ad4a7210c4286b3b89be4ed78d6fc492b3ecb" + integrity sha512-fN/LSaaL+GOd8LF4VXLke5Zf0fpvhMUbBoMVmbe8Tynk4zibcoGKeFnbEwj2DU2HxUq5/9nuvCfKl01/BOx9Dg== dependencies: "@babel/runtime" "7.15.4" - "@player-tools/xlr" "0.3.0" - "@player-tools/xlr-converters" "0.3.0" - "@player-tools/xlr-utils" "0.3.0" + "@player-tools/xlr" "0.4.0-next.3" + "@player-tools/xlr-converters" "0.4.0-next.3" + "@player-tools/xlr-utils" "0.4.0-next.3" "@types/fs-extra" "^9.0.13" "@types/node" "^16.11.12" fs-extra "^10.0.0" jsonc-parser "^2.3.1" -"@player-tools/xlr-utils@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/xlr-utils/-/xlr-utils-0.3.0.tgz#30dd71027ca4158970e1eb6173c2e365e25f78f5" - integrity sha512-6ZmV0goyOo5CGQY+zM/in6TFdstL8NT3mR7rcgCPyjfJFyKAKyq7tDOuo7PS/0r2zGVxT6N+0uHcZ+QEyfK/Gw== +"@player-tools/xlr-utils@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/xlr-utils/-/xlr-utils-0.4.0-next.3.tgz#c6d33912b4ef4e306805de3f74ff016d9bedf035" + integrity sha512-lsLdb/gXdipjPKWjcbNXdoX34fAu0AlqnqhsXm+z2aTS+VO+TK1iuwkRbxz2avK00A1vB1M7r4qid/NehxdJUA== dependencies: "@babel/runtime" "7.15.4" - "@player-tools/xlr" "0.3.0" + "@player-tools/xlr" "0.4.0-next.3" "@typescript/vfs" "^1.4.0" -"@player-tools/xlr@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@player-tools/xlr/-/xlr-0.3.0.tgz#2daf58330abc93450ad227cd4873618857f32fa3" - integrity sha512-a8Ej/+LXPZaCPhHehwUpddlISFJ8o1ARVY6XFaiqmbHPxxMNkoCiRyVG+x/yQe6ozgCOLl6CY+BrREsCnQkeGw== +"@player-tools/xlr@0.4.0-next.3": + version "0.4.0-next.3" + resolved "https://registry.yarnpkg.com/@player-tools/xlr/-/xlr-0.4.0-next.3.tgz#ff9cb1604c6595dc3c99ec939c873e9b46a426eb" + integrity sha512-BoWOSyzEJHUzZpfnCPEHf9HXZGK0EG5OCU2n32ie4uGnO+smUyYstygXJVKeknlJfpMHPuE/2Qxk0NE35JehBw== dependencies: "@babel/runtime" "7.15.4" -"@player-ui/types@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@player-ui/types/-/types-0.2.0.tgz#72990b70621d65ceebf02da9f813481e8eb17692" - integrity sha512-Lj7XNWQ9bHeVQbPJQwObXhYJddvr5iEhFCivCdbREuqiuF7RhG6Sodn3nT88L8EQPr78n88I7oxD6G8fQ/iEjw== +"@player-ui/partial-match-registry@0.4.0-next.7": + version "0.4.0-next.7" + resolved "https://registry.yarnpkg.com/@player-ui/partial-match-registry/-/partial-match-registry-0.4.0-next.7.tgz#579710d4cee77c8a75dfda02b3c2f74652e56dfa" + integrity sha512-JCoIZvH+VPmrDJECtHxD1p6quzYAlvKCKwpcLTb/clIYIR5xXr0H5xqqkQi+32y9nZjIkj9p8Tw0qmQ/oLplvg== + dependencies: + "@babel/runtime" "7.15.4" + "@types/dlv" "^1.1.2" + dlv "^1.1.3" + sorted-array "^2.0.4" + +"@player-ui/player@0.4.0-next.7": + version "0.4.0-next.7" + resolved "https://registry.yarnpkg.com/@player-ui/player/-/player-0.4.0-next.7.tgz#d0e6152badd077474a2c015d00396c59f90de1a2" + integrity sha512-0SlssdS7uYL5XO0kKg5mt9LqTQ+2Zxb82Z4I4P7AE9FdCLjGDU6bpH6Sm0Odf9UK0kmwvael/vcEDihunzFPEw== + dependencies: + "@babel/runtime" "7.15.4" + "@player-ui/partial-match-registry" "0.4.0-next.7" + "@player-ui/types" "0.4.0-next.7" + "@types/nested-error-stacks" "^2.1.0" + "@types/parsimmon" "^1.10.0" + arr-flatten "^1.1.0" + dequal "^2.0.2" + ebnf "^1.9.0" + error-polyfill "^0.1.3" + nested-error-stacks "^2.1.1" + p-defer "^3.0.0" + parsimmon "^1.12.0" + queue-microtask "^1.2.3" + tapable-ts "^0.2.3" + timm "^1.6.2" + +"@player-ui/types@0.4.0-next.7": + version "0.4.0-next.7" + resolved "https://registry.yarnpkg.com/@player-ui/types/-/types-0.4.0-next.7.tgz#efe9daba706ca750b34680ca94a97d41bd3782c6" + integrity sha512-mU8+eXb9de2Fmhhp7K/lUXNqKDjiFEoPGBkGwWTO/dIaHPZBR6yyALBUELXRus7xBBJjm3++B/ErjmUFIGpLDw== dependencies: "@babel/runtime" "7.15.4" @@ -3339,6 +3375,16 @@ prop-types "^15.7.2" tslib "^2.1.0" +"@reduxjs/toolkit@^1.9.5": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.7.tgz#7fc07c0b0ebec52043f8cb43510cf346405f78a6" + integrity sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ== + dependencies: + immer "^9.0.21" + redux "^4.2.1" + redux-thunk "^2.4.2" + reselect "^4.1.8" + "@rollup/plugin-image@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-image/-/plugin-image-3.0.1.tgz#2beb720d1ac3e85e73c4dc5b261ae3c43f59bde3" @@ -4254,6 +4300,11 @@ resolve-from "^5.0.0" store2 "^2.12.0" +"@swc/wasm-web@^1.3.74": + version "1.3.92" + resolved "https://registry.yarnpkg.com/@swc/wasm-web/-/wasm-web-1.3.92.tgz#c26f6f7b65ba67babf6bcbd1bcd651cd3667b975" + integrity sha512-L+w2t2GJfcrQkh4I1/LpWoAQG1qkH2siFr88B7UX8DIqseo7uYC+AKvoxuGi8/4ZSTkyK7GsAXqJu4Ng6Y0KtA== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -4440,6 +4491,11 @@ dependencies: "@types/ms" "*" +"@types/deep-equal@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03" + integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg== + "@types/dlv@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@types/dlv/-/dlv-1.1.2.tgz#02d4fcc41c5f707753427867c64fdae543031fb9" @@ -4787,6 +4843,14 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/redux-state-sync@^3.1.5": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@types/redux-state-sync/-/redux-state-sync-3.1.8.tgz#38dc91751b436d3755b7ebbf120bebd6b2ab2fdc" + integrity sha512-7NlDSJgXPXprSsqOQ9OVW74p4U6HolNksYbBQS8oUWgtvkABPXQvhvsQI5LdiIO8tTaSGZFZDVi0ssUop4w2ng== + dependencies: + broadcast-channel "^2.1.8" + redux "^4.0.1" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -6175,6 +6239,11 @@ better-opn@^2.1.1: dependencies: open "^7.0.3" +big-integer@^1.6.16: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -6311,6 +6380,33 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" +broadcast-channel@^2.1.8: + version "2.3.4" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-2.3.4.tgz#cede8a9cded517c7273063582da71fa4f809ee2c" + integrity sha512-cx1/dSb6KZ9HW1VtlqM/HLPjrdyzkKoteVmUpLXEpra00mDQW/F9ieDkoavuZMoh9/hC/6OplGzCERsZBfz/Wg== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "2.6.3" + unload "2.2.0" + +broadcast-channel@^3.1.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" + integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.1.0" + js-sha3 "0.8.0" + microseconds "0.2.0" + nano-time "1.0.0" + oblivious-set "1.0.0" + rimraf "3.0.2" + unload "2.2.0" + brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -7827,6 +7923,18 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -7852,6 +7960,15 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-data-property@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -7859,6 +7976,15 @@ define-properties@^1.1.2, define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -7961,6 +8087,11 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== +detect-node@^2.0.4, detect-node@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + detect-port@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1" @@ -8512,6 +8643,11 @@ esbuild-sunos-64@0.13.15: resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== +esbuild-wasm@0.14.23: + version "0.14.23" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.14.23.tgz#b1e9fed66362ad9f82fcf897265ee005778b9fa2" + integrity sha512-w1qhGLvUaPXiigGWIEGcnMmN/FxQ6VDLnHQIOpf29Qh9z6x4qe4gmsQyUbFBW6UsWsw/E8OJDE0XRtiV/0siYQ== + esbuild-windows-32@0.13.15: version "0.13.15" resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" @@ -9727,6 +9863,11 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + fuse.js@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c" @@ -9780,6 +9921,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-monorepo-packages@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-monorepo-packages/-/get-monorepo-packages-1.2.0.tgz#3eee88d30b11a5f65955dec6ae331958b2a168e4" @@ -10082,6 +10233,13 @@ google-protobuf@^3.6.1: resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.20.1.tgz#1b255c2b59bcda7c399df46c65206aa3c7a0ce8b" integrity sha512-XMf1+O32FjYIV3CYu6Tuh5PNbfNEU5Xu22X+Xkdb/DUexFlCzhvv7d5Iirm4AOwn8lv4al1YvIhzGrg2j9Zfzw== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -10153,11 +10311,28 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -10669,6 +10844,11 @@ image-size@1.0.0: dependencies: queue "6.0.2" +immer@^9.0.21: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + import-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" @@ -11180,7 +11360,7 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.1.2, is-regex@^1.1.4: +is-regex@^1.0.4, is-regex@^1.1.2, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -11906,6 +12086,11 @@ joycon@^3.0.1: resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -13071,6 +13256,16 @@ micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha512-yF2K4aHXKxO4OGhW7Ek2KLgKEAFbSblBLKlF6KzwQUhjK7+uAzatRr6fZ82bftdnuDQrkBHAJp5s8quj1ME3wA== + +microseconds@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" + integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -13309,6 +13504,13 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== + dependencies: + big-integer "^1.6.16" + nanoid@^3.1.23, nanoid@^3.1.30: version "3.1.30" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" @@ -13770,6 +13972,11 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== +oblivious-set@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" + integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -15548,6 +15755,18 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +redux-state-sync@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/redux-state-sync/-/redux-state-sync-3.1.4.tgz#b3a0a92a0c26d05b798c3e39e0ef215031a41323" + integrity sha512-nhJBzaXVXPXvUhQJ7m0LdoXBnrcw+cTYQ8bzW9DeJKdq6UNYynXwQWAlVUvsbT/hDV+vB6BC4DMLXkUVGpF2yQ== + dependencies: + broadcast-channel "^3.1.0" + +redux-thunk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" + integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== + redux@^4.0.0, redux@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" @@ -15555,6 +15774,13 @@ redux@^4.0.0, redux@^4.1.2: dependencies: "@babel/runtime" "^7.9.2" +redux@^4.0.1, redux@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + refractor@^3.1.0, refractor@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.5.0.tgz#334586f352dda4beaf354099b48c2d18e0819aec" @@ -15586,6 +15812,11 @@ regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4, regenerator-runtime@^0 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -15601,6 +15832,15 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp.prototype.flags@^1.2.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + regexp.prototype.flags@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" @@ -15940,6 +16180,11 @@ requireindex@^1.2.0, requireindex@~1.2.0: resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== +reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -16092,24 +16337,24 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== +rimraf@2.6.3, rimraf@~2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -rimraf@~2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== +rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" @@ -16449,6 +16694,15 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -17721,6 +17975,11 @@ ts-loader@8.2.0: micromatch "^4.0.0" semver "^7.3.4" +ts-nested-error@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ts-nested-error/-/ts-nested-error-1.2.1.tgz#88c8b53fa5bd5e57ec79deff5b8ed2dd865dbdc4" + integrity sha512-hd5aYe8XfpWSCoh8vkV+JJmFY22Q2WtUIQIWEM3dYVKnEwMwyiRbxir/kRlTbZdGhoOeKqZ1ammPR/eiS7Tdgg== + ts-node@^10.4.0: version "10.4.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" @@ -18203,6 +18462,14 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"