blob: 6d048e06e58a1247bc9db44be570888875265724 [file] [log] [blame]
/**
* @license
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {BehaviorSubject, Observable} from 'rxjs';
import {
AccountInfo,
BasePatchSetNum,
ChangeInfo,
RevisionPatchSetNum,
} from '@gerritcodereview/typescript-api/rest-api';
import {FileOwner, FilesOwners, OwnersService} from './owners-service';
import {deepEqual} from './utils';
export interface PatchRange {
patchNum: RevisionPatchSetNum;
basePatchNum: BasePatchSetNum;
}
export enum UserRole {
ANONYMOUS = 'ANONYMOUS',
CHANGE_OWNER = 'CHANGE_OWNER',
OTHER = 'OTHER',
}
export interface User {
account?: AccountInfo;
role: UserRole;
}
export interface OwnersState {
user?: User;
allFilesApproved?: boolean;
filesOwners?: FilesOwners;
}
/**
* TODO: The plugin's REST endpoint returns only files that still need to be reviewed which means that file can only have two states:
* * needs approval
* * was not subject of OWNERS file or is already approved
*/
export enum FileStatus {
NEEDS_APPROVAL = 'NEEDS_APPROVAL',
NOT_OWNED_OR_APPROVED = 'NOT_OWNED_OR_APPROVED',
}
/**
* TODO: extend FileOwnership with owners when it will be used by UI elements
*/
export interface FileOwnership {
fileStatus: FileStatus;
owners?: FileOwner[];
}
let ownersModel: OwnersModel | undefined;
export class OwnersModel extends EventTarget {
private subject$: BehaviorSubject<OwnersState> = new BehaviorSubject(
{} as OwnersState
);
public state$: Observable<OwnersState> = this.subject$.asObservable();
constructor(readonly change: ChangeInfo) {
super();
}
get state() {
return this.subject$.getValue();
}
private setState(state: OwnersState) {
this.subject$.next(Object.freeze(state));
}
setUser(user: User) {
const current = this.subject$.getValue();
if (current.user === user) return;
this.setState({...current, user});
}
setAllFilesApproved(allFilesApproved: boolean | undefined) {
const current = this.subject$.getValue();
if (current.allFilesApproved === allFilesApproved) return;
this.setState({...current, allFilesApproved});
}
setFilesOwners(filesOwners: FilesOwners | undefined) {
const current = this.subject$.getValue();
if (deepEqual(current.filesOwners, filesOwners)) return;
this.setState({...current, filesOwners});
}
static getModel(change: ChangeInfo) {
if (!ownersModel || ownersModel.change !== change) {
ownersModel = new OwnersModel(change);
}
return ownersModel;
}
}
export class ModelLoader {
constructor(
private readonly service: OwnersService,
private readonly model: OwnersModel
) {}
async loadUser() {
await this._loadProperty(
'user',
() => this.service.getLoggedInUser(),
value => this.model.setUser(value)
);
}
async loadAllFilesApproved() {
await this._loadProperty(
'allFilesApproved',
() => this.service.getAllFilesApproved(),
value => this.model.setAllFilesApproved(value)
);
}
async loadFilesOwners() {
await this._loadProperty(
'filesOwners',
() => this.service.getFilesOwners(),
value => this.model.setFilesOwners(value)
);
}
private async _loadProperty<K extends keyof OwnersState, T>(
propertyName: K,
propertyLoader: () => Promise<T>,
propertySetter: (value: T) => void
) {
if (this.model.state[propertyName] !== undefined) return;
let newValue: T;
try {
newValue = await propertyLoader();
} catch (e) {
console.error(e);
return;
}
// It is possible, that several requests is made in parallel.
// Store only the first result and discard all other results.
// (also, due to the CodeOwnersCacheApi all result must be identical)
if (this.model.state[propertyName] !== undefined) return;
propertySetter(newValue);
}
}