Draft: POC Custom status management API
What does this MR do and why?
The purpose of this MR is to build a POC for the management part of custom statuses. In particular the following:
-
🚧 Retrieve system-defined lifecycles and statuses -
✅ Retrieve custom lifecycles and statuses -
✅ Create and manage lifecycles with statuses -
✅ Create and update statuses as standalone entities -
🚧 migration from system-defined statuses to custom statuses -
🚧 general migration of statuses due to either- status is deleted from lifecycle (map to replacement)
- work item type is applied to different lifecycle (map statuses that don't exist in old lifecycle to replacement)
Contributes to #536365 (closed)
View lifecycles and statuses
No matter whether the namespace uses system-defined statuses or custom statuses there should be a single endpoint that returns all necessary data.
The current state of the POC only returns custom statuses. That will be changed shortly.
Use this GraphQL query:
Click to expand
query getLifecycles {
namespace(fullPath: "flightjs") {
id
lifecycles {
nodes {
id
name
defaultOpenStatus {
id
}
defaultClosedStatus {
id
}
defaultDuplicateStatus {
id
}
workItemTypes {
id
name
iconName
}
statuses {
id
name
iconName
color
}
}
}
}
}
See example output with system-defined lifecycles/statuses:
Click to expand
{
"data": {
"namespace": {
"id": "gid://gitlab/Group/24",
"lifecycles": {
"nodes": [
{
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Lifecycle/1",
"name": "Default",
"defaultOpenStatus": {
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1"
},
"defaultClosedStatus": {
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3"
},
"defaultDuplicateStatus": {
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5"
},
"workItemTypes": [
{
"id": "gid://gitlab/WorkItems::Type/1",
"name": "Issue",
"iconName": "issue-type-issue"
},
{
"id": "gid://gitlab/WorkItems::Type/5",
"name": "Task",
"iconName": "issue-type-task"
}
],
"statuses": [
{
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1",
"name": "To do",
"iconName": "status-waiting",
"color": "#737278"
},
{
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2",
"name": "In progress",
"iconName": "status-running",
"color": "#1f75cb"
},
{
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3",
"name": "Done",
"iconName": "status-success",
"color": "#108548"
},
{
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/4",
"name": "Won't do",
"iconName": "status-cancelled",
"color": "#DD2B0E"
},
{
"id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5",
"name": "Duplicate",
"iconName": "status-cancelled",
"color": "#DD2B0E"
}
]
}
]
}
}
},
"correlationId": "01JSEWPVBZMKRQFZPFF0FHW09J"
}
See example output with custom statuses:
Click to expand
{
"data": {
"namespace": {
"id": "gid://gitlab/Group/33",
"lifecycles": {
"nodes": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/12",
"name": "Another lifecycle",
"defaultOpenStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
},
"defaultClosedStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"defaultDuplicateStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/49"
},
"workItemTypes": [
{
"id": "gid://gitlab/WorkItems::Type/5",
"name": "Task",
"iconName": "issue-type-task"
}
],
"statuses": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
"name": "Neu",
"iconName": "status-waiting",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
"name": "In Arbeit",
"iconName": "status-running",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/49",
"name": "Nö",
"iconName": "status-cancelled",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
"name": "Erledigttt",
"iconName": "status-success",
"color": "#ff3300"
}
]
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/11",
"name": "API Lifecycle for the win",
"defaultOpenStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
},
"defaultClosedStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50"
},
"defaultDuplicateStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"workItemTypes": [
{
"id": "gid://gitlab/WorkItems::Type/5",
"name": "Task",
"iconName": "issue-type-task"
}
],
"statuses": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
"name": "Neu",
"iconName": "status-waiting",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
"name": "In Arbeit",
"iconName": "status-running",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
"name": "Erledigttt",
"iconName": "status-success",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50",
"name": "Machenwanich",
"iconName": "status-cancelled",
"color": "#00ff00"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/51",
"name": "Unused",
"iconName": "status-neutral",
"color": "#000000"
}
]
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/9",
"name": "Super duper lifecycle",
"defaultOpenStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
},
"defaultClosedStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"defaultDuplicateStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"workItemTypes": [
{
"id": "gid://gitlab/WorkItems::Type/5",
"name": "Task",
"iconName": "issue-type-task"
}
],
"statuses": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
"name": "Neu",
"iconName": "status-waiting",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
"name": "In Arbeit",
"iconName": "status-running",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
"name": "Erledigttt",
"iconName": "status-success",
"color": "#ff3300"
}
]
}
]
}
}
},
"correlationId": "01JS23BHJ4QSD6CEYN9CX0VEQH"
}
Get statuses of namespace
Depending on how we decide to create/update statuses in the management UI, we might need to fetch a list of existing statuses to attach to a lifecycle. We could maybe mask that by letting the update mutation handle that. So still TBD.
Example query:
Click to expand
query getStatuses {
namespace(fullPath: "flightjs") {
id
statuses {
nodes {
id
name
iconName
color
category
description
}
}
}
}
Example output:
Click to expand
{
"data": {
"namespace": {
"id": "gid://gitlab/Group/33",
"statuses": {
"nodes": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/51",
"name": "Unused",
"iconName": "status-neutral",
"color": "#000000",
"category": "TRIAGE",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50",
"name": "Machenwanich",
"iconName": "status-cancelled",
"color": "#00ff00",
"category": "CANCELLED",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/49",
"name": "Nö",
"iconName": "status-cancelled",
"color": "#ff3300",
"category": "CANCELLED",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
"name": "Erledigttt",
"iconName": "status-success",
"color": "#ff3300",
"category": "DONE",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
"name": "In Arbeit",
"iconName": "status-running",
"color": "#ff3300",
"category": "IN_PROGRESS",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
"name": "Neu",
"iconName": "status-waiting",
"color": "#ff3300",
"category": "TO_DO",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/6",
"name": "Updated via API",
"iconName": "status-waiting",
"color": "#5CB85C",
"category": "TO_DO",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/5",
"name": "Created via API second edition",
"iconName": "status-waiting",
"color": "#5CB85C",
"category": "TO_DO",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/4",
"name": "Created via API",
"iconName": "status-waiting",
"color": "#5CB85C",
"category": "TO_DO",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/3",
"name": "Duplicate",
"iconName": "status-cancelled",
"color": "#DD2B0E",
"category": "CANCELLED",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/2",
"name": "Done",
"iconName": "status-success",
"color": "#108548",
"category": "DONE",
"description": null
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/1",
"name": "To do",
"iconName": "status-waiting",
"color": "#737278",
"category": "TO_DO",
"description": null
}
]
}
}
},
"correlationId": "01JS23HSDHTGMXKFQ852WTPNEM"
}
Create status
Again depending on which option we want to use, we might want to create statuses using a dedicated endpoint or create them with updating/creating lifecycles. This showcases how a status create could look like:
Example mutation:
Click to expand
mutation createStatus {
statusesCreate(input: {
groupPath: "flightjs",
name: "Created via API fourth edition",
category: TO_DO,
color: "#5CB85C",
description: "This is a good status."
}) {
status {
id
name
category
color
iconName
description
}
errors
}
}
Example output:
{
"data": {
"statusesCreate": {
"status": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/52",
"name": "Created via API fourth edition",
"category": "TO_DO",
"color": "#5CB85C",
"iconName": "status-waiting",
"description": "This is a good status."
},
"errors": []
}
},
"correlationId": "01JS23Q1RD50S752BVKC43RHBK"
}
Update status
Example mutation:
Click to expand
mutation updateStatus {
statusesUpdate(input: {
groupPath: "flightjs",
id: "gid://gitlab/WorkItems::Statuses::Custom::Status/6"
name: "Updated via APIII",
category: TO_DO,
color: "#5CB85C",
description: "This is a good status."
}) {
status {
id
name
category
color
iconName
description
}
errors
}
}
Example output:
Click to expand
{
"data": {
"statusesUpdate": {
"status": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/6",
"name": "Updated via APIII",
"category": "TO_DO",
"color": "#5CB85C",
"iconName": "status-waiting",
"description": "This is a good status."
},
"errors": []
}
},
"correlationId": "01JS23RT2T577C93R9RVGV42WH"
}
Create lifecycle
This is an alternative proposal. Instead of creating and updating statuses using dedicated endpoints, we could also create and update them together with the lifecycle.
This would map nicely with the structure of the query and allow the frontend to just push updates that have been done for the lifecycle without the need to perform separate mutations. More on that on the update mutation.
Please note that the default statuses are indexes of the given status array! More on that on the update mutation.
Example mutation:
Click to expand
mutation createLifecycle {
lifecyclesCreate(input: {
groupPath: "flightjs",
name: "Super duper lifecycle",
statuses: [
{
name: "Neu",
category: TO_DO,
color: "#ff3300"
},
{
# Can also be referenced by name. And update a single attribute (color).
name: "In Arbeit",
color: "#ff3300"
},
{
# If name doesn't reference an existing status, it'll create a new one.
name: "Erledigt",
category: :DONE
color: "#ff3300"
},
{
# Can also be an existing status referenced by ID
id: "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
],
workItemTypeIds: [
"gid://gitlab/WorkItems::Type/5",
],
defaultOpenStatusIndex: 0,
defaultClosedStatusIndex: 2,
defaultDuplicateStatusIndex: 2
}) {
lifecycle {
id
name
defaultOpenStatus {
id
}
defaultClosedStatus {
id
}
defaultDuplicateStatus {
id
}
workItemTypes {
id
name
iconName
}
statuses {
id
name
iconName
color
}
}
errors
}
}
Example output:
Click to expand
{
"data": {
"lifecyclesCreate": {
"lifecycle": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/9",
"name": "Super duper lifecycle",
"defaultOpenStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
},
"defaultClosedStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"defaultDuplicateStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"workItemTypes": [
{
"id": "gid://gitlab/WorkItems::Type/5",
"name": "Task",
"iconName": "issue-type-task"
}
],
"statuses": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
"name": "Neu",
"iconName": "status-waiting",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
"name": "In Arbeit",
"iconName": "status-running",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
"name": "Erledigt",
"iconName": "status-success",
"color": "#ff3300"
}
]
},
"errors": []
}
},
"correlationId": "01JRZEQXDY93HHX834Q63DJ57D"
}
Update lifecycle and statuses
Add statuses to lifecycle (existing and new and update attributes)
From the frontend perspective you can just send everything over that you have and the backend will handle the rest. For example:
- Reference a status by ID, BE assume it exists. You provide additional attributes? BE assumes you want to change those.
- Reference status by name, BE checks whether it exists (if yes, also updates given attributes), if not creates it.
- You don't provide any statuses? Statuses remain unchanged. But you cannot set default (open/closed/duplicate) statuses.
Delete a status
Simply pass the status array without that status. The status will be unassigned from the lifecycle.
Status reordering and position
Default (open/closed/duplicate) statuses
They are defined by it's index in the statuses array. Why? Because this way we can also set a non existing status as open default.
Work item types
You just define the attached work item types.
Map old to new statuses
There are two actions that may make a migration necessary:
- status is deleted from lifecycle (map to replacement)
- work item type is applied to different lifecycle (map statuses that don't exist in old lifecycle to replacement)
You could add a mapping (from old status to new status) to the update mutation and only if that exists, those actions above would be performed. The mapping enqueues a status migration which will be handled in a background job.
Migrate from system-defined statuses to custom statuses
To reduce the complexity for the frontend, it should handle GIDs of system defined statuses as if they were custom statuses (same for lifecycles). If the BE receives a mutation that a) contains a system-defined status/lifecycle GID or b) is for a namespace that doesn't have custom statuses, it would first migrate system-defined statuses to custom statuses and them process the request. To make sure this works properly we'd then change the params of the request from the system-defined GIDs to the now newly created custom GIDs and then continue as normal.
This way the flow stays the same and system-defined GIDs can be treated the same. When the frontend then applies the result to it's data tree, it'll only use custom entities.
Click to expand
mutation updateLifecycle {
lifecyclesUpdate(input: {
groupPath: "flightjs",
id: "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/11"
statuses: [
{
id: "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
},
{
id: "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
{
id: "gid://gitlab/WorkItems::Statuses::Custom::Status/47"
},
{
id: "gid://gitlab/WorkItems::Statuses::Custom::Status/50"
},
{
name: "Unused"
category: TRIAGE
color: "#000000"
}
]
defaultOpenStatusIndex: 0,
defaultClosedStatusIndex: 2,
defaultDuplicateStatusIndex: 3
workItemTypeIds: [
"gid://gitlab/WorkItems::Type/5"
]
}) {
lifecycle {
id
name
defaultOpenStatus {
id
}
defaultClosedStatus {
id
}
defaultDuplicateStatus {
id
}
workItemTypes {
id
name
iconName
}
statuses {
id
name
iconName
color
}
}
errors
}
}
Example output:
Click to expand
{
"data": {
"lifecyclesUpdate": {
"lifecycle": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/11",
"name": "API Lifecycle for the win",
"defaultOpenStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
},
"defaultClosedStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50"
},
"defaultDuplicateStatus": {
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
},
"workItemTypes": [
{
"id": "gid://gitlab/WorkItems::Type/5",
"name": "Task",
"iconName": "issue-type-task"
}
],
"statuses": [
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
"name": "Neu",
"iconName": "status-waiting",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
"name": "In Arbeit",
"iconName": "status-running",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
"name": "Erledigttt",
"iconName": "status-success",
"color": "#ff3300"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50",
"name": "Machenwanich",
"iconName": "status-cancelled",
"color": "#00ff00"
},
{
"id": "gid://gitlab/WorkItems::Statuses::Custom::Status/51",
"name": "Unused",
"iconName": "status-neutral",
"color": "#000000"
}
]
},
"errors": []
}
},
"correlationId": "01JS21XR76W8VYEB52QPM1CG9N"
}
References
How to set up and validate locally
- Enable the
work_item_status_feature_flag
- Open http://127.0.0.1:3000/-/graphql-explorer
- Start trying out the queries and mutations. Because they only work for custom entities right now, I'd suggest to start with creating statuses/lifecycles first.
- Please also try out the several options of the lifecyclesUpdate mutation.