mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Local actions (#57451)
* feat: 🎸 create UiActionsService * feat: 🎸 add UiActionsServvice.fork() method * feat: 🎸 instantiate UiActionsService in plugin * feat: 🎸 add UiActionsService.registerTrigger(), remove old * feat: 🎸 move attach/detachAction() methods to UiActionsService * refactor: 💡 move remaining actions API to UiActionsService * chore: 🤖 clean up /trigger folder * test: 💍 move registry tests into UiActiosnService tests * fix: 🐛 fix TypeScript typecheck errors * test: 💍 add .fork() trigger tests * feat: 🎸 remove actionIds from ui_actions Trigger interface * fix: 🐛 remove usage of actionIds * fix: 🐛 attach hello world action to trigger in plugin lifecycle * feat: 🎸 fork also trigger to action attachments * fix: 🐛 clear mapping registry in .clear(), improve type
This commit is contained in:
parent
9388ff7b43
commit
ca5e25c139
36 changed files with 764 additions and 734 deletions
|
@ -18,11 +18,9 @@
|
|||
*/
|
||||
|
||||
import { Trigger } from '../../../src/plugins/ui_actions/public';
|
||||
import { HELLO_WORLD_ACTION_TYPE } from './hello_world_action';
|
||||
|
||||
export const HELLO_WORLD_TRIGGER_ID = 'HELLO_WORLD_TRIGGER_ID';
|
||||
|
||||
export const helloWorldTrigger: Trigger = {
|
||||
id: HELLO_WORLD_TRIGGER_ID,
|
||||
actionIds: [HELLO_WORLD_ACTION_TYPE],
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
||||
import { createHelloWorldAction } from './hello_world_action';
|
||||
import { createHelloWorldAction, HELLO_WORLD_ACTION_TYPE } from './hello_world_action';
|
||||
import { helloWorldTrigger } from './hello_world_trigger';
|
||||
|
||||
interface UiActionExamplesSetupDependencies {
|
||||
|
@ -33,8 +33,9 @@ interface UiActionExamplesStartDependencies {
|
|||
export class UiActionExamplesPlugin
|
||||
implements
|
||||
Plugin<void, void, UiActionExamplesSetupDependencies, UiActionExamplesStartDependencies> {
|
||||
public setup(core: CoreSetup, deps: UiActionExamplesSetupDependencies) {
|
||||
deps.uiActions.registerTrigger(helloWorldTrigger);
|
||||
public setup(core: CoreSetup, { uiActions }: UiActionExamplesSetupDependencies) {
|
||||
uiActions.registerTrigger(helloWorldTrigger);
|
||||
uiActions.attachAction(helloWorldTrigger.id, HELLO_WORLD_ACTION_TYPE);
|
||||
}
|
||||
|
||||
public start(coreStart: CoreStart, deps: UiActionExamplesStartDependencies) {
|
||||
|
|
|
@ -56,15 +56,12 @@ export class UiActionsExplorerPlugin implements Plugin<void, void, {}, StartDeps
|
|||
public setup(core: CoreSetup<{ uiActions: UiActionsStart }>, deps: SetupDeps) {
|
||||
deps.uiActions.registerTrigger({
|
||||
id: COUNTRY_TRIGGER,
|
||||
actionIds: [],
|
||||
});
|
||||
deps.uiActions.registerTrigger({
|
||||
id: PHONE_TRIGGER,
|
||||
actionIds: [],
|
||||
});
|
||||
deps.uiActions.registerTrigger({
|
||||
id: USER_TRIGGER,
|
||||
actionIds: [],
|
||||
});
|
||||
deps.uiActions.registerAction(lookUpWeatherAction);
|
||||
deps.uiActions.registerAction(viewInMapsAction);
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
|
||||
import { UiActionsSetup, Trigger } from 'src/plugins/ui_actions/public';
|
||||
import {
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
APPLY_FILTER_TRIGGER,
|
||||
|
@ -34,35 +34,30 @@ import {
|
|||
* @param api
|
||||
*/
|
||||
export const bootstrap = (uiActions: UiActionsSetup) => {
|
||||
const triggerContext = {
|
||||
const triggerContext: Trigger = {
|
||||
id: CONTEXT_MENU_TRIGGER,
|
||||
title: 'Context menu',
|
||||
description: 'Triggered on top-right corner context-menu select.',
|
||||
actionIds: [],
|
||||
};
|
||||
const triggerFilter = {
|
||||
const triggerFilter: Trigger = {
|
||||
id: APPLY_FILTER_TRIGGER,
|
||||
title: 'Filter click',
|
||||
description: 'Triggered when user applies filter to an embeddable.',
|
||||
actionIds: [],
|
||||
};
|
||||
const triggerBadge = {
|
||||
const triggerBadge: Trigger = {
|
||||
id: PANEL_BADGE_TRIGGER,
|
||||
title: 'Panel badges',
|
||||
description: 'Actions appear in title bar when an embeddable loads in a panel',
|
||||
actionIds: [],
|
||||
};
|
||||
const selectRangeTrigger = {
|
||||
const selectRangeTrigger: Trigger = {
|
||||
id: SELECT_RANGE_TRIGGER,
|
||||
title: 'Select range',
|
||||
description: 'Applies a range filter',
|
||||
actionIds: [],
|
||||
};
|
||||
const valueClickTrigger = {
|
||||
const valueClickTrigger: Trigger = {
|
||||
id: VALUE_CLICK_TRIGGER,
|
||||
title: 'Value clicked',
|
||||
description: 'Value was clicked',
|
||||
actionIds: [],
|
||||
};
|
||||
const actionApplyFilter = createFilterAction();
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { CONTEXT_MENU_TRIGGER } from '../triggers';
|
||||
import { Action, UiActionsApi } from 'src/plugins/ui_actions/public';
|
||||
import { Action, UiActionsStart } from 'src/plugins/ui_actions/public';
|
||||
import { Trigger, GetEmbeddableFactory, ViewMode } from '../types';
|
||||
import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables';
|
||||
import { EmbeddablePanel } from './embeddable_panel';
|
||||
|
@ -52,7 +52,6 @@ const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFac
|
|||
const editModeAction = createEditModeAction();
|
||||
const trigger: Trigger = {
|
||||
id: CONTEXT_MENU_TRIGGER,
|
||||
actionIds: [editModeAction.id],
|
||||
};
|
||||
const embeddableFactory = new ContactCardEmbeddableFactory(
|
||||
{} as any,
|
||||
|
@ -177,7 +176,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
|
|||
|
||||
const renderInEditModeAndOpenContextMenu = async (
|
||||
embeddableInputs: any,
|
||||
getActions: UiActionsApi['getTriggerCompatibleActions'] = () => Promise.resolve([])
|
||||
getActions: UiActionsStart['getTriggerCompatibleActions'] = () => Promise.resolve([])
|
||||
) => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ export interface Trigger {
|
|||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
actionIds: string[];
|
||||
}
|
||||
|
||||
export interface PropertySpec {
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
|
||||
import { CoreSetup, CoreStart } from 'src/core/public';
|
||||
// eslint-disable-next-line
|
||||
import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests';
|
||||
import { UiActionsApi } from 'src/plugins/ui_actions/public';
|
||||
import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks';
|
||||
import { UiActionsStart } from 'src/plugins/ui_actions/public';
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin';
|
||||
|
||||
|
@ -30,14 +30,14 @@ export interface TestPluginReturn {
|
|||
coreStart: CoreStart;
|
||||
setup: IEmbeddableSetup;
|
||||
doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart;
|
||||
uiActions: UiActionsApi;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export const testPlugin = (
|
||||
coreSetup: CoreSetup = coreMock.createSetup(),
|
||||
coreStart: CoreStart = coreMock.createStart()
|
||||
): TestPluginReturn => {
|
||||
const uiActions = uiActionsTestPlugin(coreSetup, coreStart);
|
||||
const uiActions = uiActionsPluginMock.createPlugin(coreSetup, coreStart);
|
||||
const initializerContext = {} as any;
|
||||
const plugin = new EmbeddablePublicPlugin(initializerContext);
|
||||
const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup });
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# UI Actions
|
||||
|
||||
An API for:
|
||||
- creating custom functionality (`actions`)
|
||||
- creating custom user interaction events (`triggers`)
|
||||
- attaching and detaching `actions` to `triggers`.
|
||||
- emitting `trigger` events
|
||||
- executing `actions` attached to a given `trigger`.
|
||||
- exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger.
|
||||
An API for:
|
||||
|
||||
- creating custom functionality (`actions`)
|
||||
- creating custom user interaction events (`triggers`)
|
||||
- attaching and detaching `actions` to `triggers`.
|
||||
- emitting `trigger` events
|
||||
- executing `actions` attached to a given `trigger`.
|
||||
- exposing a context menu for the user to choose the appropriate action when there are multiple actions attached to a single trigger.
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { Action } from './action';
|
||||
export { createAction } from './create_action';
|
||||
export * from './action';
|
||||
export * from './create_action';
|
||||
export * from './incompatible_action_error';
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
|
||||
export const registerAction: UiActionsApiPure['registerAction'] = ({ actions }) => action => {
|
||||
if (actions.has(action.id)) {
|
||||
throw new Error(`Action [action.id = ${action.id}] already registered.`);
|
||||
}
|
||||
|
||||
actions.set(action.id, action);
|
||||
};
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
UiActionsApi,
|
||||
UiActionsDependenciesInternal,
|
||||
UiActionsDependencies,
|
||||
UiActionsApiPure,
|
||||
} from './types';
|
||||
import { attachAction } from './triggers/attach_action';
|
||||
import { detachAction } from './triggers/detach_action';
|
||||
import { executeTriggerActions } from './triggers/execute_trigger_actions';
|
||||
import { getTrigger } from './triggers/get_trigger';
|
||||
import { getTriggerActions } from './triggers/get_trigger_actions';
|
||||
import { getTriggerCompatibleActions } from './triggers/get_trigger_compatible_actions';
|
||||
import { registerAction } from './actions/register_action';
|
||||
import { registerTrigger } from './triggers/register_trigger';
|
||||
|
||||
export const pureApi: UiActionsApiPure = {
|
||||
attachAction,
|
||||
detachAction,
|
||||
executeTriggerActions,
|
||||
getTrigger,
|
||||
getTriggerActions,
|
||||
getTriggerCompatibleActions,
|
||||
registerAction,
|
||||
registerTrigger,
|
||||
};
|
||||
|
||||
export const createApi = (deps: UiActionsDependencies) => {
|
||||
const partialApi: Partial<UiActionsApi> = {};
|
||||
const depsInternal: UiActionsDependenciesInternal = { ...deps, api: partialApi };
|
||||
for (const [key, fn] of Object.entries(pureApi)) {
|
||||
(partialApi as any)[key] = fn(depsInternal);
|
||||
}
|
||||
Object.freeze(partialApi);
|
||||
const api = partialApi as UiActionsApi;
|
||||
return { api, depsInternal };
|
||||
};
|
|
@ -19,19 +19,30 @@
|
|||
|
||||
import { PluginInitializerContext } from '../../../core/public';
|
||||
import { UiActionsPlugin } from './plugin';
|
||||
import { UiActionsService } from './service';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new UiActionsPlugin(initializerContext);
|
||||
}
|
||||
|
||||
export { UiActionsSetup, UiActionsStart } from './plugin';
|
||||
export {
|
||||
Action,
|
||||
Trigger,
|
||||
UiActionsApi,
|
||||
GetActionsCompatibleWithTrigger,
|
||||
ExecuteTriggerActions,
|
||||
} from './types';
|
||||
export { createAction } from './actions';
|
||||
export { UiActionsServiceParams, UiActionsService } from './service';
|
||||
export { Action, createAction, IncompatibleActionError } from './actions';
|
||||
export { buildContextMenuForActions } from './context_menu';
|
||||
export { IncompatibleActionError } from './triggers';
|
||||
export { Trigger } from './triggers';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Use `UiActionsStart['getTriggerCompatibleActions']` or
|
||||
* `UiActionsService['getTriggerCompatibleActions']` instead.
|
||||
*/
|
||||
export type GetActionsCompatibleWithTrigger = UiActionsService['getTriggerCompatibleActions'];
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Use `UiActionsStart['executeTriggerActions']` or
|
||||
* `UiActionsService['executeTriggerActions']` instead.
|
||||
*/
|
||||
export type ExecuteTriggerActions = UiActionsService['executeTriggerActions'];
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart } from 'src/core/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '.';
|
||||
import { plugin as pluginInitializer } from '.';
|
||||
// eslint-disable-next-line
|
||||
import { coreMock } from '../../../core/public/mocks';
|
||||
|
||||
export type Setup = jest.Mocked<UiActionsSetup>;
|
||||
|
@ -45,17 +45,20 @@ const createStartContract = (): Start => {
|
|||
getTrigger: jest.fn(),
|
||||
getTriggerActions: jest.fn((id: string) => []),
|
||||
getTriggerCompatibleActions: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
fork: jest.fn(),
|
||||
};
|
||||
|
||||
return startContract;
|
||||
};
|
||||
|
||||
const createPlugin = async () => {
|
||||
const createPlugin = (
|
||||
coreSetup: CoreSetup = coreMock.createSetup(),
|
||||
coreStart: CoreStart = coreMock.createStart()
|
||||
) => {
|
||||
const pluginInitializerContext = coreMock.createPluginInitializerContext();
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
const plugin = pluginInitializer(pluginInitializerContext);
|
||||
const setup = await plugin.setup(coreSetup);
|
||||
const setup = plugin.setup(coreSetup);
|
||||
|
||||
return {
|
||||
pluginInitializerContext,
|
||||
|
@ -63,7 +66,7 @@ const createPlugin = async () => {
|
|||
coreStart,
|
||||
plugin,
|
||||
setup,
|
||||
doStart: async () => await plugin.start(coreStart),
|
||||
doStart: (anotherCoreStart: CoreStart = coreStart) => plugin.start(anotherCoreStart),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -17,43 +17,30 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public';
|
||||
import { UiActionsApi, ActionRegistry, TriggerRegistry } from './types';
|
||||
import { createApi } from './api';
|
||||
import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public';
|
||||
import { UiActionsService } from './service';
|
||||
|
||||
export interface UiActionsSetup {
|
||||
attachAction: UiActionsApi['attachAction'];
|
||||
detachAction: UiActionsApi['detachAction'];
|
||||
registerAction: UiActionsApi['registerAction'];
|
||||
registerTrigger: UiActionsApi['registerTrigger'];
|
||||
}
|
||||
export type UiActionsSetup = Pick<
|
||||
UiActionsService,
|
||||
'attachAction' | 'detachAction' | 'registerAction' | 'registerTrigger'
|
||||
>;
|
||||
|
||||
export type UiActionsStart = UiActionsApi;
|
||||
export type UiActionsStart = PublicMethodsOf<UiActionsService>;
|
||||
|
||||
export class UiActionsPlugin implements Plugin<UiActionsSetup, UiActionsStart> {
|
||||
private readonly triggers: TriggerRegistry = new Map();
|
||||
private readonly actions: ActionRegistry = new Map();
|
||||
private api!: UiActionsApi;
|
||||
private readonly service = new UiActionsService();
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.api = createApi({ triggers: this.triggers, actions: this.actions }).api;
|
||||
}
|
||||
constructor(initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup): UiActionsSetup {
|
||||
return {
|
||||
registerTrigger: this.api.registerTrigger,
|
||||
registerAction: this.api.registerAction,
|
||||
attachAction: this.api.attachAction,
|
||||
detachAction: this.api.detachAction,
|
||||
};
|
||||
return this.service;
|
||||
}
|
||||
|
||||
public start(core: CoreStart): UiActionsStart {
|
||||
return this.api;
|
||||
return this.service;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.actions.clear();
|
||||
this.triggers.clear();
|
||||
this.service.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsDependencies } from '../types';
|
||||
|
||||
export const createDeps = (): UiActionsDependencies => {
|
||||
const deps: UiActionsDependencies = {
|
||||
actions: new Map<any, any>(),
|
||||
triggers: new Map<any, any>(),
|
||||
};
|
||||
return deps;
|
||||
};
|
||||
export * from './ui_actions_service';
|
465
src/plugins/ui_actions/public/service/ui_actions_service.test.ts
Normal file
465
src/plugins/ui_actions/public/service/ui_actions_service.test.ts
Normal file
|
@ -0,0 +1,465 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsService } from './ui_actions_service';
|
||||
import { Action } from '../actions';
|
||||
import { createRestrictedAction, createHelloWorldAction } from '../tests/test_samples';
|
||||
import { ActionRegistry, TriggerRegistry } from '../types';
|
||||
import { Trigger } from '../triggers';
|
||||
|
||||
const testAction1: Action = {
|
||||
id: 'action1',
|
||||
order: 1,
|
||||
type: 'type1',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test1',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
};
|
||||
|
||||
const testAction2: Action = {
|
||||
id: 'action2',
|
||||
order: 2,
|
||||
type: 'type2',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test2',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
};
|
||||
|
||||
describe('UiActionsService', () => {
|
||||
test('can instantiate', () => {
|
||||
new UiActionsService();
|
||||
});
|
||||
|
||||
describe('.registerTrigger()', () => {
|
||||
test('can register a trigger', () => {
|
||||
const service = new UiActionsService();
|
||||
service.registerTrigger({
|
||||
id: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getTrigger()', () => {
|
||||
test('can get Trigger from registry', () => {
|
||||
const service = new UiActionsService();
|
||||
service.registerTrigger({
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
|
||||
const trigger = service.getTrigger('bar');
|
||||
|
||||
expect(trigger).toEqual({
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws if trigger does not exist', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
expect(() => service.getTrigger('foo')).toThrowError(
|
||||
'Trigger [triggerId = foo] does not exist.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.registerAction()', () => {
|
||||
test('can register an action', () => {
|
||||
const service = new UiActionsService();
|
||||
service.registerAction({
|
||||
id: 'test',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getTriggerActions()', () => {
|
||||
const action1: Action = {
|
||||
id: 'action1',
|
||||
order: 1,
|
||||
type: 'type1',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
};
|
||||
const action2: Action = {
|
||||
id: 'action2',
|
||||
order: 2,
|
||||
type: 'type2',
|
||||
execute: async () => {},
|
||||
getDisplayName: () => 'test',
|
||||
getIconType: () => '',
|
||||
isCompatible: async () => true,
|
||||
};
|
||||
|
||||
test('returns actions set on trigger', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
service.registerAction(action1);
|
||||
service.registerAction(action2);
|
||||
service.registerTrigger({
|
||||
description: 'foo',
|
||||
id: 'trigger',
|
||||
title: 'baz',
|
||||
});
|
||||
|
||||
const list0 = service.getTriggerActions('trigger');
|
||||
|
||||
expect(list0).toHaveLength(0);
|
||||
|
||||
service.attachAction('trigger', 'action1');
|
||||
const list1 = service.getTriggerActions('trigger');
|
||||
|
||||
expect(list1).toHaveLength(1);
|
||||
expect(list1).toEqual([action1]);
|
||||
|
||||
service.attachAction('trigger', 'action2');
|
||||
const list2 = service.getTriggerActions('trigger');
|
||||
|
||||
expect(list2).toHaveLength(2);
|
||||
expect(!!list2.find(({ id }: any) => id === 'action1')).toBe(true);
|
||||
expect(!!list2.find(({ id }: any) => id === 'action2')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getTriggerCompatibleActions()', () => {
|
||||
test('can register and get actions', async () => {
|
||||
const actions: ActionRegistry = new Map();
|
||||
const service = new UiActionsService({ actions });
|
||||
const helloWorldAction = createHelloWorldAction({} as any);
|
||||
const length = actions.size;
|
||||
|
||||
service.registerAction(helloWorldAction);
|
||||
|
||||
expect(actions.size - length).toBe(1);
|
||||
expect(actions.get(helloWorldAction.id)).toBe(helloWorldAction);
|
||||
});
|
||||
|
||||
test('getTriggerCompatibleActions returns attached actions', async () => {
|
||||
const service = new UiActionsService();
|
||||
const helloWorldAction = createHelloWorldAction({} as any);
|
||||
|
||||
service.registerAction(helloWorldAction);
|
||||
|
||||
const testTrigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
};
|
||||
service.registerTrigger(testTrigger);
|
||||
service.attachAction('MY-TRIGGER', helloWorldAction.id);
|
||||
|
||||
const compatibleActions = await service.getTriggerCompatibleActions('MY-TRIGGER', {});
|
||||
|
||||
expect(compatibleActions.length).toBe(1);
|
||||
expect(compatibleActions[0].id).toBe(helloWorldAction.id);
|
||||
});
|
||||
|
||||
test('filters out actions not applicable based on the context', async () => {
|
||||
const service = new UiActionsService();
|
||||
const restrictedAction = createRestrictedAction<{ accept: boolean }>(context => {
|
||||
return context.accept;
|
||||
});
|
||||
|
||||
service.registerAction(restrictedAction);
|
||||
|
||||
const testTrigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
};
|
||||
|
||||
service.registerTrigger(testTrigger);
|
||||
service.attachAction(testTrigger.id, restrictedAction.id);
|
||||
|
||||
const compatibleActions1 = await service.getTriggerCompatibleActions(testTrigger.id, {
|
||||
accept: true,
|
||||
});
|
||||
|
||||
expect(compatibleActions1.length).toBe(1);
|
||||
|
||||
const compatibleActions2 = await service.getTriggerCompatibleActions(testTrigger.id, {
|
||||
accept: false,
|
||||
});
|
||||
|
||||
expect(compatibleActions2.length).toBe(0);
|
||||
});
|
||||
|
||||
test(`throws an error with an invalid trigger ID`, async () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
await expect(service.getTriggerCompatibleActions('I do not exist', {})).rejects.toMatchObject(
|
||||
new Error('Trigger [triggerId = I do not exist] does not exist.')
|
||||
);
|
||||
});
|
||||
|
||||
test('returns empty list if trigger not attached to any action', async () => {
|
||||
const service = new UiActionsService();
|
||||
const testTrigger: Trigger = {
|
||||
id: '123',
|
||||
title: '123',
|
||||
};
|
||||
service.registerTrigger(testTrigger);
|
||||
|
||||
const actions = await service.getTriggerCompatibleActions(testTrigger.id, {});
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.fork()', () => {
|
||||
test('returns a new instance of the service', () => {
|
||||
const service1 = new UiActionsService();
|
||||
const service2 = service1.fork();
|
||||
|
||||
expect(service1).not.toBe(service2);
|
||||
expect(service2).toBeInstanceOf(UiActionsService);
|
||||
});
|
||||
|
||||
test('triggers registered in original service are available in original an forked services', () => {
|
||||
const service1 = new UiActionsService();
|
||||
service1.registerTrigger({
|
||||
id: 'foo',
|
||||
});
|
||||
const service2 = service1.fork();
|
||||
|
||||
const trigger1 = service1.getTrigger('foo');
|
||||
const trigger2 = service2.getTrigger('foo');
|
||||
|
||||
expect(trigger1.id).toBe('foo');
|
||||
expect(trigger2.id).toBe('foo');
|
||||
});
|
||||
|
||||
test('triggers registered in forked service are not available in original service', () => {
|
||||
const service1 = new UiActionsService();
|
||||
const service2 = service1.fork();
|
||||
|
||||
service2.registerTrigger({
|
||||
id: 'foo',
|
||||
});
|
||||
|
||||
expect(() => service1.getTrigger('foo')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Trigger [triggerId = foo] does not exist."`
|
||||
);
|
||||
|
||||
const trigger2 = service2.getTrigger('foo');
|
||||
expect(trigger2.id).toBe('foo');
|
||||
});
|
||||
|
||||
test('forked service preserves trigger-to-actions mapping', () => {
|
||||
const service1 = new UiActionsService();
|
||||
|
||||
service1.registerTrigger({
|
||||
id: 'foo',
|
||||
});
|
||||
service1.registerAction(testAction1);
|
||||
service1.attachAction('foo', testAction1.id);
|
||||
|
||||
const service2 = service1.fork();
|
||||
|
||||
const actions1 = service1.getTriggerActions('foo');
|
||||
const actions2 = service2.getTriggerActions('foo');
|
||||
|
||||
expect(actions1).toHaveLength(1);
|
||||
expect(actions2).toHaveLength(1);
|
||||
expect(actions1[0].id).toBe(testAction1.id);
|
||||
expect(actions2[0].id).toBe(testAction1.id);
|
||||
});
|
||||
|
||||
test('new attachments in fork do not appear in original service', () => {
|
||||
const service1 = new UiActionsService();
|
||||
|
||||
service1.registerTrigger({
|
||||
id: 'foo',
|
||||
});
|
||||
service1.registerAction(testAction1);
|
||||
service1.registerAction(testAction2);
|
||||
service1.attachAction('foo', testAction1.id);
|
||||
|
||||
const service2 = service1.fork();
|
||||
|
||||
expect(service1.getTriggerActions('foo')).toHaveLength(1);
|
||||
expect(service2.getTriggerActions('foo')).toHaveLength(1);
|
||||
|
||||
service2.attachAction('foo', testAction2.id);
|
||||
|
||||
expect(service1.getTriggerActions('foo')).toHaveLength(1);
|
||||
expect(service2.getTriggerActions('foo')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('new attachments in original service do not appear in fork', () => {
|
||||
const service1 = new UiActionsService();
|
||||
|
||||
service1.registerTrigger({
|
||||
id: 'foo',
|
||||
});
|
||||
service1.registerAction(testAction1);
|
||||
service1.registerAction(testAction2);
|
||||
service1.attachAction('foo', testAction1.id);
|
||||
|
||||
const service2 = service1.fork();
|
||||
|
||||
expect(service1.getTriggerActions('foo')).toHaveLength(1);
|
||||
expect(service2.getTriggerActions('foo')).toHaveLength(1);
|
||||
|
||||
service1.attachAction('foo', testAction2.id);
|
||||
|
||||
expect(service1.getTriggerActions('foo')).toHaveLength(2);
|
||||
expect(service2.getTriggerActions('foo')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registries', () => {
|
||||
const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
|
||||
|
||||
test('can register trigger', () => {
|
||||
const triggers: TriggerRegistry = new Map();
|
||||
const service = new UiActionsService({ triggers });
|
||||
|
||||
service.registerTrigger({
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
|
||||
expect(triggers.get('bar')).toEqual({
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
});
|
||||
|
||||
test('can register action', () => {
|
||||
const actions: ActionRegistry = new Map();
|
||||
const service = new UiActionsService({ actions });
|
||||
|
||||
service.registerAction({
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 13,
|
||||
} as any);
|
||||
|
||||
expect(actions.get(HELLO_WORLD_ACTION_ID)).toMatchObject({
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 13,
|
||||
});
|
||||
});
|
||||
|
||||
test('can attach an action to a trigger', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
};
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerTrigger(trigger);
|
||||
service.registerAction(action);
|
||||
service.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
|
||||
|
||||
const actions = service.getTriggerActions(trigger.id);
|
||||
|
||||
expect(actions.length).toBe(1);
|
||||
expect(actions[0].id).toBe(HELLO_WORLD_ACTION_ID);
|
||||
});
|
||||
|
||||
test('can detach an action to a trigger', () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
};
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerTrigger(trigger);
|
||||
service.registerAction(action);
|
||||
service.attachAction(trigger.id, HELLO_WORLD_ACTION_ID);
|
||||
service.detachAction(trigger.id, HELLO_WORLD_ACTION_ID);
|
||||
|
||||
const actions2 = service.getTriggerActions(trigger.id);
|
||||
expect(actions2).toEqual([]);
|
||||
});
|
||||
|
||||
test('detaching an invalid action from a trigger throws an error', async () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() => service.detachAction('i do not exist', HELLO_WORLD_ACTION_ID)).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for detaching action [actionId = HELLO_WORLD_ACTION_ID].'
|
||||
);
|
||||
});
|
||||
|
||||
test('attaching an invalid action to a trigger throws an error', async () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() => service.attachAction('i do not exist', HELLO_WORLD_ACTION_ID)).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = HELLO_WORLD_ACTION_ID].'
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot register another action with the same ID', async () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
service.registerAction(action);
|
||||
expect(() => service.registerAction(action)).toThrowError(
|
||||
'Action [action.id = HELLO_WORLD_ACTION_ID] already registered.'
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot register another trigger with the same ID', async () => {
|
||||
const service = new UiActionsService();
|
||||
|
||||
const trigger = { id: 'MY-TRIGGER' } as any;
|
||||
|
||||
service.registerTrigger(trigger);
|
||||
expect(() => service.registerTrigger(trigger)).toThrowError(
|
||||
'Trigger [trigger.id = MY-TRIGGER] already registered.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
194
src/plugins/ui_actions/public/service/ui_actions_service.ts
Normal file
194
src/plugins/ui_actions/public/service/ui_actions_service.ts
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { TriggerRegistry, ActionRegistry, TriggerToActionsRegistry } from '../types';
|
||||
import { Action } from '../actions';
|
||||
import { Trigger } from '../triggers/trigger';
|
||||
import { buildContextMenuForActions, openContextMenu } from '../context_menu';
|
||||
|
||||
export interface UiActionsServiceParams {
|
||||
readonly triggers?: TriggerRegistry;
|
||||
readonly actions?: ActionRegistry;
|
||||
|
||||
/**
|
||||
* A 1-to-N mapping from `Trigger` to zero or more `Action`.
|
||||
*/
|
||||
readonly triggerToActions?: TriggerToActionsRegistry;
|
||||
}
|
||||
|
||||
export class UiActionsService {
|
||||
protected readonly triggers: TriggerRegistry;
|
||||
protected readonly actions: ActionRegistry;
|
||||
protected readonly triggerToActions: TriggerToActionsRegistry;
|
||||
|
||||
constructor({
|
||||
triggers = new Map(),
|
||||
actions = new Map(),
|
||||
triggerToActions = new Map(),
|
||||
}: UiActionsServiceParams = {}) {
|
||||
this.triggers = triggers;
|
||||
this.actions = actions;
|
||||
this.triggerToActions = triggerToActions;
|
||||
}
|
||||
|
||||
public readonly registerTrigger = (trigger: Trigger) => {
|
||||
if (this.triggers.has(trigger.id)) {
|
||||
throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered.`);
|
||||
}
|
||||
|
||||
this.triggers.set(trigger.id, trigger);
|
||||
this.triggerToActions.set(trigger.id, []);
|
||||
};
|
||||
|
||||
public readonly getTrigger = (id: string) => {
|
||||
const trigger = this.triggers.get(id);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(`Trigger [triggerId = ${id}] does not exist.`);
|
||||
}
|
||||
|
||||
return trigger;
|
||||
};
|
||||
|
||||
public readonly registerAction = (action: Action) => {
|
||||
if (this.actions.has(action.id)) {
|
||||
throw new Error(`Action [action.id = ${action.id}] already registered.`);
|
||||
}
|
||||
|
||||
this.actions.set(action.id, action);
|
||||
};
|
||||
|
||||
public readonly attachAction = (triggerId: string, actionId: string): void => {
|
||||
const trigger = this.triggers.get(triggerId);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(
|
||||
`No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${actionId}].`
|
||||
);
|
||||
}
|
||||
|
||||
const actionIds = this.triggerToActions.get(triggerId);
|
||||
|
||||
if (!actionIds!.find(id => id === actionId)) {
|
||||
this.triggerToActions.set(triggerId, [...actionIds!, actionId]);
|
||||
}
|
||||
};
|
||||
|
||||
public readonly detachAction = (triggerId: string, actionId: string) => {
|
||||
const trigger = this.triggers.get(triggerId);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(
|
||||
`No trigger [triggerId = ${triggerId}] exists, for detaching action [actionId = ${actionId}].`
|
||||
);
|
||||
}
|
||||
|
||||
const actionIds = this.triggerToActions.get(triggerId);
|
||||
|
||||
this.triggerToActions.set(
|
||||
triggerId,
|
||||
actionIds!.filter(id => id !== actionId)
|
||||
);
|
||||
};
|
||||
|
||||
public readonly getTriggerActions = (triggerId: string) => {
|
||||
// This line checks if trigger exists, otherwise throws.
|
||||
this.getTrigger!(triggerId);
|
||||
|
||||
const actionIds = this.triggerToActions.get(triggerId);
|
||||
const actions = actionIds!
|
||||
.map(actionId => this.actions.get(actionId))
|
||||
.filter(Boolean) as Action[];
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
public readonly getTriggerCompatibleActions = async <C>(triggerId: string, context: C) => {
|
||||
const actions = this.getTriggerActions!(triggerId);
|
||||
const isCompatibles = await Promise.all(actions.map(action => action.isCompatible(context)));
|
||||
return actions.reduce<Action[]>(
|
||||
(acc, action, i) => (isCompatibles[i] ? [...acc, action] : acc),
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
private async executeSingleAction<A>(action: Action<A>, actionContext: A) {
|
||||
const href = action.getHref && action.getHref(actionContext);
|
||||
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
await action.execute(actionContext);
|
||||
}
|
||||
|
||||
private async executeMultipleActions<C>(actions: Action[], actionContext: C) {
|
||||
const panel = await buildContextMenuForActions({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu: () => session.close(),
|
||||
});
|
||||
const session = openContextMenu([panel]);
|
||||
}
|
||||
|
||||
public readonly executeTriggerActions = async <C>(triggerId: string, actionContext: C) => {
|
||||
const actions = await this.getTriggerCompatibleActions!(triggerId, actionContext);
|
||||
|
||||
if (!actions.length) {
|
||||
throw new Error(
|
||||
`No compatible actions found to execute for trigger [triggerId = ${triggerId}].`
|
||||
);
|
||||
}
|
||||
|
||||
if (actions.length === 1) {
|
||||
await this.executeSingleAction(actions[0], actionContext);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeMultipleActions(actions, actionContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all registered triggers and actions.
|
||||
*/
|
||||
public readonly clear = () => {
|
||||
this.actions.clear();
|
||||
this.triggers.clear();
|
||||
this.triggerToActions.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* "Fork" a separate instance of `UiActionsService` that inherits all existing
|
||||
* triggers and actions, but going forward all new triggers and actions added
|
||||
* to this instance of `UiActionsService` are only available within this instance.
|
||||
*/
|
||||
public readonly fork = (): UiActionsService => {
|
||||
const triggers: TriggerRegistry = new Map();
|
||||
const actions: ActionRegistry = new Map();
|
||||
const triggerToActions: TriggerToActionsRegistry = new Map();
|
||||
|
||||
for (const [key, value] of this.triggers.entries()) triggers.set(key, value);
|
||||
for (const [key, value] of this.actions.entries()) actions.set(key, value);
|
||||
for (const [key, value] of this.triggerToActions.entries())
|
||||
triggerToActions.set(key, [...value]);
|
||||
|
||||
return new UiActionsService({ triggers, actions, triggerToActions });
|
||||
};
|
||||
}
|
2
src/plugins/ui_actions/public/tests/README.md
Normal file
2
src/plugins/ui_actions/public/tests/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
This folder contains integration tests for the `ui_actions` plugin and
|
||||
`test_samples` that other plugins can use in their tests.
|
|
@ -19,7 +19,8 @@
|
|||
|
||||
import { Action, createAction } from '../actions';
|
||||
import { openContextMenu } from '../context_menu';
|
||||
import { UiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin';
|
||||
import { uiActionsPluginMock } from '../mocks';
|
||||
import { Trigger } from '../triggers';
|
||||
|
||||
jest.mock('../context_menu');
|
||||
|
||||
|
@ -37,14 +38,14 @@ function createTestAction<A>(id: string, checkCompatibility: (context: A) => boo
|
|||
});
|
||||
}
|
||||
|
||||
let uiActions: UiActionsTestPluginReturn;
|
||||
let uiActions: ReturnType<typeof uiActionsPluginMock.createPlugin>;
|
||||
const reset = () => {
|
||||
uiActions = uiActionsTestPlugin();
|
||||
uiActions = uiActionsPluginMock.createPlugin();
|
||||
|
||||
uiActions.setup.registerTrigger({
|
||||
id: CONTACT_USER_TRIGGER,
|
||||
actionIds: ['SEND_MESSAGE_ACTION'],
|
||||
});
|
||||
uiActions.setup.attachAction(CONTACT_USER_TRIGGER, 'SEND_MESSAGE_ACTION');
|
||||
|
||||
executeFn.mockReset();
|
||||
openContextMenuSpy.mockReset();
|
||||
|
@ -53,14 +54,15 @@ beforeEach(reset);
|
|||
|
||||
test('executes a single action mapped to a trigger', async () => {
|
||||
const { setup, doStart } = uiActions;
|
||||
const trigger = {
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: ['test1'],
|
||||
};
|
||||
const action = createTestAction('test1', () => true);
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.registerAction(action);
|
||||
setup.attachAction(trigger.id, 'test1');
|
||||
|
||||
const context = {};
|
||||
const start = doStart();
|
||||
|
@ -72,12 +74,13 @@ test('executes a single action mapped to a trigger', async () => {
|
|||
|
||||
test('throws an error if there are no compatible actions to execute', async () => {
|
||||
const { setup, doStart } = uiActions;
|
||||
const trigger = {
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: ['testaction'],
|
||||
};
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.attachAction(trigger.id, 'testaction');
|
||||
|
||||
const context = {};
|
||||
const start = doStart();
|
||||
|
@ -88,14 +91,15 @@ test('throws an error if there are no compatible actions to execute', async () =
|
|||
|
||||
test('does not execute an incompatible action', async () => {
|
||||
const { setup, doStart } = uiActions;
|
||||
const trigger = {
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: ['test1'],
|
||||
};
|
||||
const action = createTestAction<{ name: string }>('test1', ({ name }) => name === 'executeme');
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.registerAction(action);
|
||||
setup.attachAction(trigger.id, 'test1');
|
||||
|
||||
const start = doStart();
|
||||
const context = {
|
||||
|
@ -108,16 +112,18 @@ test('does not execute an incompatible action', async () => {
|
|||
|
||||
test('shows a context menu when more than one action is mapped to a trigger', async () => {
|
||||
const { setup, doStart } = uiActions;
|
||||
const trigger = {
|
||||
const trigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: ['test1', 'test2'],
|
||||
};
|
||||
const action1 = createTestAction('test1', () => true);
|
||||
const action2 = createTestAction('test2', () => true);
|
||||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.registerAction(action1);
|
||||
setup.registerAction(action2);
|
||||
setup.attachAction(trigger.id, 'test1');
|
||||
setup.attachAction(trigger.id, 'test2');
|
||||
|
||||
expect(openContextMenu).toHaveBeenCalledTimes(0);
|
||||
|
||||
|
@ -134,7 +140,6 @@ test('passes whole action context to isCompatible()', async () => {
|
|||
const trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: ['test'],
|
||||
};
|
||||
const action = createTestAction<{ foo: string }>('test', ({ foo }) => {
|
||||
expect(foo).toEqual('bar');
|
||||
|
@ -143,6 +148,8 @@ test('passes whole action context to isCompatible()', async () => {
|
|||
|
||||
setup.registerTrigger(trigger);
|
||||
setup.registerAction(action);
|
||||
setup.attachAction(trigger.id, 'test');
|
||||
|
||||
const start = doStart();
|
||||
|
||||
const context = { foo: 'bar' };
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { Action } from '../actions';
|
||||
import { uiActionsTestPlugin } from '../tests/test_plugin';
|
||||
import { uiActionsPluginMock } from '../mocks';
|
||||
|
||||
const action1: Action = {
|
||||
id: 'action1',
|
||||
|
@ -32,11 +32,10 @@ const action2: Action = {
|
|||
} as any;
|
||||
|
||||
test('returns actions set on trigger', () => {
|
||||
const { setup, doStart } = uiActionsTestPlugin();
|
||||
const { setup, doStart } = uiActionsPluginMock.createPlugin();
|
||||
setup.registerAction(action1);
|
||||
setup.registerAction(action2);
|
||||
setup.registerTrigger({
|
||||
actionIds: [],
|
||||
description: 'foo',
|
||||
id: 'trigger',
|
||||
title: 'baz',
|
|
@ -18,35 +18,30 @@
|
|||
*/
|
||||
|
||||
import { createSayHelloAction } from '../tests/test_samples/say_hello_action';
|
||||
import { UiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin';
|
||||
import { uiActionsPluginMock } from '../mocks';
|
||||
import { createRestrictedAction, createHelloWorldAction } from '../tests/test_samples';
|
||||
import { Action } from '../actions';
|
||||
import { Trigger } from '../triggers';
|
||||
|
||||
let action: Action<{ name: string }>;
|
||||
let uiActions: UiActionsTestPluginReturn;
|
||||
let uiActions: ReturnType<typeof uiActionsPluginMock.createPlugin>;
|
||||
beforeEach(() => {
|
||||
uiActions = uiActionsTestPlugin();
|
||||
uiActions = uiActionsPluginMock.createPlugin();
|
||||
action = createSayHelloAction({} as any);
|
||||
|
||||
uiActions.setup.registerAction(action);
|
||||
uiActions.setup.registerTrigger({
|
||||
id: 'trigger',
|
||||
title: 'trigger',
|
||||
actionIds: [],
|
||||
});
|
||||
uiActions.setup.attachAction('trigger', action.id);
|
||||
});
|
||||
|
||||
test('can register and get actions', async () => {
|
||||
const { setup, plugin } = uiActions;
|
||||
test('can register action', async () => {
|
||||
const { setup } = uiActions;
|
||||
const helloWorldAction = createHelloWorldAction({} as any);
|
||||
const length = (plugin as any).actions.size;
|
||||
|
||||
setup.registerAction(helloWorldAction);
|
||||
|
||||
expect((plugin as any).actions.size - length).toBe(1);
|
||||
expect((plugin as any).actions.get(action.id)).toBe(action);
|
||||
expect((plugin as any).actions.get(helloWorldAction.id)).toBe(helloWorldAction);
|
||||
});
|
||||
|
||||
test('getTriggerCompatibleActions returns attached actions', async () => {
|
||||
|
@ -55,10 +50,9 @@ test('getTriggerCompatibleActions returns attached actions', async () => {
|
|||
|
||||
setup.registerAction(helloWorldAction);
|
||||
|
||||
const testTrigger = {
|
||||
const testTrigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: [],
|
||||
};
|
||||
setup.registerTrigger(testTrigger);
|
||||
setup.attachAction('MY-TRIGGER', helloWorldAction.id);
|
||||
|
@ -78,13 +72,13 @@ test('filters out actions not applicable based on the context', async () => {
|
|||
|
||||
setup.registerAction(restrictedAction);
|
||||
|
||||
const testTrigger = {
|
||||
const testTrigger: Trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
title: 'My trigger',
|
||||
actionIds: [restrictedAction.id],
|
||||
};
|
||||
|
||||
setup.registerTrigger(testTrigger);
|
||||
setup.attachAction(testTrigger.id, restrictedAction.id);
|
||||
|
||||
const start = doStart();
|
||||
let actions = await start.getTriggerCompatibleActions(testTrigger.id, { accept: true });
|
||||
|
@ -107,10 +101,9 @@ test(`throws an error with an invalid trigger ID`, async () => {
|
|||
|
||||
test(`with a trigger mapping that maps to an non-existing action returns empty list`, async () => {
|
||||
const { setup, doStart } = uiActions;
|
||||
const testTrigger = {
|
||||
const testTrigger: Trigger = {
|
||||
id: '123',
|
||||
title: '123',
|
||||
actionIds: ['I do not exist'],
|
||||
};
|
||||
setup.registerTrigger(testTrigger);
|
||||
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { uiActionsTestPlugin } from './test_plugin';
|
||||
export * from './test_samples';
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart } from 'src/core/public';
|
||||
import { UiActionsPlugin, UiActionsSetup, UiActionsStart } from '../plugin';
|
||||
|
||||
export interface UiActionsTestPluginReturn {
|
||||
plugin: UiActionsPlugin;
|
||||
coreSetup: CoreSetup;
|
||||
coreStart: CoreStart;
|
||||
setup: UiActionsSetup;
|
||||
doStart: (anotherCoreStart?: CoreStart) => UiActionsStart;
|
||||
}
|
||||
|
||||
export const uiActionsTestPlugin = (
|
||||
coreSetup: CoreSetup = {} as CoreSetup,
|
||||
coreStart: CoreStart = {} as CoreStart
|
||||
): UiActionsTestPluginReturn => {
|
||||
const initializerContext = {} as any;
|
||||
const plugin = new UiActionsPlugin(initializerContext);
|
||||
const setup = plugin.setup(coreSetup);
|
||||
|
||||
return {
|
||||
plugin,
|
||||
coreSetup,
|
||||
coreStart,
|
||||
setup,
|
||||
doStart: (anotherCoreStart: CoreStart = coreStart) => {
|
||||
const start = plugin.start(anotherCoreStart);
|
||||
return start;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
|
||||
export const attachAction: UiActionsApiPure['attachAction'] = ({ triggers }) => (
|
||||
triggerId,
|
||||
actionId
|
||||
) => {
|
||||
const trigger = triggers.get(triggerId);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(
|
||||
`No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${actionId}].`
|
||||
);
|
||||
}
|
||||
|
||||
if (!trigger.actionIds.find(id => id === actionId)) {
|
||||
trigger.actionIds.push(actionId);
|
||||
}
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
|
||||
export const detachAction: UiActionsApiPure['detachAction'] = ({ triggers }) => (
|
||||
triggerId,
|
||||
actionId
|
||||
) => {
|
||||
const trigger = triggers.get(triggerId);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(
|
||||
`No trigger [triggerId = ${triggerId}] exists, for detaching action [actionId = ${actionId}].`
|
||||
);
|
||||
}
|
||||
|
||||
trigger.actionIds = trigger.actionIds.filter(id => id !== actionId);
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
import { buildContextMenuForActions, openContextMenu } from '../context_menu';
|
||||
import { Action } from '../actions';
|
||||
|
||||
const executeSingleAction = async <A extends {} = {}>(action: Action<A>, actionContext: A) => {
|
||||
const href = action.getHref && action.getHref(actionContext);
|
||||
|
||||
// TODO: Do we need a `getHref()` special case?
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
await action.execute(actionContext);
|
||||
};
|
||||
|
||||
export const executeTriggerActions: UiActionsApiPure['executeTriggerActions'] = ({ api }) => async (
|
||||
triggerId,
|
||||
actionContext
|
||||
) => {
|
||||
const actions = await api.getTriggerCompatibleActions!(triggerId, actionContext);
|
||||
|
||||
if (!actions.length) {
|
||||
throw new Error(
|
||||
`No compatible actions found to execute for trigger [triggerId = ${triggerId}].`
|
||||
);
|
||||
}
|
||||
|
||||
if (actions.length === 1) {
|
||||
await executeSingleAction(actions[0], actionContext);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = await buildContextMenuForActions({
|
||||
actions,
|
||||
actionContext,
|
||||
closeMenu: () => session.close(),
|
||||
});
|
||||
const session = openContextMenu([panel]);
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createApi } from '../api';
|
||||
import { createDeps } from '../tests/helpers';
|
||||
|
||||
test('can get Trigger from registry', () => {
|
||||
const deps = createDeps();
|
||||
const { api } = createApi(deps);
|
||||
api.registerTrigger({
|
||||
actionIds: [],
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
|
||||
const trigger = api.getTrigger('bar');
|
||||
|
||||
expect(trigger).toEqual({
|
||||
actionIds: [],
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws if trigger does not exist', () => {
|
||||
const deps = createDeps();
|
||||
const { api } = createApi(deps);
|
||||
|
||||
expect(() => api.getTrigger('foo')).toThrowError('Trigger [triggerId = foo] does not exist.');
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
|
||||
export const getTrigger: UiActionsApiPure['getTrigger'] = ({ triggers }) => id => {
|
||||
const trigger = triggers.get(id);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error(`Trigger [triggerId = ${id}] does not exist.`);
|
||||
}
|
||||
|
||||
return trigger;
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
import { Action } from '../actions';
|
||||
|
||||
export const getTriggerActions: UiActionsApiPure['getTriggerActions'] = ({
|
||||
api,
|
||||
actions,
|
||||
}) => id => {
|
||||
const trigger = api.getTrigger!(id);
|
||||
return trigger.actionIds.map(actionId => actions.get(actionId)).filter(Boolean) as Action[];
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
import { Action } from '../actions/action';
|
||||
|
||||
export const getTriggerCompatibleActions: UiActionsApiPure['getTriggerCompatibleActions'] = ({
|
||||
api,
|
||||
}) => async (triggerId, context) => {
|
||||
const actions = api.getTriggerActions!(triggerId);
|
||||
const isCompatibles = await Promise.all(actions.map(action => action.isCompatible(context)));
|
||||
return actions.reduce<Action[]>(
|
||||
(acc, action, i) => (isCompatibles[i] ? [...acc, action] : acc),
|
||||
[]
|
||||
);
|
||||
};
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { IncompatibleActionError } from './incompatible_action_error';
|
||||
export { Trigger } from './trigger';
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { UiActionsApiPure } from '../types';
|
||||
|
||||
export const registerTrigger: UiActionsApiPure['registerTrigger'] = ({ triggers }) => trigger => {
|
||||
if (triggers.has(trigger.id)) {
|
||||
throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered.`);
|
||||
}
|
||||
|
||||
triggers.set(trigger.id, trigger);
|
||||
};
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { createApi } from '../api';
|
||||
import { createDeps } from '../tests/helpers';
|
||||
|
||||
const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID';
|
||||
|
||||
test('can register trigger', () => {
|
||||
const deps = createDeps();
|
||||
const { api } = createApi(deps);
|
||||
|
||||
api.registerTrigger({
|
||||
actionIds: [],
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
|
||||
expect(deps.triggers.get('bar')).toEqual({
|
||||
actionIds: [],
|
||||
description: 'foo',
|
||||
id: 'bar',
|
||||
title: 'baz',
|
||||
});
|
||||
});
|
||||
|
||||
test('can register action', () => {
|
||||
const deps = createDeps();
|
||||
const { api } = createApi(deps);
|
||||
|
||||
api.registerAction({
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 13,
|
||||
} as any);
|
||||
|
||||
expect(deps.actions.get(HELLO_WORLD_ACTION_ID)).toMatchObject({
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 13,
|
||||
});
|
||||
});
|
||||
|
||||
test('can attach an action to a trigger', () => {
|
||||
const deps = createDeps();
|
||||
const { api } = createApi(deps);
|
||||
const trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
actionIds: [],
|
||||
};
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
expect(trigger.actionIds).toEqual([]);
|
||||
|
||||
api.registerTrigger(trigger);
|
||||
api.registerAction(action);
|
||||
api.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
|
||||
|
||||
expect(trigger.actionIds).toEqual([HELLO_WORLD_ACTION_ID]);
|
||||
});
|
||||
|
||||
test('can detach an action to a trigger', () => {
|
||||
const deps = createDeps();
|
||||
const { api } = createApi(deps);
|
||||
const trigger = {
|
||||
id: 'MY-TRIGGER',
|
||||
actionIds: [],
|
||||
};
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
expect(trigger.actionIds).toEqual([]);
|
||||
|
||||
api.registerTrigger(trigger);
|
||||
api.registerAction(action);
|
||||
api.attachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
|
||||
api.detachAction('MY-TRIGGER', HELLO_WORLD_ACTION_ID);
|
||||
|
||||
expect(trigger.actionIds).toEqual([]);
|
||||
});
|
||||
|
||||
test('detaching an invalid action from a trigger throws an error', async () => {
|
||||
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
api.registerAction(action);
|
||||
expect(() => api.detachAction('i do not exist', HELLO_WORLD_ACTION_ID)).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for detaching action [actionId = HELLO_WORLD_ACTION_ID].'
|
||||
);
|
||||
});
|
||||
|
||||
test('attaching an invalid action to a trigger throws an error', async () => {
|
||||
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
api.registerAction(action);
|
||||
expect(() => api.attachAction('i do not exist', HELLO_WORLD_ACTION_ID)).toThrowError(
|
||||
'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = HELLO_WORLD_ACTION_ID].'
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot register another action with the same ID', async () => {
|
||||
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
|
||||
const action = {
|
||||
id: HELLO_WORLD_ACTION_ID,
|
||||
order: 25,
|
||||
} as any;
|
||||
|
||||
api.registerAction(action);
|
||||
expect(() => api.registerAction(action)).toThrowError(
|
||||
'Action [action.id = HELLO_WORLD_ACTION_ID] already registered.'
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot register another trigger with the same ID', async () => {
|
||||
const { api } = createApi({ actions: new Map<any, any>(), triggers: new Map<any, any>() });
|
||||
const trigger = { id: 'MY-TRIGGER' } as any;
|
||||
|
||||
api.registerTrigger(trigger);
|
||||
expect(() => api.registerTrigger(trigger)).toThrowError(
|
||||
'Trigger [trigger.id = MY-TRIGGER] already registered.'
|
||||
);
|
||||
});
|
|
@ -21,5 +21,4 @@ export interface Trigger {
|
|||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
actionIds: string[];
|
||||
}
|
||||
|
|
|
@ -20,39 +20,6 @@
|
|||
import { Action } from './actions/action';
|
||||
import { Trigger } from './triggers/trigger';
|
||||
|
||||
export { Action } from './actions';
|
||||
export { Trigger } from './triggers/trigger';
|
||||
|
||||
export type ExecuteTriggerActions = <A>(triggerId: string, actionContext: A) => Promise<void>;
|
||||
|
||||
export type GetActionsCompatibleWithTrigger = <C>(
|
||||
triggerId: string,
|
||||
context: C
|
||||
) => Promise<Action[]>;
|
||||
|
||||
export interface UiActionsApi {
|
||||
attachAction: (triggerId: string, actionId: string) => void;
|
||||
detachAction: (triggerId: string, actionId: string) => void;
|
||||
executeTriggerActions: ExecuteTriggerActions;
|
||||
getTrigger: (id: string) => Trigger;
|
||||
getTriggerActions: (id: string) => Action[];
|
||||
getTriggerCompatibleActions: <C>(triggerId: string, context: C) => Promise<Array<Action<C>>>;
|
||||
registerAction: (action: Action) => void;
|
||||
registerTrigger: (trigger: Trigger) => void;
|
||||
}
|
||||
|
||||
export interface UiActionsDependencies {
|
||||
actions: ActionRegistry;
|
||||
triggers: TriggerRegistry;
|
||||
}
|
||||
|
||||
export interface UiActionsDependenciesInternal extends UiActionsDependencies {
|
||||
api: Readonly<Partial<UiActionsApi>>;
|
||||
}
|
||||
|
||||
export type UiActionsApiPure = {
|
||||
[K in keyof UiActionsApi]: (deps: UiActionsDependenciesInternal) => UiActionsApi[K];
|
||||
};
|
||||
|
||||
export type TriggerRegistry = Map<string, Trigger>;
|
||||
export type ActionRegistry = Map<string, Action>;
|
||||
export type TriggerToActionsRegistry = Map<string, string[]>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue