Constrained children position change upon parent task move

Discuss issues related to v5.x
User avatar
gpa
Posts: 15
Joined: Fri Jul 14, 2017 12:46 pm

Constrained children position change upon parent task move

Post by gpa »

Good morning,

we would like to avoid the following behavior in our Ext Gantt (v5.1.2): dragging a parent task with some constrained children, changes the children position according to their constraints.

We have built a fiddle so that you can easily reproduce the "issue":

  1. draw a dependency between "task 1" and "task 2";
  2. move parent "task 3".

We have tried disabling some flags in the task/dependency store settings without any success:

  • taskStore.checkDependencyConstraint: false
  • taskStore.checkPotentialConflictConstraint: false
  • dependencyStore.strictDependencyValidation: false
  • dependencyStore.transitiveDependencyValidation: false

Moreover we have tried to set the flag "ManuallyMaintained: true" in our tasks, without any success.

Is there a way to do that without a "manual" fix?

Many thanks beforehand,
Gaetano

Last edited by gpa on Thu Jun 04, 2020 8:21 am, edited 1 time in total.

User avatar
pmiklashevich
Core Developer
Core Developer
Posts: 2919
Joined: Fri Apr 01, 2016 11:08 am

Re: Constrained children position change upon parent task move

Post by pmiklashevich »

Hello,

There are 2 ways to make your event stick to the date:

  1. The first one is to use ManuallyScheduled mode for the task:

    Code: Select all

    {
        "Id"                : 2,
        "Name"              : "Task 2",
        "StartDate"         : "2020-01-03T23:00:00Z",
        "EndDate"           : "2020-01-04T23:00:00Z",
        "ManuallyScheduled" : true,
        "leaf"              : true,
        "children"          : []
    }
    
    Снимок экрана 2020-06-03 в 19.51.06.png
    Снимок экрана 2020-06-03 в 19.51.06.png (59.73 KiB) Viewed 509 times
  2. The second one is to use one of the default constraints, 'muststarton' or 'startnoearlierthan'

    Code: Select all

    {
        "Id"             : 2,
        "Name"           : "Task 2",
        "StartDate"      : "2020-01-03T23:00:00Z",
        "EndDate"        : "2020-01-04T23:00:00Z",
        "ConstraintType" : "muststarton",
        "ConstraintDate" : "2020-01-03T23:00:00Z",
        "leaf"           : true,
        "children"       : []
    }
    

    Code: Select all

    {
        "Id"             : 2,
        "Name"           : "Task 2",
        "StartDate"      : "2020-01-03T23:00:00Z",
        "EndDate"        : "2020-01-04T23:00:00Z",
        "ConstraintType" : "startnoearlierthan",
        "ConstraintDate" : "2020-01-03T23:00:00Z",
        "leaf"           : true,
        "children"       : []
    }
    
    Снимок экрана 2020-06-03 в 19.50.26.png
    Снимок экрана 2020-06-03 в 19.50.26.png (64.22 KiB) Viewed 509 times

The code above works with the following task store config:

Code: Select all

    taskStore : {
        scheduleByConstraints: true,
        type  : 'gantt_taskstore',
        proxy : {
            type : 'ajax',
            url  : 'data/tasks.json'
        }
    },

We recommend to use scheduleByConstraints. It's false by default to support backward compatibility. But we insist on using it, then scheduling becomes logical. All tasks are scheduled as soon as possible. If you drag a task, a new 'startnoearlierthen' constraint will be added. Disabling cascadeChanges is not recommended unless you know why you do this and will run propagation manually. There is a useful method to reschedule all the tasks. If you're not sure that your tasks are scheduled right, we recommend to call it on load. It's called adjustToCalendar.

Code: Select all

gantt.taskStore.adjustToCalendar();

Running this code you can make sure all tasks are scheduled. So in your case the algorithm is:

  1. Load tasks
  2. Run adjustToCalendar
  3. Create a dependency
  4. Run adjustToCalendar
    Nothing should get changed after second adjustToCalendar

Also I would recommend you to add corresponding columns to be able to track constraints:

Code: Select all

    columns : [
        {
            xtype : 'namecolumn',
            width : 100
        },
        {
            xtype : 'manuallyscheduledcolumn',
            width : 100
        },
        {
            xtype : 'constrainttypecolumn',
            width : 100
        },
        {
            xtype : 'constraintdatecolumn',
            width : 100
        }
    ],

Best,
Pavel

Pavel Miklashevich - Core Developer

User avatar
gpa
Posts: 15
Joined: Fri Jul 14, 2017 12:46 pm

Re: Constrained children position change upon parent task move

Post by gpa »

Hello Pavel,
thanks a lot for your quick and comprehensive response.

In our tool we actually need to manually schedule the tasks (that's a requirement from the customer). Though, we put in place some mechanisms for highlight broken constraints and stuff like that.

I have tried enabling the "ManuallyScheduled" mode for tasks but I end up in an inconsistent state where the children are outside the parent:

Screenshot_2020-06-04_08-57-29.png
Screenshot_2020-06-04_08-57-29.png (7.86 KiB) Viewed 498 times

Is there any way to prevent that?

Thanks in advance for your help.
Gaetano


User avatar
pmiklashevich
Core Developer
Core Developer
Posts: 2919
Joined: Fri Apr 01, 2016 11:08 am

Re: Constrained children position change upon parent task move

Post by pmiklashevich »

Hello Gaetano,

In our tool we actually need to manually schedule the tasks (that's a requirement from the customer).

Then ManuallyScheduled property on the tasks should help you.

Though, we put in place some mechanisms for highlight broken constraints and stuff like that.

The thing is that manually scheduled tasks do not take a part in scheduling. They are simply ignored. You're responsible for their start/end dates.

I have tried enabling the "ManuallyScheduled" mode for tasks but I end up in an inconsistent state where the children are outside the parent

Probably you set "ManuallyScheduled" for parent tasks too. If you want parent tasks to be rescheduled automatically, please don't make them manually scheduled. Otherwise they will be ignored from the calculation too.

Best regards,
Pavel

Pavel Miklashevich - Core Developer

User avatar
pmiklashevich
Core Developer
Core Developer
Posts: 2919
Joined: Fri Apr 01, 2016 11:08 am

Re: Constrained children position change upon parent task move

Post by pmiklashevich »

Hello again,

We discussed the problem in the team and I want to clarify what scheduling behaviour you want to have. Could you please list all your requirements here. Are you satisfied with the ManuallyScheduled set for leaf tasks solution? In this case when you move Task 1 it will not change Task 2 position.

Снимок экрана 2020-06-04 в 12.08.22.png
Снимок экрана 2020-06-04 в 12.08.22.png (70.29 KiB) Viewed 491 times

Or you need to push/pull Task 2 when you move Task 1 to keep interval between them?

Thanks,
Pavel

Pavel Miklashevich - Core Developer

User avatar
gpa
Posts: 15
Joined: Fri Jul 14, 2017 12:46 pm

Re: Constrained children position change upon parent task move

Post by gpa »

Hello Pavel,
thanks a lot for your messages.

The behaviour we are trying to achieve is just having a completely manual scheduling (tasks don't move unless I move them manually) and preserving, at the same time, the parent-child relationship, so that dragging a parent also drags its children, without losing their relative positions.

Unfortunately setting "ManuallyScheduled: false" for the parent tasks and "ManuallyScheduled: true" for the children does not allow me to drag parent tasks anymore (but indeed fixes the children squeezing in case of constraints).

Any other idea?

Thanks again,
Gaetano


User avatar
pmiklashevich
Core Developer
Core Developer
Posts: 2919
Joined: Fri Apr 01, 2016 11:08 am

Re: Constrained children position change upon parent task move

Post by pmiklashevich »

Hello Gaetano,

It seems we have a bug in our sources. Basically cascadeChanges and scheduleByConstraints disabled should do the trick. But it doesn't work as expected.

Code: Select all

    taskStore : {
        cascadeChanges        : false, // THIS LINE SHOULD DO THE TRICK
        scheduleByConstraints : false,

We will try to take a look at it tomorrow and see if we can find a quick fix/workaround. We will contact you as soon as we find anything.

Please confirm we noted everything you need:

  1. Parent task should be draggable
  2. When you drag a parent task, all children should keep their relative to the parent positions
  3. When you drag a child all dependent tasks should preserve their distance to the dragged task

Ticket here: https://app.assembla.com/spaces/bryntum/tickets/9591-should-be-possible-to-drag-parent-tasks-ignoring-rescheduling-/details

Best regards,
Pavel

Pavel Miklashevich - Core Developer

User avatar
gpa
Posts: 15
Joined: Fri Jul 14, 2017 12:46 pm

Re: Constrained children position change upon parent task move

Post by gpa »

Good morning Pavel,
thanks for opening the ticket for us!

Please confirm we noted everything you need:

  1. Parent task should be draggable
  2. When you drag a parent task, all children should keep their relative to the parent positions
  3. When you drag a child all dependent tasks should preserve their distance to the dragged task

I confirm all the points. I would add also "moving children should change the parent duration accordingly". The parent-children relationship should be "stronger" than the scheduling constraint.

Thanks again,
Gaetano


User avatar
pmiklashevich
Core Developer
Core Developer
Posts: 2919
Joined: Fri Apr 01, 2016 11:08 am

Re: Constrained children position change upon parent task move

Post by pmiklashevich »

Hello Gaetano,

We have checked the issue, cascadeChanges flag was not taken into account. We will get this fixed in our latest ExtGantt version (6.x). Is it possible for you to upgrade to the latest one? If not, you can try this workaround in your 5.x version. It overrides a private method, but we do not have plans to remove it in the future.

Code: Select all

Ext.define('MyTask', {
    extend                 : 'Gnt.model.Task',
    processTaskConstraints : function (linearWalkingSequence, linearWalkingIndex, taskStore, cascadeBatch, propagationSources, forceCascadeChanges, affectedTasks, callback) {
        var me                             = this,
            step                           = linearWalkingSequence[linearWalkingIndex],
            task                           = step[0],
            color                          = step[1],
            isParent                       = task.hasChildNodes(),
            isLeaf                         = !isParent,
            needsRescheduling              = task.isMarkedForRescheduling(),
            autoScheduled                  = /*needsRescheduling || */ !(task.isManuallyScheduled() || task.isCompleted() || task.isReadOnly() || Ext.Array.contains(propagationSources, task)),
            cascadeChanges                 = forceCascadeChanges || taskStore.cascadeChanges,
            scheduleByConstraints          = taskStore.scheduleByConstraints,
            recalculateParents             = taskStore.recalculateParents,
            moveParentAsGroup              = taskStore.moveParentAsGroup,
            parentNode                     = task.parentNode,
            parentNodeStartDate            = parentNode && (parentNode.getStartDate()),
            parentNodeUnprojectedStartDate = parentNode && (parentNode.getUnprojected(parentNode.startDateField)),
            parentNodeDateOffset           = parentNode && (parentNodeStartDate - parentNodeUnprojectedStartDate),
            skipConstraintsVerification    = false,
            offsetFromParent;

    function areIncomingDependenciesAffectedOrPropagationSourcesIncoming(task, affectedTasks, propagationSources) {
        var incomingDeps = task.getIncomingDependencies(true),
            result       = false,
            i, len, dep, fromTask;
        // PATCH - no need to do this if cascadeChanges===false
        if (cascadeChanges)
            for (i = 0, len = incomingDeps.length; !result && i < len; ++i) {
                dep      = incomingDeps[i];
                fromTask = dep.getSourceTask();
                result   = fromTask && affectedTasks.hasOwnProperty(fromTask.getId()) ||
                    Ext.Array.contains(propagationSources, fromTask);
            }
        return result;
    }

    switch (true) {
        case autoScheduled && isLeaf && color == 'green' && parentNodeDateOffset && moveParentAsGroup:
        case autoScheduled && isParent && color == 'yellow' && parentNodeDateOffset && moveParentAsGroup:
            // ignore case when parent StartDate set to NULL
            // since we cannot calculate proper dates to shift child tasks at
            if (parentNodeStartDate && !scheduleByConstraints) {
                var startDate = task.getStartDate();
                if (startDate >= parentNodeUnprojectedStartDate) {
                    offsetFromParent = task.calculateDuration(parentNodeUnprojectedStartDate, startDate, null, { segments : false });
                    task.setStartDateWithoutPropagation(task.calculateEndDate(parentNodeStartDate, offsetFromParent, null, { segments : false }), true, taskStore.skipWeekendsDuringDragDrop);
                    // if the summary task starts after this one
                }
                else {
                    // force to not take segments into account during new start date calculating
                    offsetFromParent = task.calculateDuration(startDate, parentNodeUnprojectedStartDate, null, { segments : false });
                    task.setStartDateWithoutPropagation(task.calculateStartDate(parentNodeStartDate, offsetFromParent, null, { segments : false }), true, taskStore.skipWeekendsDuringDragDrop);
                }
                // Passing a parent node here limits the constraining to incoming dependencies incoming from
                // that parent node descendants only, outer nodes are not taken into account
                areIncomingDependenciesAffectedOrPropagationSourcesIncoming(task, affectedTasks, propagationSources) &&
                task.scheduleWithoutPropagation({ taskStore : taskStore, parentNode : parentNode });
            }
            else if (scheduleByConstraints) {
                task.scheduleWithoutPropagation({ taskStore : taskStore });
            }
            break;
        case isLeaf && color == 'green' && needsRescheduling:
        case isParent && color == 'yellow' && needsRescheduling:
            task.scheduleWithoutPropagation({ taskStore : taskStore });
            break;
        case autoScheduled && isLeaf && color == 'green' && cascadeChanges:
        case autoScheduled && isParent && color == 'yellow' && cascadeChanges:
            if (areIncomingDependenciesAffectedOrPropagationSourcesIncoming(task, affectedTasks, propagationSources)) {
                // If we need to schedule a summary task
                if (isParent) {
                    if (moveParentAsGroup) {
                        // BW compat: pass special flag "treatAsLeaf" forcing to calculate ES/EE dates as for non-summary tasks (trying to comply the old behavior)
                        task.scheduleWithoutPropagation({ taskStore : taskStore, treatAsLeaf : true });
                        // If we are in the "scheduleByConstraints" mode
                    }
                    if (scheduleByConstraints) {
                        // Since summary task early dates (and late ones) are calculated based on its children
                        // and on this stage we haven't stepped through the children yet (the changes are not applied to them).
                        // So let's simply mark children for further scheduling and they will be processed on the next steps.
                        task.cascadeBy(function (child) {
                            // mark leaves (and parents is if "recalculateParents" is disabled)
                            if (!Ext.Array.contains(propagationSources, child) && (task.isLeaf() || !recalculateParents)) {
                                task.markForRescheduling();
                            }
                        });
                        // no need to verify the task constraints yet
                        skipConstraintsVerification = true;
                    }
                    // if it's a leaf task we simply reschedule it
                }
                else {
                    task.scheduleWithoutPropagation({ taskStore : taskStore });
                }
            }
            // If it's a parent task and "recalculateParents" is enabled then we haven't finished w/ the task positioning yet.
            // So let's skip the task constraints check at this step. We can do it later.
            skipConstraintsVerification = skipConstraintsVerification || isParent && recalculateParents;
            break;
        case isParent && color == 'green' && recalculateParents:
            task.refreshCalculatedParentNodeData();
            break;
    }
    // If the propagation has finished done with the task
    // but it's still marked as requiring rescheduling let's reset the mark (to not affect further propagations)
    if (color == 'green' && task.isMarkedForRescheduling()) {
        task.unmarkForRescheduling();
    }
    if (task.isProjected()) {
        cascadeBatch.addAffected(task);
        affectedTasks[task.getId()] = task;
    }
    var proceedOnVerificationIsDoneCallback = function (constraintSatisfied, propagationCanceled) {
        var yellowStep,
            yellowStepIdx;
        // In case a parent node is adjusted according to its children and such an adjustment violates
        // the parent node constraint then we rewind back to the same parent node yellow step to readjust
        // it and its children once again allowing a user to reconsider (by showing him constraint violation
        // dialog, for example). We rewind by calling a callback with adjusted step index.
        if (!constraintSatisfied && isParent && autoScheduled && taskStore.recalculateParents && color == 'green') {
            yellowStep = Ext.Array.findBy(linearWalkingSequence, function (step, index) {
                var stepTask  = step[0],
                    stepColor = step[1];
                yellowStepIdx = index;
                return task === stepTask && stepColor == 'yellow';
            });
            // yellowStep must always be present in the linear walking sequence.
            callback(yellowStepIdx, constraintSatisfied, !!propagationCanceled, affectedTasks);
        }
        else {
            callback(linearWalkingIndex, constraintSatisfied, !!propagationCanceled, affectedTasks);
        }
    };
    // if verification should be skipped we proceed to the next step by calling "proceedOnVerificationIsDoneCallback" manually
    if (skipConstraintsVerification) {
        proceedOnVerificationIsDoneCallback(true, false);
    }
    // otherwise we do verification by calling "task.verifyConstraints()" and "proceedOnVerificationIsDoneCallback" will be called there
    return skipConstraintsVerification || task.verifyConstraints(proceedOnVerificationIsDoneCallback);
}
});

Let us know please how it works for you.

Cheers,
Pavel

Pavel Miklashevich - Core Developer

User avatar
gpa
Posts: 15
Joined: Fri Jul 14, 2017 12:46 pm

Re: Constrained children position change upon parent task move

Post by gpa »

Dear Pavel,

thank you very much for the patch provided. I was trying to play around with that to see if we can easily integrate it in our code, but I get some issue (see this fiddle).

I cannot move the tasks and sometimes I have some weird rendering of the dependencies:

Image Pasted at 2020-6-8 15-27.png
Image Pasted at 2020-6-8 15-27.png (10 KiB) Viewed 437 times

I am now using only:

Code: Select all

cascadeChanges: false,
scheduleByConstraints: false,

Thanks in advance for your help.
Gaetano


Post Reply