Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | 25x 25x 25x 25x 100x 75x 100x 75x 25x 25x 25x 25x 25x 40x 38x 38x 20x 2x 16x 16x 4x 4x 104x 104x 103x 103x 102x 2x 25x 21x 21x 21x 21x 21x 21x 21x 20x 19x 19x 16x 16x 13x 13x 13x 13x 1x 12x 10x | import { gql } from 'graphql-tag';
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { ConnectionManager } from '../../services/ConnectionManager';
import { getGitLabApiUrlFromContext } from '../../oauth/token-context';
import {
cleanWorkItemResponse,
extractSimpleId,
toGid,
type GitLabWorkItem,
} from '../../utils/idConversion';
import type { GraphQLClient } from '../../graphql/client';
import type { ReorderWorkItemInput } from './schema';
interface Boards {
nodes: Array<{ id: string } | null>;
}
interface Project {
id: string;
fullPath: string;
boards?: Boards | null;
group: { fullPath: string } | null;
}
interface Item {
id: string;
iid: string;
project: Project | null;
}
interface Context {
target: Item | null;
before?: Item | null;
after?: Item | null;
}
type Variables = { id: string; beforeId?: string; afterId?: string };
// Only constant aliases enter these documents. Compile each bounded shape once, not per call.
function contextQuery(
neighbours: readonly ('before' | 'after')[],
): TypedDocumentNode<Context, Variables> {
const definitions = neighbours.map((name) => `$${name}Id: WorkItemID!`).join(', ');
const fields = neighbours
.map(
(name) => `${name}: workItem(id: $${name}Id) {
id iid project { id fullPath group { fullPath } }
}`,
)
.join('\n');
return gql`query ReorderWorkItemContext($id: WorkItemID!, ${definitions}) {
target: workItem(id: $id) {
id iid project { id fullPath boards(first: 1) { nodes { id } } group { fullPath } }
}
${fields}
}`;
}
const BEFORE = contextQuery(['before']);
const AFTER = contextQuery(['after']);
const BETWEEN = contextQuery(['before', 'after']);
const GROUP_BOARD: TypedDocumentNode<
{
group: { boards: Boards | null; parent: { fullPath: string } | null } | null;
},
{ path: string }
> = gql`
query ReorderGroupBoard($path: ID!) {
group(fullPath: $path) {
boards(first: 1) {
nodes {
id
}
}
parent {
fullPath
}
}
}
`;
const MOVE: TypedDocumentNode<
{
issueMoveList: { issue: GitLabWorkItem | null; errors: string[] } | null;
},
{ input: Record<string, string> }
> = gql`
mutation ReorderIssueCard($input: IssueMoveListInput!) {
issueMoveList(input: $input) {
issue {
id
iid
title
state
webUrl
relativePosition
}
errors
}
}
`;
function projectOf(item: Item | null | undefined): Project {
if (!item?.project) throw new Error('Issue is unavailable or is not a project work item');
return item.project;
}
/** GitLab 16.0 Issue.relative_positioning_parent_projects: root group, or one personal project. */
function positioningScope(project: Project): string {
return project.group ? project.fullPath.slice(0, project.fullPath.indexOf('/')) : project.id;
}
function checkNeighbour(item: Item | null | undefined, scope: string): void {
if (positioningScope(projectOf(item)) !== scope)
throw new Error('Neighbour is outside the issue manual-ordering namespace');
}
async function findBoard(client: GraphQLClient, project: Project): Promise<string> {
const own = project.boards?.nodes[0];
if (own) return own.id;
let path = project.group?.fullPath;
// Bound traversals even if an inconsistent upstream response contains a parent cycle.
for (let requests = 0; path !== undefined && requests < 100; requests++) {
const { group } = await client.request(GROUP_BOARD, { path });
if (!group) throw new Error('Board discovery could not read the issue namespace');
const board = group.boards?.nodes[0];
if (board) return board.id;
path = group.parent?.fullPath;
}
throw new Error('No accessible issue board found within the board discovery limit');
}
/** Reordering is issue-relative, shared by boards; an accessible related board supplies context. */
export async function reorderWorkItem(input: ReorderWorkItemInput): Promise<GitLabWorkItem> {
const client = ConnectionManager.getInstance().getClient(getGitLabApiUrlFromContext());
const variables: Variables = { id: toGid(input.id, 'WorkItem') };
if (input.beforeId !== undefined) variables.beforeId = toGid(input.beforeId, 'WorkItem');
if (input.afterId !== undefined) variables.afterId = toGid(input.afterId, 'WorkItem');
const document =
input.beforeId === undefined ? AFTER : input.afterId === undefined ? BEFORE : BETWEEN;
const context = await client.request(document, variables);
if (!context.target) throw new Error('Issue is unavailable');
const project = projectOf(context.target);
const scope = positioningScope(project);
if (input.beforeId !== undefined) checkNeighbour(context.before, scope);
if (input.afterId !== undefined) checkNeighbour(context.after, scope);
const mutation: Record<string, string> = {
boardId: await findBoard(client, project),
projectPath: project.fullPath,
iid: context.target.iid,
};
// GitLab's moveAfterId names the issue AFTER the moved card, not the card to move after.
// v16.0.0-ee/app/services/boards/base_item_move_service.rb:105-110.
if (input.beforeId !== undefined) mutation.moveAfterId = extractSimpleId(input.beforeId);
if (input.afterId !== undefined) mutation.moveBeforeId = extractSimpleId(input.afterId);
const response = await client.request(MOVE, { input: mutation });
if (response.issueMoveList?.errors.length)
throw new Error(`GitLab GraphQL errors: ${response.issueMoveList.errors.join(', ')}`);
if (!response.issueMoveList?.issue) throw new Error('Issue reorder failed: no issue returned');
return cleanWorkItemResponse(response.issueMoveList.issue);
}
|