refactor: move tags to react (#753)

* refactor: move Tags list to react

* refactor: extract TagsListItem and simplify hooks

* refactor: remove comment & dead code

* fix: mobx warnings & safari bug

* fix: text select on non-safari

* fix: remove unecessary comments

* style: apply prettier format

* style: apply formatting on tags_view

* refactor: remove the angular tags rendering

* feat: add back the "select previous tag" behavior

* style: simplify code and avoid important

* style: remove note on state
This commit is contained in:
Laurent Senta
2021-11-29 17:33:00 +01:00
committed by GitHub
parent 6f3a749e52
commit 4d8ba3320a
7 changed files with 480 additions and 286 deletions

View File

@@ -33,35 +33,10 @@
.section-title-bar-header
.sk-h3.title
span.sk-bold Tags
.tag(
ng-class="{'selected' : self.state.selectedTag == tag}",
ng-click='self.selectTag(tag)',
ng-repeat='tag in self.state.tags track by tag.uuid'
)
.tag-info(ng-if="!tag.errorDecrypting")
.tag-icon #
input.title(
ng-attr-id='tag-{{tag.uuid}}',
ng-blur='self.saveTag($event, tag)'
ng-change='self.onTagTitleChange(tag)',
ng-model='self.titles[tag.uuid]',
ng-class="{'editing' : self.state.editingTag == tag}",
ng-click='self.selectTag(tag)',
ng-keyup='$event.keyCode == 13 && $event.target.blur()',
should-focus='self.state.templateTag || self.state.editingTag == tag',
sn-autofocus='true',
spellcheck='false'
)
.count {{self.state.noteCounts[tag.uuid]}}
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
.menu(ng-show='self.state.selectedTag == tag')
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
a.item(ng-click='self.selectedDeleteTag(tag)') Delete
.no-tags-placeholder(ng-show='self.state.tags.length == 0')
| No tags. Create one using the add button above.
tags(
application='self.application',
app-state='self.appState'
)
panel-resizer(
collapsable='true',
control='self.panelPuppet',

View File

@@ -1,62 +1,46 @@
import { PayloadContent } from '@standardnotes/snjs';
import { WebDirective, PanelPuppet } from '@/types';
import { PanelPuppet, WebDirective } from '@/types';
import { WebApplication } from '@/ui_models/application';
import {
SNTag,
ContentType,
ApplicationEvent,
ComponentAction,
SNSmartTag,
ComponentArea,
SNComponent,
PrefKey,
UuidString,
TagMutator
} from '@standardnotes/snjs';
import template from './tags-view.pug';
import { AppStateEvent } from '@/ui_models/app_state';
import { PANEL_NAME_TAGS } from '@/views/constants';
import { STRING_DELETE_TAG } from '@/strings';
import {
ApplicationEvent,
ComponentAction,
ComponentArea,
ContentType,
PrefKey,
SNComponent,
SNSmartTag,
SNTag,
UuidString,
} from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { confirmDialog } from '@/services/alertService';
import template from './tags-view.pug';
type NoteCounts = Partial<Record<string, number>>
type NoteCounts = Partial<Record<string, number>>;
type TagState = {
tags: SNTag[]
smartTags: SNSmartTag[]
noteCounts: NoteCounts
selectedTag?: SNTag
/** If creating a new tag, the previously selected tag will be set here, so that if new
* tag creation is canceled, the previous tag is re-selected */
previousTag?: SNTag
/** If a tag is in edit state, it will be set as the editingTag */
editingTag?: SNTag
/** If a tag is new and not yet saved, it will be set as the template tag */
templateTag?: SNTag
}
smartTags: SNSmartTag[];
noteCounts: NoteCounts;
selectedTag?: SNTag;
};
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
/** Passed through template */
readonly application!: WebApplication
private readonly panelPuppet: PanelPuppet
private unregisterComponent?: any
component?: SNComponent
readonly application!: WebApplication;
private readonly panelPuppet: PanelPuppet;
private unregisterComponent?: any;
component?: SNComponent;
/** The original name of the edtingTag before it began editing */
private editingOriginalName?: string
formData: { tagTitle?: string } = {}
titles: Partial<Record<UuidString, string>> = {}
private removeTagsObserver!: () => void
private removeFoldersObserver!: () => void
formData: { tagTitle?: string } = {};
titles: Partial<Record<UuidString, string>> = {};
private removeTagsObserver!: () => void;
private removeFoldersObserver!: () => void;
/* @ngInject */
constructor(
$timeout: ng.ITimeoutService,
) {
constructor($timeout: ng.ITimeoutService) {
super($timeout);
this.panelPuppet = {
onReady: () => this.loadPreferences()
onReady: () => this.loadPreferences(),
};
}
@@ -69,16 +53,15 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
super.deinit();
}
getInitialState() {
getInitialState(): TagState {
return {
tags: [],
smartTags: [],
noteCounts: {},
};
}
getState() {
return this.state as TagState;
getState(): TagState {
return this.state;
}
async onAppStart() {
@@ -90,10 +73,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
super.onAppLaunch();
this.loadPreferences();
this.beginStreamingItems();
const smartTags = this.application.getSmartTags();
this.setState({
smartTags: smartTags,
});
this.setState({ smartTags });
this.selectTag(smartTags[0]);
}
@@ -114,28 +96,31 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
this.removeTagsObserver = this.application.streamItems(
[ContentType.Tag, ContentType.SmartTag],
async (items) => {
const tags = items as Array<SNTag | SNSmartTag>;
await this.setState({
tags: this.application.getDisplayableItems(ContentType.Tag) as SNTag[],
smartTags: this.application.getSmartTags(),
});
for (const tag of items as Array<SNTag | SNSmartTag>) {
for (const tag of tags) {
this.titles[tag.uuid] = tag.title;
}
this.reloadNoteCounts();
const selectedTag = this.state.selectedTag;
if (selectedTag) {
/** If the selected tag has been deleted, revert to All view. */
const matchingTag = items.find((tag) => {
const matchingTag = tags.find((tag) => {
return tag.uuid === selectedTag.uuid;
}) as SNTag;
});
if (matchingTag) {
if (matchingTag.deleted) {
this.selectTag(this.getState().smartTags[0]);
} else {
this.setState({
selectedTag: matchingTag
selectedTag: matchingTag,
});
}
}
@@ -148,7 +133,7 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
onAppStateEvent(eventName: AppStateEvent) {
if (eventName === AppStateEvent.TagChanged) {
this.setState({
selectedTag: this.application.getAppState().getSelectedTag()
selectedTag: this.application.getAppState().getSelectedTag(),
});
}
}
@@ -167,37 +152,23 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
reloadNoteCounts() {
let allTags: Array<SNTag | SNSmartTag> = [];
if (this.getState().tags) {
allTags = allTags.concat(this.getState().tags);
}
if (this.getState().smartTags) {
allTags = allTags.concat(this.getState().smartTags);
}
const smartTags = this.state.smartTags;
const noteCounts: NoteCounts = {};
for (const tag of allTags) {
if (tag === this.state.templateTag) {
continue;
}
if (tag.isSmartTag) {
/** Other smart tags do not contain counts */
if (tag.isAllTag) {
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag)
.filter((note) => {
return !note.archived && !note.trashed;
});
noteCounts[tag.uuid] = notes.length;
}
} else {
const notes = this.application.referencesForItem(tag, ContentType.Note)
for (const tag of smartTags) {
/** Other smart tags do not contain counts */
if (tag.isAllTag) {
const notes = this.application
.notesMatchingSmartTag(tag as SNSmartTag)
.filter((note) => {
return !note.archived && !note.trashed;
});
noteCounts[tag.uuid] = notes.length;
}
}
this.setState({
noteCounts: noteCounts
noteCounts: noteCounts,
});
}
@@ -205,14 +176,14 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
if (!this.panelPuppet.ready) {
return;
}
const width = this.application.getPreference(PrefKey.TagsPanelWidth);
if (width) {
this.panelPuppet.setWidth!(width);
if (this.panelPuppet.isCollapsed!()) {
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
this.panelPuppet.isCollapsed!()
);
this.application
.getAppState()
.panelDidResize(PANEL_NAME_TAGS, this.panelPuppet.isCollapsed!());
}
}
}
@@ -223,36 +194,39 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
_isAtMaxWidth: boolean,
isCollapsed: boolean
) => {
this.application.setPreference(
PrefKey.TagsPanelWidth,
newWidth
).then(() => this.application.sync());
this.application.getAppState().panelDidResize(
PANEL_NAME_TAGS,
isCollapsed
);
}
this.application
.setPreference(PrefKey.TagsPanelWidth, newWidth)
.then(() => this.application.sync());
this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed);
};
registerComponentHandler() {
this.unregisterComponent = this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
if (data.item!.content_type === ContentType.Tag) {
const tag = this.application.findItem(data.item!.uuid);
if (tag) {
this.selectTag(tag as SNTag);
this.unregisterComponent =
this.application.componentManager!.registerHandler({
identifier: 'tags',
areas: [ComponentArea.TagsList],
actionHandler: (_, action, data) => {
if (action === ComponentAction.SelectItem) {
const item = data.item;
if (!item) {
return;
}
} else if (data.item!.content_type === ContentType.SmartTag) {
const matchingTag = this.getState().smartTags.find(t => t.uuid === data.item!.uuid);
this.selectTag(matchingTag as SNSmartTag);
if (item.content_type === ContentType.SmartTag) {
const matchingTag = this.getState().smartTags.find(
(t) => t.uuid === item.uuid
);
if (matchingTag) {
this.selectTag(matchingTag);
}
}
} else if (action === ComponentAction.ClearSelection) {
this.selectTag(this.getState().smartTags[0]);
}
} else if (action === ComponentAction.ClearSelection) {
this.selectTag(this.getState().smartTags[0]);
}
}
});
},
});
}
async selectTag(tag: SNTag) {
@@ -265,123 +239,11 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
async clickedAddNewTag() {
if (this.getState().editingTag) {
if (this.appState.templateTag) {
return;
}
const newTag = await this.application.createTemplateItem(
ContentType.Tag
) as SNTag;
this.setState({
tags: [newTag].concat(this.getState().tags),
previousTag: this.getState().selectedTag,
selectedTag: newTag,
editingTag: newTag,
templateTag: newTag
});
}
onTagTitleChange(tag: SNTag | SNSmartTag) {
this.setState({
editingTag: tag
});
}
async saveTag($event: Event, tag: SNTag) {
($event.target! as HTMLInputElement).blur();
if (this.getState().templateTag) {
if (!this.titles[tag.uuid]?.length) {
return this.undoCreateTag(tag);
}
return this.saveNewTag();
} else {
return this.saveTagRename(tag);
}
}
private async undoCreateTag(tag: SNTag) {
await this.setState({
templateTag: undefined,
editingTag: undefined,
selectedTag: this.appState.selectedTag,
tags: this.state.tags.filter(existingTag => existingTag !== tag)
});
delete this.titles[tag.uuid];
}
async saveTagRename(tag: SNTag) {
const newTitle = this.titles[tag.uuid] || '';
if (newTitle.length === 0) {
this.titles[tag.uuid] = this.editingOriginalName;
this.editingOriginalName = undefined;
await this.setState({
editingTag: undefined
});
return;
}
const existingTag = this.application.findTagByTitle(newTitle);
if (existingTag && existingTag.uuid !== tag.uuid) {
this.application.alertService!.alert(
"A tag with this name already exists."
);
return;
}
await this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
mutator.title = newTitle;
});
await this.setState({
editingTag: undefined
});
}
async saveNewTag() {
const newTag = this.getState().templateTag!;
const newTitle = this.titles[newTag.uuid] || '';
if (newTitle.length === 0) {
await this.setState({
templateTag: undefined
});
return;
}
const existingTag = this.application.findTagByTitle(newTitle);
if (existingTag) {
this.application.alertService!.alert(
"A tag with this name already exists."
);
this.undoCreateTag(newTag);
return;
}
const insertedTag = await this.application.insertItem(newTag);
const changedTag = await this.application.changeItem<TagMutator>(insertedTag.uuid, (m) => {
m.title = newTitle;
});
await this.setState({
templateTag: undefined,
editingTag: undefined
});
this.selectTag(changedTag as SNTag);
await this.application.saveItem(changedTag!.uuid);
}
async selectedRenameTag(tag: SNTag) {
this.editingOriginalName = tag.title;
await this.setState({
editingTag: tag
});
document.getElementById('tag-' + tag.uuid)!.focus();
}
selectedDeleteTag(tag: SNTag) {
this.removeTag(tag);
}
async removeTag(tag: SNTag) {
if (await confirmDialog({
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger'
})) {
this.application.deleteItem(tag);
this.selectTag(this.getState().smartTags[0]);
}
this.appState.createNewTag();
}
}
@@ -390,7 +252,7 @@ export class TagsView extends WebDirective {
super();
this.restrict = 'E';
this.scope = {
application: '='
application: '=',
};
this.template = template;
this.replace = true;