feat: enable folders by default and remove from experimental features

This commit is contained in:
Mo
2022-02-01 12:31:37 -06:00
parent a2d775235e
commit 70f4dd6353
16 changed files with 127 additions and 265 deletions

View File

@@ -21,8 +21,6 @@ type Props = {
export const Navigation: FunctionComponent<Props> = observer(
({ application }) => {
const appState = useMemo(() => application.getAppState(), [application]);
const enableNativeSmartTagsFeature =
appState.features.enableNativeSmartTagsFeature;
const [ref, setRef] = useState<HTMLDivElement | null>();
const [panelWidth, setPanelWidth] = useState<number>(0);
@@ -39,10 +37,6 @@ export const Navigation: FunctionComponent<Props> = observer(
};
}, [application]);
const onCreateNewTag = useCallback(() => {
appState.tags.createNewTemplate();
}, [appState]);
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.setPreference(PrefKey.TagsPanelWidth, width);
@@ -70,17 +64,6 @@ export const Navigation: FunctionComponent<Props> = observer(
<div className="sk-h3 title">
<span className="sk-bold">Views</span>
</div>
{!enableNativeSmartTagsFeature && (
<div
className="sk-button sk-secondary-contrast wide"
onClick={onCreateNewTag}
title="Create a new tag"
>
<div className="sk-label">
<i className="icon ion-plus add-button" />
</div>
</div>
)}
</div>
</div>
<div className="scrollable">

View File

@@ -164,7 +164,7 @@ export const NotesView: FunctionComponent<Props> = observer(
>
<div className="content">
<div id="notes-title-bar" className="section-title-bar">
<div className="p-4">
<div id="notes-title-bar-container">
<div className="section-title-bar-header">
<div className="sk-h2 font-semibold title">{panelTitle}</div>
<button

View File

@@ -51,7 +51,7 @@ export const PremiumFeaturesModal: FunctionalComponent<Props> = ({
<PremiumIllustration className="mb-2" />
</div>
<div className="text-lg text-center font-bold mb-1">
Enable premium features
Enable Premium Features
</div>
</AlertDialogLabel>
<AlertDialogDescription className="text-sm text-center color-grey-1 px-4.5 mb-2">
@@ -65,7 +65,7 @@ export const PremiumFeaturesModal: FunctionalComponent<Props> = ({
className="w-full rounded no-border py-2 font-bold bg-info color-info-contrast hover:brightness-130 focus:brightness-130 cursor-pointer"
ref={plansButtonRef}
>
See our plans
See Plans
</button>
</div>
</div>

View File

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

@@ -37,7 +37,6 @@ export const SmartTagsListItem: FunctionComponent<Props> = observer(
const level = 0;
const isSelected = tagsState.selected === tag;
const isEditing = tagsState.editingTag === tag;
const isSmartTagsEnabled = features.enableNativeSmartTagsFeature;
useEffect(() => {
setTitle(tag.title || '');
@@ -88,7 +87,7 @@ export const SmartTagsListItem: FunctionComponent<Props> = observer(
tagsState.remove(tag);
}, [tagsState, tag]);
const isFaded = !isSmartTagsEnabled && !tag.isAllTag;
const isFaded = !tag.isAllTag;
const iconType = smartTagIconType(tag);
return (
@@ -104,14 +103,12 @@ export const SmartTagsListItem: FunctionComponent<Props> = observer(
>
{!tag.errorDecrypting ? (
<div className="tag-info">
{isSmartTagsEnabled && (
<div className={`tag-icon mr-1`}>
<Icon
type={iconType}
className={`${isSelected ? 'color-info' : 'color-neutral'}`}
/>
</div>
)}
<div className={`tag-icon mr-1`}>
<Icon
type={iconType}
className={`${isSelected ? 'color-info' : 'color-neutral'}`}
/>
</div>
<input
className={`title ${isEditing ? 'editing' : ''}`}
id={`react-tag-${tag.uuid}`}

View File

@@ -22,7 +22,7 @@ export const TagsList: FunctionComponent<Props> = observer(({ appState }) => {
<DndProvider backend={backend}>
{allTags.length === 0 ? (
<div className="no-tags-placeholder">
No tags. Create one using the add button above.
No tags or folders. Create one using the add button above.
</div>
) : (
<>

View File

@@ -37,7 +37,6 @@ export const TagsListItem: FunctionComponent<Props> = observer(
const hasChildren = childrenTags.length > 0;
const hasFolders = features.hasFolders;
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder;
const premiumModal = usePremiumModal();
@@ -114,13 +113,13 @@ export const TagsListItem: FunctionComponent<Props> = observer(
type: ItemTypes.TAG,
item: { uuid: tag.uuid },
canDrag: () => {
return isNativeFoldersEnabled;
return true;
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}),
[tag, isNativeFoldersEnabled]
[tag]
);
const [{ isOver, canDrop }, dropRef] = useDrop<DropItem, void, DropProps>(
@@ -160,7 +159,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
>
{!tag.errorDecrypting ? (
<div className="tag-info" title={title} ref={dropRef}>
{isNativeFoldersEnabled && hasAtLeastOneFolder && (
{hasAtLeastOneFolder && (
<div
className={`tag-fold ${showChildren ? 'opened' : 'closed'}`}
onClick={hasChildren ? toggleChildren : undefined}
@@ -173,12 +172,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
/>
</div>
)}
<div
className={`tag-icon ${
isNativeFoldersEnabled ? 'draggable' : ''
} mr-1`}
ref={dragRef}
>
<div className={`tag-icon ${'draggable'} mr-1`} ref={dragRef}>
<Icon
type="hashtag"
className={`${isSelected ? 'color-info' : 'color-neutral'}`}

View File

@@ -1,7 +1,9 @@
import { TagsList } from '@/components/Tags/TagsList';
import { AppState } from '@/ui_models/app_state';
import { ApplicationEvent } from '@/__mocks__/@standardnotes/snjs';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { TagsSectionAddButton } from './TagsSectionAddButton';
import { TagsSectionTitle } from './TagsSectionTitle';
@@ -11,11 +13,51 @@ type Props = {
export const TagsSection: FunctionComponent<Props> = observer(
({ appState }) => {
const [hasMigration, setHasMigration] = useState<boolean>(false);
const checkIfMigrationNeeded = useCallback(() => {
setHasMigration(appState.application.hasTagsNeedingFoldersMigration());
}, [appState.application]);
useEffect(() => {
appState.application.addEventObserver(async (event) => {
const events = [
ApplicationEvent.CompletedInitialSync,
ApplicationEvent.SignedIn,
];
if (events.includes(event)) {
checkIfMigrationNeeded();
}
});
}, [appState.application, checkIfMigrationNeeded]);
const runMigration = useCallback(async () => {
if (
await appState.application.alertService.confirm(
'<i>Introducing native, built-in nested tags without requiring the legacy Folders component.</i><br/></br> ' +
" To get started, we'll need to migrate any tags containing a dot character to the new system.<br/></br> " +
' This migration will convert any tags with dots appearing in their name into a natural' +
' hierarchy that is compatible with the new nested tags feature.' +
' Running this migration will remove any "." characters appearing in tag names.',
'New: Folders to Nested Tags',
'Run Migration'
)
) {
appState.application.migrateTagsToFolders().then(() => {
checkIfMigrationNeeded();
});
}
}, [appState.application, checkIfMigrationNeeded]);
return (
<section>
<div className="section-title-bar">
<div className="section-title-bar-header">
<TagsSectionTitle features={appState.features} />
<TagsSectionTitle
features={appState.features}
hasMigration={hasMigration}
onClickMigration={runMigration}
/>
<TagsSectionAddButton
tags={appState.tags}
features={appState.features}

View File

@@ -10,13 +10,7 @@ type Props = {
};
export const TagsSectionAddButton: FunctionComponent<Props> = observer(
({ tags, features }) => {
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
if (!isNativeFoldersEnabled) {
return null;
}
({ tags }) => {
return (
<IconButton
focusable={true}

View File

@@ -11,33 +11,32 @@ import { useCallback } from 'preact/hooks';
type Props = {
features: FeaturesState;
hasMigration: boolean;
onClickMigration: () => void;
};
export const TagsSectionTitle: FunctionComponent<Props> = observer(
({ features }) => {
const isNativeFoldersEnabled = features.enableNativeFoldersFeature;
const hasFolders = features.hasFolders;
({ features, hasMigration, onClickMigration }) => {
const entitledToFolders = 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) {
if (entitledToFolders) {
return (
<>
<div className="sk-h3 title">
<span className="sk-bold">Folders</span>
{hasMigration && (
<label
className="ml-1 sk-bold color-info cursor-pointer"
onClick={onClickMigration}
>
Migration Available
</label>
)}
</div>
</>
);
@@ -52,7 +51,7 @@ export const TagsSectionTitle: FunctionComponent<Props> = observer(
className="ml-1 sk-bold color-grey-2 cursor-pointer"
onClick={showPremiumAlert}
>
&middot; Folders
Folders
</label>
</Tooltip>
</div>

View File

@@ -6,7 +6,6 @@ import { ErrorReporting, Tools, Defaults } from './general-segments';
import { ExtensionsLatestVersions } from '@/preferences/panes/extensions-segments';
import { Advanced } from '@/preferences/panes/account';
import { observer } from 'mobx-react-lite';
import { Migrations } from './general-segments/Migrations';
interface GeneralProps {
appState: AppState;
@@ -20,7 +19,6 @@ export const General: FunctionComponent<GeneralProps> = observer(
<Tools application={application} />
<Defaults application={application} />
<ErrorReporting appState={appState} />
<Migrations application={application} appState={appState} />
<Advanced
application={application}
appState={appState}

View File

@@ -1,103 +0,0 @@
import { Button } from '@/components/Button';
import { Icon } from '@/components/Icon';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { FunctionComponent } from 'preact';
import { useCallback, useState } from 'preact/hooks';
import {
PreferencesGroup,
PreferencesSegment,
Subtitle,
Text,
Title,
} from '../../components';
type Props = {
application: WebApplication;
appState: AppState;
};
export const CheckIcon: React.FC = () => {
return <Icon className="success min-w-4 min-h-4" type="check-bold" />;
};
const Migration3dot0dot0: FunctionComponent<Props> = ({ application }) => {
const [loading, setLoading] = useState(false);
const [complete, setComplete] = useState(false);
const [error, setError] = useState<unknown | null>(null);
const trigger = useCallback(() => {
setLoading(true);
setError(null);
setComplete(false);
application
.migrateTagDotsToHierarchy()
.then(() => {
setLoading(false);
setError(null);
setComplete(true);
})
.catch((error: unknown) => {
setLoading(false);
setError(error);
setComplete(false);
});
}, [application, setLoading, setError]);
return (
<>
<Subtitle>(3.0.0) Folders Component to Native Folders</Subtitle>
<Text>
This migration transform tags with "." in their title into a hierarchy
of parents. This lets your transform tag hierarchies created with the
folder component into native tag folders.
</Text>
<div className="flex flex-row items-center mt-3">
<Button
type="normal"
onClick={trigger}
className="m-2"
disabled={loading}
label="Run Now"
/>
{complete && (
<div className="ml-3">
<Text>Migration successful.</Text>
</div>
)}
{error && (
<div className="ml-3">
<Text>Something wrong happened. Please contact support.</Text>
</div>
)}
</div>
</>
);
};
export const Migrations: FunctionComponent<Props> = ({
application,
appState,
}) => {
const hasNativeFoldersEnabled = appState.features.enableNativeFoldersFeature;
if (!hasNativeFoldersEnabled) {
return null;
}
const hasFoldersFeature = appState.features.hasFolders;
if (!hasFoldersFeature) {
return null;
}
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Migrations</Title>
<div className="h-2 w-full" />
<Migration3dot0dot0 application={application} appState={appState} />
</PreferencesSegment>
</PreferencesGroup>
);
};

View File

@@ -42,8 +42,6 @@ export class FeaturesState {
_hasFolders: observable,
_hasSmartTags: observable,
hasFolders: computed,
enableNativeFoldersFeature: computed,
enableNativeSmartTagsFeature: computed,
_premiumAlertFeatureName: observable,
showPremiumAlert: action,
closePremiumAlert: action,
@@ -71,14 +69,6 @@ export class FeaturesState {
this.unsub();
}
public get enableNativeFoldersFeature(): boolean {
return this.enableUnfinishedFeatures;
}
public get enableNativeSmartTagsFeature(): boolean {
return this.enableUnfinishedFeatures;
}
public get hasFolders(): boolean {
return this._hasFolders;
}
@@ -97,10 +87,6 @@ export class FeaturesState {
}
private hasNativeFolders(): boolean {
if (!this.enableNativeFoldersFeature) {
return false;
}
const status = this.application.getFeatureStatus(
FeatureIdentifier.TagNesting
);
@@ -109,10 +95,6 @@ export class FeaturesState {
}
private hasNativeSmartTags(): boolean {
if (!this.enableNativeSmartTagsFeature) {
return false;
}
const status = this.application.getFeatureStatus(
FeatureIdentifier.SmartFilters
);

View File

@@ -172,10 +172,6 @@ export class TagsState {
}
getChildren(tag: SNTag): SNTag[] {
if (!this.features.enableNativeFoldersFeature) {
return [];
}
if (this.application.isTemplateItem(tag)) {
return [];
}
@@ -235,10 +231,6 @@ export class TagsState {
}
get rootTags(): SNTag[] {
if (!this.features.enableNativeFoldersFeature) {
return this.tags;
}
return this.tags.filter((tag) => !this.application.getTagParent(tag));
}
@@ -355,35 +347,20 @@ export class TagsState {
}
if (isTemplateChange) {
if (this.features.enableNativeSmartTagsFeature) {
const isSmartTagTitle = this.application.isSmartTagTitle(newTitle);
const isSmartTagTitle = this.application.isSmartTagTitle(newTitle);
if (isSmartTagTitle) {
if (!this.features.hasSmartTags) {
await this.features.showPremiumAlert(SMART_TAGS_FEATURE_NAME);
return;
}
if (isSmartTagTitle) {
if (!this.features.hasSmartTags) {
await this.features.showPremiumAlert(SMART_TAGS_FEATURE_NAME);
return;
}
const insertedTag = await this.application.createTagOrSmartTag(
newTitle
);
runInAction(() => {
this.selected = insertedTag as SNTag;
});
} else {
// Legacy code, remove me after we enableNativeSmartTagsFeature for everyone.
// See https://app.asana.com/0/0/1201612665552831/f
const insertedTag = await this.application.insertItem(tag);
const changedTag = await this.application.changeItem<TagMutator>(
insertedTag.uuid,
(m) => {
m.title = newTitle;
}
);
this.selected = changedTag as SNTag;
await this.application.saveItem(insertedTag.uuid);
}
const insertedTag = await this.application.createTagOrSmartTag(newTitle);
this.application.sync();
runInAction(() => {
this.selected = insertedTag as SNTag;
});
} else {
await this.application.changeAndSaveItem<TagMutator>(
tag.uuid,

View File

@@ -5,6 +5,8 @@
height: 100%;
}
$content-horizontal-padding: 16px;
#navigation {
user-select: none;
-moz-user-select: none;
@@ -20,15 +22,15 @@
.section-title-bar {
color: var(--sn-stylekit-secondary-foreground-color);
padding-top: 15px;
padding-top: 0.8125rem;
padding-bottom: 8px;
padding-left: 14px;
padding-right: 14px;
padding-left: $content-horizontal-padding;
padding-right: $content-horizontal-padding;
font-size: 12px;
}
.no-tags-placeholder {
padding: 0px 14px;
padding: 0px $content-horizontal-padding;
font-size: 12px;
opacity: 0.4;
margin-top: -5px;

View File

@@ -25,6 +25,10 @@
flex-direction: column;
}
#notes-title-bar-container {
padding: 0.8125rem;
}
#notes-title-bar {
font-weight: normal;
overflow: visible;
@@ -149,7 +153,7 @@
}
.flag-icons {
padding: .135rem 0;
padding: 0.135rem 0;
&,
& > * {