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:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user