Skip to content

Commit

Permalink
refactor: Split and/or trigger in two
Browse files Browse the repository at this point in the history
Closes #95
  • Loading branch information
Alorel committed Dec 15, 2022
1 parent 13195e5 commit 42381e5
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 53 deletions.
3 changes: 2 additions & 1 deletion src/lib/public-api-validation.mts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export function isTriggerNodeDefinition(v: any): v is TriggerNodeDefinition {
return false;
}

const {check, init, listen} = v as Partial<TriggerNodeDefinition>;
const {canBeDefault, check, init, listen} = v as Partial<TriggerNodeDefinition>;

return typeof check === 'function'
&& isUndefinedOr(canBeDefault, 'boolean')
&& isUndefinedOr(listen, 'function')
&& isUndefinedOr(init, 'function');
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/registries/action-registry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {NamespacedDefinition} from '../namespaced-definition.mjs';
export const enum InternalCategory {
START_SKILL = 'Start skill',
COMBAT = 'Combat',
COMBINATION = 'Combination',
CORE = 'Core',
}

Expand Down
4 changes: 0 additions & 4 deletions src/option-types/trigger-ref-option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Btn from '../ui/components/btn';
import {BinSvg} from '../ui/components/svg';
import type {TriggerConfigValue} from '../ui/components/trigger-config';
import TriggerConfig from '../ui/components/trigger-config';
import useReRender from '../ui/hooks/re-render';
import {RenderNodeMedia} from '../ui/pages/workflows-dashboard/render-node-media';
import getEvtTarget from '../ui/util/get-evt-target.mjs';
import {isTriggerRefOption} from './trigger-ref/is-trigger-ref-option.mjs';
Expand Down Expand Up @@ -107,8 +106,6 @@ interface EditOneProps extends ViewOneProps {
}

function EditOne({trigger, onChange}: EditOneProps): VNode | null {
const reRender = useReRender();

const value = useMemo((): TriggerConfigValue => ({
opts: trigger?.opts ?? {},
trigger: trigger?.trigger,
Expand All @@ -119,7 +116,6 @@ function EditOne({trigger, onChange}: EditOneProps): VNode | null {
trigger.trigger = newVal.trigger!;
trigger.opts = newVal.opts;
onChange(trigger);
reRender();
} else {
onChange(new WorkflowTrigger(newVal));
}
Expand Down
7 changes: 7 additions & 0 deletions src/public_api/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export default interface Api {

/** Common node option */
export interface NodeOptionBase extends Omit<Referenceable, 'namespace'> {
/**
* Some options, such as and/or triggers, can cause infinite recursion in the UI if they're selected as the
* default/initial
* @default true
*/
canBeDefault?: boolean;

/** Info tooltip */
description?: string;

Expand Down
7 changes: 7 additions & 0 deletions src/public_api/trigger.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export class TriggerDefinitionContext<T extends object = {}> {

/** Definition of a trigger */
export interface TriggerNodeDefinition<T extends object = {}> extends NodeDefinition<T> {
/**
* Some triggers, e.g. and/or, can cause infinite recursion in the UI if selected as the initial/default trigger.
* Such triggers should have this prop set to `false`.
* @default true
*/
canBeDefault?: boolean;

/**
* Check if the given data passes the trigger. Called when a trigger node activates.
* @param data Node data
Expand Down
89 changes: 41 additions & 48 deletions src/triggers/core/and-or-trigger.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,83 +5,76 @@ import {WorkflowTrigger} from '../../lib/data/workflow-trigger.mjs';
import {InternalCategory} from '../../lib/registries/action-registry.mjs';
import {defineLocalTrigger} from '../../lib/util/define-local.mjs';
import LazyValue from '../../lib/util/lazy-value.mjs';
import type {TriggerNodeDefinition} from '../../public_api';
import {
allTriggerSelectGroups
} from '../../ui/components/workflow-editor/categorised-node-select/categorised-node-select-impl';

const enum Cond {
AND = '&&',
OR = '||',
}

interface Data {
cond: Cond,

triggers: WorkflowTrigger[];
}

const triggerCtx = defineLocalTrigger<Data>({
category: InternalCategory.CORE,
check({cond, triggers}) {
const method = cond === Cond.OR ? 'some' : 'every';

// Call either Array.prototype.some or Array.prototype.every
return triggers[method](triggerPasses);
},
const baseDef: Omit<TriggerNodeDefinition<Data>, 'namespace' | 'label' | 'localID' | 'check' | 'media'> = {
canBeDefault: false,
category: InternalCategory.COMBINATION,
initOptions: () => ({
cond: Cond.AND,
triggers: [new WorkflowTrigger({trigger: defaultTrigger.value})],
}),
label: 'And/or',
listen({cond, triggers}) {
options: [
{
label: 'Triggers',
localID: 'triggers',
multi: true,
required: true,
type: 'TriggerRef',
},
],
};

defineLocalTrigger<Data>({
...baseDef,
check: ({triggers}) => triggers.every(triggerPasses),
label: 'And',
listen({triggers}) {
if (!triggers.length) {
return NEVER; // EMPTY won't work as it we're listening for completion and it completes immediately
}

const src$ = triggers.map(trigger => trigger.listen());

if (cond === Cond.OR) {
// As soon as one of them emits we're good to go
return merge(...src$).pipe(take(1));
}

return combineLatest(src$).pipe(

// Check that every trigger passes
filter(() => triggers.every(triggerPasses)),
take(1)
);
},
localID: 'andor',
media: cdnMedia('assets/media/pets/dragons_den.svg'),
options: [
{
enum: {
[Cond.AND]: 'And',
[Cond.OR]: 'Or',
},
label: 'Check',
localID: 'cond',
required: true,
type: String,
},
{
label: 'Triggers',
localID: 'triggers',
multi: true,
required: true,
type: 'TriggerRef',
},
],
localID: 'and',
media: 'https://raw.githubusercontent.com/Alorel/melvor-action-workflows/0.8.0/src/ui/assets/and.png',
});

const defaultTrigger = new LazyValue<TriggerDefinitionContext<any>>(() => {
// Pick the first one that isn't our trigger node
const ownId = triggerCtx.id;
defineLocalTrigger<Data>({
...baseDef,
check: ({triggers}) => triggers.some(triggerPasses),
label: 'Or',
listen({triggers}) {
if (!triggers.length) {
return NEVER; // EMPTY won't work as it we're listening for completion and it completes immediately
}

const src$ = triggers.map(trigger => trigger.listen());

// As soon as one of them emits we're good to go
return merge(...src$).pipe(take(1));
},
localID: 'or',
media: 'https://raw.githubusercontent.com/Alorel/melvor-action-workflows/0.8.0/src/ui/assets/or.png',
});

const defaultTrigger = new LazyValue<TriggerDefinitionContext<any>>(() => {
for (const cat of allTriggerSelectGroups.value) {
for (const trigger of cat.items) {
if (trigger.id !== ownId) {
if (trigger.def.canBeDefault !== false) {
return trigger;
}
}
Expand Down
Binary file added src/ui/assets/and.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/ui/assets/or.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 42381e5

Please sign in to comment.