Limit Planned Dates based on Relations

📘

How to apply this rule?

👍

The rules are filtered to only look for "Blocker" and "Dependency" relations

While setting Planned Start Date, make sure that it's later than the last inbound dependency

When you set Planned Start Date for an entity, this rule checks if the item can be actually started on this date, or there is any inbound dependency which ends later that this date. If such dependency is found, Planned Start Date of the item will be equal to Planned End Date of this dependency. Original duration of the item will be preserved.

10971097
[
  {
    "type": "source:targetprocess:EntityChanged",
    "entityTypes": [
      "UserStory",
      "Bug",
      "Task",
      "Feature",
      "Request"
    ],
    "modifications": {
      "created": false,
      "deleted": false,
      "updated": [
        "PlannedStartDate"
      ]
    }
  },
  {
    "or": [
      {
        "and": [
          {
            "value": null,
            "target": {
              "name": "PlannedStartDate",
              "type": "field",
              "target": {
                "type": "pipelineBlockOutput"
              }
            },
            "operator": {
              "type": "exists"
            }
          }
        ]
      }
    ],
    "type": "filter:Relational"
  },
  {
    "type": "action:JavaScript",
    "script": "const relations = [\"Dependency\", \"Blocker\"];\n\nconst MILLISECONDS_IN_DAY = 1000 * 3600 * 24;\n\nfunction differenceInDays(dateLeft, dateRight) {\n  return Math.round((dateLeft.getTime() - dateRight.getTime()) / MILLISECONDS_IN_DAY);\n}\n\nfunction addDays(date, days) {\n  const result = new Date(date);\n  result.setDate(result.getDate() + days);\n  return result;\n}\n\nconst api = context.getService(\"targetprocess/api/v2\");\nconst assignables = await api.queryAsync(\"Assignable\", {\n  select: `{end:MasterRelations.Where(RelationType.Name in ${JSON.stringify(relations)}).Max(MasterAssignable.PlannedEndDate)}`,\n  where: `id=${args.ResourceId}`\n});\nif (!assignables || !assignables.length) { return; }\nconst earliestStart = assignables[0].end;\nif (!earliestStart) { return; }\n\nconst dateLeft = new Date(earliestStart);\nconst dateRight = new Date(args.Current.PlannedStartDate);\nif (dateLeft <= dateRight) { return; }\n\nconst shift = differenceInDays(dateLeft, dateRight);\nconst start = dateLeft;\nconst end = args.Current.PlannedEndDate ? addDays(args.Current.PlannedEndDate, shift) : undefined;\n\nreturn {\n  command: \"targetprocess:UpdateResource\",\n  payload: {\n    resourceId: args.ResourceId,\n    resourceType: args.ResourceType,\n    fields: {\n      PlannedStartDate: start,\n      PlannedEndDate: end\n    }\n  }\n}"
  }
]

While setting Planned End Date, make sure that it's all outbound dependencies start after that

When you set Planned End Date for an entity, this rule checks if there is any intersection with an outbound dependency. If such intersection is found, Planned Start Date of this outbound dependency will be equal to Planned End Date of the item. Original duration of the outbound dependency will be preserved.

988988
[
  {
    "type": "source:targetprocess:EntityChanged",
    "entityTypes": [
      "UserStory",
      "Bug",
      "Feature",
      "Task",
      "Request"
    ],
    "modifications": {
      "created": false,
      "deleted": false,
      "updated": [
        "PlannedEndDate"
      ]
    }
  },
  {
    "or": [
      {
        "and": [
          {
            "value": null,
            "target": {
              "name": "PlannedStartDate",
              "type": "field",
              "target": {
                "type": "pipelineBlockOutput"
              }
            },
            "operator": {
              "type": "exists"
            }
          }
        ]
      }
    ],
    "type": "filter:Relational"
  },
  {
    "type": "action:JavaScript",
    "script": "const relations = [\"Dependency\", \"Blocker\"];\n\nconst MILLISECONDS_IN_DAY = 1000 * 3600 * 24;\n\nfunction differenceInDays(dateLeft, dateRight) {\n  return Math.round((dateLeft.getTime() - dateRight.getTime()) / MILLISECONDS_IN_DAY);\n}\n\nfunction addDays(date, days) {\n  const result = new Date(date);\n  result.setDate(result.getDate() + days);\n  return result;\n}\n\nconst api = context.getService(\"targetprocess/api/v2\");\nconst outbound = await api.queryAsync(\"Relation\", {\n  select: `{id:Slave.id,type:Slave.EntityType.Name,start:SlaveAssignable.PlannedStartDate,end:SlaveAssignable.PlannedEndDate}`,\n  where: `master.id=${args.ResourceId} and RelationType.Name in ${JSON.stringify(relations)}`\n});\nif (!relations || !relations.length) { return; }\n\nconst commands = [];\nconst dateLeft = new Date(args.Current.PlannedEndDate);\nfor (let relation of outbound) {\n  const dateRight = new Date(relation.start);\n  if (dateLeft > dateRight) { \n    let shift = differenceInDays(dateLeft, dateRight);\n    let start = dateLeft;\n    let end = relation.end ? addDays(relation.end, shift) : undefined;\n    commands.push({\n      command: \"targetprocess:UpdateResource\",\n      payload: {\n        resourceId: relation.id,\n        resourceType: relation.type,\n        fields: {\n          PlannedStartDate: start,\n          PlannedEndDate: end\n        }\n      }\n    });\n  }\n}\n\nreturn commands;"
  }
]

When connecting two items, make sure that they are planned in the good order

When you connect two items, this rule checks if Planned Start Date of the outbound dependency is later than Planned End Date of inbound dependency. If it's not true, outbound dependency is shifted to be started on Planned End Date of inbound dependency. Duration will be preserved.

11801180
[
  {
    "type": "source:targetprocess:EntityChanged",
    "entityTypes": [
      "Relation"
    ],
    "modifications": {
      "created": true,
      "deleted": false,
      "updated": false
    }
  },
  {
    "or": [
      {
        "and": [
          {
            "value": null,
            "target": {
              "name": "PlannedStartDate",
              "type": "field",
              "target": {
                "name": "Slave",
                "type": "field",
                "target": {
                  "type": "pipelineBlockOutput"
                }
              }
            },
            "operator": {
              "type": "exists"
            }
          },
          {
            "value": null,
            "target": {
              "name": "PlannedEndDate",
              "type": "field",
              "target": {
                "name": "Master",
                "type": "field",
                "target": {
                  "type": "pipelineBlockOutput"
                }
              }
            },
            "operator": {
              "type": "exists"
            }
          }
        ]
      }
    ],
    "type": "filter:Relational"
  },
  {
    "or": [
      {
        "and": [
          {
            "value": {
              "type": "constant",
              "value": "Blocker"
            },
            "target": {
              "name": "Name",
              "type": "field",
              "target": {
                "name": "RelationType",
                "type": "field",
                "target": {
                  "type": "pipelineBlockOutput"
                }
              }
            },
            "operator": {
              "type": "is"
            }
          }
        ]
      },
      {
        "and": [
          {
            "value": {
              "type": "constant",
              "value": "Dependency"
            },
            "target": {
              "name": "Name",
              "type": "field",
              "target": {
                "name": "RelationType",
                "type": "field",
                "target": {
                  "type": "pipelineBlockOutput"
                }
              }
            },
            "operator": {
              "type": "is"
            }
          }
        ]
      }
    ],
    "type": "filter:Relational"
  },
  {
    "type": "action:JavaScript",
    "script": "const MILLISECONDS_IN_DAY = 1000 * 3600 * 24;\n\nfunction differenceInDays(dateLeft, dateRight) {\n  return Math.round((dateLeft.getTime() - dateRight.getTime()) / MILLISECONDS_IN_DAY);\n}\n\nfunction addDays(date, days) {\n  const result = new Date(date);\n  result.setDate(result.getDate() + days);\n  return result;\n}\n\nconst api = context.getService(\"targetprocess/api/v2\");\nconst relations = await api.queryAsync(\"Relation\", {\n  select: `{masterEnd:MasterAssignable.PlannedEndDate,type:Slave.EntityType.Name,slaveStart:SlaveAssignable.PlannedStartDate,slaveEnd:SlaveAssignable.PlannedEndDate}`,\n  where: `id=${args.ResourceId}`\n});\nif (!relations || !relations.length) { return; }\nconst relation = relations[0];\n\nconst dateLeft = new Date(relation.masterEnd);\nconst dateRight = new Date(relation.slaveStart);\nif (dateLeft <= dateRight) { return; }\n\nconst shift = differenceInDays(dateLeft, dateRight);\nconst start = dateLeft;\nconst end = relation.slaveEnd ? addDays(relation.slaveEnd, shift) : undefined;\n\nreturn {\n  command: \"targetprocess:UpdateResource\",\n  payload: {\n    resourceId: args.Current.Slave.Id,\n    resourceType: relation.type,\n    fields: {\n      PlannedStartDate: start,\n      PlannedEndDate: end\n    }\n  }\n}"
  }
]