feat: native smart tags (#782)
* feat: introduce native smart tags * feat: introduce react navigation * feat: render smart tag special cases * feat: add create tag & all count * feat: move components to react + mobx * fix: workaround issue with snjs * feat: nice smart tag icons in experimental * feat: add back components * fix: typo on all tags * feat: add panel resizer + simplif code * fix: panel resize size & refresh * fix: auto select all notes * style: remove legacy tag view * style: remove legacy directives * fix: select tag from note view * feat: WIP smart tag rename * fix: template checks * fix: user can create new notes * panel: init width * fix: panel resizer ref * fix: update with new component viewer * fix: use fixed isTemplateItem & fixed findItems * refactor: rename tags panel into navigation * style: remove TODOs that are ok * feat: smart tag premium check with premium service * refactor: multi-select variables for debuggability * fix: clean deinit code * fix: prevent trigger tag changes event for the same uuid * fix: typings * fix: use minimal state * style: remove dead code * style: long variable names * refactor: move magic string to module * fix: use smart filter feature * refactor: add task id in todo
This commit is contained in:
@@ -6,20 +6,26 @@ import { NoteViewController } from '@/views/note_view/note_view_controller';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ComponentArea,
|
||||
ContentType,
|
||||
DeinitSource,
|
||||
isPayloadSourceInternalChange,
|
||||
PayloadSource,
|
||||
PrefKey,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
SNSmartTag,
|
||||
ComponentViewer,
|
||||
SNTag,
|
||||
} from '@standardnotes/snjs';
|
||||
import pull from 'lodash/pull';
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
IReactionDisposer,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
reaction,
|
||||
} from 'mobx';
|
||||
import { ActionsMenuState } from './actions_menu_state';
|
||||
import { FeaturesState } from './features_state';
|
||||
@@ -72,11 +78,6 @@ export class AppState {
|
||||
onVisibilityChange: any;
|
||||
showBetaWarning: boolean;
|
||||
|
||||
selectedTag: SNTag | undefined;
|
||||
previouslySelectedTag: SNTag | undefined;
|
||||
editingTag: SNTag | undefined;
|
||||
_templateTag: SNTag | undefined;
|
||||
|
||||
private multiEditorSupport = false;
|
||||
|
||||
readonly quickSettingsMenu = new QuickSettingsState();
|
||||
@@ -92,10 +93,16 @@ export class AppState {
|
||||
readonly features: FeaturesState;
|
||||
readonly tags: TagsState;
|
||||
readonly notesView: NotesViewState;
|
||||
|
||||
public foldersComponentViewer?: ComponentViewer;
|
||||
|
||||
isSessionsModalVisible = false;
|
||||
|
||||
private appEventObserverRemovers: (() => void)[] = [];
|
||||
|
||||
private readonly tagChangedDisposer: IReactionDisposer;
|
||||
private readonly foldersComponentViewerDisposer: () => void;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$rootScope: ng.IRootScopeService,
|
||||
@@ -160,30 +167,27 @@ export class AppState {
|
||||
this.showBetaWarning = false;
|
||||
}
|
||||
|
||||
this.selectedTag = undefined;
|
||||
this.previouslySelectedTag = undefined;
|
||||
this.editingTag = undefined;
|
||||
this._templateTag = undefined;
|
||||
this.foldersComponentViewer = undefined;
|
||||
|
||||
makeObservable(this, {
|
||||
selectedTag: computed,
|
||||
|
||||
showBetaWarning: observable,
|
||||
isSessionsModalVisible: observable,
|
||||
preferences: observable,
|
||||
|
||||
selectedTag: observable,
|
||||
previouslySelectedTag: observable,
|
||||
_templateTag: observable,
|
||||
templateTag: computed,
|
||||
createNewTag: action,
|
||||
editingTag: observable,
|
||||
setSelectedTag: action,
|
||||
removeTag: action,
|
||||
|
||||
enableBetaWarning: action,
|
||||
disableBetaWarning: action,
|
||||
openSessionsModal: action,
|
||||
closeSessionsModal: action,
|
||||
|
||||
foldersComponentViewer: observable.ref,
|
||||
setFoldersComponent: action,
|
||||
});
|
||||
|
||||
this.tagChangedDisposer = this.tagChangedNotifier();
|
||||
this.foldersComponentViewerDisposer =
|
||||
this.subscribeToFoldersComponentChanges();
|
||||
}
|
||||
|
||||
deinit(source: DeinitSource): void {
|
||||
@@ -206,6 +210,8 @@ export class AppState {
|
||||
}
|
||||
document.removeEventListener('visibilitychange', this.onVisibilityChange);
|
||||
this.onVisibilityChange = undefined;
|
||||
this.tagChangedDisposer();
|
||||
this.foldersComponentViewerDisposer();
|
||||
}
|
||||
|
||||
openSessionsModal(): void {
|
||||
@@ -234,16 +240,16 @@ export class AppState {
|
||||
if (!this.multiEditorSupport) {
|
||||
this.closeActiveNoteController();
|
||||
}
|
||||
const activeTagUuid = this.selectedTag
|
||||
? this.selectedTag.isSmartTag
|
||||
? undefined
|
||||
: this.selectedTag.uuid
|
||||
: undefined;
|
||||
|
||||
const selectedTag = this.selectedTag;
|
||||
|
||||
const activeRegularTagUuid =
|
||||
selectedTag && !selectedTag.isSmartTag ? selectedTag.uuid : undefined;
|
||||
|
||||
await this.application.noteControllerGroup.createNoteView(
|
||||
undefined,
|
||||
title,
|
||||
activeTagUuid
|
||||
activeRegularTagUuid
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,10 +281,88 @@ export class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
private tagChangedNotifier(): IReactionDisposer {
|
||||
return reaction(
|
||||
() => this.tags.selectedUuid,
|
||||
() => {
|
||||
const tag = this.tags.selected;
|
||||
const previousTag = this.tags.previouslySelected;
|
||||
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.application.isTemplateItem(tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifyEvent(AppStateEvent.TagChanged, {
|
||||
tag,
|
||||
previousTag,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async setFoldersComponent(component?: SNComponent) {
|
||||
const foldersComponentViewer = this.foldersComponentViewer;
|
||||
|
||||
if (foldersComponentViewer) {
|
||||
this.application.componentManager.destroyComponentViewer(
|
||||
foldersComponentViewer
|
||||
);
|
||||
this.foldersComponentViewer = undefined;
|
||||
}
|
||||
|
||||
if (component) {
|
||||
this.foldersComponentViewer =
|
||||
this.application.componentManager.createComponentViewer(
|
||||
component,
|
||||
undefined,
|
||||
this.tags.onFoldersComponentMessage.bind(this.tags)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToFoldersComponentChanges() {
|
||||
return this.application.streamItems(
|
||||
[ContentType.Component],
|
||||
async (items, source) => {
|
||||
if (
|
||||
isPayloadSourceInternalChange(source) ||
|
||||
source === PayloadSource.InitialObserverRegistrationPush
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const components = items as SNComponent[];
|
||||
const hasFoldersChange = !!components.find(
|
||||
(component) => component.area === ComponentArea.TagsList
|
||||
);
|
||||
if (hasFoldersChange) {
|
||||
const componentViewer = this.application.componentManager
|
||||
.componentsForArea(ComponentArea.TagsList)
|
||||
.find((component) => component.active);
|
||||
|
||||
this.setFoldersComponent(componentViewer);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public get selectedTag(): SNTag | SNSmartTag | undefined {
|
||||
return this.tags.selected;
|
||||
}
|
||||
|
||||
public set selectedTag(tag: SNTag | SNSmartTag | undefined) {
|
||||
this.tags.selected = tag;
|
||||
}
|
||||
|
||||
streamNotesAndTags() {
|
||||
this.application.streamItems(
|
||||
[ContentType.Note, ContentType.Tag],
|
||||
async (items, source) => {
|
||||
const selectedTag = this.tags.selected;
|
||||
|
||||
/** Close any note controllers for deleted/trashed/archived notes */
|
||||
if (source === PayloadSource.PreSyncSave) {
|
||||
const notes = items.filter(
|
||||
@@ -293,13 +377,13 @@ export class AppState {
|
||||
this.closeNoteController(noteController);
|
||||
} else if (
|
||||
note.trashed &&
|
||||
!this.selectedTag?.isTrashTag &&
|
||||
!selectedTag?.isTrashTag &&
|
||||
!this.searchOptions.includeTrashed
|
||||
) {
|
||||
this.closeNoteController(noteController);
|
||||
} else if (
|
||||
note.archived &&
|
||||
!this.selectedTag?.isArchiveTag &&
|
||||
!selectedTag?.isArchiveTag &&
|
||||
!this.searchOptions.includeArchived &&
|
||||
!this.application.getPreference(PrefKey.NotesShowArchived, false)
|
||||
) {
|
||||
@@ -307,17 +391,6 @@ export class AppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.selectedTag) {
|
||||
const matchingTag = items.find(
|
||||
(candidate) =>
|
||||
this.selectedTag && candidate.uuid === this.selectedTag.uuid
|
||||
);
|
||||
if (matchingTag) {
|
||||
runInAction(() => {
|
||||
this.selectedTag = matchingTag as SNTag;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -385,74 +458,6 @@ export class AppState {
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedTag(tag: SNTag) {
|
||||
if (tag.conflictOf) {
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.selectedTag === tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previouslySelectedTag = this.selectedTag;
|
||||
this.selectedTag = tag;
|
||||
|
||||
if (this.templateTag?.uuid === tag.uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifyEvent(AppStateEvent.TagChanged, {
|
||||
tag: tag,
|
||||
previousTag: this.previouslySelectedTag,
|
||||
});
|
||||
}
|
||||
|
||||
public getSelectedTag() {
|
||||
return this.selectedTag;
|
||||
}
|
||||
|
||||
public get templateTag(): SNTag | undefined {
|
||||
return this._templateTag;
|
||||
}
|
||||
|
||||
public set templateTag(tag: SNTag | undefined) {
|
||||
const previous = this._templateTag;
|
||||
this._templateTag = tag;
|
||||
|
||||
if (tag) {
|
||||
this.setSelectedTag(tag);
|
||||
this.editingTag = tag;
|
||||
} else if (previous) {
|
||||
this.selectedTag =
|
||||
previous === this.selectedTag ? undefined : this.selectedTag;
|
||||
this.editingTag =
|
||||
previous === this.editingTag ? undefined : this.editingTag;
|
||||
}
|
||||
}
|
||||
|
||||
public removeTag(tag: SNTag) {
|
||||
this.application.deleteItem(tag);
|
||||
this.setSelectedTag(this.tags.smartTags[0]);
|
||||
}
|
||||
|
||||
public async createNewTag() {
|
||||
if (this.templateTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTag = (await this.application.createTemplateItem(
|
||||
ContentType.Tag
|
||||
)) as SNTag;
|
||||
this.templateTag = newTag;
|
||||
}
|
||||
|
||||
public async undoCreateNewTag() {
|
||||
const previousTag = this.previouslySelectedTag || this.tags.smartTags[0];
|
||||
this.setSelectedTag(previousTag);
|
||||
}
|
||||
|
||||
/** Returns the tags that are referncing this note */
|
||||
public getNoteTags(note: SNNote) {
|
||||
return this.application.referencingForItem(note).filter((ref) => {
|
||||
|
||||
@@ -3,13 +3,22 @@ import {
|
||||
FeatureIdentifier,
|
||||
FeatureStatus,
|
||||
} from '@standardnotes/snjs';
|
||||
import { computed, makeObservable, observable, runInAction } from 'mobx';
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
when,
|
||||
} from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
|
||||
export const TAG_FOLDERS_FEATURE_NAME = 'Tag folders';
|
||||
export const TAG_FOLDERS_FEATURE_TOOLTIP =
|
||||
'A Plus or Pro plan is required to enable Tag folders.';
|
||||
|
||||
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags';
|
||||
|
||||
/**
|
||||
* Holds state for premium/non premium features for the current user features,
|
||||
* and eventually for in-development features (feature flags).
|
||||
@@ -19,23 +28,37 @@ export class FeaturesState {
|
||||
window?._enable_unfinished_features;
|
||||
|
||||
_hasFolders = false;
|
||||
_hasSmartTags = false;
|
||||
_premiumAlertFeatureName: string | undefined;
|
||||
|
||||
private unsub: () => void;
|
||||
|
||||
constructor(private application: WebApplication) {
|
||||
this._hasFolders = this.hasNativeFolders();
|
||||
this._hasSmartTags = this.hasNativeSmartTags();
|
||||
this._premiumAlertFeatureName = undefined;
|
||||
|
||||
makeObservable(this, {
|
||||
_hasFolders: observable,
|
||||
_hasSmartTags: observable,
|
||||
hasFolders: computed,
|
||||
enableNativeFoldersFeature: computed,
|
||||
enableNativeSmartTagsFeature: computed,
|
||||
_premiumAlertFeatureName: observable,
|
||||
showPremiumAlert: action,
|
||||
closePremiumAlert: action,
|
||||
});
|
||||
|
||||
this.showPremiumAlert = this.showPremiumAlert.bind(this);
|
||||
this.closePremiumAlert = this.closePremiumAlert.bind(this);
|
||||
|
||||
this.unsub = this.application.addEventObserver(async (eventName) => {
|
||||
switch (eventName) {
|
||||
case ApplicationEvent.FeaturesUpdated:
|
||||
case ApplicationEvent.Launched:
|
||||
runInAction(() => {
|
||||
this._hasFolders = this.hasNativeFolders();
|
||||
this._hasSmartTags = this.hasNativeSmartTags();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -52,25 +75,25 @@ export class FeaturesState {
|
||||
return this.enableUnfinishedFeatures;
|
||||
}
|
||||
|
||||
public get enableNativeSmartTagsFeature(): boolean {
|
||||
return this.enableUnfinishedFeatures;
|
||||
}
|
||||
|
||||
public get hasFolders(): boolean {
|
||||
return this._hasFolders;
|
||||
}
|
||||
|
||||
public set hasFolders(hasFolders: boolean) {
|
||||
if (!hasFolders) {
|
||||
this._hasFolders = false;
|
||||
return;
|
||||
}
|
||||
public get hasSmartTags(): boolean {
|
||||
return this._hasSmartTags;
|
||||
}
|
||||
|
||||
if (!this.hasNativeFolders()) {
|
||||
this.application.alertService?.alert(
|
||||
`${TAG_FOLDERS_FEATURE_NAME} requires at least a Plus Subscription.`
|
||||
);
|
||||
this._hasFolders = false;
|
||||
return;
|
||||
}
|
||||
public async showPremiumAlert(featureName: string): Promise<void> {
|
||||
this._premiumAlertFeatureName = featureName;
|
||||
return when(() => this._premiumAlertFeatureName === undefined);
|
||||
}
|
||||
|
||||
this._hasFolders = hasFolders;
|
||||
public async closePremiumAlert(): Promise<void> {
|
||||
this._premiumAlertFeatureName = undefined;
|
||||
}
|
||||
|
||||
private hasNativeFolders(): boolean {
|
||||
@@ -84,4 +107,16 @@ export class FeaturesState {
|
||||
|
||||
return status === FeatureStatus.Entitled;
|
||||
}
|
||||
|
||||
private hasNativeSmartTags(): boolean {
|
||||
if (!this.enableNativeSmartTagsFeature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = this.application.getFeatureStatus(
|
||||
FeatureIdentifier.SmartFilters
|
||||
);
|
||||
|
||||
return status === FeatureStatus.Entitled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,12 +115,12 @@ export class NotesState {
|
||||
async selectNote(uuid: UuidString, userTriggered?: boolean): Promise<void> {
|
||||
const note = this.application.findItem(uuid) as SNNote;
|
||||
|
||||
const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta);
|
||||
const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl);
|
||||
const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift);
|
||||
|
||||
if (note) {
|
||||
if (
|
||||
userTriggered &&
|
||||
(this.io.activeModifiers.has(KeyboardModifier.Meta) ||
|
||||
this.io.activeModifiers.has(KeyboardModifier.Ctrl))
|
||||
) {
|
||||
if (userTriggered && (hasMeta || hasCtrl)) {
|
||||
if (this.selectedNotes[uuid]) {
|
||||
delete this.selectedNotes[uuid];
|
||||
} else if (await this.application.authorizeNoteAccess(note)) {
|
||||
@@ -129,10 +129,7 @@ export class NotesState {
|
||||
this.lastSelectedNote = note;
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
userTriggered &&
|
||||
this.io.activeModifiers.has(KeyboardModifier.Shift)
|
||||
) {
|
||||
} else if (userTriggered && hasShift) {
|
||||
await this.selectNotesRange(note);
|
||||
} else {
|
||||
const shouldSelectNote =
|
||||
|
||||
@@ -495,7 +495,9 @@ export class NotesViewState {
|
||||
this.reloadNotesDisplayOptions();
|
||||
this.reloadNotes();
|
||||
|
||||
if (this.notes.length > 0) {
|
||||
const hasSomeNotes = this.notes.length > 0;
|
||||
|
||||
if (hasSomeNotes) {
|
||||
this.selectFirstNote();
|
||||
} else if (dbLoaded) {
|
||||
if (
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import { STRING_DELETE_TAG, STRING_MISSING_SYSTEM_TAG } from '@/strings';
|
||||
import {
|
||||
ComponentAction,
|
||||
ContentType,
|
||||
MessageData,
|
||||
SNSmartTag,
|
||||
SNTag,
|
||||
UuidString,
|
||||
TagMutator,
|
||||
UuidString
|
||||
} from '@standardnotes/snjs';
|
||||
import {
|
||||
action,
|
||||
@@ -10,14 +15,21 @@ import {
|
||||
makeAutoObservable,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
runInAction
|
||||
} from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
import { FeaturesState } from './features_state';
|
||||
import { FeaturesState, SMART_TAGS_FEATURE_NAME } from './features_state';
|
||||
|
||||
type AnyTag = SNTag | SNSmartTag;
|
||||
|
||||
export class TagsState {
|
||||
tags: SNTag[] = [];
|
||||
smartTags: SNSmartTag[] = [];
|
||||
allNotesCount_ = 0;
|
||||
selected_: AnyTag | undefined;
|
||||
previouslySelected_: AnyTag | undefined;
|
||||
editing_: SNTag | undefined;
|
||||
|
||||
private readonly tagsCountsState: TagsCountsState;
|
||||
|
||||
constructor(
|
||||
@@ -27,22 +39,40 @@ export class TagsState {
|
||||
) {
|
||||
this.tagsCountsState = new TagsCountsState(this.application);
|
||||
|
||||
this.selected_ = undefined;
|
||||
this.previouslySelected_ = undefined;
|
||||
this.editing_ = undefined;
|
||||
|
||||
makeObservable(this, {
|
||||
tags: observable.ref,
|
||||
smartTags: observable.ref,
|
||||
hasFolders: computed,
|
||||
hasAtLeastOneFolder: computed,
|
||||
allNotesCount_: observable,
|
||||
allNotesCount: computed,
|
||||
|
||||
selected_: observable.ref,
|
||||
previouslySelected_: observable.ref,
|
||||
previouslySelected: computed,
|
||||
editing_: observable.ref,
|
||||
selected: computed,
|
||||
selectedUuid: computed,
|
||||
editingTag: computed,
|
||||
|
||||
assignParent: action,
|
||||
|
||||
rootTags: computed,
|
||||
tagsCount: computed,
|
||||
|
||||
createNewTemplate: action,
|
||||
undoCreateNewTag: action,
|
||||
save: action,
|
||||
remove: action,
|
||||
});
|
||||
|
||||
appEventListeners.push(
|
||||
this.application.streamItems(
|
||||
[ContentType.Tag, ContentType.SmartTag],
|
||||
() => {
|
||||
(items) => {
|
||||
runInAction(() => {
|
||||
this.tags = this.application.getDisplayableItems(
|
||||
ContentType.Tag
|
||||
@@ -50,18 +80,42 @@ export class TagsState {
|
||||
this.smartTags = this.application.getSmartTags();
|
||||
|
||||
this.tagsCountsState.update(this.tags);
|
||||
this.allNotesCount_ = this.countAllNotes();
|
||||
|
||||
const selectedTag = this.selected_;
|
||||
if (selectedTag) {
|
||||
const matchingTag = items.find(
|
||||
(candidate) => candidate.uuid === selectedTag.uuid
|
||||
);
|
||||
if (matchingTag) {
|
||||
if (matchingTag.deleted) {
|
||||
this.selected_ = this.smartTags[0];
|
||||
} else {
|
||||
this.selected_ = matchingTag as AnyTag;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.selected_ = this.smartTags[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public get allLocalRootTags(): SNTag[] {
|
||||
if (this.editing_ && this.application.isTemplateItem(this.editing_)) {
|
||||
return [this.editing_, ...this.rootTags];
|
||||
}
|
||||
return this.rootTags;
|
||||
}
|
||||
|
||||
public getNotesCount(tag: SNTag): number {
|
||||
return this.tagsCountsState.counts[tag.uuid] || 0;
|
||||
}
|
||||
|
||||
getChildren(tag: SNTag): SNTag[] {
|
||||
if (!this.hasFolders) {
|
||||
if (!this.features.hasFolders) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -69,7 +123,10 @@ export class TagsState {
|
||||
return [];
|
||||
}
|
||||
|
||||
const children = this.application.getTagChildren(tag);
|
||||
const children = this.application
|
||||
.getTagChildren(tag)
|
||||
.filter((tag) => !tag.isSmartTag);
|
||||
|
||||
const childrenUuids = children.map((childTag) => childTag.uuid);
|
||||
const childrenTags = this.tags.filter((tag) =>
|
||||
childrenUuids.includes(tag.uuid)
|
||||
@@ -100,7 +157,7 @@ export class TagsState {
|
||||
}
|
||||
|
||||
get rootTags(): SNTag[] {
|
||||
if (!this.hasFolders) {
|
||||
if (!this.features.hasFolders) {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
@@ -111,12 +168,192 @@ export class TagsState {
|
||||
return this.tags.length;
|
||||
}
|
||||
|
||||
public get hasFolders(): boolean {
|
||||
return this.features.hasFolders;
|
||||
public get allNotesCount(): number {
|
||||
return this.allNotesCount_;
|
||||
}
|
||||
|
||||
public set hasFolders(hasFolders: boolean) {
|
||||
this.features.hasFolders = hasFolders;
|
||||
public get previouslySelected(): AnyTag | undefined {
|
||||
return this.previouslySelected_;
|
||||
}
|
||||
|
||||
public get selected(): AnyTag | undefined {
|
||||
return this.selected_;
|
||||
}
|
||||
|
||||
public set selected(tag: AnyTag | undefined) {
|
||||
if (tag && tag.conflictOf) {
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const selectionHasNotChanged = this.selected_?.uuid === tag?.uuid;
|
||||
|
||||
if (selectionHasNotChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previouslySelected_ = this.selected_;
|
||||
this.selected_ = tag;
|
||||
}
|
||||
|
||||
public get selectedUuid(): UuidString | undefined {
|
||||
return this.selected_?.uuid;
|
||||
}
|
||||
|
||||
public get editingTag(): SNTag | undefined {
|
||||
return this.editing_;
|
||||
}
|
||||
|
||||
public set editingTag(editingTag: SNTag | undefined) {
|
||||
this.editing_ = editingTag;
|
||||
this.selected = editingTag;
|
||||
}
|
||||
|
||||
public async createNewTemplate() {
|
||||
const isAlreadyEditingATemplate =
|
||||
this.editing_ && this.application.isTemplateItem(this.editing_);
|
||||
|
||||
if (isAlreadyEditingATemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTag = (await this.application.createTemplateItem(
|
||||
ContentType.Tag
|
||||
)) as SNTag;
|
||||
|
||||
runInAction(() => {
|
||||
this.editing_ = newTag;
|
||||
});
|
||||
}
|
||||
|
||||
public undoCreateNewTag() {
|
||||
this.editing_ = undefined;
|
||||
const previousTag = this.previouslySelected_ || this.smartTags[0];
|
||||
this.selected = previousTag;
|
||||
}
|
||||
|
||||
public async remove(tag: SNTag) {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: STRING_DELETE_TAG,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
this.application.deleteItem(tag);
|
||||
this.selected = this.smartTags[0];
|
||||
}
|
||||
}
|
||||
|
||||
public async save(tag: SNTag, newTitle: string) {
|
||||
const hasEmptyTitle = newTitle.length === 0;
|
||||
const hasNotChangedTitle = newTitle === tag.title;
|
||||
const isTemplateChange = this.application.isTemplateItem(tag);
|
||||
const hasDuplicatedTitle = !!this.application.findTagByTitle(newTitle);
|
||||
|
||||
runInAction(() => {
|
||||
this.editing_ = undefined;
|
||||
});
|
||||
|
||||
if (hasEmptyTitle || hasNotChangedTitle) {
|
||||
if (isTemplateChange) {
|
||||
this.undoCreateNewTag();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDuplicatedTitle) {
|
||||
if (isTemplateChange) {
|
||||
this.undoCreateNewTag();
|
||||
}
|
||||
this.application.alertService?.alert(
|
||||
'A tag with this name already exists.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTemplateChange) {
|
||||
if (this.features.enableNativeSmartTagsFeature) {
|
||||
const isSmartTagTitle = this.application.isSmartTagTitle(newTitle);
|
||||
|
||||
if (isSmartTagTitle) {
|
||||
if (!this.features.hasSmartTags) {
|
||||
await this.features.showPremiumAlert(SMART_TAGS_FEATURE_NAME);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const insertedTag = await this.application.createTagOrSmartTag(
|
||||
newTitle
|
||||
);
|
||||
runInAction(() => {
|
||||
this.selected = insertedTag as SNTag;
|
||||
});
|
||||
} else {
|
||||
// Legacy code, remove me after we enableNativeSmartTagsFeature for everyone.
|
||||
// See https://app.asana.com/0/0/1201612665552831/f
|
||||
const insertedTag = await this.application.insertItem(tag);
|
||||
const changedTag = await this.application.changeItem<TagMutator>(
|
||||
insertedTag.uuid,
|
||||
(m) => {
|
||||
m.title = newTitle;
|
||||
}
|
||||
);
|
||||
this.selected = changedTag as SNTag;
|
||||
await this.application.saveItem(insertedTag.uuid);
|
||||
}
|
||||
} else {
|
||||
await this.application.changeAndSaveItem<TagMutator>(
|
||||
tag.uuid,
|
||||
(mutator) => {
|
||||
mutator.title = newTitle;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private countAllNotes(): number {
|
||||
const allTag = this.application.getSmartTags().find((tag) => tag.isAllTag);
|
||||
|
||||
if (!allTag) {
|
||||
console.error(STRING_MISSING_SYSTEM_TAG);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const notes = this.application
|
||||
.notesMatchingSmartTag(allTag)
|
||||
.filter((note) => {
|
||||
return !note.archived && !note.trashed;
|
||||
});
|
||||
|
||||
return notes.length;
|
||||
}
|
||||
|
||||
public onFoldersComponentMessage(
|
||||
action: ComponentAction,
|
||||
data: MessageData
|
||||
): void {
|
||||
if (action === ComponentAction.SelectItem) {
|
||||
const item = data.item;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
item.content_type === ContentType.Tag ||
|
||||
item.content_type === ContentType.SmartTag
|
||||
) {
|
||||
const matchingTag = this.application.findItem(item.uuid);
|
||||
|
||||
if (matchingTag) {
|
||||
this.selected = matchingTag as AnyTag;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (action === ComponentAction.ClearSelection) {
|
||||
this.selected = this.smartTags[0];
|
||||
}
|
||||
}
|
||||
|
||||
public get hasAtLeastOneFolder(): boolean {
|
||||
|
||||
@@ -56,6 +56,12 @@ export class PanelResizerState {
|
||||
side,
|
||||
widthEventCallback,
|
||||
}: PanelResizerProps) {
|
||||
const currentKnownPref =
|
||||
(application.getPreference(prefKey) as number) ?? defaultWidth ?? 0;
|
||||
|
||||
this.panel = panel;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.alwaysVisible = alwaysVisible ?? false;
|
||||
this.application = application;
|
||||
this.collapsable = collapsable ?? false;
|
||||
@@ -66,16 +72,15 @@ export class PanelResizerState {
|
||||
this.lastDownX = 0;
|
||||
this.lastLeft = this.startLeft;
|
||||
this.lastWidth = this.startWidth;
|
||||
this.panel = panel;
|
||||
this.prefKey = prefKey;
|
||||
this.pressed = false;
|
||||
this.side = side;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.widthBeforeLastDblClick = 0;
|
||||
this.widthEventCallback = widthEventCallback;
|
||||
this.resizeFinishCallback = resizeFinishCallback;
|
||||
|
||||
this.setWidth(currentKnownPref, true);
|
||||
|
||||
application.addEventObserver(async () => {
|
||||
const changedWidth = application.getPreference(prefKey) as number;
|
||||
if (changedWidth !== this.lastWidth) this.setWidth(changedWidth, true);
|
||||
|
||||
Reference in New Issue
Block a user