Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added a conflict resolution dialog and a Conflicts view for easier management of conflicts #2337

Merged
merged 77 commits into from Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a46f938
feat: initial conflict resolution modal
amanharwara May 26, 2023
9edefef
Merge branch 'main' of github.com:standardnotes/app into feat/conflicts
amanharwara May 30, 2023
5a0dbb4
Merge branch 'main' of github.com:standardnotes/app into feat/conflicts
amanharwara Jun 1, 2023
7152eea
chore: stuff
amanharwara Jun 1, 2023
1f47026
fix: styling/spacing
amanharwara Jun 2, 2023
969a426
fix: stuff
amanharwara Jun 2, 2023
890591d
feat: keep selected
amanharwara Jun 2, 2023
9f53e09
feat: version info
amanharwara Jun 2, 2023
db5bcaa
fix: hide read time
amanharwara Jun 2, 2023
6172109
chore: warning color for button
amanharwara Jun 2, 2023
d7ff16c
Merge branch 'main' of github.com:standardnotes/app into feat/conflicts
amanharwara Jun 3, 2023
e3b7679
feat: hide conflicted copies in note counts
amanharwara Jun 3, 2023
2bda701
feat: mobile ui
amanharwara Jun 5, 2023
26ce36f
fix: layout shift
amanharwara Jun 5, 2023
86d1906
feat: conflicts system view
amanharwara Jun 6, 2023
184b0c8
Merge branch 'main' into feat/conflicts
amanharwara Jun 6, 2023
18831f6
Merge branch 'main' into feat/conflicts
amanharwara Jun 6, 2023
c304b19
Merge branch 'main' into feat/conflicts
amanharwara Jun 7, 2023
eb12fa4
fix: remove from conflictMap if not conflicted anymore
amanharwara Jun 8, 2023
b7ef2fc
feat: conflicts count
amanharwara Jun 8, 2023
5b100e4
fix: keep selected behavior
amanharwara Jun 8, 2023
af7b113
Merge branch 'feat/conflicts' of github.com:standardnotes/app into fe…
amanharwara Jun 8, 2023
a84c2c3
feat: multiple selection
amanharwara Jun 9, 2023
eed2040
chore: test stuff
amanharwara Jun 9, 2023
9a1566e
fix: selection
amanharwara Jun 9, 2023
f5c4671
feat: new stuff
amanharwara Jun 11, 2023
cf492ac
chore: rmeove
amanharwara Jun 11, 2023
9583f4f
fix: arrow direction
amanharwara Jun 12, 2023
c185b77
fix: stuff
amanharwara Jun 12, 2023
b66dd51
fix: delete permanently
amanharwara Jun 12, 2023
78d61f1
chore: improve mobile ui
amanharwara Jun 12, 2023
52d8fe8
chore: fix sync
amanharwara Jun 12, 2023
e6f4262
chore: remove test stuff
amanharwara Jun 12, 2023
9afecfd
feat: basic diffing
amanharwara Jun 13, 2023
081cc82
feat: diff visualizer
amanharwara Jun 13, 2023
bfd5941
fix: diff viz calc
amanharwara Jun 13, 2023
64b15d4
feat: convert super note to markdown before diffing
amanharwara Jun 14, 2023
6c384a0
feat: allow comparing super json
amanharwara Jun 14, 2023
39e67e3
fix: height
amanharwara Jun 14, 2023
56aca46
fix: diff
amanharwara Jun 14, 2023
e03e7d7
fix: views predicate
amanharwara Jun 14, 2023
8d10851
chore: revert button chnage
amanharwara Jun 14, 2023
3100ce5
refactor: split
amanharwara Jun 14, 2023
839b582
fix: mobile ux
amanharwara Jun 15, 2023
8139790
feat: scroll sync
amanharwara Jun 15, 2023
363e6b3
Merge branch 'main' into feat/conflicts
amanharwara Jun 19, 2023
ae8b5c3
feat: title diff
amanharwara Jun 21, 2023
8997d51
chore: stuff
amanharwara Jun 21, 2023
62b4198
chore: stuff
amanharwara Jun 21, 2023
afed705
chore: focus-visible
amanharwara Jun 21, 2023
f326d98
feat: dropdown button
amanharwara Jun 21, 2023
cbf95f4
chore: colors
amanharwara Jun 22, 2023
d09377e
fix: select keyboard
amanharwara Jun 22, 2023
b05a045
fix: popover behavior
amanharwara Jun 22, 2023
4174526
fix: invisible scrollbar on macos
amanharwara Jun 22, 2023
02347a8
fix: popover
amanharwara Jun 22, 2023
cbf191c
Merge branch 'feat/conflicts' of github.com:standardnotes/app into fe…
amanharwara Jun 22, 2023
6c6cc32
fix: padding
amanharwara Jun 22, 2023
60bfc24
fix: button
amanharwara Jun 22, 2023
b09ed5f
fix: popover close behavior
amanharwara Jun 22, 2023
9e75cf8
fix: padding
amanharwara Jun 22, 2023
fcda646
refactor: name
amanharwara Jun 22, 2023
c557200
refactor: conflicts count
amanharwara Jun 22, 2023
e911106
refactor: collection
amanharwara Jun 22, 2023
1517941
fix: border
amanharwara Jun 22, 2023
412016f
fix: copy
amanharwara Jun 22, 2023
ba4be62
fix: allow keep action for multiple selected
amanharwara Jun 22, 2023
cee58f2
fix: tooltip
amanharwara Jun 22, 2023
97a0644
fix: list item metadata
amanharwara Jun 23, 2023
972d167
refactor: split files
amanharwara Jun 23, 2023
719eb0a
fix: selection behavior
amanharwara Jun 23, 2023
8511986
fix: warning color
amanharwara Jun 25, 2023
11803d2
Merge branch 'main' into feat/conflicts
amanharwara Jun 25, 2023
f28630a
fix: button color
amanharwara Jun 25, 2023
0e99f55
Merge branch 'feat/conflicts' of github.com:standardnotes/app into fe…
amanharwara Jun 25, 2023
352dcea
refactor: diff
amanharwara Jun 25, 2023
8698eda
refactor: remove unused
amanharwara Jun 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/icons/src/Icons/index.ts
Expand Up @@ -186,6 +186,7 @@ import SubtractIcon from './ic-subtract.svg'
import SuperscriptIcon from './ic-superscript.svg'
import SyncIcon from './ic-sync.svg'
import TasksIcon from './ic-tasks.svg'
import TextIcon from './ic-text.svg'
import TextCircleIcon from './ic-text-circle.svg'
import TextParagraphLongIcon from './ic-text-paragraph-long.svg'
import ThemesFilledIcon from './ic-themes-filled.svg'
Expand Down Expand Up @@ -392,6 +393,7 @@ export {
SuperscriptIcon,
SyncIcon,
TasksIcon,
TextIcon,
TextCircleIcon,
TextParagraphLongIcon,
ThemesFilledIcon,
Expand Down
9 changes: 9 additions & 0 deletions packages/models/src/Domain/Runtime/Collection/Collection.ts
Expand Up @@ -187,6 +187,8 @@ export abstract class Collection<
const conflictOf = element.content.conflict_of
if (conflictOf) {
this.conflictMap.establishRelationship(conflictOf, element.uuid)
} else if (this.conflictMap.getInverseRelationships(element.uuid).length > 0) {
this.conflictMap.removeFromMap(element.uuid)
}

this.referenceMap.setAllRelationships(
Expand All @@ -203,6 +205,9 @@ export abstract class Collection<

if (element.deleted) {
this.nondeletedIndex.delete(element.uuid)
if (this.conflictMap.getInverseRelationships(element.uuid).length > 0) {
this.conflictMap.removeFromMap(element.uuid)
}
} else {
this.nondeletedIndex.add(element.uuid)
}
Expand Down Expand Up @@ -260,4 +265,8 @@ export abstract class Collection<
remove(array, { uuid: element.uuid as never })
this.typedMap[element.content_type] = array
}

public numberOfItemsWithConflicts(): number {
return this.conflictMap.directMapSize
}
}
Expand Up @@ -18,7 +18,7 @@ export class TagItemsIndex implements SNIndex {

private isItemCountable = (item: ItemInterface) => {
if (isDecryptedItem(item)) {
return !item.archived && !item.trashed
return !item.archived && !item.trashed && !item.conflictOf
}
return false
}
Expand Down
Expand Up @@ -10,8 +10,9 @@ import { FilterDisplayOptions } from './DisplayOptions'
export function computeUnifiedFilterForDisplayOptions(
options: FilterDisplayOptions,
collection: ReferenceLookupCollection,
additionalFilters: ItemFilter[] = [],
): ItemFilter {
const filters = computeFiltersForDisplayOptions(options, collection)
const filters = computeFiltersForDisplayOptions(options, collection).concat(additionalFilters)

return (item: SearchableDecryptedItem) => {
return itemPassesFilters(item, filters)
Expand Down Expand Up @@ -74,5 +75,9 @@ export function computeFiltersForDisplayOptions(
filters.push((item) => itemMatchesQuery(item, query, collection))
}

if (!viewsPredicate?.keypathIncludesString('conflict_of')) {
filters.push((item) => !item.conflictOf)
}

return filters
}
Expand Up @@ -85,7 +85,19 @@ export function BuildSmartViews(options: FilterDisplayOptions): SmartView[] {
}),
)

return [notes, files, starred, archived, trash, untagged]
const conflicts = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.Conflicts,
content_type: ContentType.SmartView,
...PayloadTimestampDefaults(),
content: FillItemContent<SmartViewContent>({
title: 'Conflicts',
predicate: conflictsPredicate(options).toJson(),
}),
}),
)

return [notes, files, starred, archived, trash, untagged, conflicts]
}

function allNotesPredicate(options: FilterDisplayOptions) {
Expand Down Expand Up @@ -203,3 +215,26 @@ function starredNotesPredicate(options: FilterDisplayOptions) {

return predicate
}

function conflictsPredicate(options: FilterDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [new Predicate('content_type', '=', ContentType.Note)]

if (options.includeTrashed === false) {
subPredicates.push(new Predicate('trashed', '=', false))
}

if (options.includeArchived === false) {
subPredicates.push(new Predicate('archived', '=', false))
}

if (options.includeProtected === false) {
subPredicates.push(new Predicate('protected', '=', false))
}

if (options.includePinned === false) {
subPredicates.push(new Predicate('pinned', '=', false))
}

const predicate = new CompoundPredicate('and', subPredicates)
return predicate
}
Expand Up @@ -8,6 +8,7 @@ export const SmartViewIcons: Record<SystemViewId, IconType> = {
[SystemViewId.TrashedNotes]: 'trash',
[SystemViewId.UntaggedNotes]: 'hashtag-off',
[SystemViewId.StarredNotes]: 'star-filled',
[SystemViewId.Conflicts]: 'merge',
}

export function systemViewIcon(id: SystemViewId): IconType {
Expand Down
Expand Up @@ -5,4 +5,5 @@ export enum SystemViewId {
TrashedNotes = 'trashed-notes',
UntaggedNotes = 'untagged-notes',
StarredNotes = 'starred-notes',
Conflicts = 'conflicts',
}
2 changes: 1 addition & 1 deletion packages/services/src/Domain/Item/ItemCounter.ts
Expand Up @@ -23,7 +23,7 @@ export class ItemCounter implements ItemCounterInterface {

continue
}
if (item.content_type === ContentType.Note) {
if (item.content_type === ContentType.Note && !item.conflictOf) {
counts.notes++

continue
Expand Down
2 changes: 2 additions & 0 deletions packages/services/src/Domain/Item/ItemsClientInterface.ts
Expand Up @@ -167,4 +167,6 @@ export interface ItemsClientInterface {
predicate: PredicateInterface<T>,
iconString?: string,
): Promise<SmartView>

numberOfNotesWithConflicts(): number
}
10 changes: 9 additions & 1 deletion packages/snjs/lib/Services/Items/ItemManager.ts
Expand Up @@ -123,6 +123,7 @@ export class ItemManager

public setPrimaryItemDisplayOptions(options: Models.DisplayOptions): void {
const override: Models.FilterDisplayOptions = {}
const additionalFilters: Models.ItemFilter[] = []

if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.AllNotes)) {
if (options.includeArchived === undefined) {
Expand All @@ -142,6 +143,9 @@ export class ItemManager
override.includeArchived = true
}
}
if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.Conflicts)) {
additionalFilters.push((item) => this.collection.conflictsOf(item.uuid).length > 0)
}

this.rebuildSystemSmartViews({ ...options, ...override })

Expand Down Expand Up @@ -174,7 +178,7 @@ export class ItemManager
}

this.navigationDisplayController.setDisplayOptions({
customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection),
customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection, additionalFilters),
...updatedOptions,
})
}
Expand Down Expand Up @@ -1410,4 +1414,8 @@ export class ItemManager
},
})
}

numberOfNotesWithConflicts(): number {
return this.collection.numberOfItemsWithConflicts()
}
}
3 changes: 2 additions & 1 deletion packages/styles/src/Styles/_scrollbar.scss
@@ -1,7 +1,8 @@
.windows-web,
.windows-desktop,
.linux-web,
.linux-desktop {
.linux-desktop,
.force-custom-scrollbar {
$thumb-width: 4px;

::-webkit-scrollbar {
Expand Down
8 changes: 8 additions & 0 deletions packages/utils/src/Domain/Uuid/UuidMap.ts
Expand Up @@ -10,6 +10,14 @@ export class UuidMap {
/** uuid to uuids that have a relationship with us */
private inverseMap: Partial<Record<string, string[]>> = {}

public get directMapSize(): number {
return Object.keys(this.directMap).length
}

public get inverseMapSize(): number {
return Object.keys(this.inverseMap).length
}

public makeCopy(): UuidMap {
const copy = new UuidMap()
copy.directMap = Object.assign({}, this.directMap)
Expand Down
5 changes: 3 additions & 2 deletions packages/web/package.json
Expand Up @@ -107,8 +107,9 @@
"app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write"
},
"dependencies": {
"@ariakit/react": "^0.2.1",
"@ariakit/react": "^0.2.8",
"@lexical/headless": "0.11.0",
"@radix-ui/react-slot": "^1.0.1"
"@radix-ui/react-slot": "^1.0.1",
"fast-diff": "^1.3.0"
}
}
Expand Up @@ -19,15 +19,15 @@
--sn-stylekit-neutral-contrast-color: #ffffff;
--sn-stylekit-success-color: #2b9612;
--sn-stylekit-success-contrast-color: #ffffff;
--sn-stylekit-warning-color: #f6a200;
--sn-stylekit-warning-color: #cc8800;
--sn-stylekit-warning-contrast-color: #ffffff;
--sn-stylekit-danger-color: #f80324;
--sn-stylekit-danger-contrast-color: #ffffff;
--sn-stylekit-editor-background-color: var(--sn-stylekit-background-color);
--sn-stylekit-editor-foreground-color: var(--sn-stylekit-foreground-color);
--sn-stylekit-background-color: var(--background-color);
--sn-stylekit-foreground-color: var(--foreground-color);
--sn-stylekit-border-color: #000000;
--sn-stylekit-border-color: #181a1b;
--sn-stylekit-contrast-background-color: #000000;
--sn-stylekit-contrast-foreground-color: #ffffff;
--sn-stylekit-contrast-border-color: #000000;
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/javascripts/Components/Button/Button.tsx
Expand Up @@ -58,7 +58,7 @@ const getClassName = (
let colors = primary ? getColorsForPrimaryVariant(style) : getColorsForNormalVariant(style)

let focusHoverStates = primary
? 'hover:brightness-125 focus:outline-none focus:brightness-125'
? 'hover:brightness-125 focus:outline-none focus-visible:brightness-125'
: 'focus:bg-contrast focus:outline-none hover:border-info hover:text-info hover:bg-contrast'

if (disabled) {
Expand All @@ -68,7 +68,7 @@ const getClassName = (
: 'focus:bg-default focus:outline-none hover:bg-default'
}

return `${rounded} font-bold ${width} ${padding} ${textSize} ${colors} ${borders} ${focusHoverStates} ${cursor}`
return `${rounded} font-bold select-none ${width} ${padding} ${textSize} ${colors} ${borders} ${focusHoverStates} ${cursor}`
}

interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
Expand Down
@@ -0,0 +1,25 @@
import { classNames } from '@standardnotes/utils'
import Icon from '../Icon/Icon'
import { ComponentPropsWithoutRef } from 'react'

const CheckIndicator = ({ checked, className, ...props }: { checked: boolean } & ComponentPropsWithoutRef<'div'>) => (
<div
className={classNames(
'relative h-5 w-5 rounded border-2 md:h-4 md:w-4',
checked ? 'border-info bg-info' : 'border-passive-1',
className,
)}
role="presentation"
{...props}
>
{checked && (
<Icon
type="check"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-info-contrast"
size="small"
/>
)}
</div>
)

export default CheckIndicator
Expand Up @@ -102,6 +102,7 @@ export const IconNameToSvgMapping = {
listed: icons.ListedIcon,
lock: icons.LockIcon,
markdown: icons.MarkdownIcon,
merge: icons.MergeIcon,
more: icons.MoreIcon,
notes: icons.NotesIcon,
paragraph: icons.TextParagraphLongIcon,
Expand All @@ -126,6 +127,7 @@ export const IconNameToSvgMapping = {
superscript: icons.SuperscriptIcon,
sync: icons.SyncIcon,
tasks: icons.TasksIcon,
text: icons.TextIcon,
themes: icons.ThemesIcon,
trash: icons.TrashIcon,
tune: icons.TuneIcon,
Expand Down
Expand Up @@ -23,6 +23,7 @@ type Props = {
isBidirectional: boolean
inlineFlex?: boolean
className?: string
readonly?: boolean
}

const LinkedItemBubble = ({
Expand All @@ -36,6 +37,7 @@ const LinkedItemBubble = ({
isBidirectional,
inlineFlex,
className,
readonly,
}: Props) => {
const ref = useRef<HTMLButtonElement>(null)
const application = useApplication()
Expand All @@ -60,6 +62,9 @@ const LinkedItemBubble = ({
const onClick: MouseEventHandler = (event) => {
if (wasClicked && event.target !== unlinkButtonRef.current) {
setWasClicked(false)
if (readonly) {
return
}
void activateItem?.(link.item)
} else {
setWasClicked(true)
Expand Down Expand Up @@ -112,7 +117,7 @@ const LinkedItemBubble = ({
onKeyDown={onKeyDown}
>
<Icon type={icon} className={classNames('mr-1 flex-shrink-0', iconClassName)} size="small" />
<span className="max-w-290px flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
<span className="flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap">
{tagTitle && <span className="text-passive-1">{tagTitle.titlePrefix}</span>}
<span className="flex items-center gap-1">
{link.type === 'linked-by' && link.item.content_type !== ContentType.Tag && (
Expand All @@ -121,7 +126,7 @@ const LinkedItemBubble = ({
{getItemTitleInContextOfLinkBubble(link.item)}
</span>
</span>
{showUnlinkButton && (
{showUnlinkButton && !readonly && (
<a
ref={unlinkButtonRef}
role="button"
Expand Down