Revert "feat: native smart tags (#782)"
This reverts commit c3772e06b4.
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.55556 3C3.69222 3 3 3.69222 3 4.55556V15.4444C3 16.3078 3.69222 17 4.55556 17H15.4444C16.3078 17 17 16.3078 17 15.4444V4.55556C17 3.69222 16.3078 3 15.4444 3H4.55556ZM4.55556 4.55556H15.4444V15.4444H4.55556V4.55556ZM6.11111 6.11111V7.66667H13.8889V6.11111H6.11111ZM6.11111 9.22222V10.7778H13.8889V9.22222H6.11111ZM6.11111 12.3333V13.8889H11.5556V12.3333H6.11111Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 458 B |
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Props> = observer(
|
||||
({ application }) => {
|
||||
const appState = useMemo(() => application.getAppState(), [application]);
|
||||
const componentViewer = appState.foldersComponentViewer;
|
||||
const enableNativeSmartTagsFeature =
|
||||
appState.features.enableNativeSmartTagsFeature;
|
||||
const [panelRef, setPanelRef] = useState<HTMLDivElement | null>(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 (
|
||||
<PremiumModalProvider state={appState.features}>
|
||||
<div
|
||||
id="tags-column"
|
||||
ref={setPanelRef}
|
||||
className="sn-component section tags"
|
||||
data-aria-label="Tags"
|
||||
>
|
||||
{componentViewer ? (
|
||||
<div className="component-view-container">
|
||||
<div className="component-view">
|
||||
<ComponentView
|
||||
componentViewer={componentViewer}
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div id="tags-content" className="content">
|
||||
<div className="tags-title-section section-title-bar">
|
||||
<div className="section-title-bar-header">
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Views</span>
|
||||
</div>
|
||||
{!enableNativeSmartTagsFeature && (
|
||||
<div
|
||||
className="sk-button sk-secondary-contrast wide"
|
||||
onClick={onCreateNewTag}
|
||||
title="Create a new tag"
|
||||
>
|
||||
<div className="sk-label">
|
||||
<i className="icon ion-plus add-button" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="scrollable">
|
||||
<div className="infinite-scroll">
|
||||
<SmartTagsSection appState={appState} />
|
||||
<TagsSection appState={appState} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{panelRef && (
|
||||
<PanelResizer
|
||||
application={application}
|
||||
collapsable={true}
|
||||
defaultWidth={150}
|
||||
panel={panelRef}
|
||||
prefKey={PrefKey.TagsPanelWidth}
|
||||
side={PanelSide.Right}
|
||||
resizeFinishCallback={panelResizeFinishCallback}
|
||||
widthEventCallback={panelWidthEventCallback}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PremiumModalProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const NavigationDirective = toDirective<Props>(Navigation);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -124,9 +124,9 @@ const NotesView: FunctionComponent<Props> = observer(
|
||||
};
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = (
|
||||
_lastWidth,
|
||||
_lastLeft,
|
||||
_isMaxWidth,
|
||||
_w,
|
||||
_l,
|
||||
_mw,
|
||||
isCollapsed
|
||||
) => {
|
||||
appState.noteTags.reloadTagsContainerMaxWidth();
|
||||
|
||||
@@ -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 | string>(null);
|
||||
|
||||
export const PremiumModalProvider: FunctionalComponent<Props> = 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 && (
|
||||
<PremiumFeaturesModal
|
||||
showModal={!!featureName}
|
||||
featureName={featureName}
|
||||
onClose={close}
|
||||
/>
|
||||
)}
|
||||
<PremiumModalProvider_ value={{ activate }}>
|
||||
{children}
|
||||
</PremiumModalProvider_>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
const showModal = !!featureName;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<PremiumFeaturesModal
|
||||
showModal={!!featureName}
|
||||
featureName={featureName}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
)}
|
||||
<PremiumModalProvider_ value={{ activate }}>
|
||||
{children}
|
||||
</PremiumModalProvider_>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Props> = observer(
|
||||
({ tagsState, featuresState }) => {
|
||||
const premiumModal = usePremiumModal();
|
||||
const isNativeFoldersEnabled = featuresState.enableNativeFoldersFeature;
|
||||
const hasFolders = featuresState.hasFolders;
|
||||
const hasFolders = tagsState.hasFolders;
|
||||
|
||||
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
|
||||
() => ({
|
||||
@@ -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<Props> = observer(
|
||||
({ appState }) => {
|
||||
const allTags = appState.tags.smartTags;
|
||||
|
||||
return (
|
||||
<>
|
||||
{allTags.map((tag) => {
|
||||
return (
|
||||
<SmartTagsListItem
|
||||
key={tag.uuid}
|
||||
tag={tag}
|
||||
tagsState={appState.tags}
|
||||
features={appState.features}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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<Props> = observer(
|
||||
({ tag, tagsState, features }) => {
|
||||
const [title, setTitle] = useState(tag.title || '');
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<>
|
||||
<div
|
||||
className={`tag ${isSelected ? 'selected' : ''} ${
|
||||
isFaded ? 'faded' : ''
|
||||
}`}
|
||||
onClick={selectCurrentTag}
|
||||
style={{ paddingLeft: `${level + 0.5}rem` }}
|
||||
>
|
||||
{!tag.errorDecrypting ? (
|
||||
<div className="tag-info">
|
||||
{isSmartTagsEnabled && (
|
||||
<div className={`tag-icon mr-1`}>
|
||||
<Icon
|
||||
type={iconType}
|
||||
className={`${isSelected ? 'color-info' : 'color-neutral'}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className={`title ${isEditing ? 'editing' : ''}`}
|
||||
id={`react-tag-${tag.uuid}`}
|
||||
onBlur={onBlur}
|
||||
onInput={onInput}
|
||||
value={title}
|
||||
onKeyUp={onKeyUp}
|
||||
spellCheck={false}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<div className="count">
|
||||
{tag.isAllTag && tagsState.allNotesCount}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!tag.isSystemSmartTag && (
|
||||
<div className="meta">
|
||||
{tag.conflictOf && (
|
||||
<div className="danger small-text font-bold">
|
||||
Conflicted Copy {tag.conflictOf}
|
||||
</div>
|
||||
)}
|
||||
{tag.errorDecrypting && !tag.waitingForKey && (
|
||||
<div className="danger small-text font-bold">Missing Keys</div>
|
||||
)}
|
||||
{tag.errorDecrypting && tag.waitingForKey && (
|
||||
<div className="info small-text font-bold">
|
||||
Waiting For Keys
|
||||
</div>
|
||||
)}
|
||||
{isSelected && (
|
||||
<div className="menu">
|
||||
{!isEditing && (
|
||||
<a className="item" onClick={onClickRename}>
|
||||
Rename
|
||||
</a>
|
||||
)}
|
||||
{isEditing && (
|
||||
<a className="item" onClick={onClickSave}>
|
||||
Save
|
||||
</a>
|
||||
)}
|
||||
<a className="item" onClick={onClickDelete}>
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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<Props> = observer(
|
||||
({ appState }) => {
|
||||
return (
|
||||
<section>
|
||||
<SmartTagsList appState={appState} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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<Props> = observer(({ appState }) => {
|
||||
const tagsState = appState.tags;
|
||||
const allTags = tagsState.allLocalRootTags;
|
||||
|
||||
const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend;
|
||||
|
||||
return (
|
||||
<DndProvider backend={backend}>
|
||||
{allTags.length === 0 ? (
|
||||
<div className="no-tags-placeholder">
|
||||
No tags. Create one using the add button above.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{allTags.map((tag) => {
|
||||
return (
|
||||
<TagsListItem
|
||||
level={0}
|
||||
key={tag.uuid}
|
||||
tag={tag}
|
||||
tagsState={tagsState}
|
||||
features={appState.features}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<RootTagDropZone
|
||||
tagsState={appState.tags}
|
||||
featuresState={appState.features}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DndProvider>
|
||||
);
|
||||
});
|
||||
@@ -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<Props> = observer(
|
||||
({ appState }) => {
|
||||
const TagAddButton: FunctionComponent<{
|
||||
appState: AppState;
|
||||
features: FeaturesState;
|
||||
}> = observer(({ appState, features }) => {
|
||||
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
|
||||
|
||||
if (!isNativeFoldersEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon="add"
|
||||
title="Create a new tag"
|
||||
focusable={true}
|
||||
onClick={() => 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 (
|
||||
<section>
|
||||
<div className="tags-title-section section-title-bar">
|
||||
<div className="section-title-bar-header">
|
||||
<TagsSectionTitle features={appState.features} />
|
||||
<TagsSectionAddButton
|
||||
tags={appState.tags}
|
||||
features={appState.features}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Tags</span>
|
||||
</div>
|
||||
<TagsList appState={appState} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasFolders) {
|
||||
return (
|
||||
<>
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Folders</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Tags</span>
|
||||
<Tooltip label={TAG_FOLDERS_FEATURE_TOOLTIP}>
|
||||
<label
|
||||
className="ml-1 sk-bold color-grey-2 cursor-pointer"
|
||||
onClick={showPremiumAlert}
|
||||
>
|
||||
Folders
|
||||
</label>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const TagsSection: FunctionComponent<Props> = observer(
|
||||
({ application, appState }) => {
|
||||
return (
|
||||
<PremiumModalProvider>
|
||||
<section>
|
||||
<div className="tags-title-section section-title-bar">
|
||||
<div className="section-title-bar-header">
|
||||
<TagTitle features={appState.features} />
|
||||
<TagAddButton appState={appState} features={appState.features} />
|
||||
</div>
|
||||
</div>
|
||||
<TagsList application={application} appState={appState} />
|
||||
</section>
|
||||
</PremiumModalProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const TagsSectionDirective = toDirective<Props>(TagsSection);
|
||||
|
||||
@@ -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<Props> = observer(
|
||||
({ tags, features }) => {
|
||||
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
|
||||
|
||||
if (!isNativeFoldersEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
focusable={true}
|
||||
icon="add"
|
||||
title="Create a new tag"
|
||||
onClick={() => tags.createNewTemplate()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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<Props> = 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 (
|
||||
<>
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Tags</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasFolders) {
|
||||
return (
|
||||
<>
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Folders</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sk-h3 title">
|
||||
<span className="sk-bold">Tags</span>
|
||||
<Tooltip label={TAG_FOLDERS_FEATURE_TOOLTIP}>
|
||||
<label
|
||||
className="ml-1 sk-bold color-grey-2 cursor-pointer"
|
||||
onClick={showPremiumAlert}
|
||||
>
|
||||
Folders
|
||||
</label>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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 };
|
||||
153
app/assets/javascripts/components/TagsList.tsx
Normal file
153
app/assets/javascripts/components/TagsList.tsx
Normal file
@@ -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<Props> = 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<TagMutator>(
|
||||
insertedTag.uuid,
|
||||
(m) => {
|
||||
m.title = newTitle;
|
||||
}
|
||||
);
|
||||
|
||||
selectTag(changedTag as SNTag);
|
||||
await application.saveItem(insertedTag.uuid);
|
||||
} else {
|
||||
await application.changeAndSaveItem<TagMutator>(
|
||||
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 (
|
||||
<PremiumModalProvider>
|
||||
<DndProvider backend={backend}>
|
||||
{allTags.length === 0 ? (
|
||||
<div className="no-tags-placeholder">
|
||||
No tags. Create one using the add button above.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{allTags.map((tag) => {
|
||||
return (
|
||||
<TagsListItem
|
||||
level={0}
|
||||
key={tag.uuid}
|
||||
tag={tag}
|
||||
tagsState={appState.tags}
|
||||
selectTag={selectTag}
|
||||
saveTag={saveTag}
|
||||
removeTag={removeTag}
|
||||
appState={appState}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<RootTagDropZone
|
||||
tagsState={appState.tags}
|
||||
featuresState={appState.features}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DndProvider>
|
||||
</PremiumModalProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const TagsListDirective = toDirective<Props>(TagsList);
|
||||
@@ -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<Props> = observer(
|
||||
({ tag, features, tagsState, level }) => {
|
||||
({ tag, selectTag, saveTag, removeTag, appState, tagsState, level }) => {
|
||||
const [title, setTitle] = useState(tag.title || '');
|
||||
const inputRef = useRef<HTMLInputElement>(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<Props> = 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<HTMLInputElement>) => {
|
||||
@@ -95,16 +116,18 @@ export const TagsListItem: FunctionComponent<Props> = 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<Props> = observer(
|
||||
key={tag.uuid}
|
||||
tag={tag}
|
||||
tagsState={tagsState}
|
||||
features={features}
|
||||
selectTag={selectTag}
|
||||
saveTag={saveTag}
|
||||
removeTag={removeTag}
|
||||
appState={appState}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -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.';
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 && (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 =
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<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 set hasFolders(hasFolders: boolean) {
|
||||
this.features.hasFolders = hasFolders;
|
||||
}
|
||||
|
||||
public get hasAtLeastOneFolder(): boolean {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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])?)$/;
|
||||
|
||||
@@ -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';
|
||||
|
||||
43
app/assets/javascripts/views/tags/tags-view.pug
Normal file
43
app/assets/javascripts/views/tags/tags-view.pug
Normal file
@@ -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'"
|
||||
)
|
||||
298
app/assets/javascripts/views/tags/tags_view.ts
Normal file
298
app/assets/javascripts/views/tags/tags_view.ts
Normal file
@@ -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<Record<string, number>>;
|
||||
|
||||
type TagState = {
|
||||
smartTags: SNSmartTag[];
|
||||
noteCounts: NoteCounts;
|
||||
selectedTag?: SNTag;
|
||||
componentViewer?: ComponentViewer;
|
||||
};
|
||||
|
||||
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
/** 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<Record<UuidString, string>> = {};
|
||||
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<SNTag | SNSmartTag>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
#tags-column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tags {
|
||||
width: 180px;
|
||||
flex-grow: 0;
|
||||
|
||||
Reference in New Issue
Block a user