Skip to content

Commit 0cee6f1

Browse files
author
GitLab Bot
committed
Add latest changes from gitlab-org/gitlab@master
1 parent dcd0161 commit 0cee6f1

File tree

86 files changed

+10278
-196
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+10278
-196
lines changed

app/assets/javascripts/issues/list/components/issues_list_app.vue

+30-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import CsvImportExportButtons from '~/issuable/components/csv_import_export_butt
1414
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
1515
import { IssuableStatus } from '~/issues/constants';
1616
import axios from '~/lib/utils/axios_utils';
17+
import { fetchPolicies } from '~/lib/graphql';
1718
import { isPositiveInteger } from '~/lib/utils/number_utils';
1819
import { scrollUp } from '~/lib/utils/scroll_utils';
1920
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
@@ -191,8 +192,15 @@ export default {
191192
update(data) {
192193
return data[this.namespace]?.issues.nodes ?? [];
193194
},
195+
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
196+
// We need this for handling loading state when using frontend cache
197+
// See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106004#note_1217325202 for details
198+
notifyOnNetworkStatusChange: true,
194199
result({ data }) {
195-
this.pageInfo = data?.[this.namespace]?.issues.pageInfo ?? {};
200+
if (!data) {
201+
return;
202+
}
203+
this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
196204
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
197205
},
198206
error(error) {
@@ -355,6 +363,7 @@ export default {
355363
token: LabelToken,
356364
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
357365
fetchLabels: this.fetchLabels,
366+
fetchLatestLabels: this.glFeatures.frontendCaching ? this.fetchLatestLabels : null,
358367
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
359368
},
360369
{
@@ -477,6 +486,17 @@ export default {
477486
shouldDisableTextSearch() {
478487
return this.isAnonymousSearchDisabled && !this.isSignedIn;
479488
},
489+
// due to the issues with cache-and-network, we need this hack to check if there is any data for the query in the cache.
490+
// if we have cached data, we disregard the loading state
491+
isLoading() {
492+
return (
493+
this.$apollo.queries.issues.loading &&
494+
!this.$apollo.provider.clients.defaultClient.readQuery({
495+
query: getIssuesQuery,
496+
variables: this.queryVariables,
497+
})
498+
);
499+
},
480500
},
481501
watch: {
482502
$route(newValue, oldValue) {
@@ -515,11 +535,12 @@ export default {
515535
fetchReleases(search) {
516536
return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search);
517537
},
518-
fetchLabels(search) {
538+
fetchLabelsWithFetchPolicy(search, fetchPolicy = fetchPolicies.CACHE_FIRST) {
519539
return this.$apollo
520540
.query({
521541
query: searchLabelsQuery,
522542
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
543+
fetchPolicy,
523544
})
524545
.then(({ data }) => data[this.namespace]?.labels.nodes)
525546
.then((labels) =>
@@ -528,6 +549,12 @@ export default {
528549
labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())),
529550
);
530551
},
552+
fetchLabels(search) {
553+
return this.fetchLabelsWithFetchPolicy(search);
554+
},
555+
fetchLatestLabels(search) {
556+
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
557+
},
531558
fetchMilestones(search) {
532559
return this.$apollo
533560
.query({
@@ -774,7 +801,7 @@ export default {
774801
:current-tab="state"
775802
:tab-counts="tabCounts"
776803
:truncate-counts="!isProject"
777-
:issuables-loading="$apollo.queries.issues.loading"
804+
:issuables-loading="isLoading"
778805
:is-manual-ordering="isManualOrdering"
779806
:show-bulk-edit-sidebar="showBulkEditSidebar"
780807
:show-pagination-controls="showPaginationControls"

app/assets/javascripts/issues/list/graphql.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ const resolvers = {
2222
},
2323
};
2424

25-
export const gqlClient = createDefaultClient(resolvers);
25+
export const gqlClient = gon.features?.frontendCaching
26+
? createDefaultClient(resolvers, { localCacheKey: 'issues_list' })
27+
: createDefaultClient(resolvers);

app/assets/javascripts/issues/list/queries/get_issues.query.graphql

+6-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ query getIssues(
3131
$firstPageSize: Int
3232
$lastPageSize: Int
3333
) {
34-
group(fullPath: $fullPath) @skip(if: $isProject) {
34+
group(fullPath: $fullPath) @skip(if: $isProject) @persist {
3535
id
3636
issues(
3737
includeSubgroups: true
@@ -58,16 +58,18 @@ query getIssues(
5858
first: $firstPageSize
5959
last: $lastPageSize
6060
) {
61+
__persist
6162
pageInfo {
6263
...PageInfo
6364
}
6465
nodes {
66+
__persist
6567
...IssueFragment
6668
reference(full: true)
6769
}
6870
}
6971
}
70-
project(fullPath: $fullPath) @include(if: $isProject) {
72+
project(fullPath: $fullPath) @include(if: $isProject) @persist {
7173
id
7274
issues(
7375
iid: $iid
@@ -95,10 +97,12 @@ query getIssues(
9597
first: $firstPageSize
9698
last: $lastPageSize
9799
) {
100+
__persist
98101
pageInfo {
99102
...PageInfo
100103
}
101104
nodes {
105+
__persist
102106
...IssueFragment
103107
}
104108
}

app/assets/javascripts/issues/list/queries/issue.fragment.graphql

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fragment IssueFragment on Issue {
2020
type
2121
assignees @skip(if: $hideUsers) {
2222
nodes {
23+
__persist
2324
id
2425
avatarUrl
2526
name
@@ -28,6 +29,7 @@ fragment IssueFragment on Issue {
2829
}
2930
}
3031
author @skip(if: $hideUsers) {
32+
__persist
3133
id
3234
avatarUrl
3335
name
@@ -36,13 +38,15 @@ fragment IssueFragment on Issue {
3638
}
3739
labels {
3840
nodes {
41+
__persist
3942
id
4043
color
4144
title
4245
description
4346
}
4447
}
4548
milestone {
49+
__persist
4650
id
4751
dueDate
4852
startDate

app/assets/javascripts/issues/list/queries/search_labels.query.graphql

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
#import "./label.fragment.graphql"
22

33
query searchLabels($fullPath: ID!, $search: String, $isProject: Boolean = false) {
4-
group(fullPath: $fullPath) @skip(if: $isProject) {
4+
group(fullPath: $fullPath) @skip(if: $isProject) @persist {
55
id
66
labels(searchTerm: $search, includeAncestorGroups: true, includeDescendantGroups: true) {
7+
__persist
78
nodes {
9+
__persist
810
...Label
911
}
1012
}
1113
}
12-
project(fullPath: $fullPath) @include(if: $isProject) {
14+
project(fullPath: $fullPath) @include(if: $isProject) @persist {
1315
id
1416
labels(searchTerm: $search, includeAncestorGroups: true) {
17+
__persist
1518
nodes {
19+
__persist
1620
...Label
1721
}
1822
}

app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default {
4040
:is-event-processed="editTimelineEventActive"
4141
:previous-occurred-at="event.occurredAt"
4242
:previous-note="event.note"
43+
:previous-tags="event.timelineEventTags.nodes"
4344
is-editing
4445
@save-event="saveEvent"
4546
@cancel="$emit('hide-edit')"

app/assets/javascripts/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ mutation UpdateTimelineEvent($input: TimelineEventUpdateInput!) {
77
action
88
occurredAt
99
createdAt
10+
timelineEventTags {
11+
nodes {
12+
id
13+
name
14+
}
15+
}
1016
}
1117
errors
1218
}

app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
44
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
55
import { __, sprintf } from '~/locale';
66
import { MAX_TEXT_LENGTH, TIMELINE_EVENT_TAGS, timelineFormI18n } from './constants';
7-
import { getUtcShiftedDate } from './utils';
7+
import { getUtcShiftedDate, getPreviousEventTags } from './utils';
88
99
export default {
1010
name: 'TimelineEventsForm',
@@ -77,7 +77,7 @@ export default {
7777
hourPickerInput: placeholderDate.getHours(),
7878
minutePickerInput: placeholderDate.getMinutes(),
7979
datePickerInput: placeholderDate,
80-
selectedTags: [...this.previousTags],
80+
selectedTags: getPreviousEventTags(this.previousTags),
8181
};
8282
},
8383
computed: {

app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export default {
105105
id: eventDetails.id,
106106
note: eventDetails.note,
107107
occurredAt: eventDetails.occurredAt,
108+
timelineEventTagNames: eventDetails.timelineEventTags,
108109
},
109110
},
110111
})

app/assets/javascripts/issues/show/components/incidents/utils.js

+8
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@ export const getUtcShiftedDate = (ISOString = null) => {
3232

3333
return date;
3434
};
35+
36+
/**
37+
* Returns an array of previously set event tags
38+
* @param {array} timelineEventTagsNodes
39+
* @returns {array}
40+
*/
41+
export const getPreviousEventTags = (timelineEventTagsNodes = []) =>
42+
timelineEventTagsNodes.map(({ name }) => name);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// this file is based on https://github.com/apollographql/apollo-cache-persist/blob/master/examples/react-native/src/utils/persistence/persistLink.ts
2+
// with some heavy refactororing
3+
4+
/* eslint-disable consistent-return */
5+
/* eslint-disable @gitlab/require-i18n-strings */
6+
/* eslint-disable no-param-reassign */
7+
import { visit } from 'graphql';
8+
import { ApolloLink } from '@apollo/client/core';
9+
import traverse from 'traverse';
10+
11+
const extractPersistDirectivePaths = (originalQuery, directive = 'persist') => {
12+
const paths = [];
13+
const fragmentPaths = {};
14+
const fragmentPersistPaths = {};
15+
16+
const query = visit(originalQuery, {
17+
FragmentSpread: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
18+
const root = ancestors.find(
19+
({ kind }) => kind === 'OperationDefinition' || kind === 'FragmentDefinition',
20+
);
21+
22+
const rootKey = root.kind === 'FragmentDefinition' ? root.name.value : '$ROOT';
23+
24+
const fieldPath = ancestors
25+
.filter(({ kind }) => kind === 'Field')
26+
.map(({ name: { value } }) => value);
27+
28+
fragmentPaths[name] = [rootKey].concat(fieldPath);
29+
},
30+
Directive: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
31+
if (name === directive) {
32+
const fieldPath = ancestors
33+
.filter(({ kind }) => kind === 'Field')
34+
.map(({ name: { value } }) => value);
35+
36+
const fragmentDefinition = ancestors.find(({ kind }) => kind === 'FragmentDefinition');
37+
38+
// If we are inside a fragment, we must save the reference.
39+
if (fragmentDefinition) {
40+
fragmentPersistPaths[fragmentDefinition.name.value] = fieldPath;
41+
} else if (fieldPath.length) {
42+
paths.push(fieldPath);
43+
}
44+
return null;
45+
}
46+
},
47+
});
48+
49+
// In case there are any FragmentDefinition items, we need to combine paths.
50+
if (Object.keys(fragmentPersistPaths).length) {
51+
visit(originalQuery, {
52+
FragmentSpread: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
53+
if (fragmentPersistPaths[name]) {
54+
let fieldPath = ancestors
55+
.filter(({ kind }) => kind === 'Field')
56+
.map(({ name: { value } }) => value);
57+
58+
fieldPath = fieldPath.concat(fragmentPersistPaths[name]);
59+
60+
const fragment = name;
61+
let parent = fragmentPaths[fragment][0];
62+
63+
while (parent && parent !== '$ROOT' && fragmentPaths[parent]) {
64+
fieldPath = fragmentPaths[parent].slice(1).concat(fieldPath);
65+
// eslint-disable-next-line prefer-destructuring
66+
parent = fragmentPaths[parent][0];
67+
}
68+
69+
paths.push(fieldPath);
70+
}
71+
},
72+
});
73+
}
74+
75+
return { query, paths };
76+
};
77+
78+
/**
79+
* Given a data result object path, return the equivalent query selection path.
80+
*
81+
* @param {Array} path The data result object path. i.e.: ["a", 0, "b"]
82+
* @return {String} the query selection path. i.e.: "a.b"
83+
*/
84+
const toQueryPath = (path) => path.filter((key) => Number.isNaN(Number(key))).join('.');
85+
86+
const attachPersists = (paths, object) => {
87+
const queryPaths = paths.map(toQueryPath);
88+
function mapperFunction() {
89+
if (
90+
!this.isRoot &&
91+
this.node &&
92+
typeof this.node === 'object' &&
93+
Object.keys(this.node).length &&
94+
!Array.isArray(this.node)
95+
) {
96+
const path = toQueryPath(this.path);
97+
98+
this.update({
99+
__persist: Boolean(
100+
queryPaths.find(
101+
(queryPath) => queryPath.indexOf(path) === 0 || path.indexOf(queryPath) === 0,
102+
),
103+
),
104+
...this.node,
105+
});
106+
}
107+
}
108+
109+
return traverse(object).map(mapperFunction);
110+
};
111+
112+
export const getPersistLink = () => {
113+
return new ApolloLink((operation, forward) => {
114+
const { query, paths } = extractPersistDirectivePaths(operation.query);
115+
116+
// Noop if not a persist query
117+
if (!paths.length) {
118+
return forward(operation);
119+
}
120+
121+
// Replace query with one without @persist directives.
122+
operation.query = query;
123+
124+
// Remove requesting __persist fields.
125+
operation.query = visit(operation.query, {
126+
Field: ({ name: { value: name } }) => {
127+
if (name === '__persist') {
128+
return null;
129+
}
130+
},
131+
});
132+
133+
return forward(operation).map((result) => {
134+
if (result.data) {
135+
result.data = attachPersists(paths, result.data);
136+
}
137+
138+
return result;
139+
});
140+
});
141+
};

0 commit comments

Comments
 (0)