feat: ability to favorite tags + customize icon (#1858)
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"@types/jest": "^28.1.5",
|
"@types/jest": "^28.1.5",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||||
|
"eslint": "*",
|
||||||
"eslint-plugin-prettier": "*",
|
"eslint-plugin-prettier": "*",
|
||||||
"jest": "^28.1.2",
|
"jest": "^28.1.2",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^28.0.5",
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
||||||
import { PredicateInterface, PredicateJsonForm } from '../../Runtime/Predicate/Interface'
|
import { PredicateInterface } from '../../Runtime/Predicate/Interface'
|
||||||
import { predicateFromJson } from '../../Runtime/Predicate/Generators'
|
import { predicateFromJson } from '../../Runtime/Predicate/Generators'
|
||||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
|
||||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||||
|
import { SystemViewId } from './SystemViewId'
|
||||||
|
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
|
||||||
|
import { SmartViewDefaultIconName, systemViewIcon } from './SmartViewIcons'
|
||||||
|
import { SmartViewContent } from './SmartViewContent'
|
||||||
|
|
||||||
export const SMART_TAG_DSL_PREFIX = '!['
|
export const SMART_TAG_DSL_PREFIX = '!['
|
||||||
|
|
||||||
export enum SystemViewId {
|
|
||||||
AllNotes = 'all-notes',
|
|
||||||
Files = 'files',
|
|
||||||
ArchivedNotes = 'archived-notes',
|
|
||||||
TrashedNotes = 'trashed-notes',
|
|
||||||
UntaggedNotes = 'untagged-notes',
|
|
||||||
StarredNotes = 'starred-notes',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SmartViewContent extends ItemContent {
|
|
||||||
title: string
|
|
||||||
predicate: PredicateJsonForm
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSystemView(view: SmartView): boolean {
|
export function isSystemView(view: SmartView): boolean {
|
||||||
return Object.values(SystemViewId).includes(view.uuid as SystemViewId)
|
return Object.values(SystemViewId).includes(view.uuid as SystemViewId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A tag that defines a predicate that consumers can use
|
|
||||||
* to retrieve a dynamic list of items.
|
|
||||||
*/
|
|
||||||
export class SmartView extends DecryptedItem<SmartViewContent> {
|
export class SmartView extends DecryptedItem<SmartViewContent> {
|
||||||
public readonly predicate!: PredicateInterface<DecryptedItem>
|
public readonly predicate!: PredicateInterface<DecryptedItem>
|
||||||
public readonly title: string
|
public readonly title: string
|
||||||
|
public readonly iconString: IconType | EmojiString
|
||||||
|
|
||||||
constructor(payload: DecryptedPayloadInterface<SmartViewContent>) {
|
constructor(payload: DecryptedPayloadInterface<SmartViewContent>) {
|
||||||
super(payload)
|
super(payload)
|
||||||
this.title = String(this.content.title || '')
|
this.title = String(this.content.title || '')
|
||||||
|
|
||||||
|
if (isSystemView(this)) {
|
||||||
|
this.iconString = systemViewIcon(this.uuid as SystemViewId)
|
||||||
|
} else {
|
||||||
|
this.iconString = this.payload.content.iconString || SmartViewDefaultIconName
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.predicate = this.content.predicate && predicateFromJson(this.content.predicate)
|
this.predicate = this.content.predicate && predicateFromJson(this.content.predicate)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
|
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
|
||||||
import { SNNote } from '../Note/Note'
|
import { SNNote } from '../Note/Note'
|
||||||
import { SmartViewContent, SmartView, SystemViewId } from './SmartView'
|
import { SmartView } from './SmartView'
|
||||||
|
import { SmartViewContent } from './SmartViewContent'
|
||||||
|
import { SystemViewId } from './SystemViewId'
|
||||||
import { ItemWithTags } from '../../Runtime/Display/Search/ItemWithTags'
|
import { ItemWithTags } from '../../Runtime/Display/Search/ItemWithTags'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { TagContent } from './../Tag/TagContent'
|
||||||
|
import { PredicateJsonForm } from '../../Runtime/Predicate/Interface'
|
||||||
|
|
||||||
|
export interface SmartViewContent extends TagContent {
|
||||||
|
predicate: PredicateJsonForm
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SystemViewId } from './SystemViewId'
|
||||||
|
import { IconType } from '../../Utilities/Icon/IconType'
|
||||||
|
|
||||||
|
export const SmartViewIcons: Record<SystemViewId, IconType> = {
|
||||||
|
[SystemViewId.AllNotes]: 'notes',
|
||||||
|
[SystemViewId.Files]: 'folder',
|
||||||
|
[SystemViewId.ArchivedNotes]: 'archive',
|
||||||
|
[SystemViewId.TrashedNotes]: 'trash',
|
||||||
|
[SystemViewId.UntaggedNotes]: 'hashtag-off',
|
||||||
|
[SystemViewId.StarredNotes]: 'star-filled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function systemViewIcon(id: SystemViewId): IconType {
|
||||||
|
return SmartViewIcons[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SmartViewDefaultIconName: IconType = 'restore'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export enum SystemViewId {
|
||||||
|
AllNotes = 'all-notes',
|
||||||
|
Files = 'files',
|
||||||
|
ArchivedNotes = 'archived-notes',
|
||||||
|
TrashedNotes = 'trashed-notes',
|
||||||
|
UntaggedNotes = 'untagged-notes',
|
||||||
|
StarredNotes = 'starred-notes',
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './SmartView'
|
export * from './SmartView'
|
||||||
export * from './SmartViewBuilder'
|
export * from './SmartViewBuilder'
|
||||||
|
export * from './SystemViewId'
|
||||||
|
export * from './SmartViewContent'
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource'
|
import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource'
|
||||||
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
|
import { DecryptedPayload } from './../../Abstract/Payload/Implementations/DecryptedPayload'
|
||||||
import { SNTag, TagContent } from './Tag'
|
import { SNTag } from './Tag'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||||
import { ContentReference } from '../../Abstract/Reference/ContentReference'
|
import { ContentReference } from '../../Abstract/Reference/ContentReference'
|
||||||
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
|
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||||
|
import { TagContent } from './TagContent'
|
||||||
|
|
||||||
const randUuid = () => String(Math.random())
|
const randUuid = () => String(Math.random())
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
|
import { VectorIconNameOrEmoji, IconType } from './../../Utilities/Icon/IconType'
|
||||||
import { ContentType, Uuid } from '@standardnotes/common'
|
import { ContentType, Uuid } from '@standardnotes/common'
|
||||||
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
||||||
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
||||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
|
||||||
import { ContentReference } from '../../Abstract/Reference/ContentReference'
|
import { ContentReference } from '../../Abstract/Reference/ContentReference'
|
||||||
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
|
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
|
||||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||||
|
import { TagContent, TagContentSpecialized } from './TagContent'
|
||||||
|
|
||||||
export const TagFolderDelimitter = '.'
|
export const TagFolderDelimitter = '.'
|
||||||
|
|
||||||
interface TagInterface {
|
export const DefaultTagIconName: IconType = 'hashtag'
|
||||||
title: string
|
|
||||||
expanded: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TagContent = TagInterface & ItemContent
|
|
||||||
|
|
||||||
export const isTag = (x: ItemInterface): x is SNTag => x.content_type === ContentType.Tag
|
export const isTag = (x: ItemInterface): x is SNTag => x.content_type === ContentType.Tag
|
||||||
|
|
||||||
export class SNTag extends DecryptedItem<TagContent> implements TagInterface {
|
export class SNTag extends DecryptedItem<TagContent> implements TagContentSpecialized {
|
||||||
public readonly title: string
|
public readonly title: string
|
||||||
|
public readonly iconString: VectorIconNameOrEmoji
|
||||||
/** Whether to render child tags in view hierarchy. Opposite of collapsed. */
|
|
||||||
public readonly expanded: boolean
|
public readonly expanded: boolean
|
||||||
|
|
||||||
constructor(payload: DecryptedPayloadInterface<TagContent>) {
|
constructor(payload: DecryptedPayloadInterface<TagContent>) {
|
||||||
super(payload)
|
super(payload)
|
||||||
this.title = this.payload.content.title || ''
|
this.title = this.payload.content.title || ''
|
||||||
this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true
|
this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true
|
||||||
|
this.iconString = this.payload.content.iconString || DefaultTagIconName
|
||||||
}
|
}
|
||||||
|
|
||||||
get noteReferences(): ContentReference[] {
|
get noteReferences(): ContentReference[] {
|
||||||
|
|||||||
11
packages/models/src/Domain/Syncable/Tag/TagContent.ts
Normal file
11
packages/models/src/Domain/Syncable/Tag/TagContent.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { IconType } from './../../Utilities/Icon/IconType'
|
||||||
|
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||||
|
import { EmojiString } from '../../Utilities/Icon/IconType'
|
||||||
|
|
||||||
|
export interface TagContentSpecialized {
|
||||||
|
title: string
|
||||||
|
expanded: boolean
|
||||||
|
iconString: IconType | EmojiString
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TagContent = TagContentSpecialized & ItemContent
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { TagContent, SNTag } from './Tag'
|
import { SNTag } from './Tag'
|
||||||
|
import { TagContent } from './TagContent'
|
||||||
import { FileItem } from '../File'
|
import { FileItem } from '../File'
|
||||||
import { SNNote } from '../Note'
|
import { SNNote } from '../Note'
|
||||||
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
|
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
|
||||||
@@ -17,6 +18,10 @@ export class TagMutator extends DecryptedItemMutator<TagContent> {
|
|||||||
this.mutableContent.expanded = expanded
|
this.mutableContent.expanded = expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set iconString(iconString: string) {
|
||||||
|
this.mutableContent.iconString = iconString
|
||||||
|
}
|
||||||
|
|
||||||
public makeChildOf(tag: SNTag): void {
|
public makeChildOf(tag: SNTag): void {
|
||||||
const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
|
const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './Tag'
|
export * from './Tag'
|
||||||
export * from './TagMutator'
|
export * from './TagMutator'
|
||||||
|
export * from './TagContent'
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
export type VectorIconNameOrEmoji = EmojiString | IconType
|
||||||
|
|
||||||
|
export type EmojiString = string
|
||||||
|
|
||||||
export type IconType =
|
export type IconType =
|
||||||
| 'accessibility'
|
| 'accessibility'
|
||||||
| 'account-card-details-outline'
|
| 'account-card-details-outline'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TagContent } from './../../Syncable/Tag/Tag'
|
import { TagContent } from './../../Syncable/Tag/TagContent'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { FillItemContent, ItemContent } from '../../Abstract/Content/ItemContent'
|
import { FillItemContent, ItemContent } from '../../Abstract/Content/ItemContent'
|
||||||
import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * from './Abstract/Component/ActionObserver'
|
export * from './Abstract/Component/ActionObserver'
|
||||||
export * from './Abstract/Component/ComponentViewerEvent'
|
|
||||||
export * from './Abstract/Component/ComponentMessage'
|
|
||||||
export * from './Abstract/Component/ComponentEventObserver'
|
export * from './Abstract/Component/ComponentEventObserver'
|
||||||
|
export * from './Abstract/Component/ComponentMessage'
|
||||||
|
export * from './Abstract/Component/ComponentViewerEvent'
|
||||||
export * from './Abstract/Component/IncomingComponentItemPayload'
|
export * from './Abstract/Component/IncomingComponentItemPayload'
|
||||||
export * from './Abstract/Component/KeyboardModifier'
|
export * from './Abstract/Component/KeyboardModifier'
|
||||||
export * from './Abstract/Component/MessageData'
|
export * from './Abstract/Component/MessageData'
|
||||||
@@ -43,9 +43,9 @@ export * from './Runtime/Collection/Payload/ImmutablePayloadCollection'
|
|||||||
export * from './Runtime/Collection/Payload/PayloadCollection'
|
export * from './Runtime/Collection/Payload/PayloadCollection'
|
||||||
export * from './Runtime/Deltas'
|
export * from './Runtime/Deltas'
|
||||||
export * from './Runtime/DirtyCounter/DirtyCounter'
|
export * from './Runtime/DirtyCounter/DirtyCounter'
|
||||||
|
export * from './Runtime/Display'
|
||||||
export * from './Runtime/Display/ItemDisplayController'
|
export * from './Runtime/Display/ItemDisplayController'
|
||||||
export * from './Runtime/Display/Types'
|
export * from './Runtime/Display/Types'
|
||||||
export * from './Runtime/Display'
|
|
||||||
export * from './Runtime/History'
|
export * from './Runtime/History'
|
||||||
export * from './Runtime/Index/ItemDelta'
|
export * from './Runtime/Index/ItemDelta'
|
||||||
export * from './Runtime/Index/SNIndex'
|
export * from './Runtime/Index/SNIndex'
|
||||||
@@ -70,6 +70,7 @@ export * from './Syncable/SmartView'
|
|||||||
export * from './Syncable/Tag'
|
export * from './Syncable/Tag'
|
||||||
export * from './Syncable/Theme'
|
export * from './Syncable/Theme'
|
||||||
export * from './Syncable/UserPrefs'
|
export * from './Syncable/UserPrefs'
|
||||||
|
export * from './Utilities/Icon/IconType'
|
||||||
export * from './Utilities/Item/FindItem'
|
export * from './Utilities/Item/FindItem'
|
||||||
export * from './Utilities/Item/ItemContentsDiffer'
|
export * from './Utilities/Item/ItemContentsDiffer'
|
||||||
export * from './Utilities/Item/ItemContentsEqual'
|
export * from './Utilities/Item/ItemContentsEqual'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NoteType } from '@standardnotes/features'
|
import { NoteType } from '@standardnotes/features'
|
||||||
import { IconType } from '@Lib/Types/IconType'
|
import { IconType } from '@standardnotes/models'
|
||||||
|
|
||||||
export class IconsController {
|
export class IconsController {
|
||||||
getIconForFileType(type: string): IconType {
|
getIconForFileType(type: string): IconType {
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from './ApplicationEventPayload'
|
export * from './ApplicationEventPayload'
|
||||||
export * from './IconType'
|
|
||||||
export * from './UuidString'
|
export * from './UuidString'
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
|
|
||||||
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||||
|
|
||||||
|
const { selected: selectedTag } = navigationController
|
||||||
|
|
||||||
|
const icon = selectedTag?.iconString
|
||||||
|
|
||||||
const isFilesSmartView = useMemo(
|
const isFilesSmartView = useMemo(
|
||||||
() => navigationController.selected?.uuid === SystemViewId.Files,
|
() => navigationController.selected?.uuid === SystemViewId.Files,
|
||||||
[navigationController.selected?.uuid],
|
[navigationController.selected?.uuid],
|
||||||
@@ -259,6 +263,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
<ContentListHeader
|
<ContentListHeader
|
||||||
application={application}
|
application={application}
|
||||||
panelTitle={panelTitle}
|
panelTitle={panelTitle}
|
||||||
|
icon={icon}
|
||||||
addButtonLabel={addButtonLabel}
|
addButtonLabel={addButtonLabel}
|
||||||
addNewItem={addNewItem}
|
addNewItem={addNewItem}
|
||||||
isFilesSmartView={isFilesSmartView}
|
isFilesSmartView={isFilesSmartView}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
|
|||||||
import Popover from '@/Components/Popover/Popover'
|
import Popover from '@/Components/Popover/Popover'
|
||||||
import DisplayOptionsMenu from './DisplayOptionsMenu'
|
import DisplayOptionsMenu from './DisplayOptionsMenu'
|
||||||
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
|
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
|
||||||
|
import { IconType } from '@standardnotes/snjs'
|
||||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -14,6 +15,7 @@ type Props = {
|
|||||||
isNativeMobileWeb: WebApplication['isNativeMobileWeb']
|
isNativeMobileWeb: WebApplication['isNativeMobileWeb']
|
||||||
}
|
}
|
||||||
panelTitle: string
|
panelTitle: string
|
||||||
|
icon?: IconType | string
|
||||||
addButtonLabel: string
|
addButtonLabel: string
|
||||||
addNewItem: () => void
|
addNewItem: () => void
|
||||||
isFilesSmartView: boolean
|
isFilesSmartView: boolean
|
||||||
@@ -23,6 +25,7 @@ type Props = {
|
|||||||
const ContentListHeader = ({
|
const ContentListHeader = ({
|
||||||
application,
|
application,
|
||||||
panelTitle,
|
panelTitle,
|
||||||
|
icon,
|
||||||
addButtonLabel,
|
addButtonLabel,
|
||||||
addNewItem,
|
addNewItem,
|
||||||
isFilesSmartView,
|
isFilesSmartView,
|
||||||
@@ -41,8 +44,19 @@ const ContentListHeader = ({
|
|||||||
<div className="section-title-bar-header items-start gap-1 overflow-hidden">
|
<div className="section-title-bar-header items-start gap-1 overflow-hidden">
|
||||||
<NavigationMenuButton />
|
<NavigationMenuButton />
|
||||||
<div className="flex min-w-0 flex-grow flex-col break-words">
|
<div className="flex min-w-0 flex-grow flex-col break-words">
|
||||||
<div className="text-lg font-semibold text-text">{panelTitle}</div>
|
<div className={`flex min-w-0 flex-grow flex-row ${!optionsSubtitle ? 'items-center' : ''}`}>
|
||||||
{optionsSubtitle && <div className="text-xs text-passive-0">{optionsSubtitle}</div>}
|
{icon && (
|
||||||
|
<Icon
|
||||||
|
type={icon as IconType}
|
||||||
|
size={'large'}
|
||||||
|
className={`ml-0.5 mr-1.5 text-neutral ${optionsSubtitle ? 'mt-1' : ''}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex min-w-0 flex-grow flex-col break-words">
|
||||||
|
<div className="text-lg font-semibold text-text">{panelTitle}</div>
|
||||||
|
{optionsSubtitle && <div className="text-xs text-passive-0">{optionsSubtitle}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="relative" ref={displayOptionsContainerRef}>
|
<div className="relative" ref={displayOptionsContainerRef}>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const ListItemTags: FunctionComponent<Props> = ({ hideTags, tags }) => {
|
|||||||
className="inline-flex items-center rounded-sm bg-passive-4-opacity-variant py-1 px-1.5 text-foreground"
|
className="inline-flex items-center rounded-sm bg-passive-4-opacity-variant py-1 px-1.5 text-foreground"
|
||||||
key={tag.uuid}
|
key={tag.uuid}
|
||||||
>
|
>
|
||||||
<Icon type="hashtag" className="mr-1 text-passive-1" size="small" />
|
<Icon type={tag.iconString} className="mr-1 text-passive-1" size="small" />
|
||||||
<span>{tag.title}</span>
|
<span>{tag.title}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export type DisplayableListItemProps = AbstractListItemProps & {
|
|||||||
tags: {
|
tags: {
|
||||||
uuid: SNTag['uuid']
|
uuid: SNTag['uuid']
|
||||||
title: SNTag['title']
|
title: SNTag['title']
|
||||||
|
iconString: SNTag['iconString']
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ICONS } from '@/Components/Icon/Icon'
|
import { IconNameToSvgMapping } from '@/Components/Icon/IconNameToSvgMapping'
|
||||||
|
|
||||||
export const getFileIconComponent = (iconType: string, className: string) => {
|
export const getFileIconComponent = (iconType: string, className: string) => {
|
||||||
const IconComponent = ICONS[iconType as keyof typeof ICONS]
|
const IconComponent = IconNameToSvgMapping[iconType as keyof typeof IconNameToSvgMapping]
|
||||||
|
|
||||||
return <IconComponent className={className} />
|
return <IconComponent className={className} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +1,77 @@
|
|||||||
import { FunctionComponent, useMemo } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import { IconType } from '@standardnotes/snjs'
|
import { VectorIconNameOrEmoji } from '@standardnotes/snjs'
|
||||||
import * as icons from '@standardnotes/icons'
|
import { IconNameToSvgMapping } from './IconNameToSvgMapping'
|
||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
export const ICONS = {
|
|
||||||
'account-circle': icons.AccountCircleIcon,
|
|
||||||
'arrow-left': icons.ArrowLeftIcon,
|
|
||||||
'arrow-right': icons.ArrowRightIcon,
|
|
||||||
'arrows-sort-down': icons.ArrowsSortDownIcon,
|
|
||||||
'arrows-sort-up': icons.ArrowsSortUpIcon,
|
|
||||||
'attachment-file': icons.AttachmentFileIcon,
|
|
||||||
'check-bold': icons.CheckBoldIcon,
|
|
||||||
'check-circle': icons.CheckCircleIcon,
|
|
||||||
'chevron-down': icons.ChevronDownIcon,
|
|
||||||
'chevron-left': icons.ChevronLeftIcon,
|
|
||||||
'chevron-right': icons.ChevronRightIcon,
|
|
||||||
'clear-circle-filled': icons.ClearCircleFilledIcon,
|
|
||||||
'cloud-off': icons.CloudOffIcon,
|
|
||||||
'diamond-filled': icons.DiamondFilledIcon,
|
|
||||||
'eye-off': icons.EyeOffIcon,
|
|
||||||
'file-doc': icons.FileDocIcon,
|
|
||||||
'file-image': icons.FileImageIcon,
|
|
||||||
'file-mov': icons.FileMovIcon,
|
|
||||||
'file-music': icons.FileMusicIcon,
|
|
||||||
'file-other': icons.FileOtherIcon,
|
|
||||||
'file-pdf': icons.FilePdfIcon,
|
|
||||||
'file-ppt': icons.FilePptIcon,
|
|
||||||
'file-xls': icons.FileXlsIcon,
|
|
||||||
'file-zip': icons.FileZipIcon,
|
|
||||||
'hashtag-off': icons.HashtagOffIcon,
|
|
||||||
'link-off': icons.LinkOffIcon,
|
|
||||||
'list-bulleted': icons.ListBulleted,
|
|
||||||
'lock-filled': icons.LockFilledIcon,
|
|
||||||
'menu-arrow-down-alt': icons.MenuArrowDownAlt,
|
|
||||||
'menu-arrow-down': icons.MenuArrowDownIcon,
|
|
||||||
'menu-arrow-right': icons.MenuArrowRightIcon,
|
|
||||||
'menu-close': icons.MenuCloseIcon,
|
|
||||||
'menu-variant': icons.MenuVariantIcon,
|
|
||||||
'notes-filled': icons.NotesFilledIcon,
|
|
||||||
'pencil-filled': icons.PencilFilledIcon,
|
|
||||||
'pencil-off': icons.PencilOffIcon,
|
|
||||||
'pin-filled': icons.PinFilledIcon,
|
|
||||||
'plain-text': icons.PlainTextIcon,
|
|
||||||
'premium-feature': icons.PremiumFeatureIcon,
|
|
||||||
'rich-text': icons.RichTextIcon,
|
|
||||||
'sort-descending': icons.SortDescendingIcon,
|
|
||||||
'star-circle-filled': icons.StarCircleFilled,
|
|
||||||
'star-filled': icons.StarFilledIcon,
|
|
||||||
'star-variant-filled': icons.StarVariantFilledIcon,
|
|
||||||
'trash-filled': icons.TrashFilledIcon,
|
|
||||||
'trash-sweep': icons.TrashSweepIcon,
|
|
||||||
'user-add': icons.UserAddIcon,
|
|
||||||
'user-switch': icons.UserSwitch,
|
|
||||||
accessibility: icons.AccessibilityIcon,
|
|
||||||
add: icons.AddIcon,
|
|
||||||
archive: icons.ArchiveIcon,
|
|
||||||
asterisk: icons.AsteriskIcon,
|
|
||||||
authenticator: icons.AuthenticatorIcon,
|
|
||||||
check: icons.CheckIcon,
|
|
||||||
close: icons.CloseIcon,
|
|
||||||
code: icons.CodeIcon,
|
|
||||||
copy: icons.CopyIcon,
|
|
||||||
dashboard: icons.DashboardIcon,
|
|
||||||
diamond: icons.DiamondIcon,
|
|
||||||
download: icons.DownloadIcon,
|
|
||||||
editor: icons.EditorIcon,
|
|
||||||
email: icons.EmailIcon,
|
|
||||||
eye: icons.EyeIcon,
|
|
||||||
file: icons.FileIcon,
|
|
||||||
folder: icons.FolderIcon,
|
|
||||||
hashtag: icons.HashtagIcon,
|
|
||||||
help: icons.HelpIcon,
|
|
||||||
history: icons.HistoryIcon,
|
|
||||||
info: icons.InfoIcon,
|
|
||||||
keyboard: icons.KeyboardIcon,
|
|
||||||
link: icons.LinkIcon,
|
|
||||||
listed: icons.ListedIcon,
|
|
||||||
lock: icons.LockIcon,
|
|
||||||
markdown: icons.MarkdownIcon,
|
|
||||||
more: icons.MoreIcon,
|
|
||||||
notes: icons.NotesIcon,
|
|
||||||
password: icons.PasswordIcon,
|
|
||||||
pencil: icons.PencilIcon,
|
|
||||||
pin: icons.PinIcon,
|
|
||||||
restore: icons.RestoreIcon,
|
|
||||||
search: icons.SearchIcon,
|
|
||||||
security: icons.SecurityIcon,
|
|
||||||
server: icons.ServerIcon,
|
|
||||||
settings: icons.SettingsIcon,
|
|
||||||
share: icons.ShareIcon,
|
|
||||||
signIn: icons.SignInIcon,
|
|
||||||
signOut: icons.SignOutIcon,
|
|
||||||
spreadsheets: icons.SpreadsheetsIcon,
|
|
||||||
star: icons.StarIcon,
|
|
||||||
subtract: icons.SubtractIcon,
|
|
||||||
sync: icons.SyncIcon,
|
|
||||||
tasks: icons.TasksIcon,
|
|
||||||
themes: icons.ThemesIcon,
|
|
||||||
trash: icons.TrashIcon,
|
|
||||||
tune: icons.TuneIcon,
|
|
||||||
unarchive: icons.UnarchiveIcon,
|
|
||||||
unpin: icons.UnpinIcon,
|
|
||||||
user: icons.UserIcon,
|
|
||||||
view: icons.ViewIcon,
|
|
||||||
warning: icons.WarningIcon,
|
|
||||||
window: icons.WindowIcon,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: IconType
|
type: VectorIconNameOrEmoji
|
||||||
className?: string
|
className?: string
|
||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
size?: 'small' | 'medium' | 'normal' | 'custom'
|
size?: 'small' | 'medium' | 'normal' | 'large' | 'custom'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContainerDimensions = {
|
||||||
|
small: 'w-3.5 h-3.5',
|
||||||
|
medium: 'w-4 h-4',
|
||||||
|
normal: 'w-5 h-5',
|
||||||
|
large: 'w-6 h-6',
|
||||||
|
custom: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiContainerDimensions = {
|
||||||
|
small: 'w-4 h-4',
|
||||||
|
medium: 'w-5 h-5',
|
||||||
|
normal: 'w-5 h-5',
|
||||||
|
large: 'w-7 h-6',
|
||||||
|
custom: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiOffset = {
|
||||||
|
small: '',
|
||||||
|
medium: '',
|
||||||
|
normal: '-mt-0.5',
|
||||||
|
large: '',
|
||||||
|
custom: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiSize = {
|
||||||
|
small: 'text-xs',
|
||||||
|
medium: 'text-sm',
|
||||||
|
normal: 'text-base',
|
||||||
|
large: 'text-lg',
|
||||||
|
custom: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconComponent = (type: VectorIconNameOrEmoji) => {
|
||||||
|
return IconNameToSvgMapping[type as keyof typeof IconNameToSvgMapping]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isIconEmoji = (type: VectorIconNameOrEmoji): boolean => {
|
||||||
|
return getIconComponent(type) == undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon: FunctionComponent<Props> = ({ type, className = '', ariaLabel, size = 'normal' }) => {
|
const Icon: FunctionComponent<Props> = ({ type, className = '', ariaLabel, size = 'normal' }) => {
|
||||||
const IconComponent = ICONS[type as keyof typeof ICONS]
|
const IconComponent = getIconComponent(type)
|
||||||
|
|
||||||
const dimensions = useMemo(() => {
|
|
||||||
switch (size) {
|
|
||||||
case 'small':
|
|
||||||
return 'w-3.5 h-3.5'
|
|
||||||
case 'medium':
|
|
||||||
return 'w-4 h-4'
|
|
||||||
case 'custom':
|
|
||||||
return ''
|
|
||||||
default:
|
|
||||||
return 'w-5 h-5'
|
|
||||||
}
|
|
||||||
}, [size])
|
|
||||||
|
|
||||||
if (!IconComponent) {
|
if (!IconComponent) {
|
||||||
return null
|
return (
|
||||||
|
<label
|
||||||
|
className={classNames(
|
||||||
|
'fill-current',
|
||||||
|
'text-center',
|
||||||
|
EmojiSize[size],
|
||||||
|
EmojiContainerDimensions[size],
|
||||||
|
EmojiOffset[size],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconComponent
|
<IconComponent
|
||||||
className={`${dimensions} fill-current ${className}`}
|
className={`${ContainerDimensions[size]} fill-current ${className}`}
|
||||||
role="img"
|
role="img"
|
||||||
{...(ariaLabel ? { 'aria-label': ariaLabel } : { 'aria-hidden': true })}
|
{...(ariaLabel ? { 'aria-label': ariaLabel } : { 'aria-hidden': true })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import * as icons from '@standardnotes/icons'
|
||||||
|
|
||||||
|
export const IconNameToSvgMapping = {
|
||||||
|
'account-circle': icons.AccountCircleIcon,
|
||||||
|
'arrow-left': icons.ArrowLeftIcon,
|
||||||
|
'arrow-right': icons.ArrowRightIcon,
|
||||||
|
'arrows-sort-down': icons.ArrowsSortDownIcon,
|
||||||
|
'arrows-sort-up': icons.ArrowsSortUpIcon,
|
||||||
|
'attachment-file': icons.AttachmentFileIcon,
|
||||||
|
'check-bold': icons.CheckBoldIcon,
|
||||||
|
'check-circle': icons.CheckCircleIcon,
|
||||||
|
'chevron-down': icons.ChevronDownIcon,
|
||||||
|
'chevron-left': icons.ChevronLeftIcon,
|
||||||
|
'chevron-right': icons.ChevronRightIcon,
|
||||||
|
'clear-circle-filled': icons.ClearCircleFilledIcon,
|
||||||
|
'cloud-off': icons.CloudOffIcon,
|
||||||
|
'diamond-filled': icons.DiamondFilledIcon,
|
||||||
|
'eye-off': icons.EyeOffIcon,
|
||||||
|
'file-doc': icons.FileDocIcon,
|
||||||
|
'file-image': icons.FileImageIcon,
|
||||||
|
'file-mov': icons.FileMovIcon,
|
||||||
|
'file-music': icons.FileMusicIcon,
|
||||||
|
'file-other': icons.FileOtherIcon,
|
||||||
|
'file-pdf': icons.FilePdfIcon,
|
||||||
|
'file-ppt': icons.FilePptIcon,
|
||||||
|
'file-xls': icons.FileXlsIcon,
|
||||||
|
'file-zip': icons.FileZipIcon,
|
||||||
|
'hashtag-off': icons.HashtagOffIcon,
|
||||||
|
'link-off': icons.LinkOffIcon,
|
||||||
|
'list-bulleted': icons.ListBulleted,
|
||||||
|
'lock-filled': icons.LockFilledIcon,
|
||||||
|
'menu-arrow-down-alt': icons.MenuArrowDownAlt,
|
||||||
|
'menu-arrow-down': icons.MenuArrowDownIcon,
|
||||||
|
'menu-arrow-right': icons.MenuArrowRightIcon,
|
||||||
|
'menu-close': icons.MenuCloseIcon,
|
||||||
|
'menu-variant': icons.MenuVariantIcon,
|
||||||
|
'notes-filled': icons.NotesFilledIcon,
|
||||||
|
'pencil-filled': icons.PencilFilledIcon,
|
||||||
|
'pencil-off': icons.PencilOffIcon,
|
||||||
|
'pin-filled': icons.PinFilledIcon,
|
||||||
|
'plain-text': icons.PlainTextIcon,
|
||||||
|
'premium-feature': icons.PremiumFeatureIcon,
|
||||||
|
'rich-text': icons.RichTextIcon,
|
||||||
|
'sort-descending': icons.SortDescendingIcon,
|
||||||
|
'star-circle-filled': icons.StarCircleFilled,
|
||||||
|
'star-filled': icons.StarFilledIcon,
|
||||||
|
'star-variant-filled': icons.StarVariantFilledIcon,
|
||||||
|
'trash-filled': icons.TrashFilledIcon,
|
||||||
|
'trash-sweep': icons.TrashSweepIcon,
|
||||||
|
'user-add': icons.UserAddIcon,
|
||||||
|
'user-switch': icons.UserSwitch,
|
||||||
|
'fullscreen-exit': icons.FullscreenExitIcon,
|
||||||
|
accessibility: icons.AccessibilityIcon,
|
||||||
|
add: icons.AddIcon,
|
||||||
|
archive: icons.ArchiveIcon,
|
||||||
|
asterisk: icons.AsteriskIcon,
|
||||||
|
authenticator: icons.AuthenticatorIcon,
|
||||||
|
check: icons.CheckIcon,
|
||||||
|
close: icons.CloseIcon,
|
||||||
|
code: icons.CodeIcon,
|
||||||
|
copy: icons.CopyIcon,
|
||||||
|
dashboard: icons.DashboardIcon,
|
||||||
|
diamond: icons.DiamondIcon,
|
||||||
|
download: icons.DownloadIcon,
|
||||||
|
editor: icons.EditorIcon,
|
||||||
|
email: icons.EmailIcon,
|
||||||
|
eye: icons.EyeIcon,
|
||||||
|
file: icons.FileIcon,
|
||||||
|
folder: icons.FolderIcon,
|
||||||
|
hashtag: icons.HashtagIcon,
|
||||||
|
help: icons.HelpIcon,
|
||||||
|
history: icons.HistoryIcon,
|
||||||
|
info: icons.InfoIcon,
|
||||||
|
keyboard: icons.KeyboardIcon,
|
||||||
|
link: icons.LinkIcon,
|
||||||
|
listed: icons.ListedIcon,
|
||||||
|
lock: icons.LockIcon,
|
||||||
|
markdown: icons.MarkdownIcon,
|
||||||
|
more: icons.MoreIcon,
|
||||||
|
notes: icons.NotesIcon,
|
||||||
|
password: icons.PasswordIcon,
|
||||||
|
pencil: icons.PencilIcon,
|
||||||
|
pin: icons.PinIcon,
|
||||||
|
restore: icons.RestoreIcon,
|
||||||
|
search: icons.SearchIcon,
|
||||||
|
security: icons.SecurityIcon,
|
||||||
|
server: icons.ServerIcon,
|
||||||
|
settings: icons.SettingsIcon,
|
||||||
|
share: icons.ShareIcon,
|
||||||
|
signIn: icons.SignInIcon,
|
||||||
|
signOut: icons.SignOutIcon,
|
||||||
|
spreadsheets: icons.SpreadsheetsIcon,
|
||||||
|
star: icons.StarIcon,
|
||||||
|
subtract: icons.SubtractIcon,
|
||||||
|
sync: icons.SyncIcon,
|
||||||
|
tasks: icons.TasksIcon,
|
||||||
|
themes: icons.ThemesIcon,
|
||||||
|
trash: icons.TrashIcon,
|
||||||
|
tune: icons.TuneIcon,
|
||||||
|
unarchive: icons.UnarchiveIcon,
|
||||||
|
unpin: icons.UnpinIcon,
|
||||||
|
user: icons.UserIcon,
|
||||||
|
view: icons.ViewIcon,
|
||||||
|
warning: icons.WarningIcon,
|
||||||
|
window: icons.WindowIcon,
|
||||||
|
}
|
||||||
131
packages/web/src/javascripts/Components/Icon/IconPicker.tsx
Normal file
131
packages/web/src/javascripts/Components/Icon/IconPicker.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { EmojiString, Platform, VectorIconNameOrEmoji } from '@standardnotes/snjs'
|
||||||
|
import { FunctionComponent, useMemo, useRef, useState } from 'react'
|
||||||
|
import Dropdown from '../Dropdown/Dropdown'
|
||||||
|
import { DropdownItem } from '../Dropdown/DropdownItem'
|
||||||
|
import { isIconEmoji } from './Icon'
|
||||||
|
import { IconNameToSvgMapping } from './IconNameToSvgMapping'
|
||||||
|
import { IconPickerType } from './IconPickerType'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedValue: VectorIconNameOrEmoji
|
||||||
|
onIconChange: (value?: string) => void
|
||||||
|
platform: Platform
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconPicker = ({ selectedValue, onIconChange, platform, className }: Props) => {
|
||||||
|
const iconOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
[...Object.keys(IconNameToSvgMapping)].map(
|
||||||
|
(value) =>
|
||||||
|
({
|
||||||
|
label: value,
|
||||||
|
value: value,
|
||||||
|
icon: value,
|
||||||
|
} as DropdownItem),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSelectedEmoji = isIconEmoji(selectedValue)
|
||||||
|
const isMacOS = platform === Platform.MacWeb || platform === Platform.MacDesktop
|
||||||
|
const isWindows = platform === Platform.WindowsWeb || platform === Platform.WindowsDesktop
|
||||||
|
|
||||||
|
const emojiInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [emojiInputFocused, setEmojiInputFocused] = useState(true)
|
||||||
|
const [currentType, setCurrentType] = useState<IconPickerType>(isSelectedEmoji ? 'emoji' : 'icon')
|
||||||
|
const [emojiInputValue, setEmojiInputValue] = useState(isSelectedEmoji ? selectedValue : '')
|
||||||
|
|
||||||
|
const selectTab = (type: IconPickerType | 'reset') => {
|
||||||
|
if (type === 'reset') {
|
||||||
|
onIconChange(undefined)
|
||||||
|
setEmojiInputValue('')
|
||||||
|
} else {
|
||||||
|
setCurrentType(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabButton: FunctionComponent<{
|
||||||
|
label: string
|
||||||
|
type: IconPickerType | 'reset'
|
||||||
|
}> = ({ type, label }) => {
|
||||||
|
const isSelected = currentType === type
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`relative mr-2 cursor-pointer border-0 bg-default pb-1.5 text-sm focus:shadow-none ${
|
||||||
|
isSelected ? 'font-medium text-info' : 'text-text'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
selectTab(type)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIconChange = (value: string) => {
|
||||||
|
onIconChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmojiChange = (value: EmojiString) => {
|
||||||
|
setEmojiInputValue(value)
|
||||||
|
|
||||||
|
const emojiLength = [...value].length
|
||||||
|
if (emojiLength === 1) {
|
||||||
|
onIconChange(value)
|
||||||
|
emojiInputRef.current?.blur()
|
||||||
|
setEmojiInputFocused(false)
|
||||||
|
} else {
|
||||||
|
setEmojiInputFocused(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex h-full flex-grow flex-col overflow-auto ${className}`}>
|
||||||
|
<div className="flex">
|
||||||
|
<TabButton label="Icon" type={'icon'} />
|
||||||
|
<TabButton label="Emoji" type={'emoji'} />
|
||||||
|
<TabButton label="Reset" type={'reset'} />
|
||||||
|
</div>
|
||||||
|
<div className={'mt-2 h-full min-h-0 overflow-auto'}>
|
||||||
|
{currentType === 'icon' && (
|
||||||
|
<Dropdown
|
||||||
|
id="change-tag-icon-dropdown"
|
||||||
|
label="Change the icon for a tag"
|
||||||
|
items={iconOptions}
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={handleIconChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentType === 'emoji' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref={emojiInputRef}
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus={emojiInputFocused}
|
||||||
|
className="w-full flex-grow rounded border border-solid border-passive-3 bg-default px-2 py-1 text-base font-bold text-text focus:shadow-none focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
value={emojiInputValue}
|
||||||
|
onChange={({ target: input }) => handleEmojiChange((input as HTMLInputElement)?.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-passive-0">
|
||||||
|
Use your keyboard to enter or paste in an emoji character.
|
||||||
|
</div>
|
||||||
|
{isMacOS && (
|
||||||
|
<div className="mt-2 text-xs text-passive-0">On macOS: ⌘ + ⌃ + Space bar to bring up emoji picker.</div>
|
||||||
|
)}
|
||||||
|
{isWindows && (
|
||||||
|
<div className="mt-2 text-xs text-passive-0">On Windows: Windows key + . to bring up emoji picker.</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconPicker
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export type IconPickerType = 'icon' | 'emoji'
|
||||||
@@ -6,6 +6,7 @@ import { IconType } from '@standardnotes/snjs'
|
|||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||||
import { MenuItemType } from './MenuItemType'
|
import { MenuItemType } from './MenuItemType'
|
||||||
import RadioIndicator from '../Radio/RadioIndicator'
|
import RadioIndicator from '../Radio/RadioIndicator'
|
||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
|
||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -40,7 +41,10 @@ const MenuItem = forwardRef(
|
|||||||
<li className="list-none" role="none">
|
<li className="list-none" role="none">
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
className={classNames(
|
||||||
|
'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5',
|
||||||
|
'text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
|
||||||
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(!checked)
|
onChange(!checked)
|
||||||
}}
|
}}
|
||||||
@@ -59,7 +63,12 @@ const MenuItem = forwardRef(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
className={`flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item ${className}`}
|
className={classNames(
|
||||||
|
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left',
|
||||||
|
'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground',
|
||||||
|
'focus:bg-info-backdrop focus:shadow-none md:text-menu-item',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
import { IconType } from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
export enum AppPaneId {
|
export enum AppPaneId {
|
||||||
Navigation = 'NavigationColumn',
|
Navigation = 'NavigationColumn',
|
||||||
Items = 'ItemsColumn',
|
Items = 'ItemsColumn',
|
||||||
Editor = 'EditorColumn',
|
Editor = 'EditorColumn',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppPaneTitles = {
|
|
||||||
[AppPaneId.Navigation]: 'Navigation',
|
|
||||||
[AppPaneId.Items]: 'Notes & Files',
|
|
||||||
[AppPaneId.Editor]: 'Editor',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppPaneIcons: Record<AppPaneId, IconType> = {
|
|
||||||
[AppPaneId.Navigation]: 'hashtag',
|
|
||||||
[AppPaneId.Items]: 'notes',
|
|
||||||
[AppPaneId.Editor]: 'plain-text',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Icon from '@/Components/Icon/Icon'
|
|||||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
import '@reach/tooltip/styles.css'
|
import '@reach/tooltip/styles.css'
|
||||||
import { SmartView, SystemViewId, IconType, isSystemView } from '@standardnotes/snjs'
|
import { SmartView, SystemViewId, isSystemView } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
FormEventHandler,
|
FormEventHandler,
|
||||||
@@ -27,19 +27,6 @@ type Props = {
|
|||||||
const PADDING_BASE_PX = 14
|
const PADDING_BASE_PX = 14
|
||||||
const PADDING_PER_LEVEL_PX = 21
|
const PADDING_PER_LEVEL_PX = 21
|
||||||
|
|
||||||
const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
|
|
||||||
const mapping: Record<SystemViewId, IconType> = {
|
|
||||||
[SystemViewId.AllNotes]: isSelected ? 'notes-filled' : 'notes',
|
|
||||||
[SystemViewId.Files]: 'folder',
|
|
||||||
[SystemViewId.ArchivedNotes]: 'archive',
|
|
||||||
[SystemViewId.TrashedNotes]: 'trash',
|
|
||||||
[SystemViewId.UntaggedNotes]: 'hashtag-off',
|
|
||||||
[SystemViewId.StarredNotes]: 'star-filled',
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapping[view.uuid as SystemViewId] || 'hashtag'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIconClass = (view: SmartView, isSelected: boolean): string => {
|
const getIconClass = (view: SmartView, isSelected: boolean): string => {
|
||||||
const mapping: Partial<Record<SystemViewId, string>> = {
|
const mapping: Partial<Record<SystemViewId, string>> = {
|
||||||
[SystemViewId.StarredNotes]: 'text-warning',
|
[SystemViewId.StarredNotes]: 'text-warning',
|
||||||
@@ -111,7 +98,6 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
|||||||
}, [tagsState, view])
|
}, [tagsState, view])
|
||||||
|
|
||||||
const isFaded = false
|
const isFaded = false
|
||||||
const iconType = smartViewIconType(view, isSelected)
|
|
||||||
const iconClass = getIconClass(view, isSelected)
|
const iconClass = getIconClass(view, isSelected)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -127,7 +113,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
|||||||
>
|
>
|
||||||
<div className="tag-info">
|
<div className="tag-info">
|
||||||
<div className={'tag-icon mr-2'}>
|
<div className={'tag-icon mr-2'}>
|
||||||
<Icon type={iconType} className={iconClass} />
|
<Icon type={view.iconString} className={iconClass} />
|
||||||
</div>
|
</div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ 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 { SNTag } from '@standardnotes/snjs'
|
import { SNTag, VectorIconNameOrEmoji, DefaultTagIconName } from '@standardnotes/snjs'
|
||||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||||
import Popover from '../Popover/Popover'
|
import Popover from '../Popover/Popover'
|
||||||
|
import IconPicker from '../Icon/IconPicker'
|
||||||
|
|
||||||
type ContextMenuProps = {
|
type ContextMenuProps = {
|
||||||
navigationController: NavigationController
|
navigationController: NavigationController
|
||||||
@@ -22,7 +23,7 @@ type ContextMenuProps = {
|
|||||||
const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => {
|
const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => {
|
||||||
const premiumModal = usePremiumModal()
|
const premiumModal = usePremiumModal()
|
||||||
|
|
||||||
const { contextMenuOpen, contextMenuClickLocation } = navigationController
|
const { contextMenuOpen, contextMenuClickLocation, application } = navigationController
|
||||||
|
|
||||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||||
useCloseOnClickOutside(contextMenuRef, () => navigationController.setContextMenuOpen(false))
|
useCloseOnClickOutside(contextMenuRef, () => navigationController.setContextMenuOpen(false))
|
||||||
@@ -39,6 +40,7 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
|
|||||||
|
|
||||||
const onClickRename = useCallback(() => {
|
const onClickRename = useCallback(() => {
|
||||||
navigationController.setContextMenuOpen(false)
|
navigationController.setContextMenuOpen(false)
|
||||||
|
navigationController.editingFrom = navigationController.contextMenuOpenFrom
|
||||||
navigationController.editingTag = selectedTag
|
navigationController.editingTag = selectedTag
|
||||||
}, [navigationController, selectedTag])
|
}, [navigationController, selectedTag])
|
||||||
|
|
||||||
@@ -51,6 +53,15 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
|
|||||||
[selectedTag.userModifiedDate],
|
[selectedTag.userModifiedDate],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleIconChange = (value?: VectorIconNameOrEmoji) => {
|
||||||
|
navigationController.setIcon(selectedTag, value || DefaultTagIconName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickStar = useCallback(() => {
|
||||||
|
navigationController.setFavorite(selectedTag, !selectedTag.starred).catch(console.error)
|
||||||
|
navigationController.setContextMenuOpen(false)
|
||||||
|
}, [navigationController, selectedTag])
|
||||||
|
|
||||||
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
|
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,6 +73,20 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
|
|||||||
>
|
>
|
||||||
<div ref={contextMenuRef}>
|
<div ref={contextMenuRef}>
|
||||||
<Menu a11yLabel="Tag context menu" isOpen={contextMenuOpen}>
|
<Menu a11yLabel="Tag context menu" isOpen={contextMenuOpen}>
|
||||||
|
<IconPicker
|
||||||
|
key={'icon-picker'}
|
||||||
|
onIconChange={handleIconChange}
|
||||||
|
selectedValue={selectedTag.iconString}
|
||||||
|
platform={application.platform}
|
||||||
|
className={'px-3 py-1.5'}
|
||||||
|
/>
|
||||||
|
<HorizontalSeparator classes="my-2" />
|
||||||
|
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickStar}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="star" className="mr-2 text-neutral" />
|
||||||
|
{selectedTag.starred ? 'Unfavorite' : 'Favorite'}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
<MenuItem type={MenuItemType.IconButton} 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" />
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export type TagListSectionType = 'all' | 'favorites'
|
||||||
@@ -5,15 +5,17 @@ import { FunctionComponent, useCallback } from 'react'
|
|||||||
import { DndProvider } from 'react-dnd'
|
import { DndProvider } from 'react-dnd'
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||||
import RootTagDropZone from './RootTagDropZone'
|
import RootTagDropZone from './RootTagDropZone'
|
||||||
|
import { TagListSectionType } from './TagListSection'
|
||||||
import { TagsListItem } from './TagsListItem'
|
import { TagsListItem } from './TagsListItem'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
viewControllerManager: ViewControllerManager
|
viewControllerManager: ViewControllerManager
|
||||||
|
type: TagListSectionType
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
const TagsList: FunctionComponent<Props> = ({ viewControllerManager, type }: Props) => {
|
||||||
const tagsState = viewControllerManager.navigationController
|
const navigationController = viewControllerManager.navigationController
|
||||||
const allTags = tagsState.allLocalRootTags
|
const allTags = type === 'all' ? navigationController.allLocalRootTags : navigationController.starredTags
|
||||||
|
|
||||||
const backend = HTML5Backend
|
const backend = HTML5Backend
|
||||||
|
|
||||||
@@ -23,10 +25,11 @@ const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) =>
|
|||||||
x: posX,
|
x: posX,
|
||||||
y: posY,
|
y: posY,
|
||||||
})
|
})
|
||||||
|
viewControllerManager.navigationController.setContextMenuOpenFrom(type)
|
||||||
viewControllerManager.navigationController.reloadContextMenuLayout()
|
viewControllerManager.navigationController.reloadContextMenuLayout()
|
||||||
viewControllerManager.navigationController.setContextMenuOpen(true)
|
viewControllerManager.navigationController.setContextMenuOpen(true)
|
||||||
},
|
},
|
||||||
[viewControllerManager],
|
[viewControllerManager, type],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onContextMenu = useCallback(
|
const onContextMenu = useCallback(
|
||||||
@@ -49,17 +52,20 @@ const TagsList: FunctionComponent<Props> = ({ viewControllerManager }: Props) =>
|
|||||||
level={0}
|
level={0}
|
||||||
key={tag.uuid}
|
key={tag.uuid}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
tagsState={tagsState}
|
type={type}
|
||||||
|
navigationController={navigationController}
|
||||||
features={viewControllerManager.featuresController}
|
features={viewControllerManager.featuresController}
|
||||||
linkingController={viewControllerManager.linkingController}
|
linkingController={viewControllerManager.linkingController}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<RootTagDropZone
|
{type === 'all' && (
|
||||||
tagsState={viewControllerManager.navigationController}
|
<RootTagDropZone
|
||||||
featuresState={viewControllerManager.featuresController}
|
tagsState={viewControllerManager.navigationController}
|
||||||
/>
|
featuresState={viewControllerManager.featuresController}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { KeyboardKey } from '@standardnotes/ui-services'
|
|||||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
import '@reach/tooltip/styles.css'
|
import '@reach/tooltip/styles.css'
|
||||||
import { SNTag } from '@standardnotes/snjs'
|
import { IconType, SNTag } from '@standardnotes/snjs'
|
||||||
import { computed } from 'mobx'
|
import { computed } from 'mobx'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
FormEventHandler,
|
FormEventHandler,
|
||||||
FunctionComponent,
|
FunctionComponent,
|
||||||
KeyboardEventHandler,
|
KeyboardEventHandler,
|
||||||
|
MouseEvent,
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -26,10 +27,12 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
|
|||||||
import { mergeRefs } from '@/Hooks/mergeRefs'
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
|
import { TagListSectionType } from './TagListSection'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: SNTag
|
tag: SNTag
|
||||||
tagsState: NavigationController
|
type: TagListSectionType
|
||||||
|
navigationController: NavigationController
|
||||||
features: FeaturesController
|
features: FeaturesController
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
level: number
|
level: number
|
||||||
@@ -40,25 +43,27 @@ const PADDING_BASE_PX = 14
|
|||||||
const PADDING_PER_LEVEL_PX = 21
|
const PADDING_PER_LEVEL_PX = 21
|
||||||
|
|
||||||
export const TagsListItem: FunctionComponent<Props> = observer(
|
export const TagsListItem: FunctionComponent<Props> = observer(
|
||||||
({ tag, features, tagsState, level, onContextMenu, linkingController }) => {
|
({ tag, type, features, navigationController: navigationController, level, onContextMenu, linkingController }) => {
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
|
const isFavorite = type === 'favorites'
|
||||||
|
|
||||||
const [title, setTitle] = useState(tag.title || '')
|
const [title, setTitle] = useState(tag.title || '')
|
||||||
const [subtagTitle, setSubtagTitle] = useState('')
|
const [subtagTitle, setSubtagTitle] = useState('')
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const subtagInputRef = useRef<HTMLInputElement>(null)
|
const subtagInputRef = useRef<HTMLInputElement>(null)
|
||||||
const menuButtonRef = useRef<HTMLAnchorElement>(null)
|
const menuButtonRef = useRef<HTMLAnchorElement>(null)
|
||||||
|
|
||||||
const isSelected = tagsState.selected === tag
|
const isSelected = navigationController.selected === tag
|
||||||
const isEditing = tagsState.editingTag === tag
|
const isEditing = navigationController.editingTag === tag && navigationController.editingFrom === type
|
||||||
const isAddingSubtag = tagsState.addingSubtagTo === tag
|
const isAddingSubtag = navigationController.addingSubtagTo === tag
|
||||||
const noteCounts = computed(() => tagsState.getNotesCount(tag))
|
const noteCounts = computed(() => navigationController.getNotesCount(tag))
|
||||||
|
|
||||||
const childrenTags = computed(() => tagsState.getChildren(tag)).get()
|
const childrenTags = computed(() => navigationController.getChildren(tag)).get()
|
||||||
const hasChildren = childrenTags.length > 0
|
const hasChildren = childrenTags.length > 0
|
||||||
|
|
||||||
const hasFolders = features.hasFolders
|
const hasFolders = features.hasFolders
|
||||||
const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder
|
const hasAtLeastOneFolder = navigationController.hasAtLeastOneFolder
|
||||||
|
|
||||||
const premiumModal = usePremiumModal()
|
const premiumModal = usePremiumModal()
|
||||||
|
|
||||||
@@ -76,27 +81,28 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
setTitle(tag.title || '')
|
setTitle(tag.title || '')
|
||||||
}, [setTitle, tag])
|
}, [setTitle, tag])
|
||||||
|
|
||||||
const toggleChildren: MouseEventHandler = useCallback(
|
const toggleChildren = useCallback(
|
||||||
(e) => {
|
(e?: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e?.stopPropagation()
|
||||||
const shouldShowChildren = !showChildren
|
const shouldShowChildren = !showChildren
|
||||||
setShowChildren(shouldShowChildren)
|
setShowChildren(shouldShowChildren)
|
||||||
tagsState.setExpanded(tag, shouldShowChildren)
|
navigationController.setExpanded(tag, shouldShowChildren)
|
||||||
},
|
},
|
||||||
[showChildren, tag, tagsState],
|
[showChildren, tag, navigationController],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectCurrentTag = useCallback(async () => {
|
const selectCurrentTag = useCallback(async () => {
|
||||||
await tagsState.setSelectedTag(tag, {
|
await navigationController.setSelectedTag(tag, {
|
||||||
userTriggered: true,
|
userTriggered: true,
|
||||||
})
|
})
|
||||||
|
toggleChildren()
|
||||||
toggleAppPane(AppPaneId.Items)
|
toggleAppPane(AppPaneId.Items)
|
||||||
}, [tagsState, tag, toggleAppPane])
|
}, [navigationController, tag, toggleAppPane, toggleChildren])
|
||||||
|
|
||||||
const onBlur = useCallback(() => {
|
const onBlur = useCallback(() => {
|
||||||
tagsState.save(tag, title).catch(console.error)
|
navigationController.save(tag, title).catch(console.error)
|
||||||
setTitle(tag.title)
|
setTitle(tag.title)
|
||||||
}, [tagsState, tag, title, setTitle])
|
}, [navigationController, tag, title, setTitle])
|
||||||
|
|
||||||
const onInput: FormEventHandler = useCallback(
|
const onInput: FormEventHandler = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -128,9 +134,9 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onSubtagInputBlur = useCallback(() => {
|
const onSubtagInputBlur = useCallback(() => {
|
||||||
tagsState.createSubtagAndAssignParent(tag, subtagTitle).catch(console.error)
|
navigationController.createSubtagAndAssignParent(tag, subtagTitle).catch(console.error)
|
||||||
setSubtagTitle('')
|
setSubtagTitle('')
|
||||||
}, [subtagTitle, tag, tagsState])
|
}, [subtagTitle, tag, navigationController])
|
||||||
|
|
||||||
const onSubtagKeyDown: KeyboardEventHandler = useCallback(
|
const onSubtagKeyDown: KeyboardEventHandler = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -166,21 +172,21 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
() => ({
|
() => ({
|
||||||
accept: ItemTypes.TAG,
|
accept: ItemTypes.TAG,
|
||||||
canDrop: (item) => {
|
canDrop: (item) => {
|
||||||
return tagsState.isValidTagParent(tag, item as SNTag)
|
return navigationController.isValidTagParent(tag, item as SNTag)
|
||||||
},
|
},
|
||||||
drop: (item) => {
|
drop: (item) => {
|
||||||
if (!hasFolders) {
|
if (!hasFolders) {
|
||||||
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
|
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tagsState.assignParent(item.uuid, tag.uuid).catch(console.error)
|
navigationController.assignParent(item.uuid, tag.uuid).catch(console.error)
|
||||||
},
|
},
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
isOver: !!monitor.isOver(),
|
isOver: !!monitor.isOver(),
|
||||||
canDrop: !!monitor.canDrop(),
|
canDrop: !!monitor.canDrop(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
[tag, tagsState, hasFolders, premiumModal],
|
[tag, navigationController, hasFolders, premiumModal],
|
||||||
)
|
)
|
||||||
|
|
||||||
const readyToDrop = isOver && canDrop
|
const readyToDrop = isOver && canDrop
|
||||||
@@ -194,16 +200,16 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenuOpen = tagsState.contextMenuOpen
|
const contextMenuOpen = navigationController.contextMenuOpen
|
||||||
const menuButtonRect = menuButtonRef.current?.getBoundingClientRect()
|
const menuButtonRect = menuButtonRef.current?.getBoundingClientRect()
|
||||||
|
|
||||||
if (contextMenuOpen) {
|
if (contextMenuOpen) {
|
||||||
tagsState.setContextMenuOpen(false)
|
navigationController.setContextMenuOpen(false)
|
||||||
} else {
|
} else {
|
||||||
onContextMenu(tag, menuButtonRect.right, menuButtonRect.top)
|
onContextMenu(tag, menuButtonRect.right, menuButtonRect.top)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onContextMenu, tagsState, tag],
|
[onContextMenu, navigationController, tag],
|
||||||
)
|
)
|
||||||
|
|
||||||
const tagRef = useRef<HTMLDivElement>(null)
|
const tagRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -248,7 +254,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="tag-info" title={title} ref={dropRef}>
|
<div className="tag-info" title={title} ref={dropRef}>
|
||||||
{hasAtLeastOneFolder && (
|
{hasAtLeastOneFolder && !isFavorite && (
|
||||||
<div className="tag-fold-container">
|
<div className="tag-fold-container">
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@@ -262,12 +268,12 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={'tag-icon draggable mr-2'} ref={dragRef}>
|
<div className={'tag-icon draggable mr-2'} ref={dragRef}>
|
||||||
<Icon type="hashtag" className={`${isSelected ? 'text-info' : 'text-neutral'}`} />
|
<Icon type={tag.iconString as IconType} className={`${isSelected ? 'text-info' : 'text-neutral'}`} />
|
||||||
</div>
|
</div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
className={'title editing focus:shadow-none focus:outline-none'}
|
className={'title editing focus:shadow-none focus:outline-none'}
|
||||||
id={`react-tag-${tag.uuid}`}
|
id={`react-tag-${tag.uuid}-${type}`}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onInput={onInput}
|
onInput={onInput}
|
||||||
value={title}
|
value={title}
|
||||||
@@ -278,7 +284,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={'title overflow-hidden text-left focus:shadow-none focus:outline-none'}
|
className={'title overflow-hidden text-left focus:shadow-none focus:outline-none'}
|
||||||
id={`react-tag-${tag.uuid}`}
|
id={`react-tag-${tag.uuid}-${type}`}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
@@ -334,7 +340,8 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
level={level + 1}
|
level={level + 1}
|
||||||
key={tag.uuid}
|
key={tag.uuid}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
tagsState={tagsState}
|
type={type}
|
||||||
|
navigationController={navigationController}
|
||||||
features={features}
|
features={features}
|
||||||
linkingController={linkingController}
|
linkingController={linkingController}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
|||||||
@@ -52,22 +52,37 @@ const TagsSection: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
|||||||
}, [viewControllerManager, checkIfMigrationNeeded])
|
}, [viewControllerManager, checkIfMigrationNeeded])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<>
|
||||||
<div className={'section-title-bar'}>
|
{viewControllerManager.navigationController.starredTags.length > 0 && (
|
||||||
<div className="section-title-bar-header">
|
<section>
|
||||||
<TagsSectionTitle
|
<div className={'section-title-bar'}>
|
||||||
features={viewControllerManager.featuresController}
|
<div className="section-title-bar-header">
|
||||||
hasMigration={hasMigration}
|
<div className="title text-sm">
|
||||||
onClickMigration={runMigration}
|
<span className="font-bold">Favorites</span>
|
||||||
/>
|
</div>
|
||||||
<TagsSectionAddButton
|
</div>
|
||||||
tags={viewControllerManager.navigationController}
|
</div>
|
||||||
features={viewControllerManager.featuresController}
|
<TagsList type="favorites" viewControllerManager={viewControllerManager} />
|
||||||
/>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div className={'section-title-bar'}>
|
||||||
|
<div className="section-title-bar-header">
|
||||||
|
<TagsSectionTitle
|
||||||
|
features={viewControllerManager.featuresController}
|
||||||
|
hasMigration={hasMigration}
|
||||||
|
onClickMigration={runMigration}
|
||||||
|
/>
|
||||||
|
<TagsSectionAddButton
|
||||||
|
tags={viewControllerManager.navigationController}
|
||||||
|
features={viewControllerManager.featuresController}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<TagsList type="all" viewControllerManager={viewControllerManager} />
|
||||||
<TagsList viewControllerManager={viewControllerManager} />
|
</section>
|
||||||
</section>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class ItemListController
|
|||||||
items: ListableContentItem[] = []
|
items: ListableContentItem[] = []
|
||||||
notesToDisplay = 0
|
notesToDisplay = 0
|
||||||
pageSize = 0
|
pageSize = 0
|
||||||
panelTitle = 'All Notes'
|
panelTitle = 'Notes'
|
||||||
panelWidth = 0
|
panelWidth = 0
|
||||||
renderedItems: ListableContentItem[] = []
|
renderedItems: ListableContentItem[] = []
|
||||||
searchSubmitted = false
|
searchSubmitted = false
|
||||||
|
|||||||
@@ -236,9 +236,11 @@ export class LinkingController extends AbstractViewController {
|
|||||||
} else if (item instanceof FileItem) {
|
} else if (item instanceof FileItem) {
|
||||||
const icon = this.application.iconsController.getIconForFileType(item.mimeType)
|
const icon = this.application.iconsController.getIconForFileType(item.mimeType)
|
||||||
return [icon, 'text-info']
|
return [icon, 'text-info']
|
||||||
|
} else if (item instanceof SNTag) {
|
||||||
|
return [item.iconString as IconType, 'text-info']
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['hashtag', 'text-info']
|
throw new Error('Unhandled case in getLinkedItemIcon')
|
||||||
}
|
}
|
||||||
|
|
||||||
activateItem = async (item: LinkableItem): Promise<AppPaneId | undefined> => {
|
activateItem = async (item: LinkableItem): Promise<AppPaneId | undefined> => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
SystemViewId,
|
SystemViewId,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
InternalEventPublishStrategy,
|
InternalEventPublishStrategy,
|
||||||
|
VectorIconNameOrEmoji,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
|
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { WebApplication } from '../../Application/Application'
|
import { WebApplication } from '../../Application/Application'
|
||||||
@@ -24,6 +25,7 @@ import { AnyTag } from './AnyTagType'
|
|||||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||||
import { Persistable } from '../Abstract/Persistable'
|
import { Persistable } from '../Abstract/Persistable'
|
||||||
|
import { TagListSectionType } from '@/Components/Tags/TagListSection'
|
||||||
|
|
||||||
export type NavigationControllerPersistableValue = {
|
export type NavigationControllerPersistableValue = {
|
||||||
selectedTagUuid: AnyTag['uuid']
|
selectedTagUuid: AnyTag['uuid']
|
||||||
@@ -35,14 +37,17 @@ export class NavigationController
|
|||||||
{
|
{
|
||||||
tags: SNTag[] = []
|
tags: SNTag[] = []
|
||||||
smartViews: SmartView[] = []
|
smartViews: SmartView[] = []
|
||||||
|
starredTags: SNTag[] = []
|
||||||
allNotesCount_ = 0
|
allNotesCount_ = 0
|
||||||
selectedUuid: AnyTag['uuid'] | undefined = undefined
|
selectedUuid: AnyTag['uuid'] | undefined = undefined
|
||||||
selected_: AnyTag | undefined
|
selected_: AnyTag | undefined
|
||||||
previouslySelected_: AnyTag | undefined
|
previouslySelected_: AnyTag | undefined
|
||||||
editing_: SNTag | SmartView | undefined
|
editing_: SNTag | SmartView | undefined
|
||||||
|
editingFrom?: TagListSectionType
|
||||||
addingSubtagTo: SNTag | undefined
|
addingSubtagTo: SNTag | undefined
|
||||||
|
|
||||||
contextMenuOpen = false
|
contextMenuOpen = false
|
||||||
|
contextMenuOpenFrom?: TagListSectionType
|
||||||
contextMenuPosition: { top?: number; left: number; bottom?: number } = {
|
contextMenuPosition: { top?: number; left: number; bottom?: number } = {
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -66,6 +71,7 @@ export class NavigationController
|
|||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
tags: observable,
|
tags: observable,
|
||||||
|
starredTags: observable,
|
||||||
smartViews: observable.ref,
|
smartViews: observable.ref,
|
||||||
hasAtLeastOneFolder: computed,
|
hasAtLeastOneFolder: computed,
|
||||||
allNotesCount_: observable,
|
allNotesCount_: observable,
|
||||||
@@ -111,7 +117,7 @@ export class NavigationController
|
|||||||
this.application.streamItems([ContentType.Tag, ContentType.SmartView], ({ changed, removed }) => {
|
this.application.streamItems([ContentType.Tag, ContentType.SmartView], ({ changed, removed }) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.tags = this.application.items.getDisplayableTags()
|
this.tags = this.application.items.getDisplayableTags()
|
||||||
|
this.starredTags = this.tags.filter((tag) => tag.starred)
|
||||||
this.smartViews = this.application.items.getSmartViews()
|
this.smartViews = this.application.items.getSmartViews()
|
||||||
|
|
||||||
const currentSelectedTag = this.selected_
|
const currentSelectedTag = this.selected_
|
||||||
@@ -266,6 +272,10 @@ export class NavigationController
|
|||||||
this.addingSubtagTo = tag
|
this.addingSubtagTo = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContextMenuOpenFrom(section: TagListSectionType): void {
|
||||||
|
this.contextMenuOpenFrom = section
|
||||||
|
}
|
||||||
|
|
||||||
setContextMenuOpen(open: boolean): void {
|
setContextMenuOpen(open: boolean): void {
|
||||||
this.contextMenuOpen = open
|
this.contextMenuOpen = open
|
||||||
}
|
}
|
||||||
@@ -476,6 +486,22 @@ export class NavigationController
|
|||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setFavorite(tag: SNTag, favorite: boolean) {
|
||||||
|
return this.application.mutator
|
||||||
|
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||||
|
mutator.starred = favorite
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
public setIcon(tag: SNTag, icon: VectorIconNameOrEmoji) {
|
||||||
|
this.application.mutator
|
||||||
|
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||||
|
mutator.iconString = icon
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
public get editingTag(): SNTag | SmartView | undefined {
|
public get editingTag(): SNTag | SmartView | undefined {
|
||||||
return this.editing_
|
return this.editing_
|
||||||
}
|
}
|
||||||
@@ -500,6 +526,7 @@ export class NavigationController
|
|||||||
}
|
}
|
||||||
|
|
||||||
public undoCreateNewTag() {
|
public undoCreateNewTag() {
|
||||||
|
this.editingFrom = undefined
|
||||||
this.editing_ = undefined
|
this.editing_ = undefined
|
||||||
const previousTag = this.previouslySelected_ || this.smartViews[0]
|
const previousTag = this.previouslySelected_ || this.smartViews[0]
|
||||||
void this.setSelectedTag(previousTag)
|
void this.setSelectedTag(previousTag)
|
||||||
@@ -528,6 +555,7 @@ export class NavigationController
|
|||||||
const hasDuplicatedTitle = siblings.some((other) => other.title.toLowerCase() === newTitle.toLowerCase())
|
const hasDuplicatedTitle = siblings.some((other) => other.title.toLowerCase() === newTitle.toLowerCase())
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.editingFrom = undefined
|
||||||
this.editing_ = undefined
|
this.editing_ = undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6666,6 +6666,7 @@ __metadata:
|
|||||||
"@types/jest": ^28.1.5
|
"@types/jest": ^28.1.5
|
||||||
"@types/lodash": ^4.14.182
|
"@types/lodash": ^4.14.182
|
||||||
"@typescript-eslint/eslint-plugin": ^5.30.0
|
"@typescript-eslint/eslint-plugin": ^5.30.0
|
||||||
|
eslint: "*"
|
||||||
eslint-plugin-prettier: "*"
|
eslint-plugin-prettier: "*"
|
||||||
jest: ^28.1.2
|
jest: ^28.1.2
|
||||||
lodash: ^4.17.21
|
lodash: ^4.17.21
|
||||||
|
|||||||
Reference in New Issue
Block a user