Skip to content

Commit dbd02de

Browse files
authored
Merge pull request desktop#17687 from desktop/conflict-resolution-announcements
Conflict resolution announcements
2 parents 8a60f18 + 36aa3a0 commit dbd02de

File tree

7 files changed

+123
-47
lines changed

7 files changed

+123
-47
lines changed

app/src/ui/dialog/error.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as OcticonSymbol from '../octicons/octicons.generated'
1616
export class DialogError extends React.Component {
1717
public render() {
1818
return (
19-
<div className="dialog-error" role="alert">
19+
<div className="dialog-banner dialog-error" role="alert">
2020
<Octicon symbol={OcticonSymbol.stop} />
2121
<div>{this.props.children}</div>
2222
</div>

app/src/ui/dialog/success.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react'
2+
import { Octicon } from '../octicons'
3+
import * as OcticonSymbol from '../octicons/octicons.generated'
4+
5+
/**
6+
* A component used for displaying short success messages inline
7+
* in a dialog. These success messages (there can be more than one)
8+
* should be rendered as the first child of the <Dialog> component
9+
* and support arbitrary content.
10+
*
11+
* The content (success message) is paired with a check icon and receive
12+
* special styling.
13+
*
14+
* Provide `children` to display content inside the error dialog.
15+
*/
16+
export class DialogSuccess extends React.Component {
17+
public render() {
18+
return (
19+
<div className="dialog-banner dialog-success" role="alert">
20+
<Octicon symbol={OcticonSymbol.check} />
21+
<div>{this.props.children}</div>
22+
</div>
23+
)
24+
}
25+
}

app/src/ui/lib/conflicts/unmerged-file.tsx

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
getLabelForManualResolutionOption,
3030
} from '../../../lib/status'
3131

32+
const defaultConflictsResolvedMessage = 'No conflicts remaining'
33+
3234
/**
3335
* Renders an unmerged file status and associated buttons for the merge conflicts modal
3436
* (An "unmerged file" can be conflicted _and_ resolved or _just_ conflicted)
@@ -132,30 +134,31 @@ const renderResolvedFile: React.FunctionComponent<{
132134
readonly branch?: string
133135
readonly dispatcher: Dispatcher
134136
}> = props => {
137+
const fileStatusSummary = getResolvedFileStatusSummary(
138+
props.status,
139+
props.manualResolution,
140+
props.branch
141+
)
135142
return (
136143
<li key={props.path} className="unmerged-file-status-resolved">
137144
<Octicon symbol={OcticonSymbol.fileCode} className="file-octicon" />
138-
<div className="column-left">
145+
<div className="column-left" id={props.path}>
139146
<PathText path={props.path} />
140-
{renderResolvedFileStatusSummary({
141-
path: props.path,
142-
status: props.status,
143-
branch: props.branch,
144-
manualResolution: props.manualResolution,
145-
repository: props.repository,
146-
dispatcher: props.dispatcher,
147-
})}
147+
<div className="file-conflicts-status">{fileStatusSummary}</div>
148148
</div>
149-
<Button
150-
className="undo-button"
151-
onClick={makeUndoManualResolutionClickHandler(
152-
props.path,
153-
props.repository,
154-
props.dispatcher
155-
)}
156-
>
157-
Undo
158-
</Button>
149+
{fileStatusSummary === defaultConflictsResolvedMessage ? null : (
150+
<Button
151+
className="undo-button"
152+
onClick={makeUndoManualResolutionClickHandler(
153+
props.path,
154+
props.repository,
155+
props.dispatcher
156+
)}
157+
ariaDescribedBy={props.path}
158+
>
159+
Undo
160+
</Button>
161+
)}
159162
<div className="green-circle">
160163
<Octicon symbol={OcticonSymbol.check} />
161164
</div>
@@ -415,31 +418,20 @@ function resolvedFileStatusString(
415418
if (manualResolution === ManualConflictResolution.theirs) {
416419
return getUnmergedStatusEntryDescription(status.entry.them, branch)
417420
}
418-
return 'No conflicts remaining'
421+
return defaultConflictsResolvedMessage
419422
}
420423

421-
const renderResolvedFileStatusSummary: React.FunctionComponent<{
422-
path: string
423-
status: ConflictedFileStatus
424-
repository: Repository
425-
dispatcher: Dispatcher
426-
manualResolution?: ManualConflictResolution
424+
const getResolvedFileStatusSummary = (
425+
status: ConflictedFileStatus,
426+
manualResolution?: ManualConflictResolution,
427427
branch?: string
428-
}> = props => {
429-
if (
430-
isConflictWithMarkers(props.status) &&
431-
props.status.conflictMarkerCount === 0
432-
) {
433-
return <div className="file-conflicts-status">No conflicts remaining</div>
434-
}
435-
436-
const statusString = resolvedFileStatusString(
437-
props.status,
438-
props.manualResolution,
439-
props.branch
440-
)
428+
) => {
429+
const noConflictMarkers =
430+
isConflictWithMarkers(status) && status.conflictMarkerCount === 0
441431

442-
return <div className="file-conflicts-status">{statusString}</div>
432+
return noConflictMarkers
433+
? defaultConflictsResolvedMessage
434+
: resolvedFileStatusString(status, manualResolution, branch)
443435
}
444436

445437
/** returns the name of the branch that corresponds to the chosen manual resolution */

app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../../lib/conflicts'
2121
import { ManualConflictResolution } from '../../../models/manual-conflict-resolution'
2222
import { OkCancelButtonGroup } from '../../dialog/ok-cancel-button-group'
23+
import { DialogSuccess } from '../../dialog/success'
2324

2425
interface IConflictsDialogProps {
2526
readonly dispatcher: Dispatcher
@@ -46,6 +47,7 @@ interface IConflictsDialogState {
4647
readonly isCommitting: boolean
4748
readonly isAborting: boolean
4849
readonly isFileResolutionOptionsMenuOpen: boolean
50+
readonly countResolved: number | null
4951
}
5052

5153
/**
@@ -63,6 +65,7 @@ export class ConflictsDialog extends React.Component<
6365
isCommitting: false,
6466
isAborting: false,
6567
isFileResolutionOptionsMenuOpen: false,
68+
countResolved: null,
6669
}
6770
}
6871

@@ -96,6 +99,19 @@ export class ConflictsDialog extends React.Component<
9699
}
97100
}
98101

102+
public componentDidUpdate(): void {
103+
const { workingDirectory, manualResolutions } = this.props
104+
105+
const resolvedConflicts = getResolvedFiles(
106+
workingDirectory,
107+
manualResolutions
108+
)
109+
110+
if (resolvedConflicts.length !== (this.state.countResolved ?? 0)) {
111+
this.setState({ countResolved: resolvedConflicts.length })
112+
}
113+
}
114+
99115
/**
100116
* Invokes submit callback and dismisses modal
101117
*/
@@ -172,6 +188,30 @@ export class ConflictsDialog extends React.Component<
172188
)
173189
}
174190

191+
public renderBanner(conflictedFilesCount: number) {
192+
const { countResolved } = this.state
193+
if (countResolved === null) {
194+
return
195+
}
196+
197+
if (countResolved === 0) {
198+
return <DialogSuccess>All resolutions have been undone.</DialogSuccess>
199+
}
200+
201+
if (conflictedFilesCount === 0) {
202+
return (
203+
<DialogSuccess>All conflicted files have been resolved. </DialogSuccess>
204+
)
205+
}
206+
207+
const conflictPluralized = countResolved === 1 ? 'file has' : 'files have'
208+
return (
209+
<DialogSuccess>
210+
{countResolved} conflicted {conflictPluralized} been resolved.
211+
</DialogSuccess>
212+
)
213+
}
214+
175215
public render() {
176216
const {
177217
workingDirectory,
@@ -202,6 +242,7 @@ export class ConflictsDialog extends React.Component<
202242
loading={this.state.isCommitting}
203243
disabled={this.state.isCommitting}
204244
>
245+
{this.renderBanner(conflictedFiles.length)}
205246
<DialogContent>
206247
{this.renderContent(unmergedFiles, conflictedFiles.length)}
207248
</DialogContent>

app/styles/_variables.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,10 @@ $overlay-background-color: rgba(0, 0, 0, 0.4);
458458
--form-error-border-color: #{$red-200};
459459
--form-error-text-color: #{$red-800};
460460

461+
--dialog-banner-success-background: #{$green-100};
462+
--dialog-banner-success-border-color: #{$green-300};
463+
--dialog-banner-success-text-color: #{$green-800};
464+
461465
// Inline form errors, displayed after the input field
462466
--input-warning-text-color: var(--dialog-warning-color);
463467
--input-error-text-color: #{$red-800};

app/styles/themes/_dark.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@ body.theme-dark {
357357
--form-error-border-color: #{$red-900};
358358
--form-error-text-color: var(--text-color);
359359

360+
--dialog-banner-success-background: rgba(22, 92, 38, 0.7);
361+
--dialog-banner-success-border-color: #{$green-800};
362+
--dialog-banner-success-text-color: var(--text-color);
363+
360364
// Inline form errors, displayed after the input field
361365
--input-warning-text-color: var(--dialog-warning-color);
362366
--input-error-text-color: #{$red-300};

app/styles/ui/_dialog.scss

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,9 @@ dialog {
344344
}
345345
}
346346

347-
// Inline error component rendered at the top of the dialog just below
347+
// Dialog banner components rendered at the top of the dialog just below
348348
// the header (if the dialog has one).
349-
.dialog-error {
349+
.dialog-banner {
350350
display: flex;
351351
padding: var(--spacing);
352352
align-items: center;
@@ -356,10 +356,6 @@ dialog {
356356
// while still forcing line breaks if necessary.
357357
white-space: pre-wrap;
358358

359-
background: var(--form-error-background);
360-
border-bottom: 1px solid var(--form-error-border-color);
361-
color: var(--form-error-text-color);
362-
363359
> .octicon {
364360
flex-grow: 0;
365361
flex-shrink: 0;
@@ -370,6 +366,20 @@ dialog {
370366
overflow-wrap: break-word;
371367
overflow-x: hidden;
372368
}
369+
370+
&.dialog-error {
371+
color: var(--form-error-text-color);
372+
background: var(--form-error-background);
373+
border-bottom: 1px solid var(--form-error-border-color);
374+
border-top: 1px solid var(--form-error-border-color);
375+
}
376+
377+
&.dialog-success {
378+
color: var(--dialog-banner-success-text-color);
379+
background: var(--dialog-banner-success-background);
380+
border-top: 1px solid var(--dialog-banner-success-border-color);
381+
border-bottom: 1px solid var(--dialog-banner-success-border-color);
382+
}
373383
}
374384

375385
&#app-error {

0 commit comments

Comments
 (0)