| /** |
| * @license |
| * Copyright (C) 2021 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 {distinctUntilChanged, map} from 'rxjs/operators'; |
| import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api'; |
| import { |
| CommentInfo, |
| PathToCommentsInfoMap, |
| RobotCommentInfo, |
| } from '../../types/common'; |
| import {addPath, DraftInfo} from '../../utils/comment-util'; |
| |
| interface CommentState { |
| comments: PathToCommentsInfoMap; |
| robotComments: {[path: string]: RobotCommentInfo[]}; |
| drafts: {[path: string]: DraftInfo[]}; |
| portedComments: PathToCommentsInfoMap; |
| portedDrafts: PathToCommentsInfoMap; |
| /** |
| * If a draft is discarded by the user, then we temporarily keep it in this |
| * array in case the user decides to Undo the discard operation and bring the |
| * draft back. Once restored, the draft is removed from this array. |
| */ |
| discardedDrafts: DraftInfo[]; |
| } |
| |
| const initialState: CommentState = { |
| comments: {}, |
| robotComments: {}, |
| drafts: {}, |
| portedComments: {}, |
| portedDrafts: {}, |
| discardedDrafts: [], |
| }; |
| |
| const privateState$ = new BehaviorSubject(initialState); |
| |
| export function _testOnly_resetState() { |
| privateState$.next(initialState); |
| } |
| |
| // Re-exporting as Observable so that you can only subscribe, but not emit. |
| export const commentState$: Observable<CommentState> = privateState$; |
| |
| export function _testOnly_getState() { |
| return privateState$.getValue(); |
| } |
| |
| export function _testOnly_setState(state: CommentState) { |
| privateState$.next(state); |
| } |
| |
| export const drafts$ = commentState$.pipe( |
| map(commentState => commentState.drafts), |
| distinctUntilChanged() |
| ); |
| |
| export const discardedDrafts$ = commentState$.pipe( |
| map(commentState => commentState.discardedDrafts), |
| distinctUntilChanged() |
| ); |
| |
| // Emits a new value even if only a single draft is changed. Components should |
| // aim to subsribe to something more specific. |
| export const changeComments$ = commentState$.pipe( |
| map( |
| commentState => |
| new ChangeComments( |
| commentState.comments, |
| commentState.robotComments, |
| commentState.drafts, |
| commentState.portedComments, |
| commentState.portedDrafts |
| ) |
| ), |
| distinctUntilChanged() |
| ); |
| |
| function publishState(state: CommentState) { |
| privateState$.next(state); |
| } |
| |
| export function updateStateComments(comments?: { |
| [path: string]: CommentInfo[]; |
| }) { |
| const nextState = {...privateState$.getValue()}; |
| nextState.comments = addPath(comments) || {}; |
| publishState(nextState); |
| } |
| |
| export function updateStateRobotComments(robotComments?: { |
| [path: string]: RobotCommentInfo[]; |
| }) { |
| const nextState = {...privateState$.getValue()}; |
| nextState.robotComments = addPath(robotComments) || {}; |
| publishState(nextState); |
| } |
| |
| export function updateStateDrafts(drafts?: {[path: string]: DraftInfo[]}) { |
| const nextState = {...privateState$.getValue()}; |
| nextState.drafts = addPath(drafts) || {}; |
| publishState(nextState); |
| } |
| |
| export function updateStatePortedComments( |
| portedComments?: PathToCommentsInfoMap |
| ) { |
| const nextState = {...privateState$.getValue()}; |
| nextState.portedComments = portedComments || {}; |
| publishState(nextState); |
| } |
| |
| export function updateStatePortedDrafts(portedDrafts?: PathToCommentsInfoMap) { |
| const nextState = {...privateState$.getValue()}; |
| nextState.portedDrafts = portedDrafts || {}; |
| publishState(nextState); |
| } |
| |
| export function updateStateAddDiscardedDraft(draft: DraftInfo) { |
| const nextState = {...privateState$.getValue()}; |
| nextState.discardedDrafts = [...nextState.discardedDrafts, draft]; |
| publishState(nextState); |
| } |
| |
| export function updateStateUndoDiscardedDraft(draftID?: string) { |
| const nextState = {...privateState$.getValue()}; |
| const drafts = [...nextState.discardedDrafts]; |
| const index = drafts.findIndex(d => d.id === draftID); |
| if (index === -1) { |
| throw new Error('discarded draft not found'); |
| } |
| drafts.splice(index, 1); |
| nextState.discardedDrafts = drafts; |
| publishState(nextState); |
| } |
| |
| export function updateStateAddDraft(draft: DraftInfo) { |
| const nextState = {...privateState$.getValue()}; |
| if (!draft.path) throw new Error('draft path undefined'); |
| nextState.drafts = {...nextState.drafts}; |
| const drafts = nextState.drafts; |
| if (!drafts[draft.path]) drafts[draft.path] = [] as DraftInfo[]; |
| else drafts[draft.path] = [...drafts[draft.path]]; |
| const index = drafts[draft.path].findIndex( |
| d => |
| (d.__draftID && d.__draftID === draft.__draftID) || |
| (d.id && d.id === draft.id) |
| ); |
| if (index !== -1) { |
| drafts[draft.path][index] = draft; |
| } else { |
| drafts[draft.path].push(draft); |
| } |
| publishState(nextState); |
| } |
| |
| export function updateStateUpdateDraft(draft: DraftInfo) { |
| const nextState = {...privateState$.getValue()}; |
| if (!draft.path) throw new Error('draft path undefined'); |
| nextState.drafts = {...nextState.drafts}; |
| const drafts = nextState.drafts; |
| if (!drafts[draft.path]) |
| throw new Error('draft: trying to edit non-existent draft'); |
| drafts[draft.path] = [...drafts[draft.path]]; |
| const index = drafts[draft.path].findIndex( |
| d => |
| (d.__draftID && d.__draftID === draft.__draftID) || |
| (d.id && d.id === draft.id) |
| ); |
| if (index === -1) return; |
| drafts[draft.path][index] = draft; |
| publishState(nextState); |
| } |
| |
| export function updateStateDeleteDraft(draft: DraftInfo) { |
| const nextState = {...privateState$.getValue()}; |
| if (!draft.path) throw new Error('draft path undefined'); |
| nextState.drafts = {...nextState.drafts}; |
| const drafts = nextState.drafts; |
| const index = (drafts[draft.path] || []).findIndex( |
| d => |
| (d.__draftID && d.__draftID === draft.__draftID) || |
| (d.id && d.id === draft.id) |
| ); |
| if (index === -1) return; |
| const discardedDraft = drafts[draft.path][index]; |
| drafts[draft.path] = [...drafts[draft.path]]; |
| drafts[draft.path].splice(index, 1); |
| publishState(nextState); |
| updateStateAddDiscardedDraft(discardedDraft); |
| } |