feat: implement tags folder as experimental feature (#788)

* feat: add tag folders support basics

* feat: add draggability to tags

* feat: add drag & drop draft

* feat: fold folders

* fix: do not select on fold / unfold tags

* style: clean the isValidTag call

* feat: add native folder toggle

* feat: add touch mobile support

* ui: add nicer design & icons

* style: render full-width tag items

* feat: nicer looking dropzone

* style: fix arguments

* fix: tag template rendering in list items

* feat: tag can be dragged over the whole item

* fix: cancel / reset title after save

* fix: disable drag completely when needed

* fix: invalid tag parents

* feat: add paying feature

* feat: with paid feature tooltip

* feat: tag has a plus feature

* feat: add premium modal

* style: simplif code

* refactor: extract feature_state & simplif code

* fix: icons and icons svg

* style: remove comment

* feat: tag folders naming

* feat: use the feature notification

* fix: tag folders copy

* style: variable names

* style: remove & clean comments

* refactor: remove is-mobile library

* feat: tags folder experimental (#10)

* feat: hide native folders behind experimental flag

* fix: better tags resizing

* fix: merge global window

* style: rename params

* refactor: remove level of indirection in feature toggle

* feat: recursively add tags to note on create (#9)

* fix: use add tags folder hierarchy & isTemplateItem (#13)

* fix: use new snjs add tag hierarchy

* fix: use new snjs isTemplateItem

* feat: tags folder premium (#774)

* feat: upgrade premium in tags section

refactor: move TagsSection to react

feat: show premium on Tag section

feat: keep drag and drop features always active

fix: drag & drop tweak with premium

feat: premium messages

fix: remove fill in svg icons

fix: change tag list color (temporary)

style: remove dead code

refactor: clarify names and modules

fix: draggable behind feature toggle

feat: add button in TagSection & design

* feat: fix features loading with app state (#775)

* fix: distinguish between app launch and start

* fix: update state for footer too

* fix: wait for application launch event

Co-authored-by: Laurent Senta <laurent@singulargarden.com>

* feat: tags folder with folder text design (#776)

* feat: add folder text

* fix: sn stylekit colors

* fix: root drop zone

* chore: upgrade stylekit

* fix: hide dropzone when feature is disabled

* chore: bump versions now that they are released

Co-authored-by: Mo <me@bitar.io>

* feat: tags folder design review (#785)

* fix: upgrade design after review

* fix: tweak dropzone

* fix: sync after assign parent

* fix: tsc error on build

* fix: vertical center the fold arrows

* fix: define our own hoist for react-dnd

* feat: hide fold when there are no folders

* fix: show children usability + resize UI

* fix: use old colors for now, theme compat

* fix: tweak alignment and add title

* fix: meta offset with folders

* fix: tweak tag size

* fix: observable setup

* fix: use link-off icon on dropzone

* fix: more tweak on text sizes

Co-authored-by: Mo <me@bitar.io>
This commit is contained in:
Laurent Senta
2021-12-23 14:34:02 +01:00
committed by GitHub
parent 4a5a202a37
commit 237cd91acd
33 changed files with 1048 additions and 221 deletions

View File

@@ -24,8 +24,10 @@ import MarkdownIcon from '../../icons/ic-markdown.svg';
import CodeIcon from '../../icons/ic-code.svg';
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
import AddIcon from '../../icons/ic-add.svg';
import HelpIcon from '../../icons/ic-help.svg';
import KeyboardIcon from '../../icons/ic-keyboard.svg';
import ListBulleted from '../../icons/ic-list-bulleted.svg';
import ListedIcon from '../../icons/ic-listed.svg';
import SecurityIcon from '../../icons/ic-security.svg';
import SettingsIcon from '../../icons/ic-settings.svg';
@@ -53,11 +55,17 @@ 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';
import { toDirective } from './utils';
import { FunctionalComponent } from 'preact';
const ICONS = {
'menu-arrow-down-alt': MenuArrowDownAlt,
'menu-arrow-right': MenuArrowRight,
'arrows-sort-up': ArrowsSortUpIcon,
'arrows-sort-down': ArrowsSortDownIcon,
lock: LockIcon,
@@ -94,8 +102,11 @@ const ICONS = {
more: MoreIcon,
tune: TuneIcon,
accessibility: AccessibilityIcon,
add: AddIcon,
help: HelpIcon,
keyboard: KeyboardIcon,
'list-bulleted': ListBulleted,
'link-off': LinkOffIcon,
listed: ListedIcon,
security: SecurityIcon,
settings: SettingsIcon,
@@ -111,7 +122,7 @@ const ICONS = {
'menu-arrow-down': MenuArrowDownIcon,
'menu-close': MenuCloseIcon,
window: WindowIcon,
'premium-feature': PremiumFeatureIcon
'premium-feature': PremiumFeatureIcon,
};
export type IconType = keyof typeof ICONS;

View File

@@ -0,0 +1 @@
export { usePremiumModal, PremiumModalProvider } from './usePremiumModal';

View File

@@ -0,0 +1,49 @@
import { FunctionalComponent } from 'preact';
import { useCallback, useContext, useState } from 'preact/hooks';
import { createContext } from 'react';
import { PremiumFeaturesModal } from '../PremiumFeaturesModal';
type PremiumModalContextData = {
activate: (featureName: string) => void;
};
const PremiumModalContext = createContext<PremiumModalContextData | null>(null);
const PremiumModalProvider_ = PremiumModalContext.Provider;
export const usePremiumModal = (): PremiumModalContextData => {
const value = useContext(PremiumModalContext);
if (!value) {
throw new Error('invalid PremiumModal context');
}
return value;
};
export const PremiumModalProvider: FunctionalComponent = ({ children }) => {
const [featureName, setFeatureName] = useState<null | string>(null);
const activate = setFeatureName;
const closeModal = useCallback(() => {
setFeatureName(null);
}, [setFeatureName]);
const showModal = !!featureName;
return (
<>
{showModal && (
<PremiumFeaturesModal
showModal={!!featureName}
featureName={featureName}
onClose={closeModal}
/>
)}
<PremiumModalProvider_ value={{ activate }}>
{children}
</PremiumModalProvider_>
</>
);
};

View File

@@ -2,7 +2,7 @@ import { WebApplication } from '@/ui_models/application';
import { FeatureIdentifier } from '@standardnotes/features';
import { FeatureStatus } from '@standardnotes/snjs';
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { useCallback, useState } from 'preact/hooks';
import { JSXInternal } from 'preact/src/jsx';
import { Icon } from '../Icon';
import { PremiumFeaturesModal } from '../PremiumFeaturesModal';
@@ -10,46 +10,48 @@ import { Switch } from '../Switch';
type Props = {
application: WebApplication;
closeQuickSettingsMenu: () => void;
focusModeEnabled: boolean;
setFocusModeEnabled: (enabled: boolean) => void;
onToggle: (value: boolean) => void;
onClose: () => void;
isEnabled: boolean;
};
export const FocusModeSwitch: FunctionComponent<Props> = ({
application,
closeQuickSettingsMenu,
focusModeEnabled,
setFocusModeEnabled,
onToggle,
onClose,
isEnabled,
}) => {
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const isEntitledToFocusMode =
const isEntitled =
application.getFeatureStatus(FeatureIdentifier.FocusMode) ===
FeatureStatus.Entitled;
const toggleFocusMode = (
e: JSXInternal.TargetedMouseEvent<HTMLButtonElement>
) => {
e.preventDefault();
if (isEntitledToFocusMode) {
setFocusModeEnabled(!focusModeEnabled);
closeQuickSettingsMenu();
} else {
setShowUpgradeModal(true);
}
};
const toggle = useCallback(
(e: JSXInternal.TargetedMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (isEntitled) {
onToggle(!isEnabled);
onClose();
} else {
setShowUpgradeModal(true);
}
},
[isEntitled, isEnabled, onToggle, setShowUpgradeModal, onClose]
);
return (
<>
<button
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
onClick={toggleFocusMode}
onClick={toggle}
>
<div className="flex items-center">
<Icon type="menu-close" className="color-neutral mr-2" />
Focused Writing
</div>
{isEntitledToFocusMode ? (
<Switch className="px-0" checked={focusModeEnabled} />
{isEntitled ? (
<Switch className="px-0" checked={isEnabled} />
) : (
<div title="Premium feature">
<Icon type="premium-feature" />

View File

@@ -6,10 +6,10 @@ import {
DisclosurePanel,
} from '@reach/disclosure';
import {
ContentType,
SNTheme,
ComponentArea,
ContentType,
SNComponent,
SNTheme,
} from '@standardnotes/snjs';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
@@ -301,9 +301,9 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
))}
<FocusModeSwitch
application={application}
closeQuickSettingsMenu={closeQuickSettingsMenu}
focusModeEnabled={focusModeEnabled}
setFocusModeEnabled={setFocusModeEnabled}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<div className="h-1px my-2 bg-border"></div>
<button

View File

@@ -0,0 +1,64 @@
import {
FeaturesState,
TAG_FOLDERS_FEATURE_NAME,
} from '@/ui_models/app_state/features_state';
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';
type Props = {
tagsState: TagsState;
featuresState: FeaturesState;
};
export const RootTagDropZone: React.FC<Props> = observer(
({ tagsState, featuresState }) => {
const premiumModal = usePremiumModal();
const isNativeFoldersEnabled = featuresState.enableNativeFoldersFeature;
const hasFolders = tagsState.hasFolders;
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
() => ({
accept: ItemTypes.TAG,
canDrop: () => {
return true;
},
drop: (item) => {
if (!hasFolders) {
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME);
return;
}
tagsState.assignParent(item.uuid, undefined);
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}),
[tagsState, hasFolders, premiumModal]
);
if (!isNativeFoldersEnabled || !hasFolders) {
return null;
}
return (
<div
ref={dropRef}
className={`root-drop ${canDrop ? 'active' : ''} ${
isOver ? 'is-over' : ''
}`}
>
<Icon className="color-neutral" type="link-off" />
<p className="content">
Move the tag here to <br />
remove it from its folder.
</p>
</div>
);
}
);

View File

@@ -1,12 +1,12 @@
import { ComponentChildren, FunctionalComponent } from 'preact';
import { useState } from 'preact/hooks';
import { HTMLProps } from 'react';
import {
CustomCheckboxContainer,
CustomCheckboxInput,
CustomCheckboxInputProps,
} from '@reach/checkbox';
import '@reach/checkbox/styles.css';
import { ComponentChildren, FunctionalComponent } from 'preact';
import { useState } from 'preact/hooks';
import { HTMLProps } from 'react';
export type SwitchProps = HTMLProps<HTMLInputElement> & {
checked?: boolean;
@@ -23,6 +23,10 @@ export const Switch: FunctionalComponent<SwitchProps> = (
const [checkedState, setChecked] = useState(props.checked || false);
const checked = props.checked ?? checkedState;
const className = props.className ?? '';
const isDisabled = !!props.disabled;
const isActive = checked && !isDisabled;
return (
<label
className={`sn-component flex justify-between items-center cursor-pointer px-3 ${className}`}
@@ -35,7 +39,8 @@ export const Switch: FunctionalComponent<SwitchProps> = (
setChecked(event.target.checked);
props.onChange?.(event.target.checked);
}}
className={`sn-switch ${checked ? 'bg-info' : 'bg-neutral'}`}
className={`sn-switch ${isActive ? 'bg-info' : 'bg-neutral'}`}
disabled={props.disabled}
>
<CustomCheckboxInput
{...({

View File

@@ -0,0 +1,108 @@
import { TagsList } from '@/components/TagsList';
import { toDirective } from '@/components/utils';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import {
FeaturesState,
TAG_FOLDERS_FEATURE_NAME,
TAG_FOLDERS_FEATURE_TOOLTIP,
} from '@/ui_models/app_state/features_state';
import { Tooltip } from '@reach/tooltip';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback } from 'preact/hooks';
import { IconButton } from '../IconButton';
import { PremiumModalProvider, usePremiumModal } from '../Premium';
type Props = {
application: WebApplication;
appState: AppState;
};
const TagAddButton: FunctionComponent<{
appState: AppState;
features: FeaturesState;
}> = observer(({ appState, features }) => {
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
if (!isNativeFoldersEnabled) {
return null;
}
return (
<IconButton
icon="add"
title="Create a new tag"
focusable={true}
onClick={() => appState.createNewTag()}
/>
);
});
const TagTitle: FunctionComponent<{
features: FeaturesState;
}> = observer(({ features }) => {
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
const hasFolders = features.hasFolders;
const modal = usePremiumModal();
const showPremiumAlert = useCallback(() => {
modal.activate(TAG_FOLDERS_FEATURE_NAME);
}, [modal]);
if (!isNativeFoldersEnabled) {
return (
<>
<div className="sk-h3 title">
<span className="sk-bold">Tags</span>
</div>
</>
);
}
if (hasFolders) {
return (
<>
<div className="sk-h3 title">
<span className="sk-bold">Folders</span>
</div>
</>
);
}
return (
<>
<div className="sk-h3 title">
<span className="sk-bold">Tags</span>
<Tooltip label={TAG_FOLDERS_FEATURE_TOOLTIP}>
<label
className="ml-1 sk-bold color-grey-2 cursor-pointer"
onClick={showPremiumAlert}
>
Folders
</label>
</Tooltip>
</div>
</>
);
});
export const TagsSection: FunctionComponent<Props> = observer(
({ application, appState }) => {
return (
<PremiumModalProvider>
<section>
<div className="tags-title-section section-title-bar">
<div className="section-title-bar-header">
<TagTitle features={appState.features} />
<TagAddButton appState={appState} features={appState.features} />
</div>
</div>
<TagsList application={application} appState={appState} />
</section>
</PremiumModalProvider>
);
}
);
export const TagsSectionDirective = toDirective<Props>(TagsSection);

View File

@@ -1,12 +1,18 @@
import { PremiumModalProvider } from '@/components/Premium';
import { confirmDialog } from '@/services/alertService';
import { STRING_DELETE_TAG } from '@/strings';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { isMobile } from '@/utils';
import { SNTag, TagMutator } from '@standardnotes/snjs';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback } from 'preact/hooks';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TouchBackend } from 'react-dnd-touch-backend';
import { RootTagDropZone } from './RootTagDropZone';
import { TagsListItem } from './TagsListItem';
import { toDirective } from './utils';
@@ -28,8 +34,9 @@ const tagsWithOptionalTemplate = (
export const TagsList: FunctionComponent<Props> = observer(
({ application, appState }) => {
const templateTag = appState.templateTag;
const tags = appState.tags.tags;
const allTags = tagsWithOptionalTemplate(templateTag, tags);
const rootTags = appState.tags.rootTags;
const allTags = tagsWithOptionalTemplate(templateTag, rootTags);
const selectTag = useCallback(
(tag: SNTag) => {
@@ -106,29 +113,39 @@ export const TagsList: FunctionComponent<Props> = observer(
[appState]
);
const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend;
return (
<>
{allTags.length === 0 ? (
<div className="no-tags-placeholder">
No tags. Create one using the add button above.
</div>
) : (
<>
{allTags.map((tag) => {
return (
<TagsListItem
key={tag.uuid}
tag={tag}
selectTag={selectTag}
saveTag={saveTag}
removeTag={removeTag}
appState={appState}
/>
);
})}
</>
)}
</>
<PremiumModalProvider>
<DndProvider backend={backend}>
{allTags.length === 0 ? (
<div className="no-tags-placeholder">
No tags. Create one using the add button above.
</div>
) : (
<>
{allTags.map((tag) => {
return (
<TagsListItem
level={0}
key={tag.uuid}
tag={tag}
tagsState={appState.tags}
selectTag={selectTag}
saveTag={saveTag}
removeTag={removeTag}
appState={appState}
/>
);
})}
<RootTagDropZone
tagsState={appState.tags}
featuresState={appState.features}
/>
</>
)}
</DndProvider>
</PremiumModalProvider>
);
}
);

View File

@@ -1,26 +1,47 @@
import {
FeaturesState,
TAG_FOLDERS_FEATURE_NAME,
} from '@/ui_models/app_state/features_state';
import { TagsState } from '@/ui_models/app_state/tags_state';
import '@reach/tooltip/styles.css';
import { SNTag } from '@standardnotes/snjs';
import { computed, runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { FunctionComponent, JSX } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { useDrag, useDrop } from 'react-dnd';
import { Icon } from './Icon';
import { usePremiumModal } from './Premium';
export enum ItemTypes {
TAG = 'TAG',
}
export type DropItemTag = { uuid: string };
export type DropItem = DropItemTag;
export type DropProps = { isOver: boolean; canDrop: boolean };
type Props = {
tag: SNTag;
tagsState: TagsState;
selectTag: (tag: SNTag) => void;
removeTag: (tag: SNTag) => void;
saveTag: (tag: SNTag, newTitle: string) => void;
appState: TagsListState;
level: number;
};
export type TagsListState = {
readonly selectedTag: SNTag | undefined;
tags: TagsState;
editingTag: SNTag | undefined;
features: FeaturesState;
};
export const TagsListItem: FunctionComponent<Props> = observer(
({ tag, selectTag, saveTag, removeTag, appState }) => {
({ tag, selectTag, saveTag, removeTag, appState, tagsState, level }) => {
const [title, setTitle] = useState(tag.title || '');
const inputRef = useRef<HTMLInputElement>(null);
@@ -28,10 +49,36 @@ export const TagsListItem: FunctionComponent<Props> = observer(
const isEditing = appState.editingTag === tag;
const noteCounts = computed(() => appState.tags.getNotesCount(tag));
const childrenTags = computed(() => tagsState.getChildren(tag)).get();
const hasChildren = childrenTags.length > 0;
const hasFolders = 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]);
const toggleChildren = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
setShowChildren((x) => !x);
},
[setShowChildren]
);
const selectCurrentTag = useCallback(() => {
if (isEditing || isSelected) {
return;
@@ -41,7 +88,8 @@ export const TagsListItem: FunctionComponent<Props> = observer(
const onBlur = useCallback(() => {
saveTag(tag, title);
}, [tag, saveTag, title]);
setTitle(tag.title);
}, [tag, saveTag, title, setTitle]);
const onInput = useCallback(
(e: JSX.TargetedEvent<HTMLInputElement>) => {
@@ -81,56 +129,142 @@ export const TagsListItem: FunctionComponent<Props> = observer(
removeTag(tag);
}, [removeTag, tag]);
const [, dragRef] = useDrag(
() => ({
type: ItemTypes.TAG,
item: { uuid: tag.uuid },
canDrag: () => {
return isNativeFoldersEnabled;
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}),
[tag, hasFolders]
);
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
() => ({
accept: ItemTypes.TAG,
canDrop: (item) => {
return tagsState.isValidTagParent(tag.uuid, item.uuid);
},
drop: (item) => {
if (!hasFolders) {
premiumModal.activate(TAG_FOLDERS_FEATURE_NAME);
return;
}
tagsState.assignParent(item.uuid, tag.uuid);
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}),
[tag, tagsState, hasFolders, premiumModal]
);
const readyToDrop = isOver && canDrop;
return (
<div
className={`tag ${isSelected ? 'selected' : ''}`}
onClick={selectCurrentTag}
>
{!tag.errorDecrypting ? (
<div className="tag-info">
<div className="tag-icon">#</div>
<input
className={`title ${isEditing ? 'editing' : ''}`}
id={`react-tag-${tag.uuid}`}
onBlur={onBlur}
onInput={onInput}
value={title}
onKeyUp={onKeyUp}
spellCheck={false}
ref={inputRef}
/>
<div className="count">{noteCounts.get()}</div>
</div>
) : null}
{tag.conflictOf && (
<div className="danger small-text font-bold">
Conflicted Copy {tag.conflictOf}
</div>
)}
{tag.errorDecrypting && !tag.waitingForKey && (
<div className="danger small-text font-bold">Missing Keys</div>
)}
{tag.errorDecrypting && tag.waitingForKey && (
<div className="info small-text font-bold">Waiting For Keys</div>
)}
{isSelected && (
<div className="menu">
{!isEditing && (
<a className="item" onClick={onClickRename}>
Rename
</a>
<>
<div
className={`tag ${isSelected ? 'selected' : ''} ${
readyToDrop ? 'is-drag-over' : ''
}`}
onClick={selectCurrentTag}
ref={dragRef}
style={{ paddingLeft: `${level * 21 + 10}px` }}
>
{!tag.errorDecrypting ? (
<div className="tag-info" title={title} ref={dropRef}>
{hasFolders && isNativeFoldersEnabled && hasAtLeastOneFolder && (
<div
className={`tag-fold ${showChildren ? 'opened' : 'closed'}`}
onClick={hasChildren ? toggleChildren : undefined}
>
<Icon
className={`color-neutral ${!hasChildren ? 'hidden' : ''}`}
type={
showChildren ? 'menu-arrow-down-alt' : 'menu-arrow-right'
}
/>
</div>
)}
<div
className={`tag-icon ${
isNativeFoldersEnabled ? 'draggable' : ''
} mr-1`}
ref={dragRef}
>
<Icon
type="hashtag"
className={`${isSelected ? 'color-info' : 'color-neutral'}`}
/>
</div>
<input
className={`title ${isEditing ? 'editing' : ''}`}
id={`react-tag-${tag.uuid}`}
onBlur={onBlur}
onInput={onInput}
value={title}
onKeyUp={onKeyUp}
spellCheck={false}
ref={inputRef}
/>
<div className="count">{noteCounts.get()}</div>
</div>
) : null}
<div className={`meta ${hasAtLeastOneFolder ? 'with-folders' : ''}`}>
{tag.conflictOf && (
<div className="danger small-text font-bold">
Conflicted Copy {tag.conflictOf}
</div>
)}
{isEditing && (
<a className="item" onClick={onClickSave}>
Save
</a>
{tag.errorDecrypting && !tag.waitingForKey && (
<div className="danger small-text font-bold">Missing Keys</div>
)}
{tag.errorDecrypting && tag.waitingForKey && (
<div className="info small-text font-bold">Waiting For Keys</div>
)}
{isSelected && (
<div className="menu">
{!isEditing && (
<a className="item" onClick={onClickRename}>
Rename
</a>
)}
{isEditing && (
<a className="item" onClick={onClickSave}>
Save
</a>
)}
<a className="item" onClick={onClickDelete}>
Delete
</a>
</div>
)}
<a className="item" onClick={onClickDelete}>
Delete
</a>
</div>
</div>
{showChildren && (
<>
{childrenTags.map((tag) => {
return (
<TagsListItem
level={level + 1}
key={tag.uuid}
tag={tag}
tagsState={tagsState}
selectTag={selectTag}
saveTag={saveTag}
removeTag={removeTag}
appState={appState}
/>
);
})}
</>
)}
</div>
</>
);
}
);