Custom Elements Best Practice

Hello,

For my CS thesis, I’m trying to heavily break down the modeler. Although I’m struggling in choosing which are the best ways to achieve the following:

  1. Remove for my usecase unnecessary elements from the modeler and the properties panel:

    • I achieved this already by providing a custom PaletteProvider and a custom PropertiesProvider.
  2. Some of the Camunda properties should have default values or be auto-generated:

    • The isExecutable should always be set to true. The user should not have the option to change that, as I removed the entry out of the general group in the properties.
    • The id is auto-generated by the chosen name.
  3. I want different types of UserTasks/ServiceTasks, where the difference is mainly the execution listener delegates to use. In the modeler, I want to be able to choose between the offered elements via the palette or the context menu. The best option would be with a custom icon or so.
    EDIT: Achieved this by extending UserTask with a custom group in the properties panel, see post below.

    • Should I create different user task entries in the palette which are of type UserTask, or should I use templates for that?
    • I would rather not use templates, since i want to bind some logic to the elements, for example i want to generate UserTask related Inputs based on some data in my app.
    • If the preferred way is templates, what is the current state of that function? Somehow I’m heavily struggling with binding this feature in and providing the template chooser

This is a lot and the learning curve of this library is hard, but i already love working with this and would be happy about any help! :heartbeat:

I have achieved most of my functionalities by extending the UserTask with a custom group Lesson, which fetches some IDs from local storage inside my app.

Now I’m wondering:

  • Is it possible to achieve the following: There is a parent dropdown called lessonToSolve and selectObjective. Depending on which is chosen, either the former lessonType dropdown is shown or, when selectObjective is selected, a similar objectiveType dropdown is displayed.

  • Are there any major flaws in what I’m doing here? So far it works fine, but I’m new to this and want to avoid major mistakes.

Here is my code to extend UserTasks:

export function UserTaskGroup(element, translate) {
  const group = {
    label: translate('Lesson'),
    id: 'Lesson',
    component: Group,
    entries: [...UserTaskProps({ element, translate })]
  };
  if (group.entries.length) {
    return group;
  }
  return null;
}

function UserTaskProps(props) {
  const { element, translate } = props;
  if (!(is(element, 'bpmn:UserTask'))) {
    return [];
  }
  const entries = [];

  entries.push({
    id: 'lesson',
    component: LessonType,
    isEdited: isSelectEntryEdited,
    get(element, node) {
      const lessonId = getLesson(element);
      return { lesson: lessonId || '' };
    },
    set(element, values) {
      const lessonId = values.lesson;
      const businessObject = element.businessObject;
      businessObject.set('lesson', lessonId);
      return [element];
    }
  });

  return entries;
}

function LessonType(props) {
  const { element } = props;
  const bpmnFactory = useService('bpmnFactory');
  const translate = useService('translate');
  const modeling = useService('modeling');

  const getValue = () => {
    return getLesson(element);
  };

  const setValue = (value) => {
    const businessObject = element.businessObject;
    const lessons = getOptions();
    const selectedLesson = lessons.find(lesson => lesson.value === value);
    const label = selectedLesson ? selectedLesson.label : '';

    businessObject.set('lesson', value);
    businessObject.set('name', label);
    businessObject.set('camunda:assignee', '${studentId}');

    setInputParameters(element, modeling, bpmnFactory, value);
    addExecutionListener(element, modeling, bpmnFactory);
  };

  const getOptions = () => {
    const lessonStore = useLessonStore();
    const lessons = toRaw(lessonStore.lessons);
    const publishedLessons = lessons.filter(lesson => lesson.lessonDTO.published);
    return publishedLessons.map(lesson => ({
      value: lesson.lessonDTO.uuid,
      label: lesson.lessonDTO.title
    }));
  };

  return jsx(SelectEntry, {
    element: element,
    id: "lesson",
    label: translate('Lesson to solve'),
    getValue: getValue,
    setValue: setValue,
    getOptions: getOptions,
    validate: (element) => {
      if (!element) {
        return translate('Lesson is required.');
      }
    }
  });
}

function getLesson(element) {
  const businessObject = element.businessObject;
  return businessObject.get('lesson');
}

function addExecutionListener(element, modeling, bpmnFactory) {
  const businessObject = getBusinessObject(element);

  if (!businessObject.extensionElements) {
    businessObject.extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
      values: []
    });
  }

  const extensionElements = businessObject.extensionElements;

  const executionListenerStart = bpmnFactory.create('camunda:ExecutionListener', {
    event: 'start',
    delegateExpression: '${lessonUserTaskDelegate}'
  });

  const executionListenerEnd = bpmnFactory.create('camunda:ExecutionListener', {
    event: 'end',
    delegateExpression: '${lessonUserTaskDelegate}'
  });

  extensionElements.get('values').push(executionListenerStart);
  extensionElements.get('values').push(executionListenerEnd);

  modeling.updateProperties(element, {
    extensionElements: extensionElements
  });
}

function setInputParameters(element, modeling, bpmnFactory, lessonId) {
  const businessObject = getBusinessObject(element);

  if (!businessObject.extensionElements) {
    businessObject.extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
      values: []
    });
  }

  const extensionElements = businessObject.extensionElements;

  const inputParameter = bpmnFactory.create('camunda:InputParameter', {
    name: 'lessonId',
    value: lessonId
  });

  let inputOutput = extensionElements.values.find(value => is(value, 'camunda:InputOutput'));

  if (!inputOutput) {
    inputOutput = bpmnFactory.create('camunda:InputOutput', {
      inputParameters: [],
      outputParameters: []
    });
    extensionElements.values.push(inputOutput);
  }

  inputOutput.inputParameters.push(inputParameter);

  modeling.updateProperties(element, {
    extensionElements: extensionElements
  });
}

Hey, great to see you’ve been able to do most of the customization on your own! To have a properties panel input depend on another input should be pretty straight forward. Only add the input to the properties panel group you’re returning if the other input (and therefore your element) has the desired value.

1 Like

Thank you for your reply! I tried to solve it like this:

function UserTaskProps(props) {
  const { element, translate } = props;
  if (!(is(element, 'bpmn:UserTask'))) {
    return [];
  }
  const entries = [];

  entries.push({
    id: 'lessonType',
    component: TaskType,
    isEdited: isSelectEntryEdited,
    get(element, node) {
      const taskType = getTaskType(element);
      return { taskType: taskType || '' };
    },
    set(element, values) {
      const businessObject = element.businessObject;
      businessObject.set('type', values.type);
      return [element];
    }
  });

  const taskType = getTaskType(element);
  
  if (taskType === 'solveLesson') {
    entries.push({
      id: 'lesson',
      component: LessonType,
      isEdited: isSelectEntryEdited,
      get(element, node) {
        const lesson = getLesson(element);
        return { lesson: lesson || '' };
      },
      set(element, values) {
        const businessObject = element.businessObject;
        businessObject.set('lesson', values.lesson);
        return [element];
      }
    });
  }

  return entries;

}

Sadly, this only gets triggered if I open the UserTask again. Is there a way to recognize the change when choosing the lessonType?

Also, I’m struggling to set the default value for isExecutable and the mentioned process id/name behavior. What would be a good approach for this?

I see, so, the problem is that you’re directly modifying the element instead of triggering a command that would ensure everyone else is notified about what you’re doing and the properties panel updates. Because the properties panel does not get notified at the moment you have to trigger the update manually by selecting the element again. This is the problematic code:

To fix this you can use the modeling API:

set(element, values) {
  modeling.updateProperties(element, {
    type: values.type
  });
}

You can also have a look at the properties panel implementation to see countless examples of that: bpmn-js-properties-panel/src/provider/bpmn/properties/IdProps.js at main · bpmn-io/bpmn-js-properties-panel · GitHub

1 Like

That helped, was able to achieve everything i planned! The modeller turned out great. (Beside the default process properties)

Thank you a lot :slight_smile:

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.