feat: add tag id to tag context menu (#1185)
This commit is contained in:
@@ -12,7 +12,7 @@ import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
|||||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||||
import WorkspaceSwitcherOption from './WorkspaceSwitcher/WorkspaceSwitcherOption'
|
import WorkspaceSwitcherOption from './WorkspaceSwitcher/WorkspaceSwitcherOption'
|
||||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||||
import { formatLastSyncDate } from '@/Utils/FormatLastSyncDate'
|
import { formatLastSyncDate } from '@/Utils/DateUtils'
|
||||||
import Spinner from '@/Components/Spinner/Spinner'
|
import Spinner from '@/Components/Spinner/Spinner'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're
|
|||||||
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
|
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
|
||||||
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
||||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||||
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu'
|
|
||||||
import { ToastContainer } from '@standardnotes/toast'
|
import { ToastContainer } from '@standardnotes/toast'
|
||||||
import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal'
|
import FilePreviewModalWrapper from '@/Components/FilePreview/FilePreviewModal'
|
||||||
import ContentListView from '@/Components/ContentListView/ContentListView'
|
import ContentListView from '@/Components/ContentListView/ContentListView'
|
||||||
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu'
|
||||||
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper'
|
||||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||||
|
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -214,7 +214,10 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
noteTagsController={viewControllerManager.noteTagsController}
|
noteTagsController={viewControllerManager.noteTagsController}
|
||||||
historyModalController={viewControllerManager.historyModalController}
|
historyModalController={viewControllerManager.historyModalController}
|
||||||
/>
|
/>
|
||||||
<TagsContextMenuWrapper viewControllerManager={viewControllerManager} />
|
<TagContextMenuWrapper
|
||||||
|
navigationController={viewControllerManager.navigationController}
|
||||||
|
featuresController={viewControllerManager.featuresController}
|
||||||
|
/>
|
||||||
<FileContextMenuWrapper
|
<FileContextMenuWrapper
|
||||||
filesController={viewControllerManager.filesController}
|
filesController={viewControllerManager.filesController}
|
||||||
selectionController={viewControllerManager.selectionController}
|
selectionController={viewControllerManager.selectionController}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, s
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={contextMenuRef}
|
ref={contextMenuRef}
|
||||||
className="max-h-120 fixed flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
className="max-h-120 fixed z-dropdown-menu flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||||
style={{
|
style={{
|
||||||
...contextMenuStyle,
|
...contextMenuStyle,
|
||||||
maxHeight: contextMenuMaxHeight,
|
maxHeight: contextMenuMaxHeight,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
|||||||
import { NotesOptionsProps } from './NotesOptionsProps'
|
import { NotesOptionsProps } from './NotesOptionsProps'
|
||||||
import { NotesController } from '@/Controllers/NotesController'
|
import { NotesController } from '@/Controllers/NotesController'
|
||||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
|
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||||
|
|
||||||
type DeletePermanentlyButtonProps = {
|
type DeletePermanentlyButtonProps = {
|
||||||
closeOnBlur: NotesOptionsProps['closeOnBlur']
|
closeOnBlur: NotesOptionsProps['closeOnBlur']
|
||||||
@@ -78,13 +79,6 @@ const calculateReadTime = (words: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: Date | undefined) => {
|
|
||||||
if (!date) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return `${date.toDateString()} ${date.toLocaleTimeString()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const NoteAttributes: FunctionComponent<{
|
const NoteAttributes: FunctionComponent<{
|
||||||
application: SNApplication
|
application: SNApplication
|
||||||
note: SNNote
|
note: SNNote
|
||||||
@@ -93,9 +87,9 @@ const NoteAttributes: FunctionComponent<{
|
|||||||
|
|
||||||
const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
|
const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
|
||||||
|
|
||||||
const dateLastModified = useMemo(() => formatDate(note.userModifiedDate), [note.userModifiedDate])
|
const dateLastModified = useMemo(() => formatDateForContextMenu(note.userModifiedDate), [note.userModifiedDate])
|
||||||
|
|
||||||
const dateCreated = useMemo(() => formatDate(note.created_at), [note.created_at])
|
const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at])
|
||||||
|
|
||||||
const editor = application.componentManager.editorForNote(note)
|
const editor = application.componentManager.editorForNote(note)
|
||||||
const format = editor?.package_info?.file_type || 'txt'
|
const format = editor?.package_info?.file_type || 'txt'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
|
|||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { FunctionComponent, useState } from 'react'
|
import { FunctionComponent, useState } from 'react'
|
||||||
import { formatLastSyncDate } from '@/Utils/FormatLastSyncDate'
|
import { formatLastSyncDate } from '@/Utils/DateUtils'
|
||||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef, useMemo } from 'react'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import Menu from '@/Components/Menu/Menu'
|
import Menu from '@/Components/Menu/Menu'
|
||||||
import MenuItem from '@/Components/Menu/MenuItem'
|
import MenuItem from '@/Components/Menu/MenuItem'
|
||||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
|
||||||
import { SNTag } from '@standardnotes/snjs'
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
import { isControllerDealloced } from '@/Controllers/Abstract/IsControllerDealloced'
|
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||||
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
|
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||||
|
|
||||||
type WrapperProps = {
|
type ContextMenuProps = {
|
||||||
viewControllerManager: ViewControllerManager
|
navigationController: NavigationController
|
||||||
}
|
isEntitledToFolders: boolean
|
||||||
|
|
||||||
type ContextMenuProps = WrapperProps & {
|
|
||||||
selectedTag: SNTag
|
selectedTag: SNTag
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagsContextMenu = observer(({ viewControllerManager, selectedTag }: ContextMenuProps) => {
|
const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => {
|
||||||
const premiumModal = usePremiumModal()
|
const premiumModal = usePremiumModal()
|
||||||
|
|
||||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = viewControllerManager.navigationController
|
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = navigationController
|
||||||
|
|
||||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||||
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) =>
|
useCloseOnClickOutside(contextMenuRef, () => navigationController.setContextMenuOpen(false))
|
||||||
viewControllerManager.navigationController.setContextMenuOpen(open),
|
|
||||||
)
|
|
||||||
|
|
||||||
const reloadContextMenuLayout = useCallback(() => {
|
const reloadContextMenuLayout = useCallback(() => {
|
||||||
viewControllerManager.navigationController.reloadContextMenuLayout()
|
navigationController.reloadContextMenuLayout()
|
||||||
}, [viewControllerManager])
|
}, [navigationController])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', reloadContextMenuLayout)
|
window.addEventListener('resize', reloadContextMenuLayout)
|
||||||
@@ -40,28 +37,35 @@ const TagsContextMenu = observer(({ viewControllerManager, selectedTag }: Contex
|
|||||||
}, [reloadContextMenuLayout])
|
}, [reloadContextMenuLayout])
|
||||||
|
|
||||||
const onClickAddSubtag = useCallback(() => {
|
const onClickAddSubtag = useCallback(() => {
|
||||||
if (!viewControllerManager.featuresController.hasFolders) {
|
if (!isEntitledToFolders) {
|
||||||
premiumModal.activate('Folders')
|
premiumModal.activate('Folders')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
viewControllerManager.navigationController.setContextMenuOpen(false)
|
navigationController.setContextMenuOpen(false)
|
||||||
viewControllerManager.navigationController.setAddingSubtagTo(selectedTag)
|
navigationController.setAddingSubtagTo(selectedTag)
|
||||||
}, [viewControllerManager, selectedTag, premiumModal])
|
}, [isEntitledToFolders, navigationController, selectedTag, premiumModal])
|
||||||
|
|
||||||
const onClickRename = useCallback(() => {
|
const onClickRename = useCallback(() => {
|
||||||
viewControllerManager.navigationController.setContextMenuOpen(false)
|
navigationController.setContextMenuOpen(false)
|
||||||
viewControllerManager.navigationController.editingTag = selectedTag
|
navigationController.editingTag = selectedTag
|
||||||
}, [viewControllerManager, selectedTag])
|
}, [navigationController, selectedTag])
|
||||||
|
|
||||||
const onClickDelete = useCallback(() => {
|
const onClickDelete = useCallback(() => {
|
||||||
viewControllerManager.navigationController.remove(selectedTag, true).catch(console.error)
|
navigationController.remove(selectedTag, true).catch(console.error)
|
||||||
}, [viewControllerManager, selectedTag])
|
}, [navigationController, selectedTag])
|
||||||
|
|
||||||
|
const tagLastModified = useMemo(
|
||||||
|
() => formatDateForContextMenu(selectedTag.userModifiedDate),
|
||||||
|
[selectedTag.userModifiedDate],
|
||||||
|
)
|
||||||
|
|
||||||
|
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
|
||||||
|
|
||||||
return contextMenuOpen ? (
|
return contextMenuOpen ? (
|
||||||
<div
|
<div
|
||||||
ref={contextMenuRef}
|
ref={contextMenuRef}
|
||||||
className="max-h-120 fixed flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
className="max-h-120 fixed z-dropdown-menu flex min-w-60 flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||||
style={{
|
style={{
|
||||||
...contextMenuPosition,
|
...contextMenuPosition,
|
||||||
maxHeight: contextMenuMaxHeight,
|
maxHeight: contextMenuMaxHeight,
|
||||||
@@ -71,48 +75,39 @@ const TagsContextMenu = observer(({ viewControllerManager, selectedTag }: Contex
|
|||||||
a11yLabel="Tag context menu"
|
a11yLabel="Tag context menu"
|
||||||
isOpen={contextMenuOpen}
|
isOpen={contextMenuOpen}
|
||||||
closeMenu={() => {
|
closeMenu={() => {
|
||||||
viewControllerManager.navigationController.setContextMenuOpen(false)
|
navigationController.setContextMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
||||||
type={MenuItemType.IconButton}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
className={'justify-between py-1.5'}
|
|
||||||
onClick={onClickAddSubtag}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Icon type="add" className="mr-2 text-neutral" />
|
<Icon type="add" className="mr-2 text-neutral" />
|
||||||
Add subtag
|
Add subtag
|
||||||
</div>
|
</div>
|
||||||
{!viewControllerManager.featuresController.hasFolders && <Icon type="premium-feature" />}
|
{!isEntitledToFolders && <Icon type="premium-feature" />}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem type={MenuItemType.IconButton} onBlur={closeOnBlur} className={'py-1.5'} onClick={onClickRename}>
|
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickRename}>
|
||||||
<Icon type="pencil-filled" className="mr-2 text-neutral" />
|
<Icon type="pencil-filled" className="mr-2 text-neutral" />
|
||||||
Rename
|
Rename
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem type={MenuItemType.IconButton} onBlur={closeOnBlur} className={'py-1.5'} onClick={onClickDelete}>
|
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickDelete}>
|
||||||
<Icon type="trash" className="mr-2 text-danger" />
|
<Icon type="trash" className="mr-2 text-danger" />
|
||||||
<span className="text-danger">Delete</span>
|
<span className="text-danger">Delete</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
<div className="px-3 pt-1 pb-1.5 text-xs font-medium text-neutral">
|
||||||
|
<div className="mb-1">
|
||||||
|
<span className="font-semibold">Last modified:</span> {tagLastModified}
|
||||||
|
</div>
|
||||||
|
<div className="mb-1">
|
||||||
|
<span className="font-semibold">Created:</span> {tagCreatedAt}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">Tag ID:</span> {selectedTag.uuid}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
})
|
|
||||||
|
|
||||||
TagsContextMenu.displayName = 'TagsContextMenu'
|
|
||||||
|
|
||||||
const TagsContextMenuWrapper = ({ viewControllerManager }: WrapperProps) => {
|
|
||||||
if (isControllerDealloced(viewControllerManager)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedTag = viewControllerManager.navigationController.selected
|
|
||||||
|
|
||||||
if (!selectedTag || !(selectedTag instanceof SNTag)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <TagsContextMenu viewControllerManager={viewControllerManager} selectedTag={selectedTag} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(TagsContextMenuWrapper)
|
export default observer(TagContextMenu)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
|
import TagContextMenu from './TagContextMenu'
|
||||||
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
|
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
navigationController: NavigationController
|
||||||
|
featuresController: FeaturesController
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagContextMenuWrapper = ({ navigationController, featuresController }: Props) => {
|
||||||
|
const selectedTag = navigationController.selected
|
||||||
|
|
||||||
|
if (!selectedTag || !(selectedTag instanceof SNTag)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagContextMenu
|
||||||
|
navigationController={navigationController}
|
||||||
|
isEntitledToFolders={featuresController.hasFolders}
|
||||||
|
selectedTag={selectedTag}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(TagContextMenuWrapper)
|
||||||
@@ -3,3 +3,11 @@ import { dateToLocalizedString } from '@standardnotes/snjs/'
|
|||||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||||
return dateToLocalizedString(lastUpdatedDate)
|
return dateToLocalizedString(lastUpdatedDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatDateForContextMenu = (date: Date | undefined) => {
|
||||||
|
if (!date) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${date.toDateString()} ${date.toLocaleTimeString()}`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user