Skip to content

Commit

Permalink
Add note's tags to history screen (#2817)
Browse files Browse the repository at this point in the history
* Add interactive prop to Tag chip

interactive prop determines if the tag should display the remove button and pointer cursor

* Add note revisions component

This component displays a preview of the content and the tags of the current selected revision.

* Add note revisions in app layout

* Update tag chip component test snapshot

* Add unit tests for interactive state

* Add note tags selectors

This selector is now used in tag-field and note-revisions components to simplify the rendering of tags.

* Add deleted state to tag chip component

* Add restore tags action

This action will be executed when a revision that contains tags that don't exist is restored. It also triggers updates in tags bucket in Simperium.

* Improve a11y in note revision

The div that wraps the note revision content is now the one that controls the ARIA visibility.

* Move tags restoration logic into note revision restoration

* Refactor noteTags selector to return an array of tags

Since we don't really need the tag's hash, now this selector simply return an array of tags.

* Update noteTags selector to include email tags

When restoring a note revision, email tags are included so we should display them too in the note's history.

* Remove unused tagNameOf import

* Use name instead of un-hashed value for tag restore

* Update deleted style of tag chip component

* Add deleted tags toggle

* Calculate tag list when restoring a note revision

When restoring a revision, the tag list has to be calculated having the following rules in mind:
1. New email tags shouldn't be included because they might grant undesired users permission.
2. Email tags of current note should be kept to prevent modify user permissions, therefore all of them have to be included.
3. Depending on user selection, tags that were previously deleted could be included.

* Remove email tag condition from tag-field component

The noteTags selector is already filtering them so it's not needed.

* Use checkbox instead of toggle in deleted tags

* Add getRevision selector

This selector will return the revision object to be passed to the restore note revision Redux action.

* Keep note system tags when restoring a revision

* Remove deleted property from Tag type

noteTags selector has been refactored due to this change, now it only returns the canonical tag names.

* Rename toggle deleted tags action and reducer

* Remove restoreNoteRevision action creator

* Remove isEmailTag import from data middleware

* Rename tags class name to be less generic
  • Loading branch information
fluiddot authored Apr 26, 2021
1 parent 73706d7 commit 702c404
Show file tree
Hide file tree
Showing 19 changed files with 364 additions and 106 deletions.
5 changes: 3 additions & 2 deletions lib/app-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import NoteToolbar from '../note-toolbar';
import RevisionSelector from '../revision-selector';
import SearchField from '../search-field';
import SimplenoteCompactLogo from '../icons/simplenote-compact';
import NoteRevisions from '../note-revisions';
import TransitionDelayEnter from '../components/transition-delay-enter';
import actions from '../state/actions';
import * as selectors from '../state/selectors';
Expand Down Expand Up @@ -36,7 +37,7 @@ type StateProps = {
keyboardShortcuts: boolean;
keyboardShortcutsAreOpen: boolean;
openedNote: T.EntityId | null;
openedRevision: number | null;
openedRevision: T.Note | null;
showNoteList: boolean;
showRevisions: boolean;
};
Expand Down Expand Up @@ -136,7 +137,7 @@ export class AppLayout extends Component<Props> {
>
<NoteToolbar aria-hidden={hiddenByRevisions} />
{showRevisions ? (
<NotePreview
<NoteRevisions
aria-hidden={hiddenByRevisions}
noteId={openedNote}
note={openedRevision}
Expand Down
8 changes: 2 additions & 6 deletions lib/components/note-preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,9 @@ type DispatchProps = {
openNote: (noteId: T.EntityId) => any;
};

type Props = OwnProps &
StateProps &
DispatchProps &
React.HTMLProps<HTMLDivElement>;
type Props = OwnProps & StateProps & DispatchProps;

export const NotePreview: FunctionComponent<Props> = ({
'aria-hidden': ariaHidden,
editNote,
isFocused,
note,
Expand Down Expand Up @@ -166,7 +162,7 @@ export const NotePreview: FunctionComponent<Props> = ({
}, [note?.content, searchQuery, showRenderedView]);

return (
<div aria-hidden={ariaHidden} className="note-detail-wrapper">
<div className="note-detail-wrapper">
<div className="note-detail note-detail-preview">
<div
ref={previewNode}
Expand Down
2 changes: 1 addition & 1 deletion lib/components/tag-chip/__snapshots__/test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`TagChip should not introduce visual regressions 1`] = `
<div
className="tag-chip"
className="tag-chip interactive"
data-tag-name="spline"
>
spline
Expand Down
18 changes: 12 additions & 6 deletions lib/components/tag-chip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ import type * as T from '../../types';

type OwnProps = {
onSelect?: (event: React.MouseEvent<HTMLDivElement>) => any;
selected: boolean;
selected?: boolean;
interactive?: boolean;
deleted?: boolean;
tagName: T.TagName | undefined;
};

const TagChip: FunctionComponent<OwnProps> = ({
onSelect,
selected,
selected = false,
interactive = true,
deleted = false,
tagName,
}) => (
<div
className={classNames('tag-chip', { selected })}
className={classNames('tag-chip', { selected, interactive, deleted })}
data-tag-name={tagName}
onClick={onSelect}
>
{tagName}
<span className="remove-tag-icon">
<SmallCrossIcon />
</span>
{interactive && (
<span className="remove-tag-icon">
<SmallCrossIcon />
</span>
)}
</div>
);

Expand Down
13 changes: 12 additions & 1 deletion lib/components/tag-chip/style.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.tag-chip {
cursor: pointer;
flex: none;
margin: 2px 8px 6px 0;
padding: 1px 14px 3px;
Expand All @@ -19,6 +18,14 @@
}
}

&.interactive {
cursor: pointer;
}

&.deleted {
background: $studio-red-5;
}

.remove-tag-icon {
display: none;
position: absolute;
Expand Down Expand Up @@ -46,6 +53,10 @@
.tag-chip {
background: $studio-gray-70;
color: $studio-white;

&.deleted {
background: $studio-red-70;
}
}

.remove-tag-icon {
Expand Down
58 changes: 44 additions & 14 deletions lib/components/tag-chip/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,60 @@ describe('TagChip', () => {
expect(selectIt).toHaveBeenCalled();
});

it('should not include the `selected` class by default', () => {
const chip = shallow(<TagChip tagName="spline" />);
describe('selected state', () => {
it('should not include the `selected` class by default', () => {
const chip = shallow(<TagChip tagName="spline" />);

expect(chip.hasClass('selected')).toBe(false);
});
expect(chip.hasClass('selected')).toBe(false);
});

it('should include the `selected` class when selected', () => {
const chip = shallow(<TagChip tagName="spline" selected />);

expect(chip.hasClass('selected')).toBe(true);
});

it('should toggle the `selected` class with prop changes', () => {
const chip = shallow(<TagChip tagName="spline" />);

expect(chip.hasClass('selected')).toBe(false);

chip.setProps({ selected: true });

it('should include the `selected` class when selected', () => {
const chip = shallow(<TagChip tagName="spline" selected />);
expect(chip.hasClass('selected')).toBe(true);

expect(chip.hasClass('selected')).toBe(true);
chip.setProps({ selected: false });

expect(chip.hasClass('selected')).toBe(false);
});
});

it('should toggle the `selected` class with prop changes', () => {
const chip = shallow(<TagChip tagName="spline" />);
describe('interactive state', () => {
it('should include the `interactive` class by default', () => {
const chip = shallow(<TagChip tagName="spline" />);

expect(chip.hasClass('interactive')).toBe(true);
});

it('should not include the `interactive` class when is not interactive', () => {
const chip = shallow(<TagChip tagName="spline" interactive={false} />);

expect(chip.hasClass('interactive')).toBe(false);
});

it('should toggle the `interactive` class with prop changes', () => {
const chip = shallow(<TagChip tagName="spline" />);

expect(chip.hasClass('selected')).toBe(false);
expect(chip.hasClass('interactive')).toBe(true);

chip.setProps({ selected: true });
chip.setProps({ interactive: false });

expect(chip.hasClass('selected')).toBe(true);
expect(chip.hasClass('interactive')).toBe(false);

chip.setProps({ selected: false });
chip.setProps({ interactive: true });

expect(chip.hasClass('selected')).toBe(false);
expect(chip.hasClass('interactive')).toBe(true);
});
});

it('should not introduce visual regressions', () => {
Expand Down
72 changes: 72 additions & 0 deletions lib/note-revisions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';

import NotePreview from '../components/note-preview';
import TagChip from '../components/tag-chip';
import { noteCanonicalTags } from '../state/selectors';
import { tagHashOf } from '../utils/tag-hash';

import type * as S from '../state';
import type * as T from '../types';

type OwnProps = {
noteId: T.EntityId;
note?: T.Note;
};

type StateProps = {
tags: Array<{ name: T.TagName; deleted: boolean }>;
noteId: T.EntityId | null;
note: T.Note | null;
};

type Props = OwnProps &
StateProps &
Pick<React.HTMLProps<HTMLDivElement>, 'aria-hidden'>;

export class NoteRevisions extends Component<Props> {
static displayName = 'NoteRevisions';

render() {
const { note, noteId, tags, 'aria-hidden': ariaHidden } = this.props;

return (
<div aria-hidden={ariaHidden} className="note-revisions">
<NotePreview noteId={noteId} note={note} />
<div className="note-revisions-tag-list">
{tags.map(({ name, deleted }) => (
<TagChip
key={name}
tagName={name}
interactive={false}
deleted={deleted}
/>
))}
</div>
</div>
);
}
}

const mapStateToProps: S.MapState<StateProps, OwnProps> = (state, props) => {
const noteId = props.noteId ?? state.ui.openedNote;
const note = props.note ?? state.data.notes.get(noteId);
const restoreDeletedTags = state.ui.restoreDeletedTags;

const tags = noteCanonicalTags(state, note)
.map((tagName) => {
const tagHash = tagHashOf(tagName);
return { name: tagName, deleted: !state.data.tags.has(tagHash) };
})
.filter((tag) => {
return restoreDeletedTags || !tag.deleted;
});

return {
tags,
noteId,
note,
};
};

export default connect(mapStateToProps)(NoteRevisions);
17 changes: 17 additions & 0 deletions lib/note-revisions/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.note-revisions {
display: flex;
flex-direction: column;
flex: 1 1 auto;
padding-top: 20px;

.note-revisions-tag-list {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
line-height: 1.75em;
white-space: nowrap;
overflow: auto;
max-height: calc(2.5 * 1.75em + 16px); // about 2.5 rows
padding: 8px 12px;
}
}
Loading

0 comments on commit 702c404

Please sign in to comment.