From b57350c8991a87f6a9534878e9f35c5a7f31ae61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 5 Jan 2022 16:11:17 +0100 Subject: [PATCH] Revert "feat: native smart tags (#782)" This reverts commit c3772e06b459e22d54a69ef307cfb74500c2e16d. --- app/assets/icons/ic-notes.svg | 3 - app/assets/javascripts/app.ts | 81 +++-- app/assets/javascripts/components/Icon.tsx | 2 - .../javascripts/components/Navigation.tsx | 117 ------- app/assets/javascripts/components/NoteTag.tsx | 2 +- .../javascripts/components/NotesView.tsx | 6 +- .../components/Premium/usePremiumModal.tsx | 50 ++- .../components/{Tags => }/RootTagDropZone.tsx | 8 +- .../components/Tags/SmartTagsList.tsx | 29 -- .../components/Tags/SmartTagsListItem.tsx | 163 ---------- .../components/Tags/SmartTagsSection.tsx | 19 -- .../javascripts/components/Tags/TagsList.tsx | 48 --- .../components/Tags/TagsSection.tsx | 111 ++++++- .../components/Tags/TagsSectionAddButton.tsx | 30 -- .../components/Tags/TagsSectionTitle.tsx | 62 ---- .../javascripts/components/Tags/dragndrop.ts | 9 - .../javascripts/components/TagsList.tsx | 153 +++++++++ .../components/{Tags => }/TagsListItem.tsx | 68 ++-- app/assets/javascripts/strings.ts | 2 - .../ui_models/app_state/app_state.ts | 217 +++++++------ .../ui_models/app_state/features_state.ts | 63 +--- .../ui_models/app_state/notes_state.ts | 15 +- .../ui_models/app_state/notes_view_state.ts | 4 +- .../ui_models/app_state/tags_state.ts | 261 +-------------- .../javascripts/ui_models/panel_resizer.ts | 11 +- .../views/application/application-view.pug | 2 +- .../views/application/application_view.ts | 12 +- app/assets/javascripts/views/constants.ts | 2 +- app/assets/javascripts/views/index.ts | 1 + .../javascripts/views/tags/tags-view.pug | 43 +++ .../javascripts/views/tags/tags_view.ts | 298 ++++++++++++++++++ app/assets/stylesheets/_focused.scss | 6 +- app/assets/stylesheets/_tags.scss | 4 - 33 files changed, 870 insertions(+), 1032 deletions(-) delete mode 100644 app/assets/icons/ic-notes.svg delete mode 100644 app/assets/javascripts/components/Navigation.tsx rename app/assets/javascripts/components/{Tags => }/RootTagDropZone.tsx (88%) delete mode 100644 app/assets/javascripts/components/Tags/SmartTagsList.tsx delete mode 100644 app/assets/javascripts/components/Tags/SmartTagsListItem.tsx delete mode 100644 app/assets/javascripts/components/Tags/SmartTagsSection.tsx delete mode 100644 app/assets/javascripts/components/Tags/TagsList.tsx delete mode 100644 app/assets/javascripts/components/Tags/TagsSectionAddButton.tsx delete mode 100644 app/assets/javascripts/components/Tags/TagsSectionTitle.tsx delete mode 100644 app/assets/javascripts/components/Tags/dragndrop.ts create mode 100644 app/assets/javascripts/components/TagsList.tsx rename app/assets/javascripts/components/{Tags => }/TagsListItem.tsx (81%) create mode 100644 app/assets/javascripts/views/tags/tags-view.pug create mode 100644 app/assets/javascripts/views/tags/tags_view.ts diff --git a/app/assets/icons/ic-notes.svg b/app/assets/icons/ic-notes.svg deleted file mode 100644 index ece661333..000000000 --- a/app/assets/icons/ic-notes.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index bd5d4e066..2d1762e8d 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -20,32 +20,23 @@ declare global { } } -import { ComponentViewDirective } from '@/components/ComponentView'; -import { NavigationDirective } from '@/components/Navigation'; -import { PinNoteButtonDirective } from '@/components/PinNoteButton'; -import { IsWebPlatform, WebAppVersion } from '@/version'; -import { - ApplicationGroupView, - ApplicationView, ChallengeModal, - FooterView, NoteGroupViewDirective, - NoteViewDirective -} from '@/views'; import { SNLog } from '@standardnotes/snjs'; import angular from 'angular'; -import { AccountMenuDirective } from './components/AccountMenu'; -import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; -import { IconDirective } from './components/Icon'; -import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes'; -import { NoAccountWarningDirective } from './components/NoAccountWarning'; -import { NotesContextMenuDirective } from './components/NotesContextMenu'; -import { NotesListOptionsDirective } from './components/NotesListOptionsMenu'; -import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel'; -import { NotesViewDirective } from './components/NotesView'; -import { NoteTagsContainerDirective } from './components/NoteTagsContainer'; -import { ProtectedNoteOverlayDirective } from './components/ProtectedNoteOverlay'; -import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu'; -import { SearchOptionsDirective } from './components/SearchOptions'; -import { SessionsModalDirective } from './components/SessionsModal'; +import { configRoutes } from './routes'; + +import { ApplicationGroup } from './ui_models/application_group'; +import { AccountSwitcher } from './views/account_switcher/account_switcher'; + +import { + ApplicationGroupView, + ApplicationView, + NoteGroupViewDirective, + NoteViewDirective, + TagsView, + FooterView, + ChallengeModal, +} from '@/views'; + import { autofocus, clickOutside, @@ -55,31 +46,49 @@ import { infiniteScroll, lowercase, selectOnFocus, - snEnter + snEnter, } from './directives/functional'; + import { ActionsMenu, EditorMenu, - HistoryMenu, InputModal, MenuRow, PanelResizer, PasswordWizard, PermissionsModal, RevisionPreviewModal, - SyncResolutionMenu + HistoryMenu, + SyncResolutionMenu, } from './directives/views'; + import { trusted } from './filters'; -import { PreferencesDirective } from './preferences'; -import { PurchaseFlowDirective } from './purchaseFlow'; -import { configRoutes } from './routes'; -import { Bridge } from './services/bridge'; +import { isDev } from './utils'; import { BrowserBridge } from './services/browserBridge'; import { startErrorReporting } from './services/errorReporting'; import { StartApplication } from './startApplication'; -import { ApplicationGroup } from './ui_models/application_group'; -import { isDev } from './utils'; -import { AccountSwitcher } from './views/account_switcher/account_switcher'; +import { Bridge } from './services/bridge'; +import { SessionsModalDirective } from './components/SessionsModal'; +import { NoAccountWarningDirective } from './components/NoAccountWarning'; +import { ProtectedNoteOverlayDirective } from './components/ProtectedNoteOverlay'; +import { SearchOptionsDirective } from './components/SearchOptions'; +import { AccountMenuDirective } from './components/AccountMenu'; +import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal'; +import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes'; +import { NotesContextMenuDirective } from './components/NotesContextMenu'; +import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel'; +import { IconDirective } from './components/Icon'; +import { NoteTagsContainerDirective } from './components/NoteTagsContainer'; +import { PreferencesDirective } from './preferences'; +import { WebAppVersion, IsWebPlatform } from '@/version'; +import { NotesListOptionsDirective } from './components/NotesListOptionsMenu'; +import { PurchaseFlowDirective } from './purchaseFlow'; +import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu'; +import { ComponentViewDirective } from '@/components/ComponentView'; +import { TagsListDirective } from '@/components/TagsList'; +import { NotesViewDirective } from './components/NotesView'; +import { PinNoteButtonDirective } from '@/components/PinNoteButton'; +import { TagsSectionDirective } from './components/Tags/TagsSection'; function reloadHiddenFirefoxTab(): boolean { /** @@ -134,6 +143,7 @@ const startApplication: StartApplication = async function startApplication( .directive('applicationView', () => new ApplicationView()) .directive('noteGroupView', () => new NoteGroupViewDirective()) .directive('noteView', () => new NoteViewDirective()) + .directive('tagsView', () => new TagsView()) .directive('footerView', () => new FooterView()); // Directives - Functional @@ -178,7 +188,8 @@ const startApplication: StartApplication = async function startApplication( .directive('notesListOptionsMenu', NotesListOptionsDirective) .directive('icon', IconDirective) .directive('noteTagsContainer', NoteTagsContainerDirective) - .directive('navigation', NavigationDirective) + .directive('tagsList', TagsListDirective) + .directive('tagsSection', TagsSectionDirective) .directive('preferences', PreferencesDirective) .directive('purchaseFlow', PurchaseFlowDirective) .directive('notesView', NotesViewDirective) diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 6d5b6d3bc..bb82794cf 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -23,7 +23,6 @@ import AuthenticatorIcon from '../../icons/ic-authenticator.svg'; import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg'; import TasksIcon from '../../icons/ic-tasks.svg'; import MarkdownIcon from '../../icons/ic-markdown.svg'; -import NotesIcon from '../../icons/ic-notes.svg'; import CodeIcon from '../../icons/ic-code.svg'; import AccessibilityIcon from '../../icons/ic-accessibility.svg'; @@ -70,7 +69,6 @@ import { FunctionalComponent } from 'preact'; const ICONS = { 'menu-arrow-down-alt': MenuArrowDownAlt, 'menu-arrow-right': MenuArrowRight, - notes: NotesIcon, 'arrows-sort-up': ArrowsSortUpIcon, 'arrows-sort-down': ArrowsSortDownIcon, lock: LockIcon, diff --git a/app/assets/javascripts/components/Navigation.tsx b/app/assets/javascripts/components/Navigation.tsx deleted file mode 100644 index b3e5dcfd4..000000000 --- a/app/assets/javascripts/components/Navigation.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { ComponentView } from '@/components/ComponentView'; -import { PanelResizer } from '@/components/PanelResizer'; -import { SmartTagsSection } from '@/components/Tags/SmartTagsSection'; -import { TagsSection } from '@/components/Tags/TagsSection'; -import { toDirective } from '@/components/utils'; -import { - PanelSide, - ResizeFinishCallback, -} from '@/directives/views/panelResizer'; -import { WebApplication } from '@/ui_models/application'; -import { PANEL_NAME_NAVIGATION } from '@/views/constants'; -import { PrefKey } from '@standardnotes/snjs'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; -import { PremiumModalProvider } from './Premium'; - -type Props = { - application: WebApplication; -}; - -export const Navigation: FunctionComponent = observer( - ({ application }) => { - const appState = useMemo(() => application.getAppState(), [application]); - const componentViewer = appState.foldersComponentViewer; - const enableNativeSmartTagsFeature = - appState.features.enableNativeSmartTagsFeature; - const [panelRef, setPanelRef] = useState(null); - - useEffect(() => { - const elem = document.querySelector( - 'navigation' - ) as HTMLDivElement | null; - setPanelRef(elem); - }, [setPanelRef]); - - const onCreateNewTag = useCallback(() => { - appState.tags.createNewTemplate(); - }, [appState]); - - const panelResizeFinishCallback: ResizeFinishCallback = useCallback( - (_lastWidth, _lastLeft, _isMaxWidth, isCollapsed) => { - appState.noteTags.reloadTagsContainerMaxWidth(); - appState.panelDidResize(PANEL_NAME_NAVIGATION, isCollapsed); - }, - [appState] - ); - - const panelWidthEventCallback = useCallback(() => { - appState.noteTags.reloadTagsContainerMaxWidth(); - }, [appState]); - - return ( - -
- {componentViewer ? ( -
-
- -
-
- ) : ( -
-
-
-
- Views -
- {!enableNativeSmartTagsFeature && ( -
-
- -
-
- )} -
-
-
-
- - -
-
-
- )} - {panelRef && ( - - )} -
-
- ); - } -); - -export const NavigationDirective = toDirective(Navigation); diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index bd6ecd767..91dc12aba 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -32,7 +32,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => { const onTagClick = (event: MouseEvent) => { if (tagClicked && event.target !== deleteTagRef.current) { setTagClicked(false); - appState.selectedTag = tag; + appState.setSelectedTag(tag); } else { setTagClicked(true); } diff --git a/app/assets/javascripts/components/NotesView.tsx b/app/assets/javascripts/components/NotesView.tsx index 622bd31aa..6c1b8e0df 100644 --- a/app/assets/javascripts/components/NotesView.tsx +++ b/app/assets/javascripts/components/NotesView.tsx @@ -124,9 +124,9 @@ const NotesView: FunctionComponent = observer( }; const panelResizeFinishCallback: ResizeFinishCallback = ( - _lastWidth, - _lastLeft, - _isMaxWidth, + _w, + _l, + _mw, isCollapsed ) => { appState.noteTags.reloadTagsContainerMaxWidth(); diff --git a/app/assets/javascripts/components/Premium/usePremiumModal.tsx b/app/assets/javascripts/components/Premium/usePremiumModal.tsx index a30e9dbf9..d52b15823 100644 --- a/app/assets/javascripts/components/Premium/usePremiumModal.tsx +++ b/app/assets/javascripts/components/Premium/usePremiumModal.tsx @@ -1,5 +1,3 @@ -import { FeaturesState } from '@/ui_models/app_state/features_state'; -import { observer } from 'mobx-react-lite'; import { FunctionalComponent } from 'preact'; import { useCallback, useContext, useState } from 'preact/hooks'; import { createContext } from 'react'; @@ -23,31 +21,29 @@ export const usePremiumModal = (): PremiumModalContextData => { return value; }; -interface Props { - state: FeaturesState; -} +export const PremiumModalProvider: FunctionalComponent = ({ children }) => { + const [featureName, setFeatureName] = useState(null); -export const PremiumModalProvider: FunctionalComponent = observer( - ({ state, children }) => { - const featureName = state._premiumAlertFeatureName; - const activate = state.showPremiumAlert; - const close = state.closePremiumAlert; + const activate = setFeatureName; - const showModal = !!featureName; + const closeModal = useCallback(() => { + setFeatureName(null); + }, [setFeatureName]); - return ( - <> - {showModal && ( - - )} - - {children} - - - ); - } -); + const showModal = !!featureName; + + return ( + <> + {showModal && ( + + )} + + {children} + + + ); +}; diff --git a/app/assets/javascripts/components/Tags/RootTagDropZone.tsx b/app/assets/javascripts/components/RootTagDropZone.tsx similarity index 88% rename from app/assets/javascripts/components/Tags/RootTagDropZone.tsx rename to app/assets/javascripts/components/RootTagDropZone.tsx index 1c6908a51..0e39ef5a8 100644 --- a/app/assets/javascripts/components/Tags/RootTagDropZone.tsx +++ b/app/assets/javascripts/components/RootTagDropZone.tsx @@ -1,5 +1,3 @@ -import { Icon } from '@/components/Icon'; -import { usePremiumModal } from '@/components/Premium'; import { FeaturesState, TAG_FOLDERS_FEATURE_NAME, @@ -7,7 +5,9 @@ import { import { TagsState } from '@/ui_models/app_state/tags_state'; import { observer } from 'mobx-react-lite'; import { useDrop } from 'react-dnd'; -import { DropItem, DropProps, ItemTypes } from './dragndrop'; +import { Icon } from './Icon'; +import { usePremiumModal } from './Premium'; +import { DropItem, DropProps, ItemTypes } from './TagsListItem'; type Props = { tagsState: TagsState; @@ -18,7 +18,7 @@ export const RootTagDropZone: React.FC = observer( ({ tagsState, featuresState }) => { const premiumModal = usePremiumModal(); const isNativeFoldersEnabled = featuresState.enableNativeFoldersFeature; - const hasFolders = featuresState.hasFolders; + const hasFolders = tagsState.hasFolders; const [{ isOver, canDrop }, dropRef] = useDrop( () => ({ diff --git a/app/assets/javascripts/components/Tags/SmartTagsList.tsx b/app/assets/javascripts/components/Tags/SmartTagsList.tsx deleted file mode 100644 index 77b06eec8..000000000 --- a/app/assets/javascripts/components/Tags/SmartTagsList.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { AppState } from '@/ui_models/app_state'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { SmartTagsListItem } from './SmartTagsListItem'; - -type Props = { - appState: AppState; -}; - -export const SmartTagsList: FunctionComponent = observer( - ({ appState }) => { - const allTags = appState.tags.smartTags; - - return ( - <> - {allTags.map((tag) => { - return ( - - ); - })} - - ); - } -); diff --git a/app/assets/javascripts/components/Tags/SmartTagsListItem.tsx b/app/assets/javascripts/components/Tags/SmartTagsListItem.tsx deleted file mode 100644 index 0284b0ae9..000000000 --- a/app/assets/javascripts/components/Tags/SmartTagsListItem.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Icon, IconType } from '@/components/Icon'; -import { FeaturesState } from '@/ui_models/app_state/features_state'; -import { TagsState } from '@/ui_models/app_state/tags_state'; -import '@reach/tooltip/styles.css'; -import { SNSmartTag } from '@standardnotes/snjs'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; - -type Props = { - tag: SNSmartTag; - tagsState: TagsState; - features: FeaturesState; -}; - -const smartTagIconType = (tag: SNSmartTag): IconType => { - if (tag.isAllTag) { - return 'notes'; - } - if (tag.isArchiveTag) { - return 'archive'; - } - if (tag.isTrashTag) { - return 'trash'; - } - return 'hashtag'; -}; - -export const SmartTagsListItem: FunctionComponent = observer( - ({ tag, tagsState, features }) => { - const [title, setTitle] = useState(tag.title || ''); - const inputRef = useRef(null); - - const level = 0; - const isSelected = tagsState.selected === tag; - const isEditing = tagsState.editingTag === tag; - const isSmartTagsEnabled = features.enableNativeSmartTagsFeature; - - useEffect(() => { - setTitle(tag.title || ''); - }, [setTitle, tag]); - - const selectCurrentTag = useCallback(() => { - tagsState.selected = tag; - }, [tagsState, tag]); - - const onBlur = useCallback(() => { - tagsState.save(tag, title); - setTitle(tag.title); - }, [tagsState, tag, title, setTitle]); - - const onInput = useCallback( - (e: Event) => { - const value = (e.target as HTMLInputElement).value; - setTitle(value); - }, - [setTitle] - ); - - const onKeyUp = useCallback( - (e: KeyboardEvent) => { - if (e.code === 'Enter') { - inputRef.current?.blur(); - e.preventDefault(); - } - }, - [inputRef] - ); - - useEffect(() => { - if (isEditing) { - inputRef.current?.focus(); - } - }, [inputRef, isEditing]); - - const onClickRename = useCallback(() => { - tagsState.editingTag = tag; - }, [tagsState, tag]); - - const onClickSave = useCallback(() => { - inputRef.current?.blur(); - }, [inputRef]); - - const onClickDelete = useCallback(() => { - tagsState.remove(tag); - }, [tagsState, tag]); - - const isFaded = !isSmartTagsEnabled && !tag.isAllTag; - const iconType = smartTagIconType(tag); - - return ( - <> -
- {!tag.errorDecrypting ? ( -
- {isSmartTagsEnabled && ( -
- -
- )} - -
- {tag.isAllTag && tagsState.allNotesCount} -
-
- ) : null} - {!tag.isSystemSmartTag && ( -
- {tag.conflictOf && ( -
- Conflicted Copy {tag.conflictOf} -
- )} - {tag.errorDecrypting && !tag.waitingForKey && ( -
Missing Keys
- )} - {tag.errorDecrypting && tag.waitingForKey && ( -
- Waiting For Keys -
- )} - {isSelected && ( -
- {!isEditing && ( - - Rename - - )} - {isEditing && ( - - Save - - )} - - Delete - -
- )} -
- )} -
- - ); - } -); diff --git a/app/assets/javascripts/components/Tags/SmartTagsSection.tsx b/app/assets/javascripts/components/Tags/SmartTagsSection.tsx deleted file mode 100644 index d647ca6ef..000000000 --- a/app/assets/javascripts/components/Tags/SmartTagsSection.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { WebApplication } from '@/ui_models/application'; -import { AppState } from '@/ui_models/app_state'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { SmartTagsList } from './SmartTagsList'; - -type Props = { - appState: AppState; -}; - -export const SmartTagsSection: FunctionComponent = observer( - ({ appState }) => { - return ( -
- -
- ); - } -); diff --git a/app/assets/javascripts/components/Tags/TagsList.tsx b/app/assets/javascripts/components/Tags/TagsList.tsx deleted file mode 100644 index 0d6ab0b15..000000000 --- a/app/assets/javascripts/components/Tags/TagsList.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { AppState } from '@/ui_models/app_state'; -import { isMobile } from '@/utils'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { TouchBackend } from 'react-dnd-touch-backend'; -import { RootTagDropZone } from './RootTagDropZone'; -import { TagsListItem } from './TagsListItem'; - -type Props = { - appState: AppState; -}; - -export const TagsList: FunctionComponent = observer(({ appState }) => { - const tagsState = appState.tags; - const allTags = tagsState.allLocalRootTags; - - const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend; - - return ( - - {allTags.length === 0 ? ( -
- No tags. Create one using the add button above. -
- ) : ( - <> - {allTags.map((tag) => { - return ( - - ); - })} - - - )} -
- ); -}); diff --git a/app/assets/javascripts/components/Tags/TagsSection.tsx b/app/assets/javascripts/components/Tags/TagsSection.tsx index 636206f73..5cc7a452d 100644 --- a/app/assets/javascripts/components/Tags/TagsSection.tsx +++ b/app/assets/javascripts/components/Tags/TagsSection.tsx @@ -1,29 +1,108 @@ -import { TagsList } from '@/components/Tags/TagsList'; +import { TagsList } from '@/components/TagsList'; +import { toDirective } from '@/components/utils'; +import { WebApplication } from '@/ui_models/application'; import { AppState } from '@/ui_models/app_state'; +import { + FeaturesState, + TAG_FOLDERS_FEATURE_NAME, + TAG_FOLDERS_FEATURE_TOOLTIP, +} from '@/ui_models/app_state/features_state'; +import { Tooltip } from '@reach/tooltip'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; -import { TagsSectionAddButton } from './TagsSectionAddButton'; -import { TagsSectionTitle } from './TagsSectionTitle'; +import { useCallback } from 'preact/hooks'; +import { IconButton } from '../IconButton'; +import { PremiumModalProvider, usePremiumModal } from '../Premium'; type Props = { + application: WebApplication; appState: AppState; }; -export const TagsSection: FunctionComponent = observer( - ({ appState }) => { +const TagAddButton: FunctionComponent<{ + appState: AppState; + features: FeaturesState; +}> = observer(({ appState, features }) => { + const isNativeFoldersEnabled = features.enableNativeFoldersFeature; + + if (!isNativeFoldersEnabled) { + return null; + } + + return ( + appState.createNewTag()} + /> + ); +}); + +const TagTitle: FunctionComponent<{ + features: FeaturesState; +}> = observer(({ features }) => { + const isNativeFoldersEnabled = features.enableNativeFoldersFeature; + const hasFolders = features.hasFolders; + const modal = usePremiumModal(); + + const showPremiumAlert = useCallback(() => { + modal.activate(TAG_FOLDERS_FEATURE_NAME); + }, [modal]); + + if (!isNativeFoldersEnabled) { return ( -
-
-
- - -
+ <> +
+ Tags
- -
+ + ); + } + + if (hasFolders) { + return ( + <> +
+ Folders +
+ + ); + } + + return ( + <> +
+ Tags + + + +
+ + ); +}); + +export const TagsSection: FunctionComponent = observer( + ({ application, appState }) => { + return ( + +
+
+
+ + +
+
+ +
+
); } ); + +export const TagsSectionDirective = toDirective(TagsSection); diff --git a/app/assets/javascripts/components/Tags/TagsSectionAddButton.tsx b/app/assets/javascripts/components/Tags/TagsSectionAddButton.tsx deleted file mode 100644 index 9a748b7dc..000000000 --- a/app/assets/javascripts/components/Tags/TagsSectionAddButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { IconButton } from '@/components/IconButton'; -import { AppState } from '@/ui_models/app_state'; -import { FeaturesState } from '@/ui_models/app_state/features_state'; -import { TagsState } from '@/ui_models/app_state/tags_state'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; - -type Props = { - tags: TagsState; - features: FeaturesState; -}; - -export const TagsSectionAddButton: FunctionComponent = observer( - ({ tags, features }) => { - const isNativeFoldersEnabled = features.enableNativeFoldersFeature; - - if (!isNativeFoldersEnabled) { - return null; - } - - return ( - tags.createNewTemplate()} - /> - ); - } -); diff --git a/app/assets/javascripts/components/Tags/TagsSectionTitle.tsx b/app/assets/javascripts/components/Tags/TagsSectionTitle.tsx deleted file mode 100644 index 6298dbf1e..000000000 --- a/app/assets/javascripts/components/Tags/TagsSectionTitle.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { usePremiumModal } from '@/components/Premium'; -import { - FeaturesState, - TAG_FOLDERS_FEATURE_NAME, - TAG_FOLDERS_FEATURE_TOOLTIP, -} from '@/ui_models/app_state/features_state'; -import { Tooltip } from '@reach/tooltip'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { useCallback } from 'preact/hooks'; - -type Props = { - features: FeaturesState; -}; - -export const TagsSectionTitle: FunctionComponent = observer( - ({ features }) => { - const isNativeFoldersEnabled = features.enableNativeFoldersFeature; - const hasFolders = features.hasFolders; - const modal = usePremiumModal(); - - const showPremiumAlert = useCallback(() => { - modal.activate(TAG_FOLDERS_FEATURE_NAME); - }, [modal]); - - if (!isNativeFoldersEnabled) { - return ( - <> -
- Tags -
- - ); - } - - if (hasFolders) { - return ( - <> -
- Folders -
- - ); - } - - return ( - <> -
- Tags - - - -
- - ); - } -); diff --git a/app/assets/javascripts/components/Tags/dragndrop.ts b/app/assets/javascripts/components/Tags/dragndrop.ts deleted file mode 100644 index 5a5033ad5..000000000 --- a/app/assets/javascripts/components/Tags/dragndrop.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum ItemTypes { - TAG = 'TAG', -} - -export type DropItemTag = { uuid: string }; - -export type DropItem = DropItemTag; - -export type DropProps = { isOver: boolean; canDrop: boolean }; diff --git a/app/assets/javascripts/components/TagsList.tsx b/app/assets/javascripts/components/TagsList.tsx new file mode 100644 index 000000000..66925c2c1 --- /dev/null +++ b/app/assets/javascripts/components/TagsList.tsx @@ -0,0 +1,153 @@ +import { PremiumModalProvider } from '@/components/Premium'; +import { confirmDialog } from '@/services/alertService'; +import { STRING_DELETE_TAG } from '@/strings'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { isMobile } from '@/utils'; +import { SNTag, TagMutator } from '@standardnotes/snjs'; +import { runInAction } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useCallback } from 'preact/hooks'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { TouchBackend } from 'react-dnd-touch-backend'; +import { RootTagDropZone } from './RootTagDropZone'; +import { TagsListItem } from './TagsListItem'; +import { toDirective } from './utils'; + +type Props = { + application: WebApplication; + appState: AppState; +}; + +const tagsWithOptionalTemplate = ( + template: SNTag | undefined, + tags: SNTag[] +): SNTag[] => { + if (!template) { + return tags; + } + return [template, ...tags]; +}; + +export const TagsList: FunctionComponent = observer( + ({ application, appState }) => { + const templateTag = appState.templateTag; + const rootTags = appState.tags.rootTags; + + const allTags = tagsWithOptionalTemplate(templateTag, rootTags); + + const selectTag = useCallback( + (tag: SNTag) => { + appState.setSelectedTag(tag); + }, + [appState] + ); + + const saveTag = useCallback( + async (tag: SNTag, newTitle: string) => { + const templateTag = appState.templateTag; + + const hasEmptyTitle = newTitle.length === 0; + const hasNotChangedTitle = newTitle === tag.title; + const isTemplateChange = templateTag && tag.uuid === templateTag.uuid; + const hasDuplicatedTitle = !!application.findTagByTitle(newTitle); + + runInAction(() => { + appState.templateTag = undefined; + appState.editingTag = undefined; + }); + + if (hasEmptyTitle || hasNotChangedTitle) { + if (isTemplateChange) { + appState.undoCreateNewTag(); + } + return; + } + + if (hasDuplicatedTitle) { + if (isTemplateChange) { + appState.undoCreateNewTag(); + } + application.alertService?.alert( + 'A tag with this name already exists.' + ); + return; + } + + if (isTemplateChange) { + const insertedTag = await application.insertItem(templateTag); + const changedTag = await application.changeItem( + insertedTag.uuid, + (m) => { + m.title = newTitle; + } + ); + + selectTag(changedTag as SNTag); + await application.saveItem(insertedTag.uuid); + } else { + await application.changeAndSaveItem( + tag.uuid, + (mutator) => { + mutator.title = newTitle; + } + ); + } + }, + [appState, application, selectTag] + ); + + const removeTag = useCallback( + async (tag: SNTag) => { + if ( + await confirmDialog({ + text: STRING_DELETE_TAG, + confirmButtonStyle: 'danger', + }) + ) { + appState.removeTag(tag); + } + }, + [appState] + ); + + const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend; + + return ( + + + {allTags.length === 0 ? ( +
+ No tags. Create one using the add button above. +
+ ) : ( + <> + {allTags.map((tag) => { + return ( + + ); + })} + + + )} +
+
+ ); + } +); + +export const TagsListDirective = toDirective(TagsList); diff --git a/app/assets/javascripts/components/Tags/TagsListItem.tsx b/app/assets/javascripts/components/TagsListItem.tsx similarity index 81% rename from app/assets/javascripts/components/Tags/TagsListItem.tsx rename to app/assets/javascripts/components/TagsListItem.tsx index 8dac77728..ed6e8cf5e 100644 --- a/app/assets/javascripts/components/Tags/TagsListItem.tsx +++ b/app/assets/javascripts/components/TagsListItem.tsx @@ -1,5 +1,3 @@ -import { Icon } from '@/components/Icon'; -import { usePremiumModal } from '@/components/Premium'; import { FeaturesState, TAG_FOLDERS_FEATURE_NAME, @@ -7,36 +5,56 @@ import { import { TagsState } from '@/ui_models/app_state/tags_state'; import '@reach/tooltip/styles.css'; import { SNTag } from '@standardnotes/snjs'; -import { computed } from 'mobx'; +import { computed, runInAction } from 'mobx'; import { observer } from 'mobx-react-lite'; import { FunctionComponent, JSX } from 'preact'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useDrag, useDrop } from 'react-dnd'; -import { DropItem, DropProps, ItemTypes } from './dragndrop'; +import { Icon } from './Icon'; +import { usePremiumModal } from './Premium'; + +export enum ItemTypes { + TAG = 'TAG', +} + +export type DropItemTag = { uuid: string }; + +export type DropItem = DropItemTag; + +export type DropProps = { isOver: boolean; canDrop: boolean }; type Props = { tag: SNTag; tagsState: TagsState; - features: FeaturesState; + selectTag: (tag: SNTag) => void; + removeTag: (tag: SNTag) => void; + saveTag: (tag: SNTag, newTitle: string) => void; + appState: TagsListState; level: number; }; +export type TagsListState = { + readonly selectedTag: SNTag | undefined; + tags: TagsState; + editingTag: SNTag | undefined; + features: FeaturesState; +}; + export const TagsListItem: FunctionComponent = observer( - ({ tag, features, tagsState, level }) => { + ({ tag, selectTag, saveTag, removeTag, appState, tagsState, level }) => { const [title, setTitle] = useState(tag.title || ''); const inputRef = useRef(null); - const isSelected = tagsState.selected === tag; - const isEditing = tagsState.editingTag === tag; - const noteCounts = computed(() => tagsState.getNotesCount(tag)); + const isSelected = appState.selectedTag === tag; + const isEditing = appState.editingTag === tag; + const noteCounts = computed(() => appState.tags.getNotesCount(tag)); const childrenTags = computed(() => tagsState.getChildren(tag)).get(); const hasChildren = childrenTags.length > 0; - const hasFolders = features.hasFolders; - const isNativeFoldersEnabled = features.enableNativeFoldersFeature; + const hasFolders = tagsState.hasFolders; + const isNativeFoldersEnabled = appState.features.enableNativeFoldersFeature; const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder; - const premiumModal = usePremiumModal(); const [showChildren, setShowChildren] = useState(hasChildren); @@ -62,13 +80,16 @@ export const TagsListItem: FunctionComponent = observer( ); const selectCurrentTag = useCallback(() => { - tagsState.selected = tag; - }, [tagsState, tag]); + if (isEditing || isSelected) { + return; + } + selectTag(tag); + }, [isSelected, isEditing, selectTag, tag]); const onBlur = useCallback(() => { - tagsState.save(tag, title); + saveTag(tag, title); setTitle(tag.title); - }, [tagsState, tag, title, setTitle]); + }, [tag, saveTag, title, setTitle]); const onInput = useCallback( (e: JSX.TargetedEvent) => { @@ -95,16 +116,18 @@ export const TagsListItem: FunctionComponent = observer( }, [inputRef, isEditing]); const onClickRename = useCallback(() => { - tagsState.editingTag = tag; - }, [tagsState, tag]); + runInAction(() => { + appState.editingTag = tag; + }); + }, [appState, tag]); const onClickSave = useCallback(() => { inputRef.current?.blur(); }, [inputRef]); const onClickDelete = useCallback(() => { - tagsState.remove(tag); - }, [tagsState, tag]); + removeTag(tag); + }, [removeTag, tag]); const [, dragRef] = useDrag( () => ({ @@ -232,7 +255,10 @@ export const TagsListItem: FunctionComponent = observer( key={tag.uuid} tag={tag} tagsState={tagsState} - features={features} + selectTag={selectTag} + saveTag={saveTag} + removeTag={removeTag} + appState={appState} /> ); })} diff --git a/app/assets/javascripts/strings.ts b/app/assets/javascripts/strings.ts index 75a099619..1f6f647d8 100644 --- a/app/assets/javascripts/strings.ts +++ b/app/assets/javascripts/strings.ts @@ -22,8 +22,6 @@ export const STRING_NEW_UPDATE_READY = export const STRING_DELETE_TAG = 'Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.'; -export const STRING_MISSING_SYSTEM_TAG = 'We are missing a System Tag.'; - /** @editor */ export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN = 'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.'; diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index 124fa2e16..71d4440f0 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -6,26 +6,20 @@ 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, - reaction, + runInAction, } from 'mobx'; import { ActionsMenuState } from './actions_menu_state'; import { FeaturesState } from './features_state'; @@ -78,6 +72,11 @@ 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(); @@ -93,16 +92,10 @@ 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, @@ -167,27 +160,30 @@ export class AppState { this.showBetaWarning = false; } - this.foldersComponentViewer = undefined; + this.selectedTag = undefined; + this.previouslySelectedTag = undefined; + this.editingTag = undefined; + this._templateTag = 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 { @@ -210,8 +206,6 @@ export class AppState { } document.removeEventListener('visibilitychange', this.onVisibilityChange); this.onVisibilityChange = undefined; - this.tagChangedDisposer(); - this.foldersComponentViewerDisposer(); } openSessionsModal(): void { @@ -240,16 +234,16 @@ export class AppState { if (!this.multiEditorSupport) { this.closeActiveNoteController(); } - - const selectedTag = this.selectedTag; - - const activeRegularTagUuid = - selectedTag && !selectedTag.isSmartTag ? selectedTag.uuid : undefined; + const activeTagUuid = this.selectedTag + ? this.selectedTag.isSmartTag + ? undefined + : this.selectedTag.uuid + : undefined; await this.application.noteControllerGroup.createNoteView( undefined, title, - activeRegularTagUuid + activeTagUuid ); } @@ -281,88 +275,10 @@ 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( @@ -377,13 +293,13 @@ export class AppState { this.closeNoteController(noteController); } else if ( note.trashed && - !selectedTag?.isTrashTag && + !this.selectedTag?.isTrashTag && !this.searchOptions.includeTrashed ) { this.closeNoteController(noteController); } else if ( note.archived && - !selectedTag?.isArchiveTag && + !this.selectedTag?.isArchiveTag && !this.searchOptions.includeArchived && !this.application.getPreference(PrefKey.NotesShowArchived, false) ) { @@ -391,6 +307,17 @@ 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; + }); + } + } } ); } @@ -458,6 +385,74 @@ 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) => { diff --git a/app/assets/javascripts/ui_models/app_state/features_state.ts b/app/assets/javascripts/ui_models/app_state/features_state.ts index 323f2931f..ca022d8b4 100644 --- a/app/assets/javascripts/ui_models/app_state/features_state.ts +++ b/app/assets/javascripts/ui_models/app_state/features_state.ts @@ -3,22 +3,13 @@ import { FeatureIdentifier, FeatureStatus, } from '@standardnotes/snjs'; -import { - action, - computed, - makeObservable, - observable, - runInAction, - when, -} from 'mobx'; +import { computed, makeObservable, observable, runInAction } 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). @@ -28,37 +19,23 @@ 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: @@ -75,25 +52,25 @@ export class FeaturesState { return this.enableUnfinishedFeatures; } - public get enableNativeSmartTagsFeature(): boolean { - return this.enableUnfinishedFeatures; - } - public get hasFolders(): boolean { return this._hasFolders; } - public get hasSmartTags(): boolean { - return this._hasSmartTags; - } + public set hasFolders(hasFolders: boolean) { + if (!hasFolders) { + this._hasFolders = false; + return; + } - public async showPremiumAlert(featureName: string): Promise { - this._premiumAlertFeatureName = featureName; - return when(() => this._premiumAlertFeatureName === undefined); - } + if (!this.hasNativeFolders()) { + this.application.alertService?.alert( + `${TAG_FOLDERS_FEATURE_NAME} requires at least a Plus Subscription.` + ); + this._hasFolders = false; + return; + } - public async closePremiumAlert(): Promise { - this._premiumAlertFeatureName = undefined; + this._hasFolders = hasFolders; } private hasNativeFolders(): boolean { @@ -107,16 +84,4 @@ 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; - } } diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts index 8bfbae2c2..44dda28e1 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -115,12 +115,12 @@ export class NotesState { async selectNote(uuid: UuidString, userTriggered?: boolean): Promise { 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 && (hasMeta || hasCtrl)) { + if ( + userTriggered && + (this.io.activeModifiers.has(KeyboardModifier.Meta) || + this.io.activeModifiers.has(KeyboardModifier.Ctrl)) + ) { if (this.selectedNotes[uuid]) { delete this.selectedNotes[uuid]; } else if (await this.application.authorizeNoteAccess(note)) { @@ -129,7 +129,10 @@ export class NotesState { this.lastSelectedNote = note; }); } - } else if (userTriggered && hasShift) { + } else if ( + userTriggered && + this.io.activeModifiers.has(KeyboardModifier.Shift) + ) { await this.selectNotesRange(note); } else { const shouldSelectNote = diff --git a/app/assets/javascripts/ui_models/app_state/notes_view_state.ts b/app/assets/javascripts/ui_models/app_state/notes_view_state.ts index a7ad404bb..e0be2a0e0 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_view_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_view_state.ts @@ -495,9 +495,7 @@ export class NotesViewState { this.reloadNotesDisplayOptions(); this.reloadNotes(); - const hasSomeNotes = this.notes.length > 0; - - if (hasSomeNotes) { + if (this.notes.length > 0) { this.selectFirstNote(); } else if (dbLoaded) { if ( diff --git a/app/assets/javascripts/ui_models/app_state/tags_state.ts b/app/assets/javascripts/ui_models/app_state/tags_state.ts index 8a19847e6..13e698d89 100644 --- a/app/assets/javascripts/ui_models/app_state/tags_state.ts +++ b/app/assets/javascripts/ui_models/app_state/tags_state.ts @@ -1,13 +1,8 @@ -import { confirmDialog } from '@/services/alertService'; -import { STRING_DELETE_TAG, STRING_MISSING_SYSTEM_TAG } from '@/strings'; import { - ComponentAction, ContentType, - MessageData, SNSmartTag, SNTag, - TagMutator, - UuidString + UuidString, } from '@standardnotes/snjs'; import { action, @@ -15,21 +10,14 @@ import { makeAutoObservable, makeObservable, observable, - runInAction + runInAction, } from 'mobx'; import { WebApplication } from '../application'; -import { FeaturesState, SMART_TAGS_FEATURE_NAME } from './features_state'; - -type AnyTag = SNTag | SNSmartTag; +import { FeaturesState } from './features_state'; export class TagsState { tags: SNTag[] = []; smartTags: SNSmartTag[] = []; - allNotesCount_ = 0; - selected_: AnyTag | undefined; - previouslySelected_: AnyTag | undefined; - editing_: SNTag | undefined; - private readonly tagsCountsState: TagsCountsState; constructor( @@ -39,40 +27,22 @@ 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 @@ -80,42 +50,18 @@ 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.features.hasFolders) { + if (!this.hasFolders) { return []; } @@ -123,10 +69,7 @@ export class TagsState { return []; } - const children = this.application - .getTagChildren(tag) - .filter((tag) => !tag.isSmartTag); - + const children = this.application.getTagChildren(tag); const childrenUuids = children.map((childTag) => childTag.uuid); const childrenTags = this.tags.filter((tag) => childrenUuids.includes(tag.uuid) @@ -157,7 +100,7 @@ export class TagsState { } get rootTags(): SNTag[] { - if (!this.features.hasFolders) { + if (!this.hasFolders) { return this.tags; } @@ -168,192 +111,12 @@ export class TagsState { return this.tags.length; } - public get allNotesCount(): number { - return this.allNotesCount_; + public get hasFolders(): boolean { + return this.features.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( - insertedTag.uuid, - (m) => { - m.title = newTitle; - } - ); - this.selected = changedTag as SNTag; - await this.application.saveItem(insertedTag.uuid); - } - } else { - await this.application.changeAndSaveItem( - 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 set hasFolders(hasFolders: boolean) { + this.features.hasFolders = hasFolders; } public get hasAtLeastOneFolder(): boolean { diff --git a/app/assets/javascripts/ui_models/panel_resizer.ts b/app/assets/javascripts/ui_models/panel_resizer.ts index c53b49c9d..66ce0f9e9 100644 --- a/app/assets/javascripts/ui_models/panel_resizer.ts +++ b/app/assets/javascripts/ui_models/panel_resizer.ts @@ -56,12 +56,6 @@ 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; @@ -72,15 +66,16 @@ 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); diff --git a/app/assets/javascripts/views/application/application-view.pug b/app/assets/javascripts/views/application/application-view.pug index 58466fcf4..01630f2b5 100644 --- a/app/assets/javascripts/views/application/application-view.pug +++ b/app/assets/javascripts/views/application/application-view.pug @@ -5,7 +5,7 @@ ng-class='self.state.appClass', ng-if='!self.state.needsUnlock && self.state.launched' ) - navigation(application='self.application', appState='self.appState') + tags-view(application='self.application') notes-view( application='self.application' app-state='self.appState' diff --git a/app/assets/javascripts/views/application/application_view.ts b/app/assets/javascripts/views/application/application_view.ts index c2c41eede..cc861404b 100644 --- a/app/assets/javascripts/views/application/application_view.ts +++ b/app/assets/javascripts/views/application/application_view.ts @@ -8,7 +8,7 @@ import { Challenge, removeFromArray, } from '@standardnotes/snjs'; -import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/views/constants'; +import { PANEL_NAME_NOTES, PANEL_NAME_TAGS } from '@/views/constants'; import { STRING_DEFAULT_FILE_ERROR } from '@/strings'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { alertDialog } from '@/services/alertService'; @@ -24,7 +24,7 @@ class ApplicationViewCtrl extends PureViewCtrl< > { public platformString: string; private notesCollapsed = false; - private navigationCollapsed = false; + private tagsCollapsed = false; /** * To prevent stale state reads (setState is async), @@ -136,15 +136,15 @@ class ApplicationViewCtrl extends PureViewCtrl< if (panel === PANEL_NAME_NOTES) { this.notesCollapsed = collapsed; } - if (panel === PANEL_NAME_NAVIGATION) { - this.navigationCollapsed = collapsed; + if (panel === PANEL_NAME_TAGS) { + this.tagsCollapsed = collapsed; } let appClass = ''; if (this.notesCollapsed) { appClass += 'collapsed-notes'; } - if (this.navigationCollapsed) { - appClass += ' collapsed-navigation'; + if (this.tagsCollapsed) { + appClass += ' collapsed-tags'; } this.setState({ appClass }); } else if (eventName === AppStateEvent.WindowDidFocus) { diff --git a/app/assets/javascripts/views/constants.ts b/app/assets/javascripts/views/constants.ts index a84c79f91..57c83d9af 100644 --- a/app/assets/javascripts/views/constants.ts +++ b/app/assets/javascripts/views/constants.ts @@ -1,4 +1,4 @@ export const PANEL_NAME_NOTES = 'notes'; -export const PANEL_NAME_NAVIGATION = 'navigation'; +export const PANEL_NAME_TAGS = 'tags'; export const EMAIL_REGEX = /^([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)$/; diff --git a/app/assets/javascripts/views/index.ts b/app/assets/javascripts/views/index.ts index 81f8920df..82223ae4c 100644 --- a/app/assets/javascripts/views/index.ts +++ b/app/assets/javascripts/views/index.ts @@ -4,4 +4,5 @@ export { ApplicationView } from './application/application_view'; export { NoteGroupViewDirective } from './note_group_view/note_group_view'; export { NoteViewDirective } from './note_view/note_view'; export { FooterView } from './footer/footer_view'; +export { TagsView } from './tags/tags_view'; export { ChallengeModal } from './challenge_modal/challenge_modal'; diff --git a/app/assets/javascripts/views/tags/tags-view.pug b/app/assets/javascripts/views/tags/tags-view.pug new file mode 100644 index 000000000..4256b0181 --- /dev/null +++ b/app/assets/javascripts/views/tags/tags-view.pug @@ -0,0 +1,43 @@ +#tags-column.sn-component.section.tags(aria-label='Tags') + .component-view-container(ng-if='self.state.componentViewer') + component-view.component-view( + component-viewer='self.state.componentViewer', + application='self.application' + app-state='self.appState' + ) + #tags-content.content(ng-if='!(self.state.componentViewer)') + .tags-title-section.section-title-bar + .section-title-bar-header + .sk-h3.title + span.sk-bold Views + .sk-button.sk-secondary-contrast.wide( + ng-click='self.clickedAddNewTag()', + title='Create a new tag' + ) + .sk-label + i.icon.ion-plus.add-button + .scrollable + .infinite-scroll + .tag( + ng-class="{'selected' : self.state.selectedTag == tag, 'faded' : !tag.isAllTag}", + ng-click='self.selectTag(tag)', + ng-repeat='tag in self.state.smartTags track by tag.uuid' + ) + .tag-info + .title(ng-if="!tag.errorDecrypting") {{tag.title}} + .count(ng-show='tag.isAllTag') {{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 + tags-section( + application='self.application', + app-state='self.appState' + ) + panel-resizer( + collapsable='true', + control='self.panelPuppet', + default-width='150', + hoverable='true', + on-resize-finish='self.onPanelResize', + panel-id="'tags-column'" + ) diff --git a/app/assets/javascripts/views/tags/tags_view.ts b/app/assets/javascripts/views/tags/tags_view.ts new file mode 100644 index 000000000..69b94db50 --- /dev/null +++ b/app/assets/javascripts/views/tags/tags_view.ts @@ -0,0 +1,298 @@ +import { PanelPuppet, WebDirective } from '@/types'; +import { WebApplication } from '@/ui_models/application'; +import { AppStateEvent } from '@/ui_models/app_state'; +import { PANEL_NAME_TAGS } from '@/views/constants'; +import { + ApplicationEvent, + ComponentAction, + ComponentArea, + ComponentViewer, + ContentType, + isPayloadSourceInternalChange, + MessageData, + PayloadSource, + PrefKey, + SNComponent, + SNSmartTag, + SNTag, + UuidString, +} from '@standardnotes/snjs'; +import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; +import template from './tags-view.pug'; + +type NoteCounts = Partial>; + +type TagState = { + smartTags: SNSmartTag[]; + noteCounts: NoteCounts; + selectedTag?: SNTag; + componentViewer?: ComponentViewer; +}; + +class TagsViewCtrl extends PureViewCtrl { + /** Passed through template */ + readonly application!: WebApplication; + private readonly panelPuppet: PanelPuppet; + private unregisterComponent?: () => void; + /** The original name of the edtingTag before it began editing */ + formData: { tagTitle?: string } = {}; + titles: Partial> = {}; + private removeTagsObserver!: () => void; + private removeFoldersObserver!: () => void; + + /* @ngInject */ + constructor($timeout: ng.ITimeoutService) { + super($timeout); + this.panelPuppet = { + onReady: () => this.loadPreferences(), + }; + } + + deinit() { + this.removeTagsObserver?.(); + (this.removeTagsObserver as unknown) = undefined; + (this.removeFoldersObserver as unknown) = undefined; + this.unregisterComponent?.(); + this.unregisterComponent = undefined; + super.deinit(); + } + + getInitialState(): TagState { + return { + smartTags: [], + noteCounts: {}, + }; + } + + getState(): TagState { + return this.state; + } + + async onAppLaunch() { + super.onAppLaunch(); + this.loadPreferences(); + this.streamForFoldersComponent(); + + const smartTags = this.application.getSmartTags(); + this.setState({ smartTags }); + this.selectTag(smartTags[0]); + } + + /** @override */ + onAppIncrementalSync() { + super.onAppIncrementalSync(); + this.reloadNoteCounts(); + } + + async setFoldersComponent(component?: SNComponent) { + if (this.state.componentViewer) { + this.application.componentManager.destroyComponentViewer( + this.state.componentViewer + ); + await this.setState({ componentViewer: undefined }); + } + if (component) { + await this.setState({ + componentViewer: + this.application.componentManager.createComponentViewer( + component, + undefined, + this.handleFoldersComponentMessage.bind(this) + ), + }); + } + } + + handleFoldersComponentMessage( + action: ComponentAction, + data: MessageData + ): void { + if (action === ComponentAction.SelectItem) { + const item = data.item; + if (!item) { + return; + } + + if (item.content_type === ContentType.Tag) { + const matchingTag = this.application.findItem(item.uuid); + + if (matchingTag) { + this.selectTag(matchingTag as SNTag); + } + } else 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]); + } + } + + streamForFoldersComponent() { + this.removeFoldersObserver = 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) { + this.setFoldersComponent( + this.application.componentManager + .componentsForArea(ComponentArea.TagsList) + .find((component) => component.active) + ); + } + } + ); + + this.removeTagsObserver = this.application.streamItems( + [ContentType.Tag, ContentType.SmartTag], + async (items) => { + const tags = items as Array; + + await this.setState({ + smartTags: this.application.getSmartTags(), + }); + + 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 = tags.find((tag) => { + return tag.uuid === selectedTag.uuid; + }); + + if (matchingTag) { + if (matchingTag.deleted) { + this.selectTag(this.getState().smartTags[0]); + } else { + this.setState({ + selectedTag: matchingTag, + }); + } + } + } + } + ); + } + + /** @override */ + onAppStateEvent(eventName: AppStateEvent) { + if (eventName === AppStateEvent.TagChanged) { + this.setState({ + selectedTag: this.application.getAppState().getSelectedTag(), + }); + } + } + + /** @override */ + async onAppEvent(eventName: ApplicationEvent) { + super.onAppEvent(eventName); + switch (eventName) { + case ApplicationEvent.LocalDataIncrementalLoad: + this.reloadNoteCounts(); + break; + case ApplicationEvent.PreferencesChanged: + this.loadPreferences(); + break; + } + } + + reloadNoteCounts() { + const smartTags = this.state.smartTags; + const noteCounts: NoteCounts = {}; + + 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, + }); + } + + loadPreferences() { + 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!()); + } + } + } + + onPanelResize = ( + newWidth: number, + _lastLeft: number, + _isAtMaxWidth: boolean, + isCollapsed: boolean + ) => { + this.application + .setPreference(PrefKey.TagsPanelWidth, newWidth) + .then(() => this.application.sync()); + this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed); + }; + + async selectTag(tag: SNTag) { + if (tag.conflictOf) { + this.application.changeAndSaveItem(tag.uuid, (mutator) => { + mutator.conflictOf = undefined; + }); + } + this.application.getAppState().setSelectedTag(tag); + } + + async clickedAddNewTag() { + if (this.appState.templateTag) { + return; + } + + this.appState.createNewTag(); + } +} + +export class TagsView extends WebDirective { + constructor() { + super(); + this.restrict = 'E'; + this.scope = { + application: '=', + }; + this.template = template; + this.replace = true; + this.controller = TagsViewCtrl; + this.controllerAs = 'self'; + this.bindToController = true; + } +} diff --git a/app/assets/stylesheets/_focused.scss b/app/assets/stylesheets/_focused.scss index d6e27c824..d16d4866a 100644 --- a/app/assets/stylesheets/_focused.scss +++ b/app/assets/stylesheets/_focused.scss @@ -35,7 +35,7 @@ opacity: 1; } - navigation, + .section.tags, notes-view { will-change: opacity; animation: fade-out 1.25s forwards; @@ -45,7 +45,7 @@ flex: none !important; } - navigation:hover { + .section.tags:hover { flex: initial; width: 0px !important; } @@ -57,7 +57,7 @@ } .disable-focus-mode { - navigation, + .section.tags, notes-view { transition: width 1.25s; will-change: opacity; diff --git a/app/assets/stylesheets/_tags.scss b/app/assets/stylesheets/_tags.scss index 830a2d818..e44610a44 100644 --- a/app/assets/stylesheets/_tags.scss +++ b/app/assets/stylesheets/_tags.scss @@ -1,7 +1,3 @@ -#tags-column { - width: 100%; -} - .tags { width: 180px; flex-grow: 0;