gitlab-ce/app/assets/javascripts/graphql_shared/issuable_client.js

656 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import produce from 'immer';
import VueApollo from 'vue-apollo';
import { concatPagination } from '@apollo/client/utilities';
import errorQuery from '~/boards/graphql/client/error.query.graphql';
import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
import {
WIDGET_TYPE_NOTES,
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_CUSTOM_FIELDS,
WIDGET_TYPE_LINKED_ITEMS,
CUSTOM_FIELDS_TYPE_NUMBER,
CUSTOM_FIELDS_TYPE_TEXT,
CUSTOM_FIELDS_TYPE_SINGLE_SELECT,
CUSTOM_FIELDS_TYPE_MULTI_SELECT,
} from '~/work_items/constants';
import isExpandedHierarchyTreeChildQuery from '~/work_items/graphql/client/is_expanded_hierarchy_tree_child.query.graphql';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import activeDiscussionQuery from '~/work_items/components/design_management/graphql/client/active_design_discussion.query.graphql';
import { updateNewWorkItemCache, workItemBulkEdit } from '~/work_items/graphql/resolvers';
export const config = {
typeDefs,
cacheConfig: {
typePolicies: {
Query: {
fields: {
isShowingLabels: {
read(currentState) {
return currentState ?? true;
},
},
selectedBoardItems: {
read(currentState) {
return currentState ?? [];
},
},
boardList: {
keyArgs: ['id'],
},
epicBoardList: {
keyArgs: ['id'],
},
isExpandedHierarchyTreeChild: (_, { variables, toReference }) =>
toReference({ __typename: 'LocalWorkItemChildIsExpanded', id: variables.id }),
},
},
DesignManagement: {
merge(existing = {}, incoming) {
return { ...existing, ...incoming };
},
},
WorkItemDescriptionTemplateConnection: {
fields: {
nodes: {
read(_, { variables }) {
const templates = [
/* eslint-disable @gitlab/require-i18n-strings */
{ name: 'template 1', content: 'A template' },
{ name: 'template 2', content: 'Another template' },
{ name: 'template 3', content: 'Secret template omg wow' },
{ name: 'template 4', content: 'Another another template' },
/* eslint-enable @gitlab/require-i18n-strings */
];
if (variables.search) {
return templates.filter(({ name }) => name.includes(variables.search));
}
if (variables.name) {
return templates.filter(({ name }) => name === variables.name);
}
return templates;
},
},
},
},
Project: {
fields: {
projectMembers: {
keyArgs: ['fullPath', 'search', 'relations', 'first'],
},
},
},
Namespace: {
fields: {
merge: true,
},
},
WorkItemWidgetNotes: {
fields: {
// If we add any key args, the discussions field becomes discussions({"filter":"ONLY_ACTIVITY","first":10}) and
// kills any possibility to handle it on the widget level without hardcoding a string.
discussions: {
keyArgs: false,
},
},
},
WorkItemWidgetAwardEmoji: {
fields: {
// If we add any key args, the awardEmoji field becomes awardEmoji({"first":10}) and
// kills any possibility to handle it on the widget level without hardcoding a string.
awardEmoji: {
keyArgs: false,
},
},
},
WorkItemWidgetProgress: {
fields: {
progress: {
// We want to show null progress as 0 as per https://gitlab.com/gitlab-org/gitlab/-/issues/386117
read(existing) {
return existing === null ? 0 : existing;
},
},
},
},
DescriptionVersion: {
fields: {
startVersionId: {
read() {
// we need to set this when fetching the diff in the last 10 mins , the starting diff will be the very first one , so need to save it
return '';
},
},
},
},
WorkItemWidgetHierarchy: {
fields: {
// If we add any key args, the children field becomes children({"first":10}) and
// kills any possibility to handle it on the widget level without hardcoding a string.
children: {
keyArgs: false,
},
},
},
WorkItem: {
fields: {
// @todo: Mocking CUSTOM_FIELDS widget while not suported by backend
mockWidgets: {
read() {
return [
{
__typename: 'LocalWorkItemCustomFields',
type: WIDGET_TYPE_CUSTOM_FIELDS,
customFieldValues: [
{
id: 'gid://gitlab/CustomFieldValue/1',
customField: {
id: '1-number',
fieldType: CUSTOM_FIELDS_TYPE_NUMBER,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Number custom field label',
__typename: 'LocalWorkItemCustomField',
},
value: 5,
__typename: 'LocalWorkItemNumberFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/2',
customField: {
id: '2-number',
fieldType: CUSTOM_FIELDS_TYPE_NUMBER,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Null Number custom field label',
__typename: 'LocalWorkItemCustomField',
},
value: null,
__typename: 'LocalWorkItemNumberFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/3',
customField: {
id: '1-text',
fieldType: CUSTOM_FIELDS_TYPE_TEXT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Text custom field label',
__typename: 'LocalWorkItemCustomField',
},
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'some text',
__typename: 'LocalWorkItemTextFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/4',
customField: {
id: '11-text',
fieldType: CUSTOM_FIELDS_TYPE_TEXT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Long Text custom field label',
__typename: 'LocalWorkItemCustomField',
},
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'some long long lo ng long long long long long long texttt ng long long long long long long texttt ng long long long long long long texttt ng long long long long texttt some long long long long long long long texttt some long long long long long long long texttt some long long long long long long long texttt some long long long long long long long texttt some long long long long long long long texttt some long long long long long long long texttt some long long long long long long long texttt some long long long long long long',
__typename: 'LocalWorkItemTextFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/5',
customField: {
id: '2-text',
fieldType: CUSTOM_FIELDS_TYPE_TEXT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Link Text custom field label',
__typename: 'LocalWorkItemCustomField',
},
value: 'https://gitlab.com/gitlab-org/gitlab/-/work_items/41',
__typename: 'LocalWorkItemTextFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/6',
customField: {
id: '3-text',
fieldType: CUSTOM_FIELDS_TYPE_TEXT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Null Text custom field label',
__typename: 'LocalWorkItemCustomField',
},
value: null,
__typename: 'LocalWorkItemTextFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/7',
customField: {
id: '4-text',
fieldType: CUSTOM_FIELDS_TYPE_TEXT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Empty string Text custom field label',
__typename: 'LocalWorkItemCustomField',
},
value: '',
__typename: 'LocalWorkItemTextFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/8',
customField: {
id: '1-select',
fieldType: CUSTOM_FIELDS_TYPE_SINGLE_SELECT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Single select custom field label',
selectOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is longlonglongonglonglonglonglonglong',
},
{
id: 'select-2',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 2',
},
{
id: 'select-3',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 3',
},
],
__typename: 'LocalWorkItemCustomFieldSelect',
},
selectedOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is longlonglongonglonglonglonglonglong',
},
],
__typename: 'LocalWorkItemSelectFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/9',
customField: {
id: '2-select',
fieldType: CUSTOM_FIELDS_TYPE_SINGLE_SELECT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Null Single select custom field label',
selectOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is long long lo ng long long long long long long',
},
{
id: 'select-2',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 2',
},
{
id: 'select-3',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 3',
},
],
__typename: 'LocalWorkItemCustomFieldSelect',
},
selectedOptions: null,
__typename: 'LocalWorkItemSelectFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/10',
customField: {
id: '1-multi-select',
fieldType: CUSTOM_FIELDS_TYPE_MULTI_SELECT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Multi select custom field label',
selectOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is long long lo ng long long long long long long',
},
{
id: 'select-2',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 2',
},
{
id: 'select-3',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 3',
},
],
__typename: 'LocalWorkItemCustomFieldSelect',
},
selectedOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is long long lo ng long long long long long long',
},
{
id: 'select-2',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 2',
},
],
__typename: 'LocalWorkItemSelectFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/11',
customField: {
id: '2-multi-select',
fieldType: CUSTOM_FIELDS_TYPE_MULTI_SELECT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Null Multi select custom field label',
selectOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is long long lo ng long long long long long long',
},
{
id: 'select-2',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 2',
},
{
id: 'select-3',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 3',
},
],
__typename: 'LocalWorkItemCustomFieldSelect',
},
selectedOptions: null,
__typename: 'LocalWorkItemSelectFieldValue',
},
{
id: 'gid://gitlab/CustomFieldValue/12',
customField: {
id: '3-multi-select',
fieldType: CUSTOM_FIELDS_TYPE_MULTI_SELECT,
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'One selected Multi select custom field label',
selectOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is long long lo ng long long long long long long',
},
{
id: 'select-2',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 2',
},
{
id: 'select-3',
// eslint-disable-next-line @gitlab/require-i18n-strings
value: 'Option 3',
},
],
__typename: 'LocalWorkItemCustomFieldSelect',
},
selectedOptions: [
{
id: 'select-1',
value:
// eslint-disable-next-line @gitlab/require-i18n-strings
'Option 1 is long long lo ng long long long long long long',
},
],
__typename: 'LocalWorkItemSelectFieldValue',
},
],
},
];
},
},
// widgets policy because otherwise the subscriptions invalidate the cache
widgets: {
merge(existing = [], incoming, context) {
if (existing.length === 0) {
return incoming;
}
return existing.map((existingWidget) => {
const incomingWidget = incoming.find(
(w) => w.type && w.type === existingWidget.type,
);
// we want to concat next page of awardEmoji to the existing ones
if (incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI && context.variables.after) {
// concatPagination won't work because we were placing new widget here so we have to do this manually
return {
...incomingWidget,
awardEmoji: {
...incomingWidget.awardEmoji,
nodes: [
...existingWidget.awardEmoji.nodes,
...incomingWidget.awardEmoji.nodes,
],
},
};
}
// we want to concat next page of discussions to the existing ones
if (incomingWidget?.type === WIDGET_TYPE_NOTES && context.variables.after) {
// concatPagination won't work because we were placing new widget here so we have to do this manually
return {
...incomingWidget,
discussions: {
...incomingWidget.discussions,
nodes: [
...existingWidget.discussions.nodes,
...incomingWidget.discussions.nodes,
],
},
};
}
// we want to concat next page of children work items within Hierarchy widget to the existing ones
if (
incomingWidget?.type === WIDGET_TYPE_HIERARCHY &&
context.variables.endCursor &&
incomingWidget.children?.nodes
) {
// concatPagination won't work because we were placing new widget here so we have to do this manually
return {
...incomingWidget,
children: {
...incomingWidget.children,
nodes: [...existingWidget.children.nodes, ...incomingWidget.children.nodes],
},
};
}
// this ensures that we dont override linkedItems.workItem when updating parent
if (incomingWidget?.type === WIDGET_TYPE_LINKED_ITEMS) {
if (!incomingWidget.linkedItems) {
return existingWidget;
}
const incomindNodes = incomingWidget.linkedItems?.nodes || [];
const existingNodes = existingWidget.linkedItems?.nodes || [];
const resultNodes = incomindNodes.map((incomingNode) => {
const existingNode =
existingNodes.find((n) => n.linkId === incomingNode.linkId) ?? {};
return { ...existingNode, ...incomingNode };
});
return {
...incomingWidget,
linkedItems: {
...incomingWidget.linkedItems,
nodes: resultNodes,
},
};
}
return { ...existingWidget, ...incomingWidget };
});
},
},
},
},
MemberInterfaceConnection: {
fields: {
nodes: concatPagination(),
},
},
Group: {
fields: {
projects: {
keyArgs: ['includeSubgroups', 'search'],
},
descendantGroups: {
keyArgs: ['includeSubgroups', 'search'],
},
},
},
ProjectConnection: {
fields: {
nodes: concatPagination(),
},
},
GroupConnection: {
fields: {
nodes: concatPagination(),
},
},
MergeRequestApprovalState: {
merge: true,
},
WorkItemType: {
// this prevents child and parent work item types from overriding each other
fields: {
widgetDefinitions: {
merge(existing = [], incoming) {
if (existing.length === 0) {
return incoming;
}
return existing.map((existingWidget) => {
const incomingWidget = incoming.find(
(w) => w.type && w.type === existingWidget.type,
);
return { ...existingWidget, ...incomingWidget };
});
},
},
},
},
},
},
};
export const resolvers = {
Mutation: {
updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => {
const sourceData = cache.readQuery({ query: getIssueStateQuery });
const data = produce(sourceData, (draftData) => {
draftData.issueState = { issueType, isDirty };
});
cache.writeQuery({ query: getIssueStateQuery, data });
},
setActiveBoardItem(_, { boardItem, listId }, { cache }) {
cache.writeQuery({
query: activeBoardItemQuery,
data: { activeBoardItem: { ...boardItem, listId } },
});
return { ...boardItem, listId };
},
setSelectedBoardItems(_, { itemId }, { cache }) {
const sourceData = cache.readQuery({ query: selectedBoardItemsQuery });
cache.writeQuery({
query: selectedBoardItemsQuery,
data: { selectedBoardItems: [...sourceData.selectedBoardItems, itemId] },
});
return [...sourceData.selectedBoardItems, itemId];
},
unsetSelectedBoardItems(_, _variables, { cache }) {
cache.writeQuery({
query: selectedBoardItemsQuery,
data: { selectedBoardItems: [] },
});
return [];
},
setError(_, { error }, { cache }) {
cache.writeQuery({
query: errorQuery,
data: { boardsAppError: error },
});
return error;
},
clientToggleListCollapsed(_, { list = {}, collapsed = false }) {
return {
list: {
...list,
collapsed,
},
};
},
clientToggleEpicListCollapsed(_, { list = {}, collapsed = false }) {
return {
list: {
...list,
collapsed,
},
};
},
setIsShowingLabels(_, { isShowingLabels }, { cache }) {
cache.writeQuery({
query: isShowingLabelsQuery,
data: { isShowingLabels },
});
return isShowingLabels;
},
updateNewWorkItem(_, { input }, { cache }) {
updateNewWorkItemCache(input, cache);
},
localWorkItemBulkUpdate(_, { input }) {
return workItemBulkEdit(input);
},
toggleHierarchyTreeChild(_, { id, isExpanded = false }, { cache }) {
cache.writeQuery({
query: isExpandedHierarchyTreeChildQuery,
variables: { id },
data: {
isExpandedHierarchyTreeChild: {
id,
isExpanded,
__typename: 'LocalWorkItemChildIsExpanded',
},
},
});
},
updateActiveDesignDiscussion: (_, { id = null, source }, { cache }) => {
const data = {
activeDesignDiscussion: {
__typename: 'ActiveDesignDiscussion',
id,
source,
},
};
cache.writeQuery({ query: activeDiscussionQuery, data });
},
},
};
export const defaultClient = createDefaultClient(resolvers, config);
export const apolloProvider = new VueApollo({
defaultClient,
});