mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
refactor: transform sync modals to fc (#5181)
* sync modals * second pass * some nits * fix types * include indeterminate checkbox * use previous state setter * fix req settings modal setStates * fc sync pull button * previous state * fix staging target * sync dropdown pass * refresh branches modal state on show * state readability tweaks * fix prompt button double event * fix branch modal target * remove extra reload in history modal * fix history modal * remove logs * extract refetch * fix delete * delete local and remote * format * revert local delete change * current target * staging modal * fix lint * fix useEffect flash * add todo note
This commit is contained in:
parent
e71609df2b
commit
bf040aca31
@ -346,13 +346,13 @@ export class VCS {
|
||||
throw new Error(`Failed to find snapshot by id ${snapshotId}`);
|
||||
}
|
||||
|
||||
const potentialNewState: SnapshotState = candidates.map(candidate => ({
|
||||
const currentState: SnapshotState = candidates.map(candidate => ({
|
||||
key: candidate.key,
|
||||
blob: hashDocument(candidate.document).hash,
|
||||
name: candidate.name,
|
||||
}));
|
||||
|
||||
const delta = stateDelta(potentialNewState, rollbackSnapshot.state);
|
||||
const delta = stateDelta(currentState, rollbackSnapshot.state);
|
||||
// We need to treat removals of candidates differently because they may not yet have been stored as blobs.
|
||||
const remove: StatusCandidate[] = [];
|
||||
|
||||
@ -378,7 +378,7 @@ export class VCS {
|
||||
|
||||
async getHistoryCount(branchName?: string) {
|
||||
const branch = branchName ? await this._getBranch(branchName) : await this._getCurrentBranch();
|
||||
return branch?.snapshots.length;
|
||||
return branch?.snapshots.length || 0;
|
||||
}
|
||||
|
||||
async getHistory(count = 0) {
|
||||
|
@ -1,40 +1,25 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { HTMLAttributes, PureComponent } from 'react';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import React, { FC, HTMLAttributes, useEffect, useRef } from 'react';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLInputElement> {
|
||||
indeterminate: boolean;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class IndeterminateCheckbox extends PureComponent<Props> {
|
||||
input: HTMLInputElement | null = null;
|
||||
export const IndeterminateCheckbox: FC<Props> = ({ checked, indeterminate, ...otherProps }) => {
|
||||
const checkRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
_setRef(input: HTMLInputElement) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (this.input) {
|
||||
this.input.indeterminate = this.props.indeterminate;
|
||||
useEffect(() => {
|
||||
if (checkRef.current) {
|
||||
checkRef.current.checked = checked;
|
||||
checkRef.current.indeterminate = indeterminate;
|
||||
}
|
||||
}
|
||||
}, [checked, indeterminate]);
|
||||
|
||||
componentDidMount() {
|
||||
this._update();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._update();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
indeterminate,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
return <input ref={this._setRef} type="checkbox" {...otherProps} />;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
ref={checkRef}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ export interface ModalProps {
|
||||
}
|
||||
|
||||
export interface ModalHandle {
|
||||
show: (options?: { onHide: () => void }) => void;
|
||||
show: (options?: { onHide?: () => void }) => void;
|
||||
hide: () => void;
|
||||
toggle: () => void;
|
||||
isOpen: () => boolean;
|
||||
@ -49,8 +49,12 @@ export const Modal = forwardRef<ModalHandle, ModalProps>(({
|
||||
|
||||
const hide = useCallback(() => {
|
||||
setOpen(false);
|
||||
onHideProp?.();
|
||||
onHideArgument?.();
|
||||
if (typeof onHideProp === 'function') {
|
||||
onHideProp();
|
||||
}
|
||||
if (typeof onHideArgument === 'function') {
|
||||
onHideArgument();
|
||||
}
|
||||
}, [onHideProp, onHideArgument]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
@ -46,50 +46,36 @@ export const PromptButton = <T, >({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleConfirm = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (triggerTimeout.current !== null) {
|
||||
// Clear existing timeouts
|
||||
clearTimeout(triggerTimeout.current);
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (state === 'default') {
|
||||
// Prevent events (ex. won't close dropdown if it's in one)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// Toggle the confirmation notice
|
||||
setState('ask');
|
||||
// Set a timeout to hide the confirmation
|
||||
// using global.setTimeout to force use of the Node timeout rather than DOM timeout
|
||||
triggerTimeout.current = global.setTimeout(() => {
|
||||
setState('default');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Fire the click handler
|
||||
onClick?.(event);
|
||||
|
||||
// Set the state to done (but delay a bit to not alarm user)
|
||||
// using global.setTimeout to force use of the Node timeout rather than DOM timeout
|
||||
doneTimeout.current = global.setTimeout(() => {
|
||||
setState('done');
|
||||
}, 100);
|
||||
// Set a timeout to hide the confirmation
|
||||
// using global.setTimeout to force use of the Node timeout rather than DOM timeout
|
||||
triggerTimeout.current = global.setTimeout(() => {
|
||||
setState('default');
|
||||
|
||||
if (state === 'ask') {
|
||||
if (triggerTimeout.current !== null) {
|
||||
// Clear existing timeouts
|
||||
clearTimeout(triggerTimeout.current);
|
||||
}
|
||||
// Fire the click handler
|
||||
onClick?.(event);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleAsk = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
// Prevent events (ex. won't close dropdown if it's in one)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Toggle the confirmation notice
|
||||
setState('ask');
|
||||
|
||||
// Set a timeout to hide the confirmation
|
||||
// using global.setTimeout to force use of the Node timeout rather than DOM timeout
|
||||
triggerTimeout.current = global.setTimeout(() => {
|
||||
setState('default');
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (state === 'ask') {
|
||||
handleConfirm(event);
|
||||
} else if (state === 'default') {
|
||||
handleAsk(event);
|
||||
// Set the state to done (but delay a bit to not alarm user)
|
||||
// using global.setTimeout to force use of the Node timeout rather than DOM timeout
|
||||
doneTimeout.current = global.setTimeout(() => {
|
||||
setState('done');
|
||||
}, 100);
|
||||
// Set a timeout to hide the confirmation
|
||||
// using global.setTimeout to force use of the Node timeout rather than DOM timeout
|
||||
triggerTimeout.current = global.setTimeout(() => {
|
||||
setState('default');
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,25 +1,23 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
|
||||
import React, { FC, Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useInterval, useMount } from 'react-use';
|
||||
|
||||
import * as session from '../../../account/session';
|
||||
import { AUTOBIND_CFG, DEFAULT_BRANCH_NAME } from '../../../common/constants';
|
||||
import { DEFAULT_BRANCH_NAME } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
import { docsVersionControl } from '../../../common/documentation';
|
||||
import { strings } from '../../../common/strings';
|
||||
import * as models from '../../../models';
|
||||
import { isRemoteProject, Project } from '../../../models/project';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { Snapshot, Status, StatusCandidate } from '../../../sync/types';
|
||||
import { Status } from '../../../sync/types';
|
||||
import { pushSnapshotOnInitialize } from '../../../sync/vcs/initialize-backend-project';
|
||||
import { logCollectionMovedToProject } from '../../../sync/vcs/migrate-collections';
|
||||
import { BackendProjectWithTeam } from '../../../sync/vcs/normalize-backend-project-team';
|
||||
import { pullBackendProject } from '../../../sync/vcs/pull-backend-project';
|
||||
import { interceptAccessError } from '../../../sync/vcs/util';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { activateWorkspace } from '../../redux/modules/workspace';
|
||||
import { selectActiveWorkspaceMeta, selectRemoteProjects, selectSyncItems } from '../../redux/selectors';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
@ -37,32 +35,15 @@ import { SyncHistoryModal } from '../modals/sync-history-modal';
|
||||
import { SyncStagingModal } from '../modals/sync-staging-modal';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
// Stop refreshing if user hasn't been active in this long
|
||||
const REFRESH_USER_ACTIVITY = 1000 * 60 * 10;
|
||||
// TODO: handle refetching logic in one place not here in a component
|
||||
|
||||
// Refresh dropdown periodically
|
||||
const REFRESH_PERIOD = 1000 * 60 * 1;
|
||||
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
remoteProjects: selectRemoteProjects(state),
|
||||
syncItems: selectSyncItems(state),
|
||||
workspaceMeta: selectActiveWorkspaceMeta(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => {
|
||||
const bound = bindActionCreators({ activateWorkspace }, dispatch);
|
||||
return {
|
||||
handleActivateWorkspace: bound.activateWorkspace,
|
||||
};
|
||||
};
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
interface Props {
|
||||
workspace: Workspace;
|
||||
project: Project;
|
||||
vcs: VCS;
|
||||
syncItems: StatusCandidate[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -80,14 +61,8 @@ interface State {
|
||||
loadingPush: boolean;
|
||||
remoteBackendProjects: BackendProjectWithTeam[];
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
checkInterval: NodeJS.Timeout | null = null;
|
||||
refreshOnNextSyncItems = false;
|
||||
lastUserActivity = Date.now();
|
||||
|
||||
state: State = {
|
||||
export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
|
||||
const [state, setState] = useState<State>({
|
||||
localBranches: [],
|
||||
currentBranch: '',
|
||||
compare: {
|
||||
@ -105,133 +80,113 @@ class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
unstaged: {},
|
||||
},
|
||||
remoteBackendProjects: [],
|
||||
};
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
const remoteProjects = useSelector(selectRemoteProjects);
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
const workspaceMeta = useSelector(selectActiveWorkspaceMeta);
|
||||
|
||||
async refreshMainAttributes(extraState: Partial<State> = {}) {
|
||||
const { vcs, syncItems, workspace, project } = this.props;
|
||||
|
||||
if (!vcs.hasBackendProject() && isRemoteProject(project)) {
|
||||
const remoteBackendProjects = await vcs.remoteBackendProjectsInAnyTeam();
|
||||
const matchedBackendProjects = remoteBackendProjects.filter(p => p.rootDocumentId === workspace._id);
|
||||
this.setState({
|
||||
remoteBackendProjects: matchedBackendProjects,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const localBranches = (await vcs.getBranches()).sort();
|
||||
const currentBranch = await vcs.getBranch();
|
||||
const historyCount = await vcs.getHistoryCount();
|
||||
const status = await vcs.status(syncItems, {});
|
||||
const newState: Partial<State> = {
|
||||
status,
|
||||
historyCount,
|
||||
localBranches,
|
||||
currentBranch,
|
||||
...extraState,
|
||||
};
|
||||
|
||||
// Do the remote stuff
|
||||
const refetchRemoteBranch = useCallback(async () => {
|
||||
if (session.isLoggedIn()) {
|
||||
try {
|
||||
newState.compare = await vcs.compareRemoteBranch();
|
||||
const compare = await vcs.compareRemoteBranch();
|
||||
setState(state => ({
|
||||
...state,
|
||||
compare,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.log('Failed to compare remote branches', err.message);
|
||||
}
|
||||
}
|
||||
}, [vcs]);
|
||||
|
||||
this.setState(prevState => ({ ...prevState, ...newState }));
|
||||
}
|
||||
const refreshVCSAndRefetchRemote = useCallback(async () => {
|
||||
if (!vcs.hasBackendProject() && isRemoteProject(project)) {
|
||||
const remoteBackendProjects = await vcs.remoteBackendProjectsInAnyTeam();
|
||||
const matchedBackendProjects = remoteBackendProjects.filter(p => p.rootDocumentId === workspace._id);
|
||||
setState(state => ({
|
||||
...state,
|
||||
remoteBackendProjects: matchedBackendProjects,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const localBranches = (await vcs.getBranches()).sort();
|
||||
const currentBranch = await vcs.getBranch();
|
||||
const historyCount = await vcs.getHistoryCount();
|
||||
const status = await vcs.status(syncItems, {});
|
||||
setState(state => ({
|
||||
...state,
|
||||
status,
|
||||
historyCount,
|
||||
localBranches,
|
||||
currentBranch,
|
||||
}));
|
||||
// Do the remote stuff
|
||||
refetchRemoteBranch();
|
||||
}, [project, refetchRemoteBranch, syncItems, vcs, workspace._id]);
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({
|
||||
useInterval(() => {
|
||||
refetchRemoteBranch();
|
||||
}, REFRESH_PERIOD);
|
||||
|
||||
useMount(async () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
initializing: true,
|
||||
});
|
||||
|
||||
const { vcs, workspace, workspaceMeta, project } = this.props;
|
||||
}));
|
||||
|
||||
try {
|
||||
// NOTE pushes the first snapshot automatically
|
||||
await pushSnapshotOnInitialize({ vcs, workspace, workspaceMeta, project });
|
||||
await this.refreshMainAttributes();
|
||||
await refreshVCSAndRefetchRemote();
|
||||
} catch (err) {
|
||||
console.log('[sync_menu] Error refreshing sync state', err);
|
||||
} finally {
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
initializing: false,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Update if new sync items
|
||||
useEffect(() => {
|
||||
if (vcs.hasBackendProject()) {
|
||||
vcs.status(syncItems, {}).then(status => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
status,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh but only if the user has been active in the last n minutes
|
||||
this.checkInterval = setInterval(async () => {
|
||||
if (Date.now() - this.lastUserActivity < REFRESH_USER_ACTIVITY) {
|
||||
await this.refreshMainAttributes();
|
||||
}
|
||||
}, REFRESH_PERIOD);
|
||||
document.addEventListener('mousemove', this._handleUserActivity);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.checkInterval !== null) {
|
||||
clearInterval(this.checkInterval);
|
||||
}, [syncItems, vcs]);
|
||||
async function handleSetProject(backendProject: BackendProjectWithTeam) {
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingProjectPull: true,
|
||||
}));
|
||||
const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProjects });
|
||||
if (pulledIntoProject._id !== project._id) {
|
||||
// If pulled into a different project, reactivate the workspace
|
||||
await dispatch(activateWorkspace({ workspaceId: workspace._id }));
|
||||
logCollectionMovedToProject(workspace, pulledIntoProject);
|
||||
}
|
||||
document.removeEventListener('mousemove', this._handleUserActivity);
|
||||
await refreshVCSAndRefetchRemote();
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingProjectPull: false,
|
||||
}));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
// Update if new sync items
|
||||
if (syncItems !== prevProps.syncItems) {
|
||||
if (vcs.hasBackendProject()) {
|
||||
vcs.status(syncItems, {}).then(status => {
|
||||
this.setState({
|
||||
status,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.refreshOnNextSyncItems) {
|
||||
this.refreshMainAttributes();
|
||||
this.refreshOnNextSyncItems = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleUserActivity() {
|
||||
this.lastUserActivity = Date.now();
|
||||
}
|
||||
|
||||
_handleShowBranchesModal() {
|
||||
showModal(SyncBranchesModal, {
|
||||
onHide: this.refreshMainAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
_handleShowStagingModal() {
|
||||
showModal(SyncStagingModal, {
|
||||
onSnapshot: async () => {
|
||||
await this.refreshMainAttributes();
|
||||
},
|
||||
handlePush: async () => {
|
||||
await this._handlePushChanges();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static _handleShowLoginModal() {
|
||||
showModal(LoginModalHandle);
|
||||
}
|
||||
|
||||
async _handlePushChanges() {
|
||||
const { vcs, project: { remoteId } } = this.props;
|
||||
this.setState({
|
||||
async function handlePush() {
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingPush: true,
|
||||
});
|
||||
}));
|
||||
|
||||
try {
|
||||
const branch = await vcs.getBranch();
|
||||
await interceptAccessError({
|
||||
callback: async () => await vcs.push(remoteId),
|
||||
callback: () => vcs.push(project.remoteId),
|
||||
action: 'push',
|
||||
resourceName: branch,
|
||||
resourceType: 'branch',
|
||||
@ -244,28 +199,29 @@ class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
await this.refreshMainAttributes({
|
||||
await refreshVCSAndRefetchRemote();
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingPush: false,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async _handlePullChanges() {
|
||||
const { vcs, syncItems, project: { remoteId } } = this.props;
|
||||
this.setState({
|
||||
async function handlePull() {
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingPull: true,
|
||||
});
|
||||
}));
|
||||
|
||||
try {
|
||||
const branch = await vcs.getBranch();
|
||||
const delta = await interceptAccessError({
|
||||
callback: async () => await vcs.pull(syncItems, remoteId),
|
||||
callback: () => vcs.pull(syncItems, project.remoteId),
|
||||
action: 'pull',
|
||||
resourceName: branch,
|
||||
resourceType: 'branch',
|
||||
});
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
this.refreshOnNextSyncItems = true;
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Pull Error',
|
||||
@ -274,22 +230,13 @@ class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingPull: false,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async _handleRollback(snapshot: Snapshot) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
const delta = await vcs.rollback(snapshot.id, syncItems);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
this.refreshOnNextSyncItems = true;
|
||||
}
|
||||
|
||||
async _handleRevert() {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
async function handleRevert() {
|
||||
try {
|
||||
const delta = await vcs.rollbackToLatest(syncItems);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
@ -303,61 +250,11 @@ class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_handleShowHistoryModal() {
|
||||
showModal(SyncHistoryModal, {
|
||||
handleRollback: this._handleRollback,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleOpen() {
|
||||
await this.refreshMainAttributes();
|
||||
}
|
||||
|
||||
async _handleEnableSync() {
|
||||
this.setState({
|
||||
loadingProjectPull: true,
|
||||
});
|
||||
const { vcs, workspace } = this.props;
|
||||
await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name);
|
||||
await this.refreshMainAttributes({
|
||||
loadingProjectPull: false,
|
||||
});
|
||||
}
|
||||
|
||||
_handleShowDeleteModal() {
|
||||
showModal(SyncDeleteModal, {
|
||||
onHide: this.refreshMainAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleSetProject(backendProject: BackendProjectWithTeam) {
|
||||
const { vcs, remoteProjects, project, workspace, handleActivateWorkspace } = this.props;
|
||||
this.setState({
|
||||
loadingProjectPull: true,
|
||||
});
|
||||
|
||||
const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProjects });
|
||||
if (pulledIntoProject._id !== project._id) {
|
||||
// If pulled into a different project, reactivate the workspace
|
||||
await handleActivateWorkspace({ workspaceId: workspace._id });
|
||||
logCollectionMovedToProject(workspace, pulledIntoProject);
|
||||
}
|
||||
|
||||
await this.refreshMainAttributes({
|
||||
loadingProjectPull: false,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleSwitchBranch(branch: string) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
async function handleSwitchBranch(branch: string) {
|
||||
try {
|
||||
const delta = await vcs.checkout(syncItems, branch);
|
||||
|
||||
if (branch === DEFAULT_BRANCH_NAME) {
|
||||
const { historyCount } = this.state;
|
||||
const defaultBranchHistoryCount = await vcs.getHistoryCount(DEFAULT_BRANCH_NAME);
|
||||
|
||||
// If the default branch has no snapshots, but the current branch does
|
||||
// It will result in the workspace getting deleted
|
||||
// So we filter out the workspace from the delta to prevent this
|
||||
@ -365,7 +262,6 @@ class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
delta.remove = delta.remove.filter(e => e?.type !== models.workspace.type);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
} catch (err) {
|
||||
@ -375,257 +271,248 @@ class UnconnectedSyncDropdown extends PureComponent<Props, State> {
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
|
||||
// We can't refresh now because we won't yet have the new syncItems
|
||||
this.refreshOnNextSyncItems = true;
|
||||
// Still need to do this in case sync items don't change
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
currentBranch: branch,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
renderBranch(branch: string) {
|
||||
const { currentBranch } = this.state;
|
||||
const icon =
|
||||
branch === currentBranch ? <i className="fa fa-tag" /> : <i className="fa fa-empty" />;
|
||||
const isCurrentBranch = branch === currentBranch;
|
||||
if (!session.isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
localBranches,
|
||||
currentBranch,
|
||||
status,
|
||||
historyCount,
|
||||
loadingPull,
|
||||
loadingPush,
|
||||
loadingProjectPull,
|
||||
compare: { ahead, behind },
|
||||
initializing,
|
||||
remoteBackendProjects,
|
||||
} = state;
|
||||
const canCreateSnapshot =
|
||||
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
||||
const visibleBranches = localBranches.filter(b => !b.match(/\.hidden$/));
|
||||
const syncMenuHeader = (
|
||||
<DropdownDivider>
|
||||
Insomnia Sync{' '}
|
||||
<HelpTooltip>
|
||||
Sync and collaborate on workspaces{' '}
|
||||
<Link href={docsVersionControl}>
|
||||
<span className="no-wrap">
|
||||
<br />
|
||||
Documentation <i className="fa fa-external-link" />
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
</DropdownDivider>
|
||||
);
|
||||
|
||||
if (loadingProjectPull) {
|
||||
return (
|
||||
<DropdownItem
|
||||
key={branch}
|
||||
onClick={isCurrentBranch ? undefined : () => this._handleSwitchBranch(branch)}
|
||||
className={classnames({
|
||||
bold: isCurrentBranch,
|
||||
})}
|
||||
title={isCurrentBranch ? '' : `Switch to "${branch}"`}
|
||||
>
|
||||
{icon}
|
||||
{branch}
|
||||
</DropdownItem>
|
||||
<div>
|
||||
<button className="btn btn--compact wide">
|
||||
<i className="fa fa-refresh fa-spin" /> Initializing
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderButton() {
|
||||
const {
|
||||
currentBranch,
|
||||
compare: { ahead, behind },
|
||||
initializing,
|
||||
status,
|
||||
loadingPull,
|
||||
loadingPush,
|
||||
} = this.state;
|
||||
const canPush = ahead > 0;
|
||||
const canPull = behind > 0;
|
||||
const canCreateSnapshot =
|
||||
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
||||
const loadIcon = <i className="fa fa-spin fa-refresh fa--fixed-width" />;
|
||||
const pullToolTipMsg = canPull
|
||||
? `There ${behind === 1 ? 'is' : 'are'} ${behind} snapshot${behind === 1 ? '' : 's'} to pull`
|
||||
: 'No changes to pull';
|
||||
const pushToolTipMsg = canPush
|
||||
? `There ${ahead === 1 ? 'is' : 'are'} ${ahead} snapshot${ahead === 1 ? '' : 's'} to push`
|
||||
: 'No changes to push';
|
||||
const snapshotToolTipMsg = canCreateSnapshot ? 'Local changes made' : 'No local changes made';
|
||||
|
||||
if (currentBranch === null) {
|
||||
return <Fragment>Sync</Fragment>;
|
||||
}
|
||||
|
||||
if (!vcs.hasBackendProject()) {
|
||||
return (
|
||||
<DropdownButton
|
||||
className="btn--clicky-small btn-sync wide text-left overflow-hidden row-spaced"
|
||||
disabled={initializing}
|
||||
>
|
||||
<div className="ellipsis">
|
||||
<i className="fa fa-code-fork space-right" />{' '}
|
||||
{initializing ? 'Initializing...' : currentBranch}
|
||||
</div>
|
||||
<div className="flex space-left">
|
||||
<Tooltip message={snapshotToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cube fa--fixed-width', {
|
||||
'super-duper-faint': !canCreateSnapshot,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Only show cloud icons if logged in */}
|
||||
{session.isLoggedIn() && (
|
||||
<Fragment>
|
||||
{loadingPull ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pullToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-download fa--fixed-width', {
|
||||
'super-duper-faint': !canPull,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{loadingPush ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pushToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-upload fa--fixed-width', {
|
||||
'super-duper-faint': !canPush,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!session.isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { className, vcs } = this.props;
|
||||
const {
|
||||
localBranches,
|
||||
currentBranch,
|
||||
status,
|
||||
historyCount,
|
||||
loadingPull,
|
||||
loadingPush,
|
||||
loadingProjectPull,
|
||||
remoteBackendProjects,
|
||||
compare: { ahead, behind },
|
||||
} = this.state;
|
||||
const canCreateSnapshot =
|
||||
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
||||
const visibleBranches = localBranches.filter(b => !b.match(/\.hidden$/));
|
||||
const syncMenuHeader = (
|
||||
<DropdownDivider>
|
||||
Insomnia Sync{' '}
|
||||
<HelpTooltip>
|
||||
Sync and collaborate on workspaces{' '}
|
||||
<Link href={docsVersionControl}>
|
||||
<span className="no-wrap">
|
||||
<br />
|
||||
Documentation <i className="fa fa-external-link" />
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
</DropdownDivider>
|
||||
);
|
||||
|
||||
if (loadingProjectPull) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<button className="btn btn--compact wide">
|
||||
<i className="fa fa-refresh fa-spin" /> Initializing
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!vcs.hasBackendProject()) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown className="wide tall" onOpen={this._handleOpen}>
|
||||
<DropdownButton className="btn btn--compact wide">
|
||||
<i className="fa fa-code-fork " /> Setup Sync
|
||||
</DropdownButton>
|
||||
{syncMenuHeader}
|
||||
{remoteBackendProjects.length === 0 && (
|
||||
<DropdownItem onClick={this._handleEnableSync}>
|
||||
<i className="fa fa-plus-circle" /> Create Locally
|
||||
</DropdownItem>
|
||||
)}
|
||||
{remoteBackendProjects.map(p => (
|
||||
<DropdownItem key={p.id} onClick={() => this._handleSetProject(p)}>
|
||||
<i className="fa fa-cloud-download" /> Pull <strong>{p.name}</strong>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown className="wide tall" onOpen={this._handleOpen}>
|
||||
{this.renderButton()}
|
||||
|
||||
<div>
|
||||
<Dropdown className="wide tall" onOpen={() => refreshVCSAndRefetchRemote()}>
|
||||
<DropdownButton className="btn btn--compact wide">
|
||||
<i className="fa fa-code-fork " /> Setup Sync
|
||||
</DropdownButton>
|
||||
{syncMenuHeader}
|
||||
|
||||
{!session.isLoggedIn() && (
|
||||
<DropdownItem onClick={SyncDropdown._handleShowLoginModal}>
|
||||
<i className="fa fa-sign-in" /> Log In
|
||||
{remoteBackendProjects.length === 0 && (
|
||||
<DropdownItem
|
||||
onClick={async () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingProjectPull: true,
|
||||
}));
|
||||
await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name);
|
||||
await refreshVCSAndRefetchRemote();
|
||||
setState(state => ({
|
||||
...state,
|
||||
loadingProjectPull: false,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-plus-circle" /> Create Locally
|
||||
</DropdownItem>
|
||||
)}
|
||||
|
||||
<DropdownItem onClick={this._handleShowBranchesModal}>
|
||||
<i className="fa fa-code-fork" />
|
||||
Branches
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handleShowDeleteModal} disabled={historyCount === 0}>
|
||||
<i className="fa fa-remove" />
|
||||
Delete {strings.collection.singular}
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownDivider>Local Branches</DropdownDivider>
|
||||
{visibleBranches.map(this.renderBranch)}
|
||||
|
||||
<DropdownDivider>{currentBranch}</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={this._handleShowHistoryModal} disabled={historyCount === 0}>
|
||||
<i className="fa fa-clock-o" />
|
||||
History
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem
|
||||
onClick={this._handleRevert}
|
||||
buttonClass={PromptButton}
|
||||
stayOpenAfterClick
|
||||
disabled={!canCreateSnapshot || historyCount === 0}
|
||||
>
|
||||
<i className="fa fa-undo" />
|
||||
Revert Changes
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handleShowStagingModal} disabled={!canCreateSnapshot}>
|
||||
<i className="fa fa-cube" />
|
||||
Create Snapshot
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handlePullChanges} disabled={behind === 0 || loadingPull}>
|
||||
{loadingPull ? (
|
||||
<Fragment>
|
||||
<i className="fa fa-spin fa-refresh" /> Pulling Snapshots...
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<i className="fa fa-cloud-download" /> Pull {behind || ''} Snapshot
|
||||
{behind === 1 ? '' : 's'}
|
||||
</Fragment>
|
||||
)}
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handlePushChanges} disabled={ahead === 0 || loadingPush}>
|
||||
{loadingPush ? (
|
||||
<Fragment>
|
||||
<i className="fa fa-spin fa-refresh" /> Pushing Snapshots...
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<i className="fa fa-cloud-upload" /> Push {ahead || ''} Snapshot
|
||||
{ahead === 1 ? '' : 's'}
|
||||
</Fragment>
|
||||
)}
|
||||
</DropdownItem>
|
||||
{remoteBackendProjects.map(p => (
|
||||
<DropdownItem key={p.id} onClick={() => handleSetProject(p)}>
|
||||
<i className="fa fa-cloud-download" /> Pull <strong>{p.name}</strong>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const canPush = ahead > 0;
|
||||
const canPull = behind > 0;
|
||||
const loadIcon = <i className="fa fa-spin fa-refresh fa--fixed-width" />;
|
||||
const pullToolTipMsg = canPull
|
||||
? `There ${behind === 1 ? 'is' : 'are'} ${behind} snapshot${behind === 1 ? '' : 's'} to pull`
|
||||
: 'No changes to pull';
|
||||
const pushToolTipMsg = canPush
|
||||
? `There ${ahead === 1 ? 'is' : 'are'} ${ahead} snapshot${ahead === 1 ? '' : 's'} to push`
|
||||
: 'No changes to push';
|
||||
const snapshotToolTipMsg = canCreateSnapshot ? 'Local changes made' : 'No local changes made';
|
||||
|
||||
export const SyncDropdown = connect(mapStateToProps, mapDispatchToProps)(UnconnectedSyncDropdown);
|
||||
return (
|
||||
<div>
|
||||
<Dropdown className="wide tall" onOpen={() => refreshVCSAndRefetchRemote()}>
|
||||
{currentBranch === null ?
|
||||
<Fragment>Sync</Fragment> :
|
||||
<DropdownButton
|
||||
className="btn--clicky-small btn-sync wide text-left overflow-hidden row-spaced"
|
||||
disabled={initializing}
|
||||
>
|
||||
<div className="ellipsis">
|
||||
<i className="fa fa-code-fork space-right" />{' '}
|
||||
{initializing ? 'Initializing...' : currentBranch}
|
||||
</div>
|
||||
<div className="flex space-left">
|
||||
<Tooltip message={snapshotToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cube fa--fixed-width', {
|
||||
'super-duper-faint': !canCreateSnapshot,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Only show cloud icons if logged in */}
|
||||
{session.isLoggedIn() && (
|
||||
<Fragment>
|
||||
{loadingPull ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pullToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-download fa--fixed-width', {
|
||||
'super-duper-faint': !canPull,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{loadingPush ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pushToolTipMsg} delay={800} position="bottom">
|
||||
<i
|
||||
className={classnames('fa fa-cloud-upload fa--fixed-width', {
|
||||
'super-duper-faint': !canPush,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</DropdownButton>}
|
||||
|
||||
{syncMenuHeader}
|
||||
|
||||
{!session.isLoggedIn() && (
|
||||
<DropdownItem onClick={() => showModal(LoginModalHandle)}>
|
||||
<i className="fa fa-sign-in" /> Log In
|
||||
</DropdownItem>
|
||||
)}
|
||||
|
||||
<DropdownItem onClick={() => showModal(SyncBranchesModal, { onHide: refreshVCSAndRefetchRemote })}>
|
||||
<i className="fa fa-code-fork" />
|
||||
Branches
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={() => showModal(SyncDeleteModal, { onHide: refreshVCSAndRefetchRemote })} disabled={historyCount === 0}>
|
||||
<i className="fa fa-remove" />
|
||||
Delete {strings.collection.singular}
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownDivider>Local Branches</DropdownDivider>
|
||||
{visibleBranches.map(branch => {
|
||||
const icon = branch === currentBranch ? <i className="fa fa-tag" /> : <i className="fa fa-empty" />;
|
||||
const isCurrentBranch = branch === currentBranch;
|
||||
return <DropdownItem
|
||||
key={branch}
|
||||
onClick={isCurrentBranch ? undefined : () => handleSwitchBranch(branch)}
|
||||
className={classnames({
|
||||
bold: isCurrentBranch,
|
||||
})}
|
||||
title={isCurrentBranch ? '' : `Switch to "${branch}"`}
|
||||
>
|
||||
{icon}
|
||||
{branch}
|
||||
</DropdownItem>;
|
||||
})}
|
||||
|
||||
<DropdownDivider>{currentBranch}</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={() => showModal(SyncHistoryModal)} disabled={historyCount === 0}>
|
||||
<i className="fa fa-clock-o" />
|
||||
History
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem
|
||||
onClick={handleRevert}
|
||||
buttonClass={PromptButton}
|
||||
stayOpenAfterClick
|
||||
disabled={!canCreateSnapshot || historyCount === 0}
|
||||
>
|
||||
<i className="fa fa-undo" />
|
||||
Revert Changes
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem
|
||||
onClick={() =>
|
||||
showModal(SyncStagingModal, {
|
||||
onSnapshot: refreshVCSAndRefetchRemote,
|
||||
handlePush,
|
||||
})
|
||||
}
|
||||
disabled={!canCreateSnapshot}
|
||||
>
|
||||
<i className="fa fa-cube" />
|
||||
Create Snapshot
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={handlePull} disabled={behind === 0 || loadingPull}>
|
||||
{loadingPull ? (
|
||||
<Fragment>
|
||||
<i className="fa fa-spin fa-refresh" /> Pulling Snapshots...
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<i className="fa fa-cloud-download" /> Pull {behind || ''} Snapshot
|
||||
{behind === 1 ? '' : 's'}
|
||||
</Fragment>
|
||||
)}
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={handlePush} disabled={ahead === 0 || loadingPush}>
|
||||
{loadingPush ? (
|
||||
<Fragment>
|
||||
<i className="fa fa-spin fa-refresh" /> Pushing Snapshots...
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<i className="fa fa-cloud-upload" /> Push {ahead || ''} Snapshot
|
||||
{ahead === 1 ? '' : 's'}
|
||||
</Fragment>
|
||||
)}
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -89,15 +89,15 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
};
|
||||
// TODO: if gRPC, we should also copy the protofile to the destination workspace - INS-267
|
||||
await requestOperations.update(request, patch);
|
||||
setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
justMoved: true,
|
||||
});
|
||||
}));
|
||||
setTimeout(() => {
|
||||
setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
justMoved: false,
|
||||
});
|
||||
}));
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@ -119,15 +119,15 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
};
|
||||
// TODO: if gRPC, we should also copy the protofile to the destination workspace - INS-267
|
||||
await requestOperations.duplicate(request, patch);
|
||||
setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
justCopied: true,
|
||||
});
|
||||
}));
|
||||
setTimeout(() => {
|
||||
setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
justCopied: false,
|
||||
});
|
||||
}));
|
||||
}, 2000);
|
||||
models.stats.incrementCreatedRequests();
|
||||
}
|
||||
@ -139,9 +139,9 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
const updated = await requestOperations.update(request, {
|
||||
[event.currentTarget.name]: event.currentTarget.checked,
|
||||
});
|
||||
setState({
|
||||
setState(state => ({
|
||||
...state, request: updated,
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -195,7 +195,7 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setState({ ...state, showDescription: true })}
|
||||
onClick={() => setState(state => ({ ...state, showDescription: true }))}
|
||||
className="btn btn--outlined btn--super-duper-compact"
|
||||
>
|
||||
Add Description
|
||||
@ -237,7 +237,7 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
const updated = await requestOperations.update(request, {
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
});
|
||||
setState({ ...state, request: updated });
|
||||
setState(state => ({ ...state, request: updated }));
|
||||
}}
|
||||
>
|
||||
<option value={'global'}>Use global setting</option>
|
||||
@ -261,7 +261,7 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
onChange={event => {
|
||||
const { value } = event.currentTarget;
|
||||
const workspaceId = value === '__NULL__' ? null : value;
|
||||
setState({ ...state, activeWorkspaceIdToCopyTo: workspaceId });
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
@ -319,16 +319,16 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
const updated = await models.request.update(request, {
|
||||
description,
|
||||
});
|
||||
setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
request: updated,
|
||||
defaultPreviewMode: false,
|
||||
});
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setState({ ...state, showDescription: true })}
|
||||
onClick={() => setState(state => ({ ...state, showDescription: true }))}
|
||||
className="btn btn--outlined btn--super-duper-compact"
|
||||
>
|
||||
Add Description
|
||||
@ -415,7 +415,7 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
const updated = await models.request.update(request, {
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
});
|
||||
setState({ ...state, request: updated });
|
||||
setState(state => ({ ...state, request: updated }));
|
||||
}}
|
||||
>
|
||||
<option value={'global'}>Use global setting</option>
|
||||
@ -439,7 +439,7 @@ export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, Modal
|
||||
onChange={event => {
|
||||
const { value } = event.currentTarget;
|
||||
const workspaceId = value === '__NULL__' ? null : value;
|
||||
setState({ ...state, activeWorkspaceIdToCopyTo: workspaceId });
|
||||
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
|
||||
}}
|
||||
>
|
||||
<option value="__NULL__">-- Select Workspace --</option>
|
||||
|
@ -1,154 +1,51 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
import { interceptAccessError } from '../../../sync/vcs/util';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { selectSyncItems } from '../../redux/selectors';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
import { SyncPullButton } from '../sync-pull-button';
|
||||
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
type Props = ModalProps & {
|
||||
vcs: VCS;
|
||||
}
|
||||
|
||||
interface State {
|
||||
};
|
||||
export interface SyncBranchesModalOptions {
|
||||
error: string;
|
||||
newBranchName: string;
|
||||
currentBranch: string;
|
||||
branches: string[];
|
||||
remoteBranches: string[];
|
||||
onHide?: () => void;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class UnconnectedSyncBranchesModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
|
||||
state: State = {
|
||||
export interface SyncBranchesModalHandle {
|
||||
show: (options: SyncBranchesModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ vcs }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<SyncBranchesModalOptions>({
|
||||
error: '',
|
||||
newBranchName: '',
|
||||
branches: [],
|
||||
remoteBranches: [],
|
||||
currentBranch: '',
|
||||
};
|
||||
|
||||
_setModalRef(modal: ModalHandle) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
async _handleCheckout(branch: string) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
try {
|
||||
const delta = await vcs.checkout(syncItems, branch);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
await this.refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to checkout', err.stack);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleMerge(branch: string) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
const delta = await vcs.merge(syncItems, branch);
|
||||
|
||||
try {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
await this.refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to merge', err.stack);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleRemoteDelete(branch: string) {
|
||||
const { vcs } = this.props;
|
||||
|
||||
try {
|
||||
await vcs.removeRemoteBranch(branch);
|
||||
await this.refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to remote delete', err.stack);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleDelete(branch: string) {
|
||||
const { vcs } = this.props;
|
||||
|
||||
try {
|
||||
await vcs.removeBranch(branch);
|
||||
await this.refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to delete', err.stack);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleCreate(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
try {
|
||||
// Create new branch
|
||||
const { newBranchName } = this.state;
|
||||
await vcs.fork(newBranchName);
|
||||
// Checkout new branch
|
||||
const delta = await vcs.checkout(syncItems, newBranchName);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
// Clear branch name and refresh things
|
||||
await this.refreshState({
|
||||
newBranchName: '',
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Failed to create', err.stack);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_updateNewBranchName(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
|
||||
this.setState({ newBranchName: event.currentTarget.value });
|
||||
}
|
||||
|
||||
_handleClearError() {
|
||||
this.setState({ error: '' });
|
||||
}
|
||||
|
||||
async refreshState(newState?: Record<string, any>) {
|
||||
const { vcs } = this.props;
|
||||
});
|
||||
|
||||
const refreshState = useCallback(async () => {
|
||||
try {
|
||||
const currentBranch = await vcs.getBranch();
|
||||
const branches = (await vcs.getBranches()).sort();
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
branches,
|
||||
currentBranch,
|
||||
error: '',
|
||||
...newState,
|
||||
});
|
||||
}));
|
||||
|
||||
const remoteBranches = await interceptAccessError({
|
||||
callback: async () => (await vcs.getRemoteBranches()).filter(b => !branches.includes(b)).sort(),
|
||||
@ -156,173 +53,244 @@ export class UnconnectedSyncBranchesModal extends PureComponent<Props, State> {
|
||||
resourceName: 'remote',
|
||||
resourceType: 'branches',
|
||||
});
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
remoteBranches,
|
||||
});
|
||||
}));
|
||||
} catch (err) {
|
||||
console.log('Failed to refresh', err.stack);
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}, [vcs]);
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => modalRef.current?.hide(),
|
||||
show: ({ onHide }) => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
onHide,
|
||||
}));
|
||||
refreshState();
|
||||
modalRef.current?.show({ onHide });
|
||||
},
|
||||
}), [refreshState]);
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
async function handleCheckout(branch: string) {
|
||||
try {
|
||||
const delta = await vcs.checkout(syncItems, branch);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
await refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to checkout', err.stack);
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
}));
|
||||
}
|
||||
}
|
||||
const handleMerge = async (branch: string) => {
|
||||
const delta = await vcs.merge(syncItems, branch);
|
||||
try {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
await refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to merge', err.stack);
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
const handleRemoteDelete = async (branch: string) => {
|
||||
try {
|
||||
await vcs.removeRemoteBranch(branch);
|
||||
await refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to remote delete', err.stack);
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
async show() {
|
||||
this.modal && this.modal.show();
|
||||
await this.refreshState();
|
||||
}
|
||||
const handleDelete = async (branch: string) => {
|
||||
try {
|
||||
await vcs.removeBranch(branch);
|
||||
await refreshState();
|
||||
} catch (err) {
|
||||
console.log('Failed to delete', err.stack);
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { vcs } = this.props;
|
||||
const { branches, remoteBranches, currentBranch, newBranchName, error } = this.state;
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader>Branches</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={this._handleClearError}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={this._handleCreate}>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
New Branch Name
|
||||
<input
|
||||
type="text"
|
||||
onChange={this._updateNewBranchName}
|
||||
placeholder="testing-branch"
|
||||
value={newBranchName}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
const handleCreate = async (event: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
// Create new branch
|
||||
const { newBranchName } = state;
|
||||
await vcs.fork(newBranchName);
|
||||
// Checkout new branch
|
||||
const delta = await vcs.checkout(syncItems, newBranchName);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
// Clear branch name and refresh things
|
||||
await refreshState();
|
||||
setState(state => ({
|
||||
...state,
|
||||
newBranchName: '',
|
||||
}));
|
||||
} catch (err) {
|
||||
console.log('Failed to create', err.stack);
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
}));
|
||||
}
|
||||
};
|
||||
const { branches, remoteBranches, currentBranch, newBranchName, error } = state;
|
||||
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Branches</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: '' }))}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
New Branch Name
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => setState(state => ({ ...state, newBranchName: event.target.value }))}
|
||||
placeholder="testing-branch"
|
||||
value={newBranchName}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div className="form-control form-control--no-label width-auto">
|
||||
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Branches</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{branches.map(name => (
|
||||
<tr key={name} className="table--no-outline-row">
|
||||
<td>
|
||||
<span
|
||||
className={classnames({
|
||||
bold: name === currentBranch,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{name === currentBranch ? (
|
||||
<span className="txt-sm space-left">(current)</span>
|
||||
) : null}
|
||||
{name === 'master' && <i className="fa fa-lock space-left faint" />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Merged"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleMerge(name)}
|
||||
>
|
||||
Merge
|
||||
</PromptButton>
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch || name === 'master'}
|
||||
onClick={() => handleDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
<button
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleCheckout(name)}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{remoteBranches.length > 0 && (
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Branches</th>
|
||||
<th className="text-left">Remote Branches</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{branches.map(name => (
|
||||
{remoteBranches.map(name => (
|
||||
<tr key={name} className="table--no-outline-row">
|
||||
<td>
|
||||
<span
|
||||
className={classnames({
|
||||
bold: name === currentBranch,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{name === currentBranch ? (
|
||||
<span className="txt-sm space-left">(current)</span>
|
||||
) : null}
|
||||
{name}
|
||||
{name === 'master' && <i className="fa fa-lock space-left faint" />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<PromptButton
|
||||
{name !== 'master' && (
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => handleRemoteDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
)}
|
||||
<SyncPullButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Merged"
|
||||
branch={name}
|
||||
onPull={refreshState}
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => this._handleMerge(name)}
|
||||
vcs={vcs}
|
||||
>
|
||||
Merge
|
||||
</PromptButton>
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch || name === 'master'}
|
||||
onClick={() => this._handleDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
<button
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => this._handleCheckout(name)}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
Fetch
|
||||
</SyncPullButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{remoteBranches.length > 0 && (
|
||||
<div className="pad-top">
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Remote Branches</th>
|
||||
<th className="text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{remoteBranches.map(name => (
|
||||
<tr key={name} className="table--no-outline-row">
|
||||
<td>
|
||||
{name}
|
||||
{name === 'master' && <i className="fa fa-lock space-left faint" />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{name !== 'master' && (
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
doneMessage="Deleted"
|
||||
disabled={name === currentBranch}
|
||||
onClick={() => this._handleRemoteDelete(name)}
|
||||
>
|
||||
Delete
|
||||
</PromptButton>
|
||||
)}
|
||||
<SyncPullButton
|
||||
className="btn btn--micro btn--outlined space-left"
|
||||
branch={name}
|
||||
onPull={this.refreshState}
|
||||
disabled={name === currentBranch}
|
||||
vcs={vcs}
|
||||
>
|
||||
Fetch
|
||||
</SyncPullButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
syncItems: selectSyncItems(state),
|
||||
)}
|
||||
</ModalBody>
|
||||
</Modal >
|
||||
);
|
||||
});
|
||||
|
||||
export const SyncBranchesModal = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{ forwardRef: true },
|
||||
)(UnconnectedSyncBranchesModal);
|
||||
SyncBranchesModal.displayName = 'SyncBranchesModal';
|
||||
|
@ -1,141 +1,88 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { strings } from '../../../common/strings';
|
||||
import { interceptAccessError } from '../../../sync/vcs/util';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { Button } from '../../components/themed-button';
|
||||
import { selectActiveWorkspace } from '../../redux/selectors';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { Button } from '../themed-button';
|
||||
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
type Props = ModalProps & {
|
||||
vcs: VCS;
|
||||
}
|
||||
|
||||
interface State {
|
||||
};
|
||||
export interface SyncDeleteModalOptions {
|
||||
error: string;
|
||||
workspaceName: string;
|
||||
onHide?: () => void;
|
||||
}
|
||||
export interface SyncDeleteModalHandle {
|
||||
show: (options: SyncDeleteModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<SyncDeleteModalOptions>({
|
||||
error: '',
|
||||
workspaceName: '',
|
||||
});
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
error: '',
|
||||
workspaceName: '',
|
||||
};
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class UnconnectedSyncDeleteModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
input: HTMLInputElement | null = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = INITIAL_STATE;
|
||||
}
|
||||
|
||||
_setModalRef(modal: ModalHandle) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
_setInputRef(input: HTMLInputElement) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
_updateWorkspaceName(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
this.setState({
|
||||
workspaceName: event.currentTarget.value,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleDelete(event: React.SyntheticEvent<HTMLFormElement>) {
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => modalRef.current?.hide(),
|
||||
show: ({ onHide }) => {
|
||||
setState({
|
||||
error: '',
|
||||
workspaceName: '',
|
||||
onHide,
|
||||
});
|
||||
modalRef.current?.show({ onHide });
|
||||
},
|
||||
}), []);
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
const onSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const { vcs } = this.props;
|
||||
const { workspaceName } = this.state;
|
||||
|
||||
try {
|
||||
await interceptAccessError({
|
||||
action: 'delete',
|
||||
callback: () => vcs.archiveProject(),
|
||||
resourceName: workspaceName,
|
||||
resourceName: state.workspaceName,
|
||||
resourceType: strings.collection.singular.toLowerCase(),
|
||||
});
|
||||
this.hide();
|
||||
modalRef.current?.hide();
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
const { error, workspaceName } = state;
|
||||
|
||||
async show() {
|
||||
this.modal && this.modal.show();
|
||||
// Reset state
|
||||
this.setState(INITIAL_STATE);
|
||||
// Focus input when modal shows
|
||||
setTimeout(() => {
|
||||
this.input?.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, workspaceName } = this.state;
|
||||
const { activeWorkspace } = this.props;
|
||||
const workspaceNameElement = (
|
||||
<strong
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{activeWorkspace?.name}
|
||||
</strong>
|
||||
);
|
||||
return (
|
||||
<Modal ref={this._setModalRef} skinny>
|
||||
<ModalHeader>Delete {strings.collection.singular}</ModalHeader>
|
||||
<ModalBody className="wide pad-left pad-right text-center" noScroll>
|
||||
{error && <p className="notice error margin-bottom-sm no-margin-top">{error}</p>}
|
||||
<p className="selectable">
|
||||
This will permanently delete the {workspaceNameElement}{' '}
|
||||
{strings.collection.singular.toLowerCase()} remotely.
|
||||
</p>
|
||||
<p className="selectable">Please type {workspaceNameElement} to confirm.</p>
|
||||
|
||||
<form onSubmit={this._handleDelete}>
|
||||
<div className="form-control form-control--outlined">
|
||||
<input
|
||||
ref={this._setInputRef}
|
||||
type="text"
|
||||
onChange={this._updateWorkspaceName}
|
||||
value={workspaceName}
|
||||
/>
|
||||
<Button bg="danger" disabled={workspaceName !== activeWorkspace?.name}>
|
||||
Delete {strings.collection.singular}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
activeWorkspace: selectActiveWorkspace(state),
|
||||
return (
|
||||
<Modal ref={modalRef} skinny>
|
||||
<ModalHeader>Delete {strings.collection.singular}</ModalHeader>
|
||||
<ModalBody className="wide pad-left pad-right text-center" noScroll>
|
||||
{error && <p className="notice error margin-bottom-sm no-margin-top">{error}</p>}
|
||||
<p className="selectable">
|
||||
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>}{' '}
|
||||
{strings.collection.singular.toLowerCase()} remotely.
|
||||
</p>
|
||||
<p className="selectable">Please type {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>} to confirm.</p>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-control form-control--outlined">
|
||||
<input
|
||||
type="text"
|
||||
onChange={event => setState(state => ({ ...state, workspaceName: event.target.value }))}
|
||||
value={workspaceName}
|
||||
/>
|
||||
<Button bg="danger" disabled={workspaceName !== activeWorkspace?.name}>
|
||||
Delete {strings.collection.singular}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export const SyncDeleteModal = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{ forwardRef: true },
|
||||
)(UnconnectedSyncDeleteModal);
|
||||
SyncDeleteModal.displayName = 'SyncDeleteModal';
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { Fragment, PureComponent } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as session from '../../../account/session';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import type { Snapshot } from '../../../sync/types';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { selectSyncItems } from '../../redux/selectors';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
@ -13,143 +13,119 @@ import { HelpTooltip } from '../help-tooltip';
|
||||
import { TimeFromNow } from '../time-from-now';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
type Props = ModalProps & {
|
||||
vcs: VCS;
|
||||
}
|
||||
|
||||
};
|
||||
interface State {
|
||||
branch: string;
|
||||
history: Snapshot[];
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class SyncHistoryModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
handleRollback?: (arg0: Snapshot) => Promise<void>;
|
||||
|
||||
state: State = {
|
||||
export interface SyncHistoryModalHandle {
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const SyncHistoryModal = forwardRef<SyncHistoryModalHandle, Props>(({ vcs }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
branch: '',
|
||||
history: [],
|
||||
};
|
||||
|
||||
_setModalRef(modal: ModalHandle) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
async _handleClickRollback(snapshot: Snapshot) {
|
||||
// TODO: unsound non-null assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await this.handleRollback!(snapshot);
|
||||
await this.refreshState();
|
||||
}
|
||||
|
||||
async refreshState(newState?: Record<string, any>) {
|
||||
const { vcs } = this.props;
|
||||
const branch = await vcs.getBranch();
|
||||
const history = await vcs.getHistory();
|
||||
this.setState({
|
||||
branch,
|
||||
history: history.sort((a, b) => (a.created < b.created ? 1 : -1)),
|
||||
...newState,
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
async show(options: { handleRollback: (arg0: Snapshot) => Promise<void> }) {
|
||||
this.modal?.show();
|
||||
this.handleRollback = options.handleRollback;
|
||||
await this.refreshState();
|
||||
}
|
||||
|
||||
static renderAuthorName(snapshot: Snapshot) {
|
||||
let name = '';
|
||||
let email = '';
|
||||
});
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async () => {
|
||||
const branch = await vcs.getBranch();
|
||||
const history = await vcs.getHistory();
|
||||
setState({
|
||||
branch,
|
||||
history: history.sort((a, b) => (a.created < b.created ? 1 : -1)),
|
||||
});
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), [vcs]);
|
||||
|
||||
const authorName = (snapshot: Snapshot) => {
|
||||
let fullName = '';
|
||||
if (snapshot.authorAccount) {
|
||||
const { firstName, lastName } = snapshot.authorAccount;
|
||||
name += `${firstName} ${lastName}`;
|
||||
email = snapshot.authorAccount.email;
|
||||
fullName += `${firstName} ${lastName}`;
|
||||
}
|
||||
|
||||
if (snapshot.author === session.getAccountId()) {
|
||||
name += ' (you)';
|
||||
fullName += ' (you)';
|
||||
}
|
||||
|
||||
if (name) {
|
||||
return (
|
||||
<Fragment>
|
||||
{name}{' '}
|
||||
<HelpTooltip
|
||||
info
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
delay={500}
|
||||
>
|
||||
{email}
|
||||
</HelpTooltip>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { branch, history } = this.state;
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader>
|
||||
Branch History: <i>{branch}</i>
|
||||
</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
<table className="table--fancy table--striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Message</th>
|
||||
<th className="text-left">When</th>
|
||||
<th className="text-left">Author</th>
|
||||
<th className="text-right">Objects</th>
|
||||
<th className="text-right">
|
||||
Restore
|
||||
<HelpTooltip>
|
||||
This will revert the workspace to that state stored in the snapshot
|
||||
</HelpTooltip>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{history.map(snapshot => (
|
||||
<tr key={snapshot.id}>
|
||||
<td>
|
||||
<Tooltip message={snapshot.id} selectable wide delay={500}>
|
||||
{snapshot.name}
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<TimeFromNow
|
||||
className="no-wrap"
|
||||
timestamp={snapshot.created}
|
||||
intervalSeconds={30}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-left">{SyncHistoryModal.renderAuthorName(snapshot)}</td>
|
||||
<td className="text-right">{snapshot.state.length}</td>
|
||||
<td className="text-right">
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined"
|
||||
onClick={() => this._handleClickRollback(snapshot)}
|
||||
return fullName;
|
||||
};
|
||||
const { branch, history } = state;
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>
|
||||
Branch History: <i>{branch}</i>
|
||||
</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
<table className="table--fancy table--striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Message</th>
|
||||
<th className="text-left">When</th>
|
||||
<th className="text-left">Author</th>
|
||||
<th className="text-right">Objects</th>
|
||||
<th className="text-right">
|
||||
Restore
|
||||
<HelpTooltip>
|
||||
This will revert the workspace to that state stored in the snapshot
|
||||
</HelpTooltip>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{history.map(snapshot => (
|
||||
<tr key={snapshot.id}>
|
||||
<td>
|
||||
<Tooltip message={snapshot.id} selectable wide delay={500}>
|
||||
{snapshot.name}
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<TimeFromNow
|
||||
className="no-wrap"
|
||||
timestamp={snapshot.created}
|
||||
intervalSeconds={30}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-left">{Boolean(authorName(snapshot)) ? (
|
||||
<>
|
||||
{authorName(snapshot)}{' '}
|
||||
<HelpTooltip
|
||||
info
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
delay={500}
|
||||
>
|
||||
Restore
|
||||
</PromptButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
{snapshot.authorAccount?.email || ''}
|
||||
</HelpTooltip>
|
||||
</>
|
||||
) : '--'}</td>
|
||||
<td className="text-right">{snapshot.state.length}</td>
|
||||
<td className="text-right">
|
||||
<PromptButton
|
||||
className="btn btn--micro btn--outlined"
|
||||
onClick={async () => {
|
||||
const delta = await vcs.rollback(snapshot.id, syncItems);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
await db.batchModifyDocs(delta);
|
||||
}}
|
||||
>
|
||||
Restore
|
||||
</PromptButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
SyncHistoryModal.displayName = 'SyncHistoryModal';
|
||||
|
@ -1,143 +1,103 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import type { DocumentKey, MergeConflict } from '../../../sync/types';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { selectSyncItems } from '../../redux/selectors';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import type { MergeConflict } from '../../../sync/types';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
vcs: VCS;
|
||||
export interface SyncMergeModalOptions {
|
||||
conflicts?: MergeConflict[];
|
||||
handleDone?: (conflicts?: MergeConflict[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
conflicts: MergeConflict[];
|
||||
export interface SyncMergeModalHandle {
|
||||
show: (options: SyncMergeModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class UnconnectedSyncMergeModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
_handleDone?: (arg0: MergeConflict[]) => void;
|
||||
|
||||
state: State = {
|
||||
export const SyncMergeModal = forwardRef<SyncMergeModalHandle, ModalProps>((_, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const [state, setState] = useState<SyncMergeModalOptions>({
|
||||
conflicts: [],
|
||||
};
|
||||
});
|
||||
|
||||
_setModalRef(modal: ModalHandle) {
|
||||
this.modal = modal;
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => modalRef.current?.hide(),
|
||||
show: ({ conflicts, handleDone }) => {
|
||||
setState({
|
||||
conflicts,
|
||||
handleDone,
|
||||
});
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), []);
|
||||
|
||||
_handleOk() {
|
||||
// TODO: unsound non-null assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this._handleDone!(this.state.conflicts);
|
||||
const { conflicts, handleDone } = state;
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
_handleToggleSelect(key: DocumentKey, event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
const conflicts = this.state.conflicts.map(c => {
|
||||
if (c.key !== key) {
|
||||
return c;
|
||||
}
|
||||
|
||||
return { ...c, choose: event.currentTarget.value || null };
|
||||
});
|
||||
this.setState({
|
||||
conflicts,
|
||||
});
|
||||
}
|
||||
|
||||
async show(options: {
|
||||
conflicts: MergeConflict[];
|
||||
handleDone: (arg0: MergeConflict[]) => void;
|
||||
}) {
|
||||
this.modal?.show();
|
||||
this._handleDone = options.handleDone;
|
||||
this.setState({
|
||||
conflicts: options.conflicts,
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { conflicts } = this.state;
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader key="header">Resolve Conflicts</ModalHeader>
|
||||
<ModalBody key="body" className="pad text-center" noScroll>
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th
|
||||
style={{
|
||||
width: '10rem',
|
||||
}}
|
||||
>
|
||||
Choose
|
||||
</th>
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader key="header">Resolve Conflicts</ModalHeader>
|
||||
<ModalBody key="body" className="pad text-center" noScroll>
|
||||
<table className="table--fancy table--outlined">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th
|
||||
style={{
|
||||
width: '10rem',
|
||||
}}
|
||||
>
|
||||
Choose
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conflicts?.length && conflicts.map(conflict => (
|
||||
<tr key={conflict.key}>
|
||||
<td className="text-left">{conflict.name}</td>
|
||||
<td className="text-left">{conflict.message}</td>
|
||||
<td className="no-wrap">
|
||||
<label className="no-pad">
|
||||
Mine{' '}
|
||||
<input
|
||||
type="radio"
|
||||
value={conflict.mineBlob || ''}
|
||||
checked={conflict.choose === conflict.mineBlob}
|
||||
onChange={event => setState({
|
||||
...state,
|
||||
conflicts: conflicts.map(c => c.key !== conflict.key ? c : { ...c, choose: event.target.value || null }),
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<label className="no-pad margin-left">
|
||||
Theirs{' '}
|
||||
<input
|
||||
type="radio"
|
||||
value={conflict.theirsBlob || ''}
|
||||
checked={conflict.choose === conflict.theirsBlob}
|
||||
onChange={event => setState({
|
||||
...state,
|
||||
conflicts: conflicts.map(c => c.key !== conflict.key ? c : { ...c, choose: event.target.value || null }),
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conflicts.map(conflict => (
|
||||
<tr key={conflict.key}>
|
||||
<td className="text-left">{conflict.name}</td>
|
||||
<td className="text-left">{conflict.message}</td>
|
||||
<td className="no-wrap">
|
||||
<label className="no-pad">
|
||||
Mine{' '}
|
||||
<input
|
||||
type="radio"
|
||||
value={conflict.mineBlob || ''}
|
||||
checked={conflict.choose === conflict.mineBlob}
|
||||
onChange={e => this._handleToggleSelect(conflict.key, e)}
|
||||
/>
|
||||
</label>
|
||||
<label className="no-pad margin-left">
|
||||
Theirs{' '}
|
||||
<input
|
||||
type="radio"
|
||||
value={conflict.theirsBlob || ''}
|
||||
checked={conflict.choose === conflict.theirsBlob}
|
||||
onChange={e => this._handleToggleSelect(conflict.key, e)}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn" onClick={this._handleOk}>
|
||||
Submit Resolutions
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
syncItems: selectSyncItems(state),
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
handleDone?.(conflicts);
|
||||
modalRef.current?.hide();
|
||||
}}
|
||||
>
|
||||
Submit Resolutions
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal >
|
||||
);
|
||||
});
|
||||
|
||||
export const SyncMergeModal = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{ forwardRef: true },
|
||||
)(UnconnectedSyncMergeModal);
|
||||
SyncMergeModal.displayName = 'SyncMergeModal';
|
||||
|
@ -1,28 +1,23 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { Fragment, PureComponent, ReactNode } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { strings } from '../../../common/strings';
|
||||
import * as models from '../../../models';
|
||||
import { BaseModel } from '../../../models';
|
||||
import type { DocumentKey, Stage, StageEntry, Status } from '../../../sync/types';
|
||||
import { describeChanges } from '../../../sync/vcs/util';
|
||||
import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { selectSyncItems } from '../../redux/selectors';
|
||||
import { IndeterminateCheckbox } from '../base/indeterminate-checkbox';
|
||||
import { type ModalHandle, Modal } from '../base/modal';
|
||||
import { type ModalHandle, Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalFooter } from '../base/modal-footer';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
type Props = ModalProps & {
|
||||
vcs: VCS;
|
||||
}
|
||||
};
|
||||
|
||||
type LookupMap = Record<string, {
|
||||
entry: StageEntry;
|
||||
@ -31,382 +26,353 @@ type LookupMap = Record<string, {
|
||||
checked: boolean;
|
||||
}>;
|
||||
|
||||
interface State {
|
||||
export interface SyncStagingModalOptions {
|
||||
status: Status;
|
||||
message: string;
|
||||
error: string;
|
||||
branch: string;
|
||||
lookupMap: LookupMap;
|
||||
onSnapshot: () => Promise<void>;
|
||||
handlePush: () => Promise<void>;
|
||||
}
|
||||
|
||||
const _initialState: State = {
|
||||
status: {
|
||||
stage: {},
|
||||
unstaged: {},
|
||||
key: '',
|
||||
},
|
||||
branch: '',
|
||||
error: '',
|
||||
message: '',
|
||||
lookupMap: {},
|
||||
};
|
||||
export interface SyncStagingModalHandle {
|
||||
show: (options: SyncStagingModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class UnconnectedSyncStagingModal extends PureComponent<Props, State> {
|
||||
modal: ModalHandle | null = null;
|
||||
_onSnapshot: (() => void) | null = null;
|
||||
_handlePush: (() => Promise<void>) | null = null;
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
state = _initialState;
|
||||
export const SyncStagingModal = forwardRef<SyncStagingModalHandle, Props>(({ vcs }, ref) => {
|
||||
const modalRef = useRef<ModalHandle>(null);
|
||||
const syncItems = useSelector(selectSyncItems);
|
||||
const [state, setState] = useState<SyncStagingModalOptions>({
|
||||
status: {
|
||||
stage: {},
|
||||
unstaged: {},
|
||||
key: '',
|
||||
},
|
||||
branch: '',
|
||||
error: '',
|
||||
message: '',
|
||||
lookupMap: {},
|
||||
onSnapshot: async () => { },
|
||||
handlePush: async () => { },
|
||||
});
|
||||
|
||||
_setModalRef(modal: ModalHandle) {
|
||||
this.modal = modal;
|
||||
}
|
||||
const refreshVCS = useCallback(async (newStage: Stage = {}) => {
|
||||
const branch = await vcs.getBranch();
|
||||
const status = await vcs.status(syncItems, newStage);
|
||||
const lookupMap: LookupMap = {};
|
||||
const allKeys = [...Object.keys(status.stage), ...Object.keys(status.unstaged)];
|
||||
for (const key of allKeys) {
|
||||
const lastSnapshot: BaseModel | null = await vcs.blobFromLastSnapshot(key);
|
||||
const document = syncItems.find(si => si.key === key)?.document;
|
||||
const docOrLastSnapshot = document || lastSnapshot;
|
||||
const entry = status.stage[key] || status.unstaged[key];
|
||||
const hasStagingChangeAndDoc = entry && docOrLastSnapshot;
|
||||
const hasDocAndLastSnapshot = document && lastSnapshot;
|
||||
if (hasStagingChangeAndDoc) {
|
||||
lookupMap[key] = {
|
||||
changes: hasDocAndLastSnapshot ? describeChanges(document, lastSnapshot) : null,
|
||||
entry: entry,
|
||||
type: models.getModelName(docOrLastSnapshot.type),
|
||||
checked: !!status.stage[key],
|
||||
};
|
||||
}
|
||||
}
|
||||
setState(state => ({
|
||||
...state,
|
||||
status,
|
||||
branch,
|
||||
lookupMap,
|
||||
error: '',
|
||||
}));
|
||||
}, [syncItems, vcs]);
|
||||
|
||||
_setTextAreaRef(textarea: HTMLTextAreaElement) {
|
||||
this.textarea = textarea;
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async ({ onSnapshot, handlePush }) => {
|
||||
modalRef.current?.show();
|
||||
// Reset state
|
||||
setState({
|
||||
status: {
|
||||
stage: {},
|
||||
unstaged: {},
|
||||
key: '',
|
||||
},
|
||||
branch: '',
|
||||
error: '',
|
||||
message: '',
|
||||
lookupMap: {},
|
||||
onSnapshot,
|
||||
handlePush,
|
||||
});
|
||||
// Add everything to stage by default except new items
|
||||
const status: Status = await vcs.status(syncItems, {});
|
||||
const toStage: StageEntry[] = [];
|
||||
for (const key of Object.keys(status.unstaged)) {
|
||||
if ('added' in status.unstaged[key]) {
|
||||
// Don't automatically stage added resources
|
||||
continue;
|
||||
}
|
||||
toStage.push(status.unstaged[key]);
|
||||
}
|
||||
const stage = await vcs.stage(status.stage, toStage);
|
||||
await refreshVCS(stage);
|
||||
},
|
||||
}), [refreshVCS, syncItems, vcs]);
|
||||
|
||||
_handleClearError() {
|
||||
this.setState({ error: '' });
|
||||
}
|
||||
|
||||
_handleMessageChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
this.setState({ message: event.currentTarget.value });
|
||||
}
|
||||
|
||||
async _handleStageToggle(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||
const { vcs } = this.props;
|
||||
const { status } = this.state;
|
||||
const handleStageToggle = async (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const { status } = state;
|
||||
const id = event.currentTarget.name;
|
||||
const isStaged = !!status.stage[id];
|
||||
const newStage = isStaged
|
||||
? await vcs.unstage(status.stage, [status.stage[id]])
|
||||
: await vcs.stage(status.stage, [status.unstaged[id]]);
|
||||
await this.refreshMainAttributes({}, newStage);
|
||||
}
|
||||
await refreshVCS(newStage);
|
||||
};
|
||||
|
||||
async _handleAllToggle(keys: DocumentKey[], doStage: boolean) {
|
||||
const { vcs } = this.props;
|
||||
const { status } = this.state;
|
||||
const handleAllToggle = async (keys: DocumentKey[], doStage: boolean) => {
|
||||
const { status } = state;
|
||||
let stage;
|
||||
|
||||
if (doStage) {
|
||||
const entries: StageEntry[] = [];
|
||||
|
||||
for (const k of Object.keys(status.unstaged)) {
|
||||
if (keys.includes(k)) {
|
||||
entries.push(status.unstaged[k]);
|
||||
}
|
||||
}
|
||||
|
||||
stage = await vcs.stage(status.stage, entries);
|
||||
} else {
|
||||
const entries: StageEntry[] = [];
|
||||
|
||||
for (const k of Object.keys(status.stage)) {
|
||||
if (keys.includes(k)) {
|
||||
entries.push(status.stage[k]);
|
||||
}
|
||||
}
|
||||
|
||||
stage = await vcs.unstage(status.stage, entries);
|
||||
}
|
||||
await refreshVCS(stage);
|
||||
};
|
||||
|
||||
await this.refreshMainAttributes({}, stage);
|
||||
}
|
||||
|
||||
async _handleTakeSnapshotAndPush() {
|
||||
const success = await this._handleTakeSnapshot();
|
||||
|
||||
const handleTakeSnapshotAndPush = async () => {
|
||||
const success = await handleTakeSnapshot();
|
||||
if (success) {
|
||||
this._handlePush?.();
|
||||
state.handlePush?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async _handleTakeSnapshot() {
|
||||
const { vcs } = this.props;
|
||||
const handleTakeSnapshot = async () => {
|
||||
const {
|
||||
message,
|
||||
status: { stage },
|
||||
} = this.state;
|
||||
|
||||
onSnapshot,
|
||||
} = state;
|
||||
try {
|
||||
await vcs.takeSnapshot(stage, message);
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
setState(state => ({
|
||||
...state,
|
||||
error: err.message,
|
||||
});
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
this._onSnapshot?.();
|
||||
await this.refreshMainAttributes({
|
||||
message: '',
|
||||
error: '',
|
||||
});
|
||||
this.hide();
|
||||
onSnapshot?.();
|
||||
await refreshVCS();
|
||||
setState(state => ({ ...state, message: '', error: '' }));
|
||||
modalRef.current?.hide();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
async refreshMainAttributes(newState: Partial<State> = {}, newStage: Stage = {}) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
const branch = await vcs.getBranch();
|
||||
const status = await vcs.status(syncItems, newStage);
|
||||
const lookupMap: LookupMap = {};
|
||||
const allKeys = [...Object.keys(status.stage), ...Object.keys(status.unstaged)];
|
||||
const { status, message, error, branch } = state;
|
||||
const allMap = { ...status.stage, ...status.unstaged };
|
||||
const addedKeys: string[] = Object.entries(allMap)
|
||||
.filter(([, value]) => 'added' in value)
|
||||
.map(([key]) => key);
|
||||
const nonAddedKeys: string[] = Object.entries(allMap)
|
||||
.filter(([, value]) => !('added' in value))
|
||||
.map(([key]) => key);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const item = syncItems.find(si => si.key === key);
|
||||
const oldDoc: BaseModel | null = await vcs.blobFromLastSnapshot(key);
|
||||
const doc = (item && item.document) || oldDoc;
|
||||
const entry = status.stage[key] || status.unstaged[key];
|
||||
|
||||
if (!entry || !doc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let changes: string[] | null = null;
|
||||
|
||||
if (item && item.document && oldDoc) {
|
||||
changes = describeChanges(item.document, oldDoc);
|
||||
}
|
||||
|
||||
lookupMap[key] = {
|
||||
changes,
|
||||
entry: entry,
|
||||
type: models.getModelName(doc.type),
|
||||
checked: !!status.stage[key],
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
this.setState({
|
||||
status,
|
||||
branch,
|
||||
lookupMap,
|
||||
error: '',
|
||||
...newState,
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
async show(options: { onSnapshot?: () => any; handlePush: () => Promise<void> }) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
this.modal?.show();
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
this._onSnapshot = options.onSnapshot;
|
||||
this._handlePush = options.handlePush;
|
||||
// Reset state
|
||||
this.setState(_initialState);
|
||||
// Add everything to stage by default except new items
|
||||
const status: Status = await vcs.status(syncItems, {});
|
||||
const toStage: StageEntry[] = [];
|
||||
|
||||
for (const key of Object.keys(status.unstaged)) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
if (status.unstaged[key].added) {
|
||||
// Don't automatically stage added resources
|
||||
continue;
|
||||
}
|
||||
|
||||
toStage.push(status.unstaged[key]);
|
||||
}
|
||||
|
||||
const stage = await vcs.stage(status.stage, toStage);
|
||||
await this.refreshMainAttributes({}, stage);
|
||||
this.textarea?.focus();
|
||||
}
|
||||
|
||||
static renderOperation(entry: StageEntry, type: string, changes: string[]) {
|
||||
let child: JSX.Element | null = null;
|
||||
let message = '';
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION type narrowing
|
||||
if (entry.added) {
|
||||
child = <i className="fa fa-plus-circle success" />;
|
||||
message = 'Added';
|
||||
// @ts-expect-error -- TSCONVERSION type narrowing
|
||||
} else if (entry.modified) {
|
||||
child = <i className="fa fa-circle faded" />;
|
||||
message = `Modified (${changes.join(', ')})`;
|
||||
// @ts-expect-error -- TSCONVERSION type narrowing
|
||||
} else if (entry.deleted) {
|
||||
child = <i className="fa fa-minus-circle danger" />;
|
||||
message = 'Deleted';
|
||||
} else {
|
||||
child = <i className="fa fa-question-circle info" />;
|
||||
message = 'Unknown';
|
||||
}
|
||||
|
||||
if (type === models.workspace.type) {
|
||||
type = strings.collection.singular;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Tooltip message={message}>
|
||||
{child} {type}
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable(keys: DocumentKey[], title: ReactNode) {
|
||||
const { status, lookupMap } = this.state;
|
||||
|
||||
if (keys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let allUnChecked = true;
|
||||
let allChecked = true;
|
||||
|
||||
for (const key of keys.sort()) {
|
||||
if (!status.stage[key]) {
|
||||
allChecked = false;
|
||||
}
|
||||
|
||||
if (!status.unstaged[key]) {
|
||||
allUnChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
const indeterminate = !allChecked && !allUnChecked;
|
||||
return (
|
||||
<div className="pad-top">
|
||||
<strong>{title}</strong>
|
||||
<table className="table--fancy table--outlined margin-top-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label className="wide no-pad">
|
||||
<span className="txt-md">
|
||||
<IndeterminateCheckbox
|
||||
className="space-right"
|
||||
checked={allChecked}
|
||||
onChange={() => this._handleAllToggle(keys, allUnChecked)}
|
||||
indeterminate={indeterminate}
|
||||
/>
|
||||
</span>{' '}
|
||||
name
|
||||
</label>
|
||||
</th>
|
||||
<th className="text-right ">Changes</th>
|
||||
<th className="text-right">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map(key => {
|
||||
if (!lookupMap[key]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { entry, type, checked, changes } = lookupMap[key];
|
||||
return (
|
||||
<tr key={key} className="table--no-outline-row">
|
||||
<td>
|
||||
<label className="no-pad wide">
|
||||
<input
|
||||
className="space-right"
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
name={key}
|
||||
onChange={this._handleStageToggle}
|
||||
/>{' '}
|
||||
{entry.name}
|
||||
</label>
|
||||
</td>
|
||||
<td className="text-right">{changes ? changes.join(', ') : '--'}</td>
|
||||
<td className="text-right">
|
||||
{SyncStagingModal.renderOperation(entry, type, changes || [])}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, message, error, branch } = this.state;
|
||||
const nonAddedKeys: string[] = [];
|
||||
const addedKeys: string[] = [];
|
||||
const allMap = { ...status.stage, ...status.unstaged };
|
||||
const allKeys = Object.keys(allMap);
|
||||
|
||||
for (const key of allKeys) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
if (allMap[key].added) {
|
||||
addedKeys.push(key);
|
||||
} else {
|
||||
nonAddedKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader>Create Snapshot</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={this._handleClearError}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Snapshot Message
|
||||
<textarea
|
||||
ref={this._setTextAreaRef}
|
||||
cols={30}
|
||||
rows={3}
|
||||
onChange={this._handleMessageChange}
|
||||
value={message}
|
||||
placeholder="This is a helpful message that describe the changes made in this snapshot"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderTable(nonAddedKeys, 'Modified Objects')}
|
||||
{this.renderTable(addedKeys, 'Unversioned Objects')}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {branch}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={this._handleTakeSnapshot}>
|
||||
Create
|
||||
</button>
|
||||
<button className="btn" onClick={this._handleTakeSnapshotAndPush}>
|
||||
Create and Push
|
||||
return (
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>Create Snapshot</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
<button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: '' }))}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Snapshot Message
|
||||
<textarea
|
||||
cols={30}
|
||||
rows={3}
|
||||
onChange={event => setState(state => ({ ...state, message: event.target.value }))}
|
||||
value={message}
|
||||
placeholder="This is a helpful message that describe the changes made in this snapshot"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
syncItems: selectSyncItems(state),
|
||||
</div>
|
||||
<ChangesTable
|
||||
keys={nonAddedKeys}
|
||||
title='Modified Objects'
|
||||
status={status}
|
||||
lookupMap={state.lookupMap}
|
||||
toggleAll={handleAllToggle}
|
||||
toggleOne={handleStageToggle}
|
||||
/>
|
||||
<ChangesTable
|
||||
keys={addedKeys}
|
||||
title='Unversioned Objects'
|
||||
status={status}
|
||||
lookupMap={state.lookupMap}
|
||||
toggleAll={handleAllToggle}
|
||||
toggleOne={handleStageToggle}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left italic txt-sm">
|
||||
<i className="fa fa-code-fork" /> {branch}
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={handleTakeSnapshot}>
|
||||
Create
|
||||
</button>
|
||||
<button className="btn" onClick={handleTakeSnapshotAndPush}>
|
||||
Create and Push
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export const SyncStagingModal = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{ forwardRef: true },
|
||||
)(UnconnectedSyncStagingModal);
|
||||
interface OperationTooltipProps {
|
||||
entry: StageEntry;
|
||||
type: string;
|
||||
changes: string[];
|
||||
}
|
||||
const OperationTooltip = ({ entry, type, changes }: OperationTooltipProps) => {
|
||||
const operationType = type === models.workspace.type ? type = strings.collection.singular : type;
|
||||
if ('added' in entry) {
|
||||
return (
|
||||
<Tooltip message="Added">
|
||||
<i className="fa fa-plus-circle success" /> {operationType}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if ('modified' in entry) {
|
||||
return (
|
||||
<Tooltip message={`Modified (${changes.join(', ')})`}>
|
||||
<i className="fa fa-circle faded" /> {operationType}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if ('deleted' in entry) {
|
||||
return (
|
||||
<Tooltip message="Deleted">
|
||||
<i className="fa fa-minus-circle danger" /> {operationType}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip message="Unknown">
|
||||
<i className="fa fa-question-circle info" /> {operationType}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
interface ChangesTableProps {
|
||||
keys: DocumentKey[];
|
||||
title: string;
|
||||
status: Status;
|
||||
lookupMap: LookupMap;
|
||||
toggleAll: (keys: DocumentKey[], doStage: boolean) => void;
|
||||
toggleOne: (event: React.SyntheticEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
const ChangesTable = ({
|
||||
keys,
|
||||
title,
|
||||
status,
|
||||
lookupMap,
|
||||
toggleAll,
|
||||
toggleOne,
|
||||
}: ChangesTableProps) => {
|
||||
if (keys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let allUnChecked = true;
|
||||
let allChecked = true;
|
||||
for (const key of keys.sort()) {
|
||||
if (!status.stage[key]) {
|
||||
allChecked = false;
|
||||
}
|
||||
if (!status.unstaged[key]) {
|
||||
allUnChecked = false;
|
||||
}
|
||||
}
|
||||
const indeterminate = !allChecked && !allUnChecked;
|
||||
return (
|
||||
<div className="pad-top">
|
||||
<strong>{title}</strong>
|
||||
<table className="table--fancy table--outlined margin-top-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label className="wide no-pad">
|
||||
<span className="txt-md">
|
||||
<IndeterminateCheckbox
|
||||
className="space-right"
|
||||
checked={allChecked}
|
||||
onChange={() => toggleAll(keys, allUnChecked)}
|
||||
indeterminate={indeterminate}
|
||||
/>
|
||||
</span>{' '}
|
||||
name
|
||||
</label>
|
||||
</th>
|
||||
<th className="text-right ">Changes</th>
|
||||
<th className="text-right">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.filter(key => lookupMap[key]).map(key => {
|
||||
const { entry, type, checked, changes } = lookupMap[key];
|
||||
return (
|
||||
<tr key={key} className="table--no-outline-row">
|
||||
<td>
|
||||
<label className="no-pad wide">
|
||||
<input
|
||||
className="space-right"
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
name={key}
|
||||
onChange={toggleOne}
|
||||
/>{' '}
|
||||
{entry.name}
|
||||
</label>
|
||||
</td>
|
||||
<td className="text-right">{changes ? changes.join(', ') : '--'}</td>
|
||||
<td className="text-right">
|
||||
<OperationTooltip
|
||||
entry={entry}
|
||||
type={type}
|
||||
changes={changes || []}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SyncStagingModal.displayName = 'SyncStagingModal';
|
||||
|
@ -1,45 +1,29 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { FC, ReactNode, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../common/constants';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { RootState } from '../redux/modules';
|
||||
import { selectActiveProject } from '../redux/selectors';
|
||||
import { showError } from './modals';
|
||||
|
||||
type ReduxProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
interface Props {
|
||||
vcs: VCS;
|
||||
branch: string;
|
||||
onPull: (...args: any[]) => any;
|
||||
onPull: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class UnconnectedSyncPullButton extends PureComponent<Props, State> {
|
||||
_timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
state: State = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
async _handleClick() {
|
||||
const { vcs, onPull, branch, project } = this.props;
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
export const SyncPullButton: FC<Props> = props => {
|
||||
const { className, children, disabled } = props;
|
||||
const project = useSelector(selectActiveProject);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const onClick = async () => {
|
||||
const { vcs, onPull, branch } = props;
|
||||
setLoading(true);
|
||||
const newVCS = vcs.newInstance();
|
||||
const oldBranch = await newVCS.getBranch();
|
||||
let failed = false;
|
||||
|
||||
try {
|
||||
// Clone old VCS so we don't mess anything up while working on other projects
|
||||
await newVCS.checkout([], branch);
|
||||
@ -57,44 +41,15 @@ export class UnconnectedSyncPullButton extends PureComponent<Props, State> {
|
||||
// have to do this hack
|
||||
await newVCS.checkout([], oldBranch);
|
||||
}
|
||||
|
||||
// Do this a bit later so the loading doesn't seem to stop too early
|
||||
this._timeout = setTimeout(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
}, 400);
|
||||
|
||||
setLoading(false);
|
||||
if (!failed) {
|
||||
onPull?.();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._timeout !== null) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, disabled } = this.props;
|
||||
const { loading } = this.state;
|
||||
return (
|
||||
<button className={className} onClick={this._handleClick} disabled={disabled}>
|
||||
{loading && <i className="fa fa-spin fa-refresh space-right" />}
|
||||
{children || 'Pull'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
project: selectActiveProject(state),
|
||||
});
|
||||
|
||||
export const SyncPullButton = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{ forwardRef: true },
|
||||
)(UnconnectedSyncPullButton);
|
||||
};
|
||||
return (
|
||||
<button className={className} onClick={onClick} disabled={disabled}>
|
||||
{loading && <i className="fa fa-spin fa-refresh space-right" />}
|
||||
{children || 'Pull'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -282,11 +282,11 @@ const App = () => {
|
||||
|
||||
{activeWorkspace && vcs ? (
|
||||
<Fragment>
|
||||
<SyncStagingModal ref={registerModal} vcs={vcs} />
|
||||
<SyncMergeModal ref={registerModal} vcs={vcs} />
|
||||
<SyncBranchesModal ref={registerModal} vcs={vcs} />
|
||||
<SyncDeleteModal ref={registerModal} vcs={vcs} />
|
||||
<SyncHistoryModal ref={registerModal} vcs={vcs} />
|
||||
<SyncStagingModal ref={instance => registerModal(instance, 'SyncStagingModal')} vcs={vcs} />
|
||||
<SyncMergeModal ref={instance => registerModal(instance, 'SyncMergeModal')} />
|
||||
<SyncBranchesModal ref={instance => registerModal(instance, 'SyncBranchesModal')} vcs={vcs} />
|
||||
<SyncDeleteModal ref={instance => registerModal(instance, 'SyncDeleteModal')} vcs={vcs} />
|
||||
<SyncHistoryModal ref={instance => registerModal(instance, 'SyncHistoryModal')} vcs={vcs} />
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user