diff --git a/app/assets/icons/ic-link-off.svg b/app/assets/icons/ic-link-off.svg new file mode 100644 index 00000000000..3b701266cd7 --- /dev/null +++ b/app/assets/icons/ic-link-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index c296e00a2d1..96eee992f8e 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -55,6 +55,7 @@ import LockIcon from '../../icons/ic-lock.svg'; import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg'; import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg'; import WindowIcon from '../../icons/ic-window.svg'; +import LinkOffIcon from '../../icons/ic-link-off.svg'; import MenuArrowDownAlt from '../../icons/ic-menu-arrow-down-alt.svg'; import MenuArrowRight from '../../icons/ic-menu-arrow-right.svg'; @@ -105,6 +106,7 @@ const ICONS = { help: HelpIcon, keyboard: KeyboardIcon, 'list-bulleted': ListBulleted, + 'link-off': LinkOffIcon, listed: ListedIcon, security: SecurityIcon, settings: SettingsIcon, diff --git a/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx b/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx index 701be49dc35..045e3f49250 100644 --- a/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -23,7 +23,6 @@ import { themesMenuKeyDownHandler, } from './eventHandlers'; import { FocusModeSwitch } from './FocusModeSwitch'; -import { TagNestingSwitch } from './TagNestingSwitch'; import { ThemesMenuButton } from './ThemesMenuButton'; const focusModeAnimationDuration = 1255; diff --git a/app/assets/javascripts/components/RootTagDropZone.tsx b/app/assets/javascripts/components/RootTagDropZone.tsx index 67e510f68f0..0e39ef5a829 100644 --- a/app/assets/javascripts/components/RootTagDropZone.tsx +++ b/app/assets/javascripts/components/RootTagDropZone.tsx @@ -5,6 +5,7 @@ import { import { TagsState } from '@/ui_models/app_state/tags_state'; import { observer } from 'mobx-react-lite'; import { useDrop } from 'react-dnd'; +import { Icon } from './Icon'; import { usePremiumModal } from './Premium'; import { DropItem, DropProps, ItemTypes } from './TagsListItem'; @@ -52,7 +53,11 @@ export const RootTagDropZone: React.FC = observer( isOver ? 'is-over' : '' }`} > - Move the tag here to remove it from its folder. + +

+ Move the tag here to
+ remove it from its folder. +

); } diff --git a/app/assets/javascripts/components/Tags/TagsSection.tsx b/app/assets/javascripts/components/Tags/TagsSection.tsx index a0064e1015c..5cc7a452db8 100644 --- a/app/assets/javascripts/components/Tags/TagsSection.tsx +++ b/app/assets/javascripts/components/Tags/TagsSection.tsx @@ -33,6 +33,7 @@ const TagAddButton: FunctionComponent<{ appState.createNewTag()} /> ); diff --git a/app/assets/javascripts/components/TagsListItem.tsx b/app/assets/javascripts/components/TagsListItem.tsx index b03f08eaa40..ed6e8cf5e63 100644 --- a/app/assets/javascripts/components/TagsListItem.tsx +++ b/app/assets/javascripts/components/TagsListItem.tsx @@ -43,7 +43,6 @@ export type TagsListState = { export const TagsListItem: FunctionComponent = observer( ({ tag, selectTag, saveTag, removeTag, appState, tagsState, level }) => { const [title, setTitle] = useState(tag.title || ''); - const [showChildren, setShowChildren] = useState(true); const inputRef = useRef(null); const isSelected = appState.selectedTag === tag; @@ -55,8 +54,19 @@ export const TagsListItem: FunctionComponent = observer( const hasFolders = tagsState.hasFolders; const isNativeFoldersEnabled = appState.features.enableNativeFoldersFeature; + const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder; const premiumModal = usePremiumModal(); + const [showChildren, setShowChildren] = useState(hasChildren); + const [hadChildren, setHadChildren] = useState(hasChildren); + + useEffect(() => { + if (!hadChildren && hasChildren) { + setShowChildren(true); + } + setHadChildren(hasChildren); + }, [hadChildren, hasChildren]); + useEffect(() => { setTitle(tag.title || ''); }, [setTitle, tag]); @@ -164,25 +174,21 @@ export const TagsListItem: FunctionComponent = observer( }`} onClick={selectCurrentTag} ref={dragRef} - style={{ paddingLeft: `${level + 0.5}rem` }} + style={{ paddingLeft: `${level * 21 + 10}px` }} > {!tag.errorDecrypting ? ( -
- {hasFolders && isNativeFoldersEnabled && ( +
+ {hasFolders && isNativeFoldersEnabled && hasAtLeastOneFolder && (
- {hasChildren && ( - - )} +
)}
= observer(
{noteCounts.get()}
) : null} -
+
{tag.conflictOf && (
Conflicted Copy {tag.conflictOf} diff --git a/app/assets/javascripts/typings/hoist-non-react-statics.d.ts b/app/assets/javascripts/typings/hoist-non-react-statics.d.ts new file mode 100644 index 00000000000..4a0f7856f2d --- /dev/null +++ b/app/assets/javascripts/typings/hoist-non-react-statics.d.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Type definitions for hoist-non-react-statics 3.3 +// Project: https://github.com/mridgway/hoist-non-react-statics#readme +// Definitions by: JounQin , James Reggio +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.8 +// https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/hoist-non-react-statics + +declare module 'hoist-non-react-statics' { + interface REACT_STATICS { + childContextTypes: true; + contextType: true; + contextTypes: true; + defaultProps: true; + displayName: true; + getDefaultProps: true; + getDerivedStateFromError: true; + getDerivedStateFromProps: true; + mixins: true; + propTypes: true; + type: true; + } + + interface KNOWN_STATICS { + name: true; + length: true; + prototype: true; + caller: true; + callee: true; + arguments: true; + arity: true; + } + + interface MEMO_STATICS { + $$typeof: true; + compare: true; + defaultProps: true; + displayName: true; + propTypes: true; + type: true; + } + + interface FORWARD_REF_STATICS { + $$typeof: true; + render: true; + defaultProps: true; + displayName: true; + propTypes: true; + } + + export type NonReactStatics< + S extends React.ComponentType, + C extends { + [key: string]: true; + } = {} + > = { + [key in Exclude< + keyof S, + S extends React.MemoExoticComponent + ? keyof MEMO_STATICS | keyof C + : S extends React.ForwardRefExoticComponent + ? keyof FORWARD_REF_STATICS | keyof C + : keyof REACT_STATICS | keyof KNOWN_STATICS | keyof C + >]: S[key]; + }; +} diff --git a/app/assets/javascripts/ui_models/app_state/tags_state.ts b/app/assets/javascripts/ui_models/app_state/tags_state.ts index f3fff2b0297..ef63453a6c7 100644 --- a/app/assets/javascripts/ui_models/app_state/tags_state.ts +++ b/app/assets/javascripts/ui_models/app_state/tags_state.ts @@ -24,9 +24,10 @@ export class TagsState { this.tagsCountsState = new TagsCountsState(this.application); makeObservable(this, { - tags: observable, - smartTags: observable, + tags: observable.ref, + smartTags: observable.ref, hasFolders: computed, + hasAtLeastOneFolder: computed, assignParent: action, @@ -76,17 +77,22 @@ export class TagsState { return this.application.isValidTagParent(parentUuid, tagUuid); } - assignParent(tagUuid: string, parentUuid: string | undefined): void { + public async assignParent( + tagUuid: string, + parentUuid: string | undefined + ): Promise { const tag = this.application.findItem(tagUuid) as SNTag; const parent = parentUuid && (this.application.findItem(parentUuid) as SNTag); if (!parent) { - this.application.unsetTagParent(tag); + await this.application.unsetTagParent(tag); } else { - this.application.setTagParent(parent, tag); + await this.application.setTagParent(parent, tag); } + + await this.application.sync(); } get rootTags(): SNTag[] { @@ -108,6 +114,10 @@ export class TagsState { public set hasFolders(hasFolders: boolean) { this.features.hasFolders = hasFolders; } + + public get hasAtLeastOneFolder(): boolean { + return this.tags.some((tag) => !!this.application.getTagParent(tag)); + } } /** diff --git a/app/assets/stylesheets/_tags.scss b/app/assets/stylesheets/_tags.scss index 14d9626dfb4..e44610a4462 100644 --- a/app/assets/stylesheets/_tags.scss +++ b/app/assets/stylesheets/_tags.scss @@ -53,10 +53,19 @@ .root-drop { width: '100%'; - padding: 10px; + padding: 12px; opacity: 0; transition: opacity 0.3s ease-in; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + .sn-icon { + margin-right: 0.5rem; + } + &.active { opacity: 1; @@ -68,6 +77,9 @@ } .tag { + font-size: 14px; + line-height: 18px; + min-height: 30px; padding: 5px 12px; cursor: pointer; @@ -81,11 +93,26 @@ align-items: center; justify-content: space-between; - > .tag-icon { - svg { - display: block; - margin: auto; + .sn-icon { + display: block; + margin: 0 auto; + + &.hidden { + visibility: hidden; } + } + + > .tag-fold { + width: 22px; + display: flex; + align-items: center; + height: 100%; + } + + > .tag-icon { + display: flex; + align-items: center; + height: 100%; &.draggable { cursor: move; @@ -96,13 +123,12 @@ } } - > .tag-fold { - width: 22px; - } - > .title { @extend .focus\:outline-none; @extend .focus\:shadow-none; + font-size: 14px; + line-height: 18px; + width: 80%; background-color: transparent; font-weight: 600; @@ -147,7 +173,11 @@ } .meta { - padding-left: 20px; + padding-left: 3px; + + &.with-folders { + padding-left: 25px; + } > .menu { font-size: 11px; @@ -179,7 +209,7 @@ &:hover:not(.selected), &.selected, &.is-drag-over { - background-color: var(--sn-stylekit-white); + background-color: var(--sn-stylekit-secondary-contrast-background-color); color: var(--sn-stylekit-secondary-contrast-foreground-color); > .title {