blob: f003e3f90cbf85404c882a9b79853c4a551a2e8d [file] [log] [blame]
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +02001/**
2 * @license
3 * Copyright 2022 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
5 */
6import {
7 NumericChangeId,
8 RepoName,
9 RevisionPatchSetNum,
10 BasePatchSetNum,
Ben Rohlfs38ede012022-09-21 16:02:46 +020011 ChangeInfo,
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020012 PatchSetNumber,
Ben Rohlfsb91a6a42023-01-13 09:29:31 +010013 EDIT,
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020014} from '../../api/rest-api';
Ben Rohlfs7cd327d2022-09-29 17:37:28 +020015import {Tab} from '../../constants/constants';
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020016import {GerritView} from '../../services/router/router-model';
17import {UrlEncodedCommentId} from '../../types/common';
Ben Rohlfs20fe8e82022-10-14 11:13:10 +020018import {toggleSet} from '../../utils/common-util';
Ben Rohlfs7b22a292022-09-29 12:52:01 +020019import {select} from '../../utils/observable-util';
Ben Rohlfs7f8ba522022-09-16 11:02:01 +020020import {
21 encodeURL,
22 getBaseUrl,
23 getPatchRangeExpression,
24} from '../../utils/url-util';
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020025import {AttemptChoice} from '../checks/checks-util';
Ben Rohlfs2586e572022-09-16 12:55:37 +020026import {define} from '../dependency';
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020027import {Model} from '../model';
28import {ViewState} from './base';
Ben Rohlfsecc67992022-12-16 10:10:16 +010029
30export enum ChangeChildView {
31 OVERVIEW = 'OVERVIEW',
32 DIFF = 'DIFF',
33 EDIT = 'EDIT',
34}
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020035
36export interface ChangeViewState extends ViewState {
37 view: GerritView.CHANGE;
Ben Rohlfsecc67992022-12-16 10:10:16 +010038 childView: ChangeChildView;
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020039
Ben Rohlfs38ede012022-09-21 16:02:46 +020040 changeNum: NumericChangeId;
Ben Rohlfsbfc688b2022-10-21 12:38:37 +020041 repo: RepoName;
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020042 patchNum?: RevisionPatchSetNum;
43 basePatchNum?: BasePatchSetNum;
Ben Rohlfs196dc722022-12-20 18:01:33 +010044 /** Refers to comment on COMMENTS tab in OVERVIEW. */
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020045 commentId?: UrlEncodedCommentId;
Ben Rohlfsecc67992022-12-16 10:10:16 +010046
47 // TODO: Move properties that only apply to OVERVIEW into a submessage.
48
49 edit?: boolean;
Ben Rohlfs7cd327d2022-09-29 17:37:28 +020050 /** This can be a string only for plugin provided tabs. */
51 tab?: Tab | string;
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020052
Ben Rohlfsecc67992022-12-16 10:10:16 +010053 // TODO: Move properties that only apply to CHECKS tab into a submessage.
54
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020055 /** Checks related view state */
56
57 /** selected patchset for check runs (undefined=latest) */
58 checksPatchset?: PatchSetNumber;
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020059 /** regular expression for filtering check runs */
60 filter?: string;
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020061 /** selected attempt for check runs (undefined=latest) */
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020062 attempt?: AttemptChoice;
Ben Rohlfs6485eb82022-09-30 11:26:08 +020063 /** selected check runs identified by `checkName` */
Ben Rohlfs20fe8e82022-10-14 11:13:10 +020064 checksRunsSelected?: Set<string>;
Ben Rohlfs209f1412022-09-30 12:25:46 +020065 /** regular expression for filtering check results */
66 checksResultsFilter?: string;
Ben Rohlfs804e7242022-09-15 16:16:12 +020067
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020068 /** State properties that trigger one-time actions */
69
70 /** for scrolling a Change Log message into view in gr-change-view */
Ben Rohlfs804e7242022-09-15 16:16:12 +020071 messageHash?: string;
Ben Rohlfs0d125862023-04-20 22:40:04 +020072 /**
73 * For logging where the user came from. This is handled by the router, so
74 * this is not inspected by the model.
75 */
Ben Rohlfs804e7242022-09-15 16:16:12 +020076 usp?: string;
Ben Rohlfs0d125862023-04-20 22:40:04 +020077 /**
78 * Triggers all change related data to be reloaded. This is implemented by
79 * intercepting change view state updates and `forceReload` causing the view
80 * state to be wiped clean as `undefined` in an intermediate update.
81 */
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +020082 forceReload?: boolean;
83 /** triggers opening the reply dialog */
84 openReplyDialog?: boolean;
Ben Rohlfsecc67992022-12-16 10:10:16 +010085
86 /** These properties apply to the DIFF child view only. */
87 diffView?: {
88 path?: string;
Ben Rohlfsc3684352023-02-17 16:01:39 +010089 // TODO: Use LineNumber as a type, i.e. accept FILE and LOST.
Ben Rohlfsecc67992022-12-16 10:10:16 +010090 lineNum?: number;
91 leftSide?: boolean;
Ben Rohlfsecc67992022-12-16 10:10:16 +010092 };
93
94 /** These properties apply to the EDIT child view only. */
95 editView?: {
96 path?: string;
97 lineNum?: number;
98 };
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +020099}
100
Ben Rohlfs38ede012022-09-21 16:02:46 +0200101/**
102 * This is a convenience type such that you can pass a `ChangeInfo` object
103 * as the `change` property instead of having to set both the `changeNum` and
104 * `project` properties explicitly.
105 */
106export type CreateChangeUrlObject = Omit<
107 ChangeViewState,
Ben Rohlfsecc67992022-12-16 10:10:16 +0100108 'view' | 'childView' | 'changeNum' | 'repo'
Ben Rohlfs38ede012022-09-21 16:02:46 +0200109> & {
110 change: Pick<ChangeInfo, '_number' | 'project'>;
111};
112
113export function isCreateChangeUrlObject(
114 state: CreateChangeUrlObject | Omit<ChangeViewState, 'view'>
115): state is CreateChangeUrlObject {
116 return !!(state as CreateChangeUrlObject).change;
117}
118
119export function objToState(
Ben Rohlfsecc67992022-12-16 10:10:16 +0100120 obj:
121 | (CreateChangeUrlObject & {childView: ChangeChildView})
122 | Omit<ChangeViewState, 'view'>
Ben Rohlfs38ede012022-09-21 16:02:46 +0200123): ChangeViewState {
124 if (isCreateChangeUrlObject(obj)) {
125 return {
126 ...obj,
127 view: GerritView.CHANGE,
128 changeNum: obj.change._number,
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200129 repo: obj.change.project,
Ben Rohlfs38ede012022-09-21 16:02:46 +0200130 };
131 }
132 return {...obj, view: GerritView.CHANGE};
133}
134
Ben Rohlfsecc67992022-12-16 10:10:16 +0100135export function createChangeViewUrl(state: ChangeViewState): string {
136 switch (state.childView) {
137 case ChangeChildView.OVERVIEW:
138 return createChangeUrl(state);
139 case ChangeChildView.DIFF:
140 return createDiffUrl(state);
141 case ChangeChildView.EDIT:
142 return createEditUrl(state);
143 }
144}
145
Ben Rohlfs38ede012022-09-21 16:02:46 +0200146export function createChangeUrl(
Ben Rohlfsecc67992022-12-16 10:10:16 +0100147 obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>
Ben Rohlfs38ede012022-09-21 16:02:46 +0200148) {
Ben Rohlfsecc67992022-12-16 10:10:16 +0100149 const state: ChangeViewState = objToState({
150 ...obj,
151 childView: ChangeChildView.OVERVIEW,
152 });
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100153
154 let suffix = '';
Ben Rohlfs804e7242022-09-15 16:16:12 +0200155 const queries = [];
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +0200156 if (state.checksPatchset && state.checksPatchset > 0) {
157 queries.push(`checksPatchset=${state.checksPatchset}`);
158 }
Ben Rohlfs7b22a292022-09-29 12:52:01 +0200159 if (state.attempt) {
160 if (state.attempt !== 'latest') queries.push(`attempt=${state.attempt}`);
161 }
162 if (state.filter) {
163 queries.push(`filter=${state.filter}`);
164 }
Ben Rohlfs209f1412022-09-30 12:25:46 +0200165 if (state.checksResultsFilter) {
166 queries.push(`checksResultsFilter=${state.checksResultsFilter}`);
167 }
Ben Rohlfs20fe8e82022-10-14 11:13:10 +0200168 if (state.checksRunsSelected && state.checksRunsSelected.size > 0) {
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200169 queries.push(`checksRunsSelected=${[...state.checksRunsSelected].sort()}`);
170 }
Ben Rohlfs7cd327d2022-09-29 17:37:28 +0200171 if (state.tab && state.tab !== Tab.FILES) {
172 queries.push(`tab=${state.tab}`);
173 }
Ben Rohlfs804e7242022-09-15 16:16:12 +0200174 if (state.forceReload) {
175 queries.push('forceReload=true');
176 }
177 if (state.openReplyDialog) {
178 queries.push('openReplyDialog=true');
179 }
180 if (state.usp) {
181 queries.push(`usp=${state.usp}`);
182 }
183 if (state.edit) {
184 suffix += ',edit';
185 }
186 if (state.commentId) {
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100187 suffix += `/comments/${state.commentId}`;
Ben Rohlfs804e7242022-09-15 16:16:12 +0200188 }
189 if (queries.length > 0) {
190 suffix += '?' + queries.join('&');
191 }
192 if (state.messageHash) {
193 suffix += state.messageHash;
194 }
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100195
196 return `${createChangeUrlCommon(state)}${suffix}`;
197}
198
199export function createDiffUrl(
200 obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>
201) {
202 const state: ChangeViewState = objToState({
203 ...obj,
204 childView: ChangeChildView.DIFF,
205 });
206
Ben Rohlfsc02facb2023-01-27 18:46:02 +0100207 const path = `/${encodeURL(state.diffView?.path ?? '')}`;
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100208
209 let suffix = '';
210 // TODO: Move creating of comment URLs to a separate function. We are
211 // "abusing" the `commentId` property, which should only be used for pointing
212 // to comment in the COMMENTS tab of the OVERVIEW page.
213 if (state.commentId) {
214 suffix += `comment/${state.commentId}/`;
Ben Rohlfs804e7242022-09-15 16:16:12 +0200215 }
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100216
217 if (state.diffView?.lineNum) {
218 suffix += '#';
219 if (state.diffView?.leftSide) {
220 suffix += 'b';
221 }
222 suffix += state.diffView.lineNum;
223 }
224
225 return `${createChangeUrlCommon(state)}${path}${suffix}`;
226}
227
228export function createEditUrl(
229 obj: Omit<ChangeViewState, 'view' | 'childView'>
230): string {
231 const state: ChangeViewState = objToState({
232 ...obj,
233 childView: ChangeChildView.DIFF,
234 patchNum: obj.patchNum ?? EDIT,
235 });
236
Ben Rohlfsc02facb2023-01-27 18:46:02 +0100237 const path = `/${encodeURL(state.editView?.path ?? '')}`;
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100238 const line = state.editView?.lineNum;
239 const suffix = line ? `#${line}` : '';
240
241 return `${createChangeUrlCommon(state)}${path},edit${suffix}`;
242}
243
244/**
245 * The shared part of creating a change URL between OVERVIEW, DIFF and EDIT
246 * child views.
247 */
248function createChangeUrlCommon(state: ChangeViewState) {
249 let range = getPatchRangeExpression(state);
250 if (range.length) range = '/' + range;
251
252 let repo = '';
Ben Rohlfsc02facb2023-01-27 18:46:02 +0100253 if (state.repo) repo = `${encodeURL(state.repo)}/+/`;
Ben Rohlfsb91a6a42023-01-13 09:29:31 +0100254
255 return `${getBaseUrl()}/c/${repo}${state.changeNum}${range}`;
Ben Rohlfs804e7242022-09-15 16:16:12 +0200256}
257
Ben Rohlfs2586e572022-09-16 12:55:37 +0200258export const changeViewModelToken =
259 define<ChangeViewModel>('change-view-model');
260
Ben Rohlfs10dfa932022-09-19 13:43:32 +0200261export class ChangeViewModel extends Model<ChangeViewState | undefined> {
Ben Rohlfsd1009f82022-12-16 12:19:13 +0100262 public readonly changeNum$ = select(this.state$, state => state?.changeNum);
263
264 public readonly patchNum$ = select(this.state$, state => state?.patchNum);
265
266 public readonly basePatchNum$ = select(
267 this.state$,
268 state => state?.basePatchNum
269 );
270
Ben Rohlfs3c74cee2023-02-23 16:32:43 +0100271 public readonly openReplyDialog$ = select(
272 this.state$,
273 state => state?.openReplyDialog
274 );
275
Ben Rohlfsb5eba162023-03-03 12:28:08 +0100276 public readonly commentId$ = select(this.state$, state => state?.commentId);
277
Ben Rohlfs05d785e2023-04-19 15:45:06 +0200278 public readonly edit$ = select(this.state$, state => !!state?.edit);
279
Ben Rohlfs55617a32023-02-28 21:43:22 +0100280 public readonly editPath$ = select(
281 this.state$,
282 state => state?.editView?.path
283 );
284
Ben Rohlfse7408842023-01-09 22:47:53 +0100285 public readonly diffPath$ = select(
286 this.state$,
287 state => state?.diffView?.path
288 );
289
290 public readonly diffLine$ = select(
291 this.state$,
292 state => state?.diffView?.lineNum
293 );
294
295 public readonly diffLeftSide$ = select(
296 this.state$,
297 state => state?.diffView?.leftSide ?? false
298 );
299
Ben Rohlfsecc67992022-12-16 10:10:16 +0100300 public readonly childView$ = select(this.state$, state => state?.childView);
301
Ben Rohlfs583b4342023-04-19 13:15:13 +0200302 public readonly tab$ = select(this.state$, state => {
Ben Rohlfs583b4342023-04-19 13:15:13 +0200303 if (state?.tab) return state.tab;
Dhruv Srivastavafd905c42023-05-30 13:10:07 +0200304 if (state?.commentId) return Tab.COMMENT_THREADS;
Ben Rohlfs583b4342023-04-19 13:15:13 +0200305 return Tab.FILES;
306 });
Ben Rohlfs7b22a292022-09-29 12:52:01 +0200307
Ben Rohlfsa1d2c0c2022-09-29 17:03:26 +0200308 public readonly checksPatchset$ = select(
309 this.state$,
310 state => state?.checksPatchset
311 );
312
Ben Rohlfs7b22a292022-09-29 12:52:01 +0200313 public readonly attempt$ = select(this.state$, state => state?.attempt);
314
315 public readonly filter$ = select(this.state$, state => state?.filter);
316
Ben Rohlfs209f1412022-09-30 12:25:46 +0200317 public readonly checksResultsFilter$ = select(
318 this.state$,
319 state => state?.checksResultsFilter ?? ''
320 );
321
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200322 public readonly checksRunsSelected$ = select(
323 this.state$,
Ben Rohlfs20fe8e82022-10-14 11:13:10 +0200324 state => state?.checksRunsSelected ?? new Set<string>()
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200325 );
326
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +0200327 constructor() {
Ben Rohlfs10dfa932022-09-19 13:43:32 +0200328 super(undefined);
Ben Rohlfs7b22a292022-09-29 12:52:01 +0200329 this.state$.subscribe(s => {
330 if (s?.usp || s?.forceReload || s?.openReplyDialog) {
331 this.updateState({
332 usp: undefined,
333 forceReload: undefined,
334 openReplyDialog: undefined,
335 });
336 }
337 });
Ben Rohlfs0d125862023-04-20 22:40:04 +0200338 document.addEventListener('reload', this.reload);
339 }
340
341 override finalize(): void {
342 document.removeEventListener('reload', this.reload);
343 }
344
345 /**
346 * Calling this is the same as firing the 'reload' event. This is also the
347 * same as adding `forceReload` parameter in the URL. See below.
348 */
349 reload = () => {
350 const state = this.getState();
351 if (state !== undefined) this.forceLoad(state);
352 };
353
354 /**
355 * This is the destination of where the `reload()` method, the `reload` event
356 * and the `forceReload` URL parameter all end up.
357 */
358 private forceLoad(state: ChangeViewState) {
359 this.setState(undefined);
360 // We have to do this in a timeout, because we need the `undefined` value to
361 // be processed by all observers first and thus have the "reset" completed.
362 setTimeout(() => this.setState({...state, forceReload: undefined}));
363 }
364
365 override setState(state: ChangeViewState | undefined): void {
366 if (state?.forceReload) {
367 this.forceLoad(state);
368 } else {
369 super.setState(state);
370 }
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +0200371 }
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200372
373 toggleSelectedCheckRun(checkName: string) {
Ben Rohlfs20fe8e82022-10-14 11:13:10 +0200374 const current = this.getState()?.checksRunsSelected ?? new Set();
375 const next = new Set(current);
376 toggleSet(next, checkName);
377 this.updateState({checksRunsSelected: next});
Ben Rohlfs6485eb82022-09-30 11:26:08 +0200378 }
Ben Rohlfs65c2f2b2022-09-12 22:35:26 +0200379}