Merge branch 'release/10.6.0' into develop
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.16667 14.1667C9.16667 14.3877 9.25446 14.5996 9.41074 14.7559C9.56702 14.9122 9.77899 15 10 15C10.221 15 10.433 14.9122 10.5893 14.7559C10.7455 14.5996 10.8333 14.3877 10.8333 14.1667C10.8333 13.9457 10.7455 13.7337 10.5893 13.5774C10.433 13.4211 10.221 13.3333 10 13.3333C9.77899 13.3333 9.56702 13.4211 9.41074 13.5774C9.25446 13.7337 9.16667 13.9457 9.16667 14.1667ZM9.16667 2.5V5.83333H10.8333V4.23333C13.6583 4.64167 15.8333 7.05833 15.8333 10C15.8333 11.5471 15.2188 13.0308 14.1248 14.1248C13.0308 15.2188 11.5471 15.8333 10 15.8333C8.4529 15.8333 6.96917 15.2188 5.87521 14.1248C4.78125 13.0308 4.16667 11.5471 4.16667 10C4.16667 8.6 4.65833 7.31667 5.48333 6.31667L10 10.8333L11.175 9.65833L5.50833 3.99167V4.00833C3.68333 5.375 2.5 7.54167 2.5 10C2.5 11.9891 3.29018 13.8968 4.6967 15.3033C6.10322 16.7098 8.01088 17.5 10 17.5C11.9891 17.5 13.8968 16.7098 15.3033 15.3033C16.7098 13.8968 17.5 11.9891 17.5 10C17.5 8.01088 16.7098 6.10322 15.3033 4.6967C13.8968 3.29018 11.9891 2.5 10 2.5H9.16667ZM15 10C15 9.77899 14.9122 9.56702 14.7559 9.41074C14.5996 9.25446 14.3877 9.16667 14.1667 9.16667C13.9457 9.16667 13.7337 9.25446 13.5774 9.41074C13.4211 9.56702 13.3333 9.77899 13.3333 10C13.3333 10.221 13.4211 10.433 13.5774 10.5893C13.7337 10.7455 13.9457 10.8333 14.1667 10.8333C14.3877 10.8333 14.5996 10.7455 14.7559 10.5893C14.9122 10.433 15 10.221 15 10ZM5 10C5 10.221 5.0878 10.433 5.24408 10.5893C5.40036 10.7455 5.61232 10.8333 5.83333 10.8333C6.05435 10.8333 6.26631 10.7455 6.42259 10.5893C6.57887 10.433 6.66667 10.221 6.66667 10C6.66667 9.77899 6.57887 9.56702 6.42259 9.41074C6.26631 9.25446 6.05435 9.16667 5.83333 9.16667C5.61232 9.16667 5.40036 9.25446 5.24408 9.41074C5.0878 9.56702 5 9.77899 5 10Z" />
|
||||
</svg>
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 14C8 14.2652 8.10536 14.5196 8.29289 14.7071C8.48043 14.8946 8.73478 15 9 15C9.26522 15 9.51957 14.8946 9.70711 14.7071C9.89464 14.5196 10 14.2652 10 14C10 13.7348 9.89464 13.4804 9.70711 13.2929C9.51957 13.1054 9.26522 13 9 13C8.73478 13 8.48043 13.1054 8.29289 13.2929C8.10536 13.4804 8 13.7348 8 14ZM8 0V4H10V2.08C13.39 2.57 16 5.47 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.32 2.59 5.78 3.58 4.58L9 10L10.41 8.59L3.61 1.79V1.81C1.42 3.45 0 6.05 0 9C0 11.3869 0.948211 13.6761 2.63604 15.364C4.32387 17.0518 6.61305 18 9 18C11.3869 18 13.6761 17.0518 15.364 15.364C17.0518 13.6761 18 11.3869 18 9C18 6.61305 17.0518 4.32387 15.364 2.63604C13.6761 0.948211 11.3869 0 9 0H8ZM15 9C15 8.73478 14.8946 8.48043 14.7071 8.29289C14.5196 8.10536 14.2652 8 14 8C13.7348 8 13.4804 8.10536 13.2929 8.29289C13.1054 8.48043 13 8.73478 13 9C13 9.26522 13.1054 9.51957 13.2929 9.70711C13.4804 9.89464 13.7348 10 14 10C14.2652 10 14.5196 9.89464 14.7071 9.70711C14.8946 9.51957 15 9.26522 15 9ZM3 9C3 9.26522 3.10536 9.51957 3.29289 9.70711C3.48043 9.89464 3.73478 10 4 10C4.26522 10 4.51957 9.89464 4.70711 9.70711C4.89464 9.51957 5 9.26522 5 9C5 8.73478 4.89464 8.48043 4.70711 8.29289C4.51957 8.10536 4.26522 8 4 8C3.73478 8 3.48043 8.10536 3.29289 8.29289C3.10536 8.48043 3 8.73478 3 9Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10.7417 2.5L12.375 2.83333L9.25832 17.5L7.62498 17.1667L10.7417 2.5ZM16.325 10L13.3333 7.00833V4.65L18.6833 10L13.3333 15.3417V12.9833L16.325 10ZM1.31665 10L6.66665 4.65V7.00833L3.67498 10L6.66665 12.9833V15.3417L1.31665 10Z" />
|
||||
</svg>
|
||||
<svg viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.89 0L13.85 0.4L10.11 18L8.15002 17.6L11.89 0ZM18.59 9L15 5.41V2.58L21.42 9L15 15.41V12.58L18.59 9ZM0.580017 9L7.00002 2.58V5.41L3.41002 9L7.00002 12.58V15.41L0.580017 9Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 267 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.99992 14.1666C10.4419 14.1666 10.8659 13.991 11.1784 13.6784C11.491 13.3659 11.6666 12.9419 11.6666 12.4999C11.6666 11.5749 10.9166 10.8333 9.99992 10.8333C9.55789 10.8333 9.13397 11.0088 8.82141 11.3214C8.50885 11.634 8.33325 12.0579 8.33325 12.4999C8.33325 12.9419 8.50885 13.3659 8.82141 13.6784C9.13397 13.991 9.55789 14.1666 9.99992 14.1666ZM14.9999 6.66659C15.4419 6.66659 15.8659 6.84218 16.1784 7.15474C16.491 7.4673 16.6666 7.89123 16.6666 8.33325V16.6666C16.6666 17.1086 16.491 17.5325 16.1784 17.8451C15.8659 18.1577 15.4419 18.3333 14.9999 18.3333H4.99992C4.55789 18.3333 4.13397 18.1577 3.82141 17.8451C3.50885 17.5325 3.33325 17.1086 3.33325 16.6666V8.33325C3.33325 7.40825 4.08325 6.66659 4.99992 6.66659H5.83325V4.99992C5.83325 3.89485 6.27224 2.83504 7.05364 2.05364C7.83504 1.27224 8.89485 0.833252 9.99992 0.833252C10.5471 0.833252 11.0889 0.941026 11.5944 1.15042C12.1 1.35982 12.5593 1.66673 12.9462 2.05364C13.3331 2.44055 13.64 2.89988 13.8494 3.4054C14.0588 3.91093 14.1666 4.45274 14.1666 4.99992V6.66659H14.9999ZM9.99992 2.49992C9.33688 2.49992 8.70099 2.76331 8.23215 3.23215C7.76331 3.70099 7.49992 4.33688 7.49992 4.99992V6.66659H12.4999V4.99992C12.4999 4.33688 12.2365 3.70099 11.7677 3.23215C11.2988 2.76331 10.663 2.49992 9.99992 2.49992Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.4999 3.33325V4.99992H14.9999V14.9999H12.4999V16.6666H16.6666V3.33325H12.4999ZM3.33325 3.33325V16.6666H7.49992V14.9999H4.99992V4.99992H7.49992V3.33325H3.33325Z" />
|
||||
</svg>
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 0V2H14V14H11V16H16V0H11ZM0 0V16H5V14H2V2H5V0H0Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 261 B After Width: | Height: | Size: 144 B |
@@ -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 |
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 10V4H13.75V2.5H6.25V4H7V10L4.5 11.5V13H9.4V17.5H10.6V13H15.5V11.5L13 10Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 171 B |
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 3H16C16.3978 3 16.7794 3.15526 17.0607 3.43163C17.342 3.708 17.5 4.08284 17.5 4.47368V15.5263C17.5 15.9172 17.342 16.292 17.0607 16.5684C16.7794 16.8447 16.3978 17 16 17H4C3.60218 17 3.22064 16.8447 2.93934 16.5684C2.65804 16.292 2.5 15.9172 2.5 15.5263V4.47368C2.5 4.08284 2.65804 3.708 2.93934 3.43163C3.22064 3.15526 3.60218 3 4 3ZM4 5.94737V8.15789H7V5.94737H4ZM8.5 5.94737V8.15789H11.5V5.94737H8.5ZM16 8.15789V5.94737H13V8.15789H16ZM4 9.63158V11.8421H7V9.63158H4ZM4 15.5263H7V13.3158H4V15.5263ZM8.5 9.63158V11.8421H11.5V9.63158H8.5ZM8.5 15.5263H11.5V13.3158H8.5V15.5263ZM16 15.5263V13.3158H13V15.5263H16ZM16 9.63158H13V11.8421H16V9.63158Z" />
|
||||
</svg>
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.8 0.600098H16.2C16.6774 0.600098 17.1352 0.786413 17.4728 1.11806C17.8104 1.4497 18 1.8995 18 2.36852V15.6317C18 16.1007 17.8104 16.5505 17.4728 16.8821C17.1352 17.2138 16.6774 17.4001 16.2 17.4001H1.8C1.32261 17.4001 0.864773 17.2138 0.527208 16.8821C0.189642 16.5505 0 16.1007 0 15.6317V2.36852C0 1.8995 0.189642 1.4497 0.527208 1.11806C0.864773 0.786413 1.32261 0.600098 1.8 0.600098ZM1.8 4.13694V6.78957H5.4V4.13694H1.8ZM7.2 4.13694V6.78957H10.8V4.13694H7.2ZM16.2 6.78957V4.13694H12.6V6.78957H16.2ZM1.8 8.55799V11.2106H5.4V8.55799H1.8ZM1.8 15.6317H5.4V12.979H1.8V15.6317ZM7.2 8.55799V11.2106H10.8V8.55799H7.2ZM7.2 15.6317H10.8V12.979H7.2V15.6317ZM16.2 15.6317V12.979H12.6V15.6317H16.2ZM16.2 8.55799H12.6V11.2106H16.2V8.55799Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 826 B |
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M15.8333 15.8333H4.16667V4.16667H12.5V2.5H4.16667C3.24167 2.5 2.5 3.24167 2.5 4.16667V15.8333C2.5 16.2754 2.67559 16.6993 2.98816 17.0118C3.30072 17.3244 3.72464 17.5 4.16667 17.5H15.8333C16.2754 17.5 16.6993 17.3244 17.0118 17.0118C17.3244 16.6993 17.5 16.2754 17.5 15.8333V9.16667H15.8333V15.8333ZM6.59167 8.4L5.41667 9.58333L9.16667 13.3333L17.5 5L16.325 3.81667L9.16667 10.975L6.59167 8.4Z" />
|
||||
</svg>
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 16H2V2H12V0H2C0.89 0 0 0.89 0 2V16C0 16.5304 0.210714 17.0391 0.585786 17.4142C0.960859 17.7893 1.46957 18 2 18H16C16.5304 18 17.0391 17.7893 17.4142 17.4142C17.7893 17.0391 18 16.5304 18 16V8H16V16ZM4.91 7.08L3.5 8.5L8 13L18 3L16.59 1.58L8 10.17L4.91 7.08Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 492 B After Width: | Height: | Size: 355 B |
@@ -1,3 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 5V6.66667H3V5H17ZM3 15H10V13.3333H3V15ZM3 10.8333H17V9.16667H3V10.8333Z" />
|
||||
</svg>
|
||||
<svg viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.4 0V2H0.599976V0H17.4ZM0.599976 12H8.99998V10H0.599976V12ZM0.599976 7H17.4V5H0.599976V7Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 170 B After Width: | Height: | Size: 190 B |
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.5 7.49984H7.5V12.4998H2.5V7.49984ZM2.5 4.1665H17.5V5.83317H2.5V4.1665ZM17.5 7.49984V9.1665H9.16667V7.49984H17.5ZM17.5 10.8332V12.4998H9.16667V10.8332H17.5ZM2.5 14.1665H14.1667V15.8332H2.5V14.1665Z" />
|
||||
</svg>
|
||||
<path d="M2.5 7.49984H7.5V12.4998H2.5V7.49984ZM2.5 4.1665H17.5V5.83317H2.5V4.1665ZM17.5 7.49984V9.1665H9.16667V7.49984H17.5ZM17.5 10.8332V12.4998H9.16667V10.8332H17.5ZM2.5 14.1665H14.1667V15.8332H2.5V14.1665Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 293 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.49992 2.5V3.33333H3.33325V5H4.16659V15.8333C4.16659 16.2754 4.34218 16.6993 4.65474 17.0118C4.9673 17.3244 5.39122 17.5 5.83325 17.5H14.1666C14.6086 17.5 15.0325 17.3244 15.3451 17.0118C15.6577 16.6993 15.8332 16.2754 15.8332 15.8333V5H16.6666V3.33333H12.4999V2.5H7.49992ZM7.49992 6.66667H9.16658V14.1667H7.49992V6.66667ZM10.8333 6.66667H12.4999V14.1667H10.8333V6.66667Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 472 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)
|
||||
|
||||
@@ -21,9 +21,6 @@ export const AutocompleteTagResult = observer(
|
||||
|
||||
const tagResultRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const title = tagResult.title;
|
||||
const prefixTitle = appState.noteTags.getPrefixTitle(tagResult);
|
||||
|
||||
const onTagOptionClick = async (tag: SNTag) => {
|
||||
await appState.noteTags.addTagToActiveNote(tag);
|
||||
appState.noteTags.clearAutocompleteSearch();
|
||||
@@ -89,10 +86,9 @@ export const AutocompleteTagResult = observer(
|
||||
>
|
||||
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{prefixTitle && <span className="grey-2">{prefixTitle}</span>}
|
||||
{autocompleteSearchQuery === ''
|
||||
? title
|
||||
: title
|
||||
? tagResult.title
|
||||
: tagResult.title
|
||||
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
|
||||
.map((substring, index) => (
|
||||
<span
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useState } from 'preact/hooks';
|
||||
|
||||
export type DropdownItem = {
|
||||
icon?: IconType;
|
||||
iconClassName?: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
@@ -26,7 +25,10 @@ type DropdownProps = {
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type ListboxButtonProps = DropdownItem & {
|
||||
type ListboxButtonProps = {
|
||||
icon?: IconType;
|
||||
value: string | null;
|
||||
label: string;
|
||||
isExpanded: boolean;
|
||||
};
|
||||
|
||||
@@ -34,13 +36,12 @@ const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
|
||||
label,
|
||||
isExpanded,
|
||||
icon,
|
||||
iconClassName = '',
|
||||
}) => (
|
||||
<>
|
||||
<div className="sn-dropdown-button-label">
|
||||
{icon ? (
|
||||
<div className="flex mr-2">
|
||||
<Icon type={icon} className={`sn-icon--small ${iconClassName}`} />
|
||||
<Icon type={icon} className="sn-icon--small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="dropdown-selected-label">{label}</div>
|
||||
@@ -84,13 +85,11 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value);
|
||||
const icon = current ? current?.icon : null;
|
||||
const iconClassName = current ? current?.iconClassName : null;
|
||||
return CustomDropdownButton({
|
||||
value: value ? value : label.toLowerCase(),
|
||||
value,
|
||||
label,
|
||||
isExpanded,
|
||||
...(icon ? { icon } : null),
|
||||
...(iconClassName ? { iconClassName } : null),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -105,10 +104,7 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
>
|
||||
{item.icon ? (
|
||||
<div className="flex mr-3">
|
||||
<Icon
|
||||
type={item.icon}
|
||||
className={`sn-icon--small ${item.iconClassName ?? ''}`}
|
||||
/>
|
||||
<Icon type={item.icon} className="sn-icon--small" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-input">{item.label}</div>
|
||||
|
||||
@@ -3,9 +3,7 @@ import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
||||
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
|
||||
import RichTextIcon from '../../icons/ic-text-rich.svg';
|
||||
import TrashIcon from '../../icons/ic-trash.svg';
|
||||
import TrashFilledIcon from '../../icons/ic-trash-filled.svg';
|
||||
import PinIcon from '../../icons/ic-pin.svg';
|
||||
import PinFilledIcon from '../../icons/ic-pin-filled.svg';
|
||||
import UnpinIcon from '../../icons/ic-pin-off.svg';
|
||||
import ArchiveIcon from '../../icons/ic-archive.svg';
|
||||
import UnarchiveIcon from '../../icons/ic-unarchive.svg';
|
||||
@@ -23,7 +21,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';
|
||||
@@ -55,7 +52,6 @@ import ServerIcon from '../../icons/ic-server.svg';
|
||||
import EyeIcon from '../../icons/ic-eye.svg';
|
||||
import EyeOffIcon from '../../icons/ic-eye-off.svg';
|
||||
import LockIcon from '../../icons/ic-lock.svg';
|
||||
import LockFilledIcon from '../../icons/ic-lock-filled.svg';
|
||||
import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg';
|
||||
import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg';
|
||||
import WindowIcon from '../../icons/ic-window.svg';
|
||||
@@ -70,11 +66,9 @@ 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,
|
||||
'lock-filled': LockFilledIcon,
|
||||
eye: EyeIcon,
|
||||
'eye-off': EyeOffIcon,
|
||||
server: ServerIcon,
|
||||
@@ -95,9 +89,7 @@ const ICONS = {
|
||||
spreadsheets: SpreadsheetsIcon,
|
||||
tasks: TasksIcon,
|
||||
trash: TrashIcon,
|
||||
'trash-filled': TrashFilledIcon,
|
||||
pin: PinIcon,
|
||||
'pin-filled': PinFilledIcon,
|
||||
unpin: UnpinIcon,
|
||||
archive: ArchiveIcon,
|
||||
unarchive: UnarchiveIcon,
|
||||
@@ -138,22 +130,11 @@ export type IconType = keyof typeof ICONS;
|
||||
type Props = {
|
||||
type: IconType;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export const Icon: FunctionalComponent<Props> = ({
|
||||
type,
|
||||
className = '',
|
||||
ariaLabel,
|
||||
}) => {
|
||||
export const Icon: FunctionalComponent<Props> = ({ type, className = '' }) => {
|
||||
const IconComponent = ICONS[type];
|
||||
return (
|
||||
<IconComponent
|
||||
className={`sn-icon ${className}`}
|
||||
role="img"
|
||||
{...(ariaLabel ? { 'aria-label': ariaLabel } : {})}
|
||||
/>
|
||||
);
|
||||
return <IconComponent className={`sn-icon ${className}`} />;
|
||||
};
|
||||
|
||||
export const IconDirective = toDirective<Props>(Icon, {
|
||||
|
||||
@@ -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);
|
||||
@@ -10,9 +10,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
const noteTags = appState.noteTags;
|
||||
|
||||
const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags;
|
||||
const { autocompleteInputFocused, focusedTagUuid, tags } = appState.noteTags;
|
||||
|
||||
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
||||
const [tagClicked, setTagClicked] = useState(false);
|
||||
@@ -20,10 +18,6 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
|
||||
const tagRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const title = tag.title;
|
||||
const prefixTitle = noteTags.getPrefixTitle(tag);
|
||||
const longTitle = noteTags.getLongTitle(tag);
|
||||
|
||||
const deleteTag = () => {
|
||||
appState.noteTags.focusPreviousTag(tag);
|
||||
appState.noteTags.removeTagFromActiveNote(tag);
|
||||
@@ -38,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);
|
||||
}
|
||||
@@ -103,12 +97,10 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
tabIndex={getTabIndex()}
|
||||
title={longTitle}
|
||||
>
|
||||
<Icon type="hashtag" className="sn-icon--small color-info mr-1" />
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis max-w-290px">
|
||||
{prefixTitle && <span className="color-grey-1">{prefixTitle}</span>}
|
||||
{title}
|
||||
{tag.title}
|
||||
</span>
|
||||
{showDeleteButton && (
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { KeyboardKey } from '@/services/ioService';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { DisplayOptions } from '@/ui_models/app_state/notes_view_state';
|
||||
@@ -8,7 +7,6 @@ import { FunctionComponent } from 'preact';
|
||||
import { NotesListItem } from './NotesListItem';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
notes: SNNote[];
|
||||
selectedNotes: Record<string, SNNote>;
|
||||
@@ -20,30 +18,23 @@ const FOCUSABLE_BUT_NOT_TABBABLE = -1;
|
||||
const NOTES_LIST_SCROLL_THRESHOLD = 200;
|
||||
|
||||
export const NotesList: FunctionComponent<Props> = observer(
|
||||
({
|
||||
application,
|
||||
appState,
|
||||
notes,
|
||||
selectedNotes,
|
||||
displayOptions,
|
||||
paginate,
|
||||
}) => {
|
||||
({ appState, notes, selectedNotes, displayOptions, paginate }) => {
|
||||
const { selectPreviousNote, selectNextNote } = appState.notesView;
|
||||
const { hideTags, hideDate, hideNotePreview, sortBy } = displayOptions;
|
||||
|
||||
const tagsForNote = (note: SNNote): string[] => {
|
||||
const tagsStringForNote = (note: SNNote): string => {
|
||||
if (hideTags) {
|
||||
return [];
|
||||
return '';
|
||||
}
|
||||
const selectedTag = appState.selectedTag;
|
||||
if (!selectedTag) {
|
||||
return [];
|
||||
return '';
|
||||
}
|
||||
const tags = appState.getNoteTags(note);
|
||||
if (!selectedTag.isSmartTag && tags.length === 1) {
|
||||
return [];
|
||||
return '';
|
||||
}
|
||||
return tags.map((tag) => tag.title);
|
||||
return tags.map((tag) => `#${tag.title}`).join(' ');
|
||||
};
|
||||
|
||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||
@@ -55,9 +46,11 @@ export const NotesList: FunctionComponent<Props> = observer(
|
||||
appState.notes.setContextMenuOpen(true);
|
||||
};
|
||||
|
||||
const onContextMenu = (note: SNNote, posX: number, posY: number) => {
|
||||
appState.notes.selectNote(note.uuid, true);
|
||||
openNoteContextMenu(posX, posY);
|
||||
const onContextMenu = async (note: SNNote, posX: number, posY: number) => {
|
||||
await appState.notes.selectNote(note.uuid, true);
|
||||
if (selectedNotes[note.uuid]) {
|
||||
openNoteContextMenu(posX, posY);
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = (e: Event) => {
|
||||
@@ -91,10 +84,9 @@ export const NotesList: FunctionComponent<Props> = observer(
|
||||
>
|
||||
{notes.map((note) => (
|
||||
<NotesListItem
|
||||
application={application}
|
||||
key={note.uuid}
|
||||
note={note}
|
||||
tags={tagsForNote(note)}
|
||||
tags={tagsStringForNote(note)}
|
||||
selected={!!selectedNotes[note.uuid]}
|
||||
hideDate={hideDate}
|
||||
hidePreview={hideNotePreview}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { getIconAndTintForEditor } from '@/preferences/panes/general-segments';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import {
|
||||
CollectionSort,
|
||||
sanitizeHtmlString,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
note: SNNote;
|
||||
tags: string[];
|
||||
tags: string;
|
||||
hideDate: boolean;
|
||||
hidePreview: boolean;
|
||||
hideTags: boolean;
|
||||
@@ -28,6 +24,30 @@ type NoteFlag = {
|
||||
|
||||
const flagsForNote = (note: SNNote) => {
|
||||
const flags = [] as NoteFlag[];
|
||||
if (note.pinned) {
|
||||
flags.push({
|
||||
text: 'Pinned',
|
||||
class: 'info',
|
||||
});
|
||||
}
|
||||
if (note.archived) {
|
||||
flags.push({
|
||||
text: 'Archived',
|
||||
class: 'warning',
|
||||
});
|
||||
}
|
||||
if (note.locked) {
|
||||
flags.push({
|
||||
text: 'Editing Disabled',
|
||||
class: 'neutral',
|
||||
});
|
||||
}
|
||||
if (note.trashed) {
|
||||
flags.push({
|
||||
text: 'Deleted',
|
||||
class: 'danger',
|
||||
});
|
||||
}
|
||||
if (note.conflictOf) {
|
||||
flags.push({
|
||||
text: 'Conflicted Copy',
|
||||
@@ -57,7 +77,6 @@ const flagsForNote = (note: SNNote) => {
|
||||
};
|
||||
|
||||
export const NotesListItem: FunctionComponent<Props> = ({
|
||||
application,
|
||||
hideDate,
|
||||
hidePreview,
|
||||
hideTags,
|
||||
@@ -70,9 +89,6 @@ export const NotesListItem: FunctionComponent<Props> = ({
|
||||
}) => {
|
||||
const flags = flagsForNote(note);
|
||||
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt;
|
||||
const editorForNote = application.componentManager.editorForNote(note);
|
||||
const editorName = editorForNote?.name ?? 'Plain editor';
|
||||
const [icon, tint] = getIconAndTintForEditor(editorForNote?.identifier);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -81,107 +97,52 @@ export const NotesListItem: FunctionComponent<Props> = ({
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="icon">
|
||||
<Icon
|
||||
ariaLabel={`Icon for ${editorName}`}
|
||||
type={icon}
|
||||
className={`color-accessory-tint-${tint}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="meta">
|
||||
<div className="name">
|
||||
<div>{note.title}</div>
|
||||
<div className="flag-icons">
|
||||
{note.locked && (
|
||||
<span title="Editing Disabled">
|
||||
<Icon
|
||||
ariaLabel="Editing Disabled"
|
||||
type="pencil-off"
|
||||
className="sn-icon--small color-info"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{note.trashed && (
|
||||
<span title="Trashed">
|
||||
<Icon
|
||||
ariaLabel="Trashed"
|
||||
type="trash-filled"
|
||||
className="sn-icon--small color-danger"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{note.archived && (
|
||||
<span title="Archived">
|
||||
<Icon
|
||||
ariaLabel="Archived"
|
||||
type="archive"
|
||||
className="sn-icon--mid color-accessory-tint-3"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{note.pinned && (
|
||||
<span title="Pinned">
|
||||
<Icon
|
||||
ariaLabel="Pinned"
|
||||
type="pin-filled"
|
||||
className="sn-icon--small color-info"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{flags && flags.length > 0 ? (
|
||||
<div className="note-flags flex flex-wrap">
|
||||
{flags.map((flag) => (
|
||||
<div className={`flag ${flag.class}`}>
|
||||
<div className="label">{flag.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!hidePreview && !note.hidePreview && !note.protected && (
|
||||
<div className="note-preview">
|
||||
{note.preview_html && (
|
||||
<div
|
||||
className="html-preview"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtmlString(note.preview_html),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!note.preview_html && note.preview_plain && (
|
||||
<div className="plain-preview">{note.preview_plain}</div>
|
||||
)}
|
||||
{!note.preview_html && !note.preview_plain && note.text && (
|
||||
<div className="default-preview">{note.text}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!hideDate || note.protected ? (
|
||||
<div className="bottom-info faded">
|
||||
{note.protected && <span>Protected {hideDate ? '' : ' • '}</span>}
|
||||
{!hideDate && showModifiedDate && (
|
||||
<span>Modified {note.updatedAtString || 'Now'}</span>
|
||||
)}
|
||||
{!hideDate && !showModifiedDate && (
|
||||
<span>{note.createdAtString || 'Now'}</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{!hideTags && tags.length ? (
|
||||
<div className="tags-string">
|
||||
{tags.map((tag) => (
|
||||
<span className="tag color-foreground">
|
||||
<Icon
|
||||
type="hashtag"
|
||||
className="sn-icon--small color-grey-1 mr-1"
|
||||
/>
|
||||
<span>{tag}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{flags.length ? (
|
||||
<div className="note-flags flex flex-wrap">
|
||||
{flags.map((flag) => (
|
||||
<div className={`flag ${flag.class}`}>
|
||||
<div className="label">{flag.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="name">{note.title}</div>
|
||||
{!hidePreview && !note.hidePreview && !note.protected ? (
|
||||
<div className="note-preview">
|
||||
{note.preview_html ? (
|
||||
<div
|
||||
className="html-preview"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtmlString(note.preview_html),
|
||||
}}
|
||||
></div>
|
||||
) : null}
|
||||
{!note.preview_html && note.preview_plain ? (
|
||||
<div className="plain-preview">{note.preview_plain}</div>
|
||||
) : null}
|
||||
{!note.preview_html && !note.preview_plain ? (
|
||||
<div className="default-preview">{note.text}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!hideDate || note.protected ? (
|
||||
<div className="bottom-info faded">
|
||||
{note.protected ? (
|
||||
<span>Protected {hideDate ? '' : ' • '}</span>
|
||||
) : null}
|
||||
{!hideDate && showModifiedDate ? (
|
||||
<span>Modified {note.updatedAtString || 'Now'}</span>
|
||||
) : null}
|
||||
{!hideDate && !showModifiedDate ? (
|
||||
<span>{note.createdAtString || 'Now'}</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!hideTags && (
|
||||
<div className="tags-string">
|
||||
<div className="faded">{tags}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -124,9 +124,9 @@ const NotesView: FunctionComponent<Props> = observer(
|
||||
};
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = (
|
||||
_lastWidth,
|
||||
_lastLeft,
|
||||
_isMaxWidth,
|
||||
_w,
|
||||
_l,
|
||||
_mw,
|
||||
isCollapsed
|
||||
) => {
|
||||
appState.noteTags.reloadTagsContainerMaxWidth();
|
||||
@@ -230,7 +230,6 @@ const NotesView: FunctionComponent<Props> = observer(
|
||||
<NotesList
|
||||
notes={renderedNotes}
|
||||
selectedNotes={selectedNotes}
|
||||
application={application}
|
||||
appState={appState}
|
||||
displayOptions={displayOptions}
|
||||
paginate={paginate}
|
||||
|
||||
@@ -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
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -21,33 +21,32 @@ type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
type EditorOption = DropdownItem & {
|
||||
type EditorOption = {
|
||||
icon?: IconType;
|
||||
label: string;
|
||||
value: FeatureIdentifier | 'plain-editor';
|
||||
};
|
||||
|
||||
export const getIconAndTintForEditor = (
|
||||
identifier: FeatureIdentifier | undefined
|
||||
): [IconType, number] => {
|
||||
const getEditorIconType = (identifier: string): IconType | null => {
|
||||
switch (identifier) {
|
||||
case FeatureIdentifier.BoldEditor:
|
||||
case FeatureIdentifier.PlusEditor:
|
||||
return ['rich-text', 1];
|
||||
return 'rich-text';
|
||||
case FeatureIdentifier.MarkdownBasicEditor:
|
||||
case FeatureIdentifier.MarkdownMathEditor:
|
||||
case FeatureIdentifier.MarkdownMinimistEditor:
|
||||
case FeatureIdentifier.MarkdownProEditor:
|
||||
return ['markdown', 2];
|
||||
return 'markdown';
|
||||
case FeatureIdentifier.TokenVaultEditor:
|
||||
return ['authenticator', 6];
|
||||
return 'authenticator';
|
||||
case FeatureIdentifier.SheetsEditor:
|
||||
return ['spreadsheets', 5];
|
||||
return 'spreadsheets';
|
||||
case FeatureIdentifier.TaskEditor:
|
||||
return ['tasks', 3];
|
||||
return 'tasks';
|
||||
case FeatureIdentifier.CodeEditor:
|
||||
return ['code', 4];
|
||||
default:
|
||||
return ['plain-text', 1];
|
||||
return 'code';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const makeEditorDefault = (
|
||||
@@ -92,19 +91,17 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.map((editor): EditorOption => {
|
||||
const identifier = editor.package_info.identifier;
|
||||
const [iconType, tint] = getIconAndTintForEditor(identifier);
|
||||
const iconType = getEditorIconType(identifier);
|
||||
|
||||
return {
|
||||
label: editor.name,
|
||||
value: identifier,
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `color-accessory-tint-${tint}` } : null),
|
||||
};
|
||||
})
|
||||
.concat([
|
||||
{
|
||||
icon: 'plain-text',
|
||||
iconClassName: `color-accessory-tint-1`,
|
||||
label: 'Plain Editor',
|
||||
value: 'plain-editor',
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentType, SNNote, SNTag, UuidString } from '@standardnotes/snjs';
|
||||
import { SNNote, ContentType, SNTag, UuidString } from '@standardnotes/snjs';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
import { AppState } from './app_state';
|
||||
@@ -194,41 +194,4 @@ export class NoteTagsState {
|
||||
this.reloadTags();
|
||||
}
|
||||
}
|
||||
|
||||
getSortedTagsForNote(note: SNNote): SNTag[] {
|
||||
const tags = this.application.getSortedTagsForNote(note);
|
||||
|
||||
const sortFunction = (tagA: SNTag, tagB: SNTag): number => {
|
||||
const a = this.getLongTitle(tagA);
|
||||
const b = this.getLongTitle(tagB);
|
||||
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (b > a) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
return tags.sort(sortFunction);
|
||||
}
|
||||
|
||||
getPrefixTitle(tag: SNTag): string | undefined {
|
||||
const hierarchy = this.application.getTagParentChain(tag);
|
||||
|
||||
if (hierarchy.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const prefixTitle = hierarchy.map((tag) => tag.title).join('/');
|
||||
return `${prefixTitle}/`;
|
||||
}
|
||||
|
||||
getLongTitle(tag: SNTag): string {
|
||||
const hierarchy = this.application.getTagParentChain(tag);
|
||||
const tags = [...hierarchy, tag];
|
||||
const longTitle = tags.map((tag) => tag.title).join('/');
|
||||
return longTitle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +1,8 @@
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import { STRING_DELETE_TAG, STRING_MISSING_SYSTEM_TAG } from '@/strings';
|
||||
import {
|
||||
ComponentAction,
|
||||
ContentType,
|
||||
MessageData,
|
||||
SNApplication,
|
||||
SNSmartTag,
|
||||
SNTag,
|
||||
TagMutator,
|
||||
UuidString
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs';
|
||||
import {
|
||||
action,
|
||||
@@ -16,63 +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;
|
||||
|
||||
const rootTags = (application: SNApplication): SNTag[] => {
|
||||
const hasNoParent = (tag: SNTag) => !application.getTagParent(tag);
|
||||
|
||||
const allTags = application.getDisplayableItems(ContentType.Tag) as SNTag[];
|
||||
const rootTags = allTags.filter(hasNoParent);
|
||||
|
||||
return rootTags;
|
||||
};
|
||||
|
||||
const tagSiblings = (application: SNApplication, tag: SNTag): SNTag[] => {
|
||||
const withoutCurrentTag = (tags: SNTag[]) =>
|
||||
tags.filter((other) => other.uuid !== tag.uuid);
|
||||
|
||||
const isTemplateTag = application.isTemplateItem(tag);
|
||||
const parentTag = !isTemplateTag && application.getTagParent(tag);
|
||||
|
||||
if (parentTag) {
|
||||
const siblingsAndTag = application.getTagChildren(parentTag);
|
||||
return withoutCurrentTag(siblingsAndTag);
|
||||
}
|
||||
|
||||
return withoutCurrentTag(rootTags(application));
|
||||
};
|
||||
|
||||
const isValidFutureSiblings = (
|
||||
application: SNApplication,
|
||||
futureSiblings: SNTag[],
|
||||
tag: SNTag
|
||||
): boolean => {
|
||||
const siblingWithSameName = futureSiblings.find(
|
||||
(otherTag) => otherTag.title === tag.title
|
||||
);
|
||||
|
||||
if (siblingWithSameName) {
|
||||
application.alertService?.alert(
|
||||
`A tag with the name ${tag.title} already exists at this destination. Please rename this tag before moving and try again.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
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(
|
||||
@@ -82,42 +27,22 @@ export class TagsState {
|
||||
) {
|
||||
this.tagsCountsState = new TagsCountsState(this.application);
|
||||
|
||||
this.selected_ = undefined;
|
||||
this.previouslySelected_ = undefined;
|
||||
this.editing_ = undefined;
|
||||
|
||||
this.smartTags = this.application.getSmartTags();
|
||||
|
||||
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
|
||||
@@ -125,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 [];
|
||||
}
|
||||
|
||||
@@ -168,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)
|
||||
@@ -189,27 +87,12 @@ export class TagsState {
|
||||
): Promise<void> {
|
||||
const tag = this.application.findItem(tagUuid) as SNTag;
|
||||
|
||||
const currentParent = this.application.getTagParent(tag);
|
||||
const currentParentUuid = currentParent?.parentId;
|
||||
|
||||
if (currentParentUuid === parentUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent =
|
||||
parentUuid && (this.application.findItem(parentUuid) as SNTag);
|
||||
|
||||
if (!parent) {
|
||||
const futureSiblings = rootTags(this.application);
|
||||
if (!isValidFutureSiblings(this.application, futureSiblings, tag)) {
|
||||
return;
|
||||
}
|
||||
await this.application.unsetTagParent(tag);
|
||||
} else {
|
||||
const futureSiblings = this.application.getTagChildren(parent);
|
||||
if (!isValidFutureSiblings(this.application, futureSiblings, tag)) {
|
||||
return;
|
||||
}
|
||||
await this.application.setTagParent(parent, tag);
|
||||
}
|
||||
|
||||
@@ -217,7 +100,7 @@ export class TagsState {
|
||||
}
|
||||
|
||||
get rootTags(): SNTag[] {
|
||||
if (!this.features.hasFolders) {
|
||||
if (!this.hasFolders) {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
@@ -228,196 +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 siblings = tagSiblings(this.application, tag);
|
||||
const hasDuplicatedTitle = siblings.some(
|
||||
(other) => other.title.toLowerCase() === newTitle.toLowerCase()
|
||||
);
|
||||
|
||||
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
@@ -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
@@ -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;
|
||||
|
||||
@@ -123,95 +123,42 @@ notes-view {
|
||||
}
|
||||
|
||||
.note {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--sn-stylekit-border-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sn-stylekit-grey-5);
|
||||
> .name {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.9rem;
|
||||
padding-right: 0.75rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
padding: 0.9rem;
|
||||
padding-left: 0;
|
||||
border-bottom: 1px solid var(--sn-stylekit-border-color);
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.flag-icons {
|
||||
&,
|
||||
& > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& > * + * {
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-info {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
> .bottom-info {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tags-string {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.345rem;
|
||||
font-size: 0.725rem;
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.375rem 0.25rem 0.325rem;
|
||||
background-color: var(--sn-stylekit-grey-4-opacity-variant);
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.note-preview {
|
||||
font-size: var(--sn-stylekit-font-size-h3);
|
||||
margin-top: 2px;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
& > * {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.default-preview,
|
||||
.plain-preview {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1; /* number of lines to show */
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
$line-height: 18px;
|
||||
line-height: $line-height; /* fallback */
|
||||
max-height: calc(#{$line-height} * 1); /* fallback */
|
||||
}
|
||||
|
||||
.html-preview {
|
||||
@@ -228,7 +175,8 @@ notes-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 8px;
|
||||
margin-top: -4px;
|
||||
|
||||
.flag {
|
||||
padding: 4px;
|
||||
@@ -290,8 +238,13 @@ notes-view {
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--sn-stylekit-grey-5);
|
||||
border-left: 2px solid var(--sn-stylekit-info-color);
|
||||
background-color: var(--sn-stylekit-info-color);
|
||||
color: var(--sn-stylekit-info-contrast-color);
|
||||
|
||||
.note-flags .flag {
|
||||
background-color: var(--sn-stylekit-info-contrast-color);
|
||||
color: var(--sn-stylekit-info-color);
|
||||
}
|
||||
|
||||
progress {
|
||||
background-color: var(--sn-stylekit-secondary-foreground-color);
|
||||
@@ -302,7 +255,7 @@ notes-view {
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: var(--sn-stylekit-info-color);
|
||||
background-color: var(--sn-stylekit-secondary-background-color);
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
|
||||
@@ -40,11 +40,6 @@
|
||||
@extend .h-3\.5;
|
||||
@extend .w-3\.5;
|
||||
}
|
||||
|
||||
&.sn-icon--mid {
|
||||
@extend .w-4;
|
||||
@extend .h-4;
|
||||
}
|
||||
}
|
||||
|
||||
.sn-dropdown {
|
||||
@@ -782,7 +777,6 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--sn-stylekit-contrast-background-color) !important;
|
||||
@extend .color-info;
|
||||
@extend .border-info;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
#tags-column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tags {
|
||||
width: 180px;
|
||||
flex-grow: 0;
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"pug-loader": "^2.4.0",
|
||||
"sass-loader": "^12.2.0",
|
||||
"serve-static": "^1.14.1",
|
||||
"sn-stylekit": "5.2.21",
|
||||
"sn-stylekit": "5.2.20",
|
||||
"svg-jest": "^1.0.1",
|
||||
"ts-jest": "^27.0.7",
|
||||
"ts-loader": "^9.2.6",
|
||||
|
||||
@@ -9264,10 +9264,10 @@ slice-ansi@^5.0.0:
|
||||
ansi-styles "^6.0.0"
|
||||
is-fullwidth-code-point "^4.0.0"
|
||||
|
||||
sn-stylekit@5.2.21:
|
||||
version "5.2.21"
|
||||
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.21.tgz#5aec6c329949bda64a1e3c563ee594b141295d27"
|
||||
integrity sha512-rjlgo42A/kx+M4iY7HYRpnQyp4dLb2HQpEMHz+CYumOzTf/lsRy0Up5HI1haNK4/JMmpq36Eb/7BMDmvLpdXnQ==
|
||||
sn-stylekit@5.2.20:
|
||||
version "5.2.20"
|
||||
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.20.tgz#c18f40ff3aaf4c59af89152439a8efbdde35f2dd"
|
||||
integrity sha512-JymHBiZOzQPfCqHYgnVPSA2PwJqiKR268qqQoEMqI85MMAWSG3WYzuKEbd0LgfIQAKLElCxJjeZkrhejyRg+2A==
|
||||
dependencies:
|
||||
"@reach/listbox" "^0.15.0"
|
||||
"@reach/menu-button" "^0.15.1"
|
||||
|
||||