nocode-js / sequential-workflow-editor

Powerful workflow editor builder for any workflow designer.
https://nocode-js.com/
MIT License
78 stars 7 forks source link

Dynamically extending parallelStepModel - angular #40

Closed NexPlex closed 1 week ago

NexPlex commented 2 weeks ago

Hi, great application.

I'm trying to add and remove branches using the Angular version. like in this example

https://nocode-js.github.io/sequential-workflow-designer/examples/multi-conditional-switch.html

https://github.com/nocode-js/sequential-workflow-designer/blob/7ca5488da7a5b42419c9f764f004354e6ebb4245/examples/assets/multi-conditional-switch.js

I'm having a lot of difficulty with the TypeScripts restrictions; maybe you can offer some suggestions.

I tried many techniques. This is the code I have now. I am able to add the steps to the designer but not the editor panel.

I am trying to update the parallelStepModel before calling const editorRoot = originalStepEditorProvider(step, context, definition, false), but I can't get it to work. I'm not even sure if this is the best approach.

Thank you in advance.

export interface ParallelStep extends BranchedStep {
  componentType: 'switch';
  type: 'parallel';
  name;
  properties: {
      'Condition 0': Dynamic<NullableAnyVariable | string | number | boolean>;
      'Condition 1': Dynamic<NullableAnyVariable | string | number | boolean>;
      'Condition 2': Dynamic<NullableAnyVariable | string | number | boolean>;
      'Condition 3': Dynamic<NullableAnyVariable | string | number | boolean>;
      'Condition 4': Dynamic<NullableAnyVariable | string | number | boolean>;
      'Condition 5': Dynamic<NullableAnyVariable | string | number | boolean>;
  };
    branches: {
    'Condition 0': Step[];
    'Condition 1': Step[];
    'Condition 2': Step[];
    'Condition 3': Step[];
    'Condition 4': Step[];
    'Condition 5': Step[];
  };
}

model.ts
export const parallelStepModel = createBranchedStepModel<ParallelStep>('parallel', 'switch', step => {
  // Step Name Generation
  step.name().value(
    createGeneratedStringValueModel({
      generator(context) {
        return 'Parallel Condition';
      }
    })
  );

  // Step Category and Description
  step.category('Logic');
  step.description('Check condition and execute different branches.');

  // Dynamic Value Model for Conditions
  const dynamicValueModel = createDynamicValueModel({
    models: [
      createNullableAnyVariableValueModel({
        isRequired: true,
        valueTypes: ['string', 'number', 'boolean']
      }),
      createStringValueModel({}),
      createNumberValueModel({}),
      createBooleanValueModel({})
    ]
  });

  // Set Properties for Conditions
  step.property('Condition 0').value(dynamicValueModel);
  step.property('Condition 1').value(dynamicValueModel);
  step.property('Condition 2').value(dynamicValueModel);

  // Set Branches for Conditions
  step.branches().value(
    createBranchesValueModel({
      branches: {
        'Condition 0': [],  // These branches are populated dynamically when creating the step
        'Condition 1': [],
        'Condition 2': []
      }
    })
  );

});

component.ts

function appendButton(root: HTMLElement, label: string, step: Step | BranchedStep, context) {

  const button = document.createElement('button');
  button.textContent = label;
  button.addEventListener('click', () => {
    if ('branches' in step) {
      if (!step.branches) {
        step.branches = {};

      }

      // Calculate the next sequence number
      const existingBranchKeys = Object.keys(step.branches);
      let maxSequence = 2; // Start from 2 because we will increment it for the next branch
      existingBranchKeys.forEach(key => {
        const sequenceNum = parseInt(key.replace(/^\D+/g, '')); // Remove non-digit characters and parse
        if (!isNaN(sequenceNum) && sequenceNum > maxSequence) {
          maxSequence = sequenceNum;
        }
      });

      const nextSequence = maxSequence + 1;
      const branchName = `Condition ${nextSequence}`;

      // Add a new branch with a sequence number. Initialize as an empty array or with a default step object
      step.branches[branchName] = []; // Assuming branches are arrays of steps
      step.properties[branchName] = {
          "modelId": "nullableAnyVariable",
          "value": null
        },

      // Optionally, if you need to initialize with a default step, you could do something like:
      // step.branches[branchName] = [{ /* Default step object structure */ }];

      // Notify the context that children have changed
      context.notifyChildrenChanged();
            const branch = document.createElement('div');
          branch.className = 'switch-branch';

          const title = document.createElement('h4');
          title.innerText = "name";

          const label = document.createElement('label');
          label.innerText = 'Condition: ';

          root.appendChild(title);
          root.appendChild(label);
            const dynamicValueModel = createDynamicValueModel({
    models: [
      createNullableAnyVariableValueModel({
        isRequired: true,
        valueTypes: ['string', 'number', 'boolean']
      }),
      createStringValueModel({}),
      createNumberValueModel({}),
      createBooleanValueModel({})
    ]
  });
            console.log('dynamicValueModel', dynamicValueModel)
      appendConditionEditor(root,
        dynamicValueModel, value => {
            step.properties[branchName] = value;
            context.notifyPropertiesChanged();
        });
    }
  });

  // Append the button to the root element provided
  root.appendChild(button);
}
public ngOnInit() {
    this.isLoading = true;
    this.store.pipe(
      takeUntil(this._destroyed)
    ).subscribe((state: any) => {
      if (state.auth?.user) {
        this.userState = state.auth.user;
      }
    });
    const docPayload = {
      collections: ['WorkflowDefinition'],
      // filter: {'account_id': 1}
      data: null
    }
    const tablePayload = {
      tables: ['User'],
      data: null
    }
    this.store.dispatch(ResourcesPageActions.getDocumentData({payload: docPayload}));
    this.store.dispatch(ResourcesPageActions.getTableData({payload: tablePayload}));
    this.dropdownListAry['Boolean'] = [{id: false, name: 'False'}, {id: true, name: 'True'}]

    const editorProvider = EditorProvider.create(definitionModel, {
      uidGenerator: Uid.next
    });
    const activatedDefinition = editorProvider.activateDefinition();
    console.log('activatedDefinition', activatedDefinition)

    this.stepEditorProvider = editorProvider.createStepEditorProvider();
    this.rootEditorProvider = editorProvider.createRootEditorProvider();
    const originalStepEditorProvider = this.stepEditorProvider;

    this.stepEditorProvider = (step: Step | BranchedStep, context, definition) => {
      // Create a new root div as the base for our custom editor UI
      const root = document.createElement('div');
      root.className = "sqd-editor sqd-step-editor";

      // Use the original provider to get the editor component, if needed
      console.log('step', step)
      this.addNewCondition('Condition 3')
      const editorRoot = originalStepEditorProvider(step, context, definition, false);
      console.log('editorRoot', editorRoot)

      // Type guard to check if step is of type that has branches
      if ('branches' in step && step.type === 'parallel') {
        appendButton(root, 'Add Condition', step, context);
      }
      editorRoot.appendChild(root);
      return editorRoot;

    };

    this.validatorConfiguration = {
      root: editorProvider.createRootValidator(),
      step: editorProvider.createStepValidator()
    };
    this.toolboxConfiguration = {
      groups: editorProvider.getToolboxGroups()
    };

    this.updateDefinitionJSON();
  }
b4rtaz commented 2 weeks ago

Hello @NexPlex,

currently there is one recommended solution: hidden dependent + external. The hidden dependent is available in the pro version.

You can check this example: https://nocode-js.com/examples/sequential-workflow-editor-pro/webpack-pro-app/public/editors.html#branches In this examples branches are generated from the dependant property. It could be a single string as in this example but also the external editor. Also it could be 2 properties with two separated lists (strings). The UX may be not the best but should be fast for prototyping.

The external editor allows to create the best UX, because you can create a popup that provides an expected data structure to the editor. In this case you are not limited by narrow width of the editor panel.

interface Condition {
    name: string;
    rules: { var: string; something: number; foo: boolean; ... }[];
}

interface ConditionStepModel extends BranchedStep {
  type: 'condition';
  componentType: 'switch';
  properties: {
    conditions: Condition[]; // << to edit this value you use own external editor like popup.
  };
}

// model

step.property('conditions').value(
   createExternalValueModel({
      defaultValue: [],
      editorId: 'external_foo'
   })
);

step.branches()
   .dependentProperty('conditions') // << you can have more than 1 dependant property
   .value(
      createDependentValueModel({
         editorId: 'hiddenDependent',
         model: createBranchesValueModel({
            branches: {},
            dynamic: true,
         }),
         generator(context, currentValue) {
            const conditions: Condition[] = context.getPropertyValue('conditions');
            const branches: Branches = {};
            // here is a logic that creates branches from the dependant property
            return branches;
         }
      })
   );