From 5f7f755d3343dcf491a6743c609821e4b00a022c Mon Sep 17 00:00:00 2001 From: Elias Kuiter Date: Tue, 11 Jun 2019 14:08:21 +0200 Subject: [PATCH] Resolution outcome transitions --- client/src/components/CommandBarContainer.tsx | 3 +- .../FeatureDiagramRouteContainer.tsx | 42 +++++++++++-------- .../components/conflictView/ConflictView.tsx | 27 +++++++++--- .../src/components/conflictView/Operation.tsx | 7 ++-- .../src/components/conflictView/Version.tsx | 14 +++++-- .../constraintsView/ConstraintsView.tsx | 5 ++- .../components/overlays/CommandPalette.tsx | 8 ++-- .../components/overlays/OverlayContainer.tsx | 4 +- client/src/constants.ts | 4 ++ client/src/store/actions.ts | 3 +- client/src/store/reducer.ts | 16 ++++++- client/src/store/types.ts | 8 +++- client/src/stylesheets/conflict.css | 19 ++++++++- 13 files changed, 116 insertions(+), 44 deletions(-) diff --git a/client/src/components/CommandBarContainer.tsx b/client/src/components/CommandBarContainer.tsx index 9ef6855b..74a50004 100644 --- a/client/src/components/CommandBarContainer.tsx +++ b/client/src/components/CommandBarContainer.tsx @@ -83,7 +83,7 @@ const CommandBarContainer = (props: StateDerivedProps & RouteProps) => ( commands.featureDiagram.setLayout( props.featureDiagramLayout!, props.onSetFeatureDiagramLayout!), - ...enableConstraintsView(props.featureModel) + ...enableConstraintsView(props.featureModel, props.transitionConflictDescriptor) ? [makeDivider(), commands.featureDiagram.showConstraintView( props.onSetSetting!, props.settings!.views.splitAt), @@ -138,6 +138,7 @@ export default withRouter(connect( isSelectMultipleFeatures: collaborativeSession.isSelectMultipleFeatures, selectedFeatureIDs: collaborativeSession.selectedFeatureIDs, collaborators: collaborativeSession.collaborators, + transitionConflictDescriptor: collaborativeSession.transitionConflictDescriptor, featureModel: getCurrentFeatureModel(state), currentArtifactPath: collaborativeSession.artifactPath }; diff --git a/client/src/components/FeatureDiagramRouteContainer.tsx b/client/src/components/FeatureDiagramRouteContainer.tsx index aa5fc07a..2da5aa10 100644 --- a/client/src/components/FeatureDiagramRouteContainer.tsx +++ b/client/src/components/FeatureDiagramRouteContainer.tsx @@ -38,27 +38,30 @@ class FeatureDiagramRoute extends React.Component { settings={this.props.settings!} onSetSetting={this.props.onSetSetting!} renderPrimaryView={(style: CSSProperties) => - this.props.featureModel - ? - : this.props.conflictDescriptor - ? + transitioning={!!this.props.transitionConflictDescriptor} + transitionResolutionOutcome={this.props.transitionResolutionOutcome} + onEndConflictViewTransition={this.props.onEndConflictTransition!}/> + : this.props.featureModel + ? :
-
} + } renderSecondaryView={() => } - enableSecondaryView={() => enableConstraintsView(this.props.featureModel)}/>; + enableSecondaryView={() => enableConstraintsView(this.props.featureModel, this.props.transitionConflictDescriptor)}/>; } } @@ -78,6 +81,8 @@ export default withRouter(connect( ...props, featureModel: getCurrentFeatureModel(state), conflictDescriptor: getCurrentConflictDescriptor(state), + transitionResolutionOutcome: collaborativeSession.transitionResolutionOutcome, + transitionConflictDescriptor: collaborativeSession.transitionConflictDescriptor, currentArtifactPath: collaborativeSession.artifactPath, featureDiagramLayout: collaborativeSession.layout, isSelectMultipleFeatures: collaborativeSession.isSelectMultipleFeatures, @@ -99,6 +104,7 @@ export default withRouter(connect( onDeselectAllFeatures: () => dispatch(actions.ui.featureDiagram.feature.deselectAll()), onToggleFeatureGroupType: payload => dispatch(actions.server.featureDiagram.feature.properties.toggleGroup(payload)), onToggleFeatureOptional: payload => dispatch(actions.server.featureDiagram.feature.properties.toggleOptional(payload)), - onVote: payload => dispatch(actions.server.featureDiagram.vote(payload)) + onVote: payload => dispatch(actions.server.featureDiagram.vote(payload)), + onEndConflictTransition: () => dispatch(actions.ui.endConflictViewTransition()) }) )(FeatureDiagramRoute)); \ No newline at end of file diff --git a/client/src/components/conflictView/ConflictView.tsx b/client/src/components/conflictView/ConflictView.tsx index dba7b260..5ec05559 100644 --- a/client/src/components/conflictView/ConflictView.tsx +++ b/client/src/components/conflictView/ConflictView.tsx @@ -10,6 +10,7 @@ import {present} from '../../helpers/present'; import {Settings} from '../../store/settings'; import {PersonaSize} from 'office-ui-fabric-react/lib/Persona'; import Version from './Version'; +import constants from '../../constants'; interface Props { conflictDescriptor: KernelConflictDescriptor, @@ -18,7 +19,10 @@ interface Props { voterSiteIDs?: string[], votes: Votes, onVote: OnVoteFunction, - settings: Settings + settings: Settings, + transitioning: boolean, + transitionResolutionOutcome?: string, + onEndConflictViewTransition: () => void }; interface State { @@ -42,8 +46,17 @@ export default class extends React.Component { .map(([siteID, _]) => this.props.collaborators.find(collaborator => collaborator.siteID === siteID)) .filter(present); + componentDidUpdate(prevProps: Props) { + if (!prevProps.transitioning && this.props.transitioning) + window.setTimeout(this.props.onEndConflictViewTransition, + this.props.transitionResolutionOutcome === 'neutral' + ? constants.featureDiagram.conflictView.transitionNeutral + : constants.featureDiagram.conflictView.transition); + } + render(): JSX.Element { - const {conflictDescriptor, myself, collaborators, voterSiteIDs, settings} = this.props, + const {conflictDescriptor, myself, collaborators, voterSiteIDs, settings, + transitioning, transitionResolutionOutcome} = this.props, synchronized = conflictDescriptor.synchronized, pendingVotePermission = synchronized && !voterSiteIDs, allowedToVote = synchronized && !pendingVotePermission && voterSiteIDs!.includes(myself.siteID), @@ -54,7 +67,7 @@ export default class extends React.Component {
{i18n.t('conflictResolution.header')}
   - {(!synchronized || pendingVotePermission || disallowedToVote) && + {!transitioning && (!synchronized || pendingVotePermission || disallowedToVote) &&
{(!synchronized || pendingVotePermission) && } @@ -88,16 +101,18 @@ export default class extends React.Component { ownVotedVersionID={this.ownVotedVersionID()} versionID={versionID} versionIndex={versionIndex} - activeOperationID={this.state.activeOperationID} + activeOperationID={!transitioning ? this.state.activeOperationID : undefined} onSetActiveOperationID={this.onSetActiveOperationID} - activeVersionID={this.state.activeVersionID} + activeVersionID={!transitioning ? this.state.activeVersionID : undefined} onSetActiveVersionID={this.onSetActiveVersionID} myself={myself} collaborators={collaborators} collaboratorsInFavor={this.collaboratorsInFavor(versionID)} settings={settings} onVote={this.onVote} - allowedToVote={allowedToVote}/>)} + allowedToVote={allowedToVote} + transitioning={transitioning} + transitionResolutionOutcome={transitionResolutionOutcome}/>)}
); diff --git a/client/src/components/conflictView/Operation.tsx b/client/src/components/conflictView/Operation.tsx index 0cb39206..10fa17c0 100644 --- a/client/src/components/conflictView/Operation.tsx +++ b/client/src/components/conflictView/Operation.tsx @@ -15,7 +15,8 @@ interface Props { onSetActiveOperationID: (activeOperationID?: string) => void, activeVersionID?: string, myself?: Collaborator, - collaborators: Collaborator[] + collaborators: Collaborator[], + transitioning: boolean }; interface State { @@ -37,7 +38,7 @@ export default class extends React.Component { render() { const {conflictDescriptor, versionID, operationID, activeOperationID, - onSetActiveOperationID, activeVersionID, myself, collaborators} = this.props, + onSetActiveOperationID, activeVersionID, myself, collaborators, transitioning} = this.props, metadata = conflictDescriptor.metadata[operationID], hasConflicts = !!conflictDescriptor.conflicts[versionID][operationID], conflictKeys = hasConflicts && Object.keys(conflictDescriptor.conflicts[versionID][operationID]), @@ -60,7 +61,7 @@ export default class extends React.Component { (operationID === activeOperationID || (hasConflicts && conflictKeys.includes(activeOperationID))) ? 'highlightRed' - : hasConflicts && conflictEntries.length > 0 + : !transitioning && hasConflicts && conflictEntries.length > 0 ? 'highlightDarkRed' : undefined} comments={hasConflicts && conflictEntries.length > 0 diff --git a/client/src/components/conflictView/Version.tsx b/client/src/components/conflictView/Version.tsx index e689c02c..7b1e087d 100644 --- a/client/src/components/conflictView/Version.tsx +++ b/client/src/components/conflictView/Version.tsx @@ -22,7 +22,9 @@ interface Props { collaboratorsInFavor: Collaborator[], settings: Settings, onVote: (versionID: string) => () => void, - allowedToVote: boolean + allowedToVote: boolean, + transitioning: boolean, + transitionResolutionOutcome?: string }; interface State { @@ -45,13 +47,16 @@ export default class extends React.Component { render() { const {ownVotedVersionID, conflictDescriptor, versionID, versionIndex, activeOperationID, onSetActiveOperationID, activeVersionID, onSetActiveVersionID, myself, collaborators, - collaboratorsInFavor, settings, onVote, allowedToVote} = this.props, + collaboratorsInFavor, settings, onVote, allowedToVote, transitioning, + transitionResolutionOutcome} = this.props, operationIDs = conflictDescriptor.versions[versionID], ButtonComponent = ownVotedVersionID === versionID ? PrimaryButton : DefaultButton; return versionID !== 'neutral' &&
-
+
{i18n.t('conflictResolution.version')} {String.fromCharCode(65 + versionIndex)} @@ -72,7 +77,8 @@ export default class extends React.Component { onSetActiveOperationID={onSetActiveOperationID} activeVersionID={activeVersionID} myself={myself} - collaborators={collaborators}/>)} + collaborators={collaborators} + transitioning={transitioning}/>)}
0 : false; +export function enableConstraintsView(featureModel?: FeatureModel, transitionConflictDescriptor?: KernelConflictDescriptor): boolean { + return featureModel && !transitionConflictDescriptor ? featureModel.constraints.length > 0 : false; } interface Props { diff --git a/client/src/components/overlays/CommandPalette.tsx b/client/src/components/overlays/CommandPalette.tsx index 6fe3ba20..2400d898 100644 --- a/client/src/components/overlays/CommandPalette.tsx +++ b/client/src/components/overlays/CommandPalette.tsx @@ -14,6 +14,7 @@ import {enableConstraintsView} from '../constraintsView/ConstraintsView'; import {defaultSettings, Settings} from '../../store/settings'; import {preconditions} from '../../modeling/preconditions'; import {redirectToArtifactPath} from '../../router'; +import {KernelConflictDescriptor} from '../../modeling/types'; interface Props { artifactPaths: ArtifactPath[], @@ -21,6 +22,7 @@ interface Props { isOpen: boolean, featureDiagramLayout?: FeatureDiagramLayoutType, featureModel?: FeatureModel, + transitionConflictDescriptor?: KernelConflictDescriptor, settings: Settings, onDismiss: () => void, onShowOverlay: OnShowOverlayFunction, @@ -320,14 +322,14 @@ export default class extends React.Component { }, { key: 'toggleConstraintView', text: i18n.t('commandPalette.featureDiagram.toggleConstraintView'), - disabled: () => !enableConstraintsView(this.props.featureModel), + disabled: () => !enableConstraintsView(this.props.featureModel, this.props.transitionConflictDescriptor), action: this.action(() => this.props.onSetSetting( {path: 'views.splitAt', value: (splitAt: number) => splitAt > defaultSettings.views.splitAt ? defaultSettings.views.splitAt : 1})) }, { key: 'toggleConstraintViewSplitDirection', text: i18n.t('commandPalette.featureDiagram.toggleConstraintViewSplitDirection'), - disabled: () => !enableConstraintsView(this.props.featureModel), + disabled: () => !enableConstraintsView(this.props.featureModel, this.props.transitionConflictDescriptor), action: this.action(() => this.props.onSetSetting( {path: 'views.splitDirection', value: (splitDirection: 'horizontal' | 'vertical') => splitDirection === 'horizontal' ? 'vertical' : 'horizontal'})) @@ -516,7 +518,7 @@ export default class extends React.Component { }, { text: i18n.t('commandPalette.featureDiagram.constraint.set'), icon: 'Edit', - disabled: () => !enableConstraintsView(this.props.featureModel), + disabled: () => !enableConstraintsView(this.props.featureModel, this.props.transitionConflictDescriptor), action: this.actionWithArguments( [{ title: i18n.t('commandPalette.oldConstraint'), diff --git a/client/src/components/overlays/OverlayContainer.tsx b/client/src/components/overlays/OverlayContainer.tsx index 18504778..4868aa13 100644 --- a/client/src/components/overlays/OverlayContainer.tsx +++ b/client/src/components/overlays/OverlayContainer.tsx @@ -36,6 +36,7 @@ const OverlayContainer = (props: StateDerivedProps & RouteProps) => ( isOpen={props.overlay === OverlayType.commandPalette} featureDiagramLayout={props.featureDiagramLayout} featureModel={props.featureModel} + transitionConflictDescriptor={props.transitionConflictDescriptor} settings={props.settings!} onDismiss={() => props.onHideOverlay!({overlay: OverlayType.commandPalette})} onShowOverlay={props.onShowOverlay!} @@ -222,7 +223,8 @@ export default withRouter(connect( featureDiagramLayout: collaborativeSession.layout, isSelectMultipleFeatures: collaborativeSession.isSelectMultipleFeatures, selectedFeatureIDs: collaborativeSession.selectedFeatureIDs, - featureModel: getCurrentFeatureModel(state) + featureModel: getCurrentFeatureModel(state), + transitionConflictDescriptor: collaborativeSession.transitionConflictDescriptor }; }), (dispatch): StateDerivedProps => ({ diff --git a/client/src/constants.ts b/client/src/constants.ts index 8fc09904..a1d1d2c0 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -10,6 +10,10 @@ const constants = { minWidth: 500, minHeight: 500, maxCollapsibleNodes: (nodes: any[]) => nodes.length > 100 ? nodes.length / 10 : 1 + }, + conflictView: { + transition: 1000, + transitionNeutral: 600 } }, constraint: { diff --git a/client/src/store/actions.ts b/client/src/store/actions.ts index 258b2d35..0ba12620 100644 --- a/client/src/store/actions.ts +++ b/client/src/store/actions.ts @@ -71,7 +71,8 @@ const actions = { overlay: { show: createStandardAction('ui/overlay/show')<{overlay: OverlayType, overlayProps: OverlayProps, selectOneFeature?: string}>(), hide: createStandardAction('ui/overlay/hide')<{overlay: OverlayType}>() - } + }, + endConflictViewTransition: createStandardAction('ui/endConflictViewTransition')() }, server: { receive: createStandardAction('server/receiveMessage')(), diff --git a/client/src/store/reducer.ts b/client/src/store/reducer.ts index 2f5e3810..d749c1a9 100644 --- a/client/src/store/reducer.ts +++ b/client/src/store/reducer.ts @@ -262,7 +262,9 @@ function serverReceiveReducer(state: State, action: Action): State { kernelContext, kernelCombinedEffect, voterSiteIDs: undefined, - votes: {} + votes: {}, + transitionResolutionOutcome: action.payload.versionID, + transitionConflictDescriptor: (collaborativeSession).kernelCombinedEffect }; })); if (isEditingFeatureModel(state)) @@ -413,7 +415,7 @@ function uiReducer(state: State, action: Action): State { (collaborativeSession).collapsedFeatureIDs, getFeatureIDsBelowWithActualChildren(state, currentArtifactPath!, action.payload.featureIDs)) }))) - : state; + : state; case getType(actions.ui.overlay.show): state = updateOverlay(state, action.payload.overlay, action.payload.overlayProps); @@ -427,6 +429,16 @@ function uiReducer(state: State, action: Action): State { return state.overlay === action.payload.overlay ? updateOverlay(state, OverlayType.none, {}) : state; + + case getType(actions.ui.endConflictViewTransition): + return isEditingFeatureModel(state) + ? getNewState(state, 'collaborativeSessions', + getNewCollaborativeSessions(state, currentArtifactPath!, (collaborativeSession: CollaborativeSession) => ({ + ...collaborativeSession, + transitionResolutionOutcome: undefined, + transitionConflictDescriptor: undefined + }))) + : state; } return state; diff --git a/client/src/store/types.ts b/client/src/store/types.ts index 2d817ed5..490e221d 100644 --- a/client/src/store/types.ts +++ b/client/src/store/types.ts @@ -28,7 +28,9 @@ export interface FeatureDiagramCollaborativeSession extends CollaborativeSession selectedFeatureIDs: string[], collapsedFeatureIDs: string[], voterSiteIDs?: string[], - votes: Votes + votes: Votes, + transitionResolutionOutcome?: string, + transitionConflictDescriptor?: KernelConflictDescriptor }; export interface State { @@ -81,6 +83,7 @@ export type OnHideOverlayFunction = (payload: {overlay: OverlayType}) => void; export type OnFitToScreenFunction = () => void; export type OnSetSettingFunction = (payload: {path: string, value: any}) => void; export type OnResetSettingsFunction = () => void; +export type OnEndConflictViewTransitionFunction = () => void; export type OnAddArtifactFunction = (payload: {artifactPath: ArtifactPath, source?: string}) => Promise; export type OnRemoveArtifactFunction = (payload: {artifactPath: ArtifactPath}) => Promise; @@ -126,6 +129,8 @@ export type StateDerivedProps = Partial<{ selectedFeatureIDs: string[], featureModel: FeatureModel, conflictDescriptor: KernelConflictDescriptor, + transitionResolutionOutcome: string, + transitionConflictDescriptor: KernelConflictDescriptor, overlay: OverlayType, overlayProps: OverlayProps, voterSiteIDs: string[], @@ -148,6 +153,7 @@ export type StateDerivedProps = Partial<{ onFitToScreen: OnFitToScreenFunction, onSetSetting: OnSetSettingFunction, onResetSettings: OnResetSettingsFunction, + onEndConflictTransition: OnEndConflictViewTransitionFunction, onAddArtifact: OnAddArtifactFunction, onRemoveArtifact: OnRemoveArtifactFunction, diff --git a/client/src/stylesheets/conflict.css b/client/src/stylesheets/conflict.css index 8d368e3e..3b5a226d 100644 --- a/client/src/stylesheets/conflict.css +++ b/client/src/stylesheets/conflict.css @@ -68,7 +68,18 @@ } .conflict .version > div { - padding: 10px; + padding: 5px; + margin: 5px; + border-radius: 5px; + transition: all 600ms ease-in; +} + +.conflict .version > div.outcome { + background-color: rgb(200, 255, 200); +} + +.conflict .version > div.discarded { + opacity: 0; } .conflict .version > div > .header { @@ -97,7 +108,11 @@ margin-top: 7px; line-height: 1.5em; border-radius: 5px; - transition: all 70ms ease-in-out; + transition: all 70ms ease; +} + +.conflict .version > div.outcome .ms-ActivityItem, .conflict .version > div.discarded .ms-ActivityItem { + transition: all 600ms ease-in; } .conflict .version .ms-ActivityItem.highlightGreen {