From 1e493cab6f1bed58f127b38af74f3a8222153cee Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 5 Dec 2022 09:24:31 -0600 Subject: [PATCH] feat: add Super note type to list of note types --- .../src/Domain/Feature/FeatureIdentifier.ts | 2 +- .../src/Domain/Lists/ClientFeatures.ts | 11 ++++- .../src/Domain/Lists/ExperimentalFeatures.ts | 15 +------ .../Services/Features/FeaturesService.spec.ts | 10 ++++- .../lib/Services/Features/FeaturesService.ts | 14 +++--- packages/snjs/package.json | 2 +- .../ChangeEditor/ChangeEditorButton.tsx | 1 + .../ChangeEditor/ChangeEditorMenu.tsx | 10 ++++- .../Header/DisplayOptionsMenu.tsx | 5 ++- .../Components/Menu/MenuRadioButtonItem.tsx | 45 ++++++++++++++++++- .../NotesOptions/EditorMenuItem.tsx | 2 + .../Preferences/Panes/General/General.tsx | 2 +- .../Preferences/Panes/General/Moments.tsx | 2 +- .../Panes/Security/PasscodeLock.tsx | 45 ++++++++++--------- .../PremiumFeaturesModal.tsx | 2 +- .../src/javascripts/Constants/Constants.ts | 4 +- packages/web/src/javascripts/FeatureTrunk.ts | 2 +- .../Utils/DropdownItemsForEditors.ts | 16 +++---- .../Items/Icons/getIconAndTintForNoteType.ts | 7 ++- .../Utils/createEditorMenuGroups.ts | 38 ++++++++-------- 20 files changed, 148 insertions(+), 87 deletions(-) diff --git a/packages/features/src/Domain/Feature/FeatureIdentifier.ts b/packages/features/src/Domain/Feature/FeatureIdentifier.ts index 26604b3117a..990fee0b5ef 100644 --- a/packages/features/src/Domain/Feature/FeatureIdentifier.ts +++ b/packages/features/src/Domain/Feature/FeatureIdentifier.ts @@ -51,4 +51,4 @@ export enum FeatureIdentifier { */ export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe' -export const ExperimentalFeatures = [FeatureIdentifier.SuperEditor] +export const ExperimentalFeatures = [] diff --git a/packages/features/src/Domain/Lists/ClientFeatures.ts b/packages/features/src/Domain/Lists/ClientFeatures.ts index e081009c2b6..ff404c96bc8 100644 --- a/packages/features/src/Domain/Lists/ClientFeatures.ts +++ b/packages/features/src/Domain/Lists/ClientFeatures.ts @@ -1,7 +1,7 @@ import { ClientFeatureDescription } from '../Feature/FeatureDescription' import { PermissionName } from '../Permission/PermissionName' import { FeatureIdentifier } from '../Feature/FeatureIdentifier' -import { SubscriptionName } from '@standardnotes/common' +import { RoleName, SubscriptionName } from '@standardnotes/common' export function clientFeatures(): ClientFeatureDescription[] { return [ @@ -12,6 +12,15 @@ export function clientFeatures(): ClientFeatureDescription[] { permission_name: PermissionName.TagNesting, description: 'Organize your tags into folders.', }, + { + name: 'Super Notes', + identifier: FeatureIdentifier.SuperEditor, + availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + permission_name: PermissionName.SuperEditor, + description: + 'Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.', + availableInRoles: [RoleName.PlusUser, RoleName.ProUser], + }, { availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], name: 'Smart Filters', diff --git a/packages/features/src/Domain/Lists/ExperimentalFeatures.ts b/packages/features/src/Domain/Lists/ExperimentalFeatures.ts index 9403655e64a..30c3ebf5afd 100644 --- a/packages/features/src/Domain/Lists/ExperimentalFeatures.ts +++ b/packages/features/src/Domain/Lists/ExperimentalFeatures.ts @@ -1,18 +1,5 @@ -import { FeatureIdentifier } from './../Feature/FeatureIdentifier' -import { RoleName, SubscriptionName } from '@standardnotes/common' import { FeatureDescription } from '../Feature/FeatureDescription' -import { PermissionName } from '../Permission/PermissionName' export function experimentalFeatures(): FeatureDescription[] { - const superEditor: FeatureDescription = { - name: 'Super Notes', - identifier: FeatureIdentifier.SuperEditor, - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], - permission_name: PermissionName.SuperEditor, - description: - 'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.', - availableInRoles: [RoleName.PlusUser, RoleName.ProUser], - } - - return [superEditor] + return [] } diff --git a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts index 75e12f7d20e..e6beb749579 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts @@ -197,7 +197,7 @@ describe('featuresService', () => { describe('loadUserRoles()', () => { it('retrieves user roles and features from storage', async () => { - await createService().initializeFromDisk() + createService().initializeFromDisk() expect(storageService.getValue).toHaveBeenCalledWith(StorageKey.UserRoles, undefined, []) expect(storageService.getValue).toHaveBeenCalledWith(StorageKey.UserFeatures, undefined, []) }) @@ -576,6 +576,14 @@ describe('featuresService', () => { expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan) }) + it('availableInRoles-based features', async () => { + const featuresService = createService() + + await featuresService.updateRolesAndFetchFeatures('123', [RoleName.ProUser]) + + expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled) + }) + it('third party feature status', async () => { const featuresService = createService() diff --git a/packages/snjs/lib/Services/Features/FeaturesService.ts b/packages/snjs/lib/Services/Features/FeaturesService.ts index 833362a3714..c5dfbb6c5bc 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.ts @@ -506,13 +506,11 @@ export class SNFeaturesService return FeatureStatus.Entitled } - if (this.isExperimentalFeature(featureId)) { - const nativeFeature = FeaturesImports.FindNativeFeature(featureId) - if (nativeFeature) { - const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role)) - if (hasRole) { - return FeatureStatus.Entitled - } + const nativeFeature = FeaturesImports.FindNativeFeature(featureId) + if (nativeFeature && nativeFeature.availableInRoles) { + const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role)) + if (hasRole) { + return FeatureStatus.Entitled } } @@ -525,7 +523,7 @@ export class SNFeaturesService } } - const isThirdParty = FeaturesImports.FindNativeFeature(featureId) == undefined + const isThirdParty = nativeFeature == undefined if (isThirdParty) { const component = this.itemManager .getDisplayableComponents() diff --git a/packages/snjs/package.json b/packages/snjs/package.json index d327af7281b..4a3f9f20e95 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -28,7 +28,7 @@ "lint:eslint": "eslint --ext .ts lib/", "lint:fix": "eslint --fix --ext .ts lib/", "lint:tsc": "tsc --noEmit --emitDeclarationOnly false --project lib/tsconfig.json", - "test": "jest spec --coverage", + "test": "jest --coverage", "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand" }, "devDependencies": { diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index ce390953a9c..9fbdfde631c 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -28,6 +28,7 @@ const ChangeEditorButton: FunctionComponent = ({ }) const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType( note?.noteType || selectedEditor?.package_info.note_type, + true, ) const [isClickOutsideDisabled, setIsClickOutsideDisabled] = useState(false) diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 7689fa08e08..7732e26c73f 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -12,6 +12,7 @@ import { reloadFont } from '../NoteView/FontFunctions' import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon' import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter' import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem' +import { Pill } from '../Preferences/PreferencesComponents/Content' type ChangeEditorMenuProps = { application: WebApplication @@ -188,17 +189,24 @@ const ChangeEditorMenu: FunctionComponent = ({ const onClickEditorItem = () => { selectItem(item).catch(console.error) } + return (
{group.icon && } {item.name} + {item.isLabs && ( + + Labs + + )}
{!item.isEntitled && ( diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index 8c9842a3cb2..5f95a96e5f3 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -22,6 +22,7 @@ import { classNames } from '@standardnotes/utils' import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscriptionBanner' import MenuRadioButtonItem from '@/Components/Menu/MenuRadioButtonItem' import MenuSwitchButtonItem from '@/Components/Menu/MenuSwitchButtonItem' +import { Pill } from '@/Components/Preferences/PreferencesComponents/Content' const DailyEntryModeEnabled = true @@ -365,9 +366,9 @@ const DisplayOptionsMenu: FunctionComponent = ({
Daily Notebook
-
+ Labs -
+
Capture new notes daily with a calendar-based layout
diff --git a/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx b/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx index 24121210105..14bcfdacdb4 100644 --- a/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx +++ b/packages/web/src/javascripts/Components/Menu/MenuRadioButtonItem.tsx @@ -1,20 +1,60 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { classNames } from '@standardnotes/snjs' import { PlatformedKeyboardShortcut } from '@standardnotes/ui-services' -import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, ReactNode } from 'react' +import { + ComponentPropsWithoutRef, + ForwardedRef, + forwardRef, + MouseEventHandler, + ReactNode, + useCallback, + useState, +} from 'react' +import Icon from '../Icon/Icon' import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' import RadioIndicator from '../Radio/RadioIndicator' import MenuListItem from './MenuListItem' +const Tooltip = ({ text }: { text: string }) => { + const [mobileVisible, setMobileVisible] = useState(false) + const onClickMobile: MouseEventHandler = useCallback( + (event) => { + event.preventDefault() + event.stopPropagation() + setMobileVisible(!mobileVisible) + }, + [mobileVisible], + ) + + return ( +
+
+ + Note sync status +
+
+ {text} +
+
+ ) +} + type Props = { checked: boolean children: ReactNode shortcut?: PlatformedKeyboardShortcut + info?: string } & ComponentPropsWithoutRef<'button'> const MenuRadioButtonItem = forwardRef( ( - { checked, disabled, tabIndex, children, shortcut, className, ...props }: Props, + { checked, disabled, tabIndex, children, shortcut, className, info, ...props }: Props, ref: ForwardedRef, ) => { return ( @@ -37,6 +77,7 @@ const MenuRadioButtonItem = forwardRef( {shortcut && } {children} + {info && } ) diff --git a/packages/web/src/javascripts/Components/NotesOptions/EditorMenuItem.tsx b/packages/web/src/javascripts/Components/NotesOptions/EditorMenuItem.tsx index 78e257c670b..ca4bc6f4671 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/EditorMenuItem.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/EditorMenuItem.tsx @@ -5,4 +5,6 @@ export type EditorMenuItem = { component?: SNComponent isEntitled: boolean noteType: NoteType + isLabs?: boolean + description?: string } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx index e7356b6ee00..18432e14824 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx @@ -24,8 +24,8 @@ const General: FunctionComponent = ({ viewControllerManager, application, - + = ({ application }: Props) => {
Moments - Labs + Labs Professional
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/PasscodeLock.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/PasscodeLock.tsx index 4a50b9f9a9f..19d9ef1ac6a 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/PasscodeLock.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/PasscodeLock.tsx @@ -58,7 +58,7 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => { } const reloadDesktopAutoLockInterval = useCallback(async () => { - const interval = await application.getAutolockService()!.getAutoLockInterval() + const interval = await application.getAutolockService()?.getAutoLockInterval() setSelectedAutoLockInterval(interval) }, [application]) @@ -86,7 +86,7 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => { return } - await application.getAutolockService()!.setAutoLockInterval(interval) + await application.getAutolockService()?.setAutoLockInterval(interval) reloadDesktopAutoLockInterval().catch(console.error) } @@ -184,6 +184,12 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => { setPasscodeConfirmation(undefined) } + const autolockService = application.getAutolockService() + + if (!autolockService) { + return null + } + return ( <> @@ -248,25 +254,22 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => { Autolock The autolock timer begins when the window or tab loses focus.
- {application - .getAutolockService()! - .getAutoLockIntervalOptions() - .map((option) => { - return ( - selectDesktopAutoLockInterval(option.value)} - > - {option.label} - - ) - })} + {autolockService.getAutoLockIntervalOptions().map((option) => { + return ( + selectDesktopAutoLockInterval(option.value)} + > + {option.label} + + ) + })}
diff --git a/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx b/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx index 40092de239c..6316ea26da2 100644 --- a/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx +++ b/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx @@ -31,7 +31,7 @@ const PremiumFeaturesModal: FunctionComponent = ({ return ( -
+
{type === PremiumFeatureModalType.UpgradePrompt && ( = { +const FeatureTrunkStatus: Record = { [FeatureTrunkName.Super]: isDev && true, } diff --git a/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts b/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts index ad1ac2a2676..85073731340 100644 --- a/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts +++ b/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts @@ -7,6 +7,7 @@ import { DropdownItem } from '@/Components/Dropdown/DropdownItem' export type EditorOption = DropdownItem & { value: FeatureIdentifier + isLabs?: boolean } export function noteTypeForEditorOptionValue(value: EditorOption['value'], application: WebApplication): NoteType { @@ -45,14 +46,13 @@ export function getDropdownItemsForAllEditors(application: WebApplication): Edit options.push(plaintextOption) - if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) { - options.push({ - icon: SuperEditorMetadata.icon, - iconClassName: SuperEditorMetadata.iconClassName, - label: SuperEditorMetadata.name, - value: FeatureIdentifier.SuperEditor, - }) - } + options.push({ + icon: SuperEditorMetadata.icon, + iconClassName: SuperEditorMetadata.iconClassName, + label: SuperEditorMetadata.name, + value: FeatureIdentifier.SuperEditor, + isLabs: true, + }) options.sort((a, b) => { return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1 diff --git a/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts b/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts index f37e0365d0b..e94fee989a6 100644 --- a/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts +++ b/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts @@ -2,7 +2,7 @@ import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants' import { NoteType } from '@standardnotes/features' import { IconType } from '@standardnotes/models' -export function getIconAndTintForNoteType(noteType?: NoteType): [IconType, number] { +export function getIconAndTintForNoteType(noteType?: NoteType, subtle?: boolean): [IconType, number] { switch (noteType) { case NoteType.RichText: return ['rich-text', 1] @@ -17,7 +17,10 @@ export function getIconAndTintForNoteType(noteType?: NoteType): [IconType, numbe case NoteType.Code: return ['code', 4] case NoteType.Super: - return [SuperEditorMetadata.icon, SuperEditorMetadata.iconTintNumber] + return [ + subtle ? (SuperEditorMetadata.subtleIcon as IconType) : SuperEditorMetadata.icon, + SuperEditorMetadata.iconTintNumber, + ] default: return [PlainEditorMetadata.icon, PlainEditorMetadata.iconTintNumber] } diff --git a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts index 2daf70f4cce..8c5ca46b7b6 100644 --- a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts +++ b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts @@ -72,7 +72,7 @@ const insertInstalledComponentsInMap = ( }) } -const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, application: WebApplication): EditorMenuGroup[] => { +const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, _application: WebApplication): EditorMenuGroup[] => { const groups: EditorMenuGroup[] = [ { icon: 'plain-text', @@ -80,6 +80,13 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, application: WebAppli title: 'Plain text', items: map[NoteType.Plain], }, + { + icon: SuperEditorMetadata.icon, + iconClassName: SuperEditorMetadata.iconClassName, + title: SuperEditorMetadata.name, + items: map[NoteType.Super], + featured: true, + }, { icon: 'rich-text', iconClassName: 'text-accessory-tint-1', @@ -124,16 +131,6 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, application: WebAppli }, ] - if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) { - groups.splice(1, 0, { - icon: SuperEditorMetadata.icon, - iconClassName: SuperEditorMetadata.iconClassName, - title: SuperEditorMetadata.name, - items: map[NoteType.Super], - featured: true, - }) - } - return groups } @@ -146,7 +143,16 @@ const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap noteType: NoteType.Plain, }, ], - [NoteType.Super]: [], + [NoteType.Super]: [ + { + name: SuperEditorMetadata.name, + isEntitled: application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled, + noteType: NoteType.Super, + isLabs: true, + description: + 'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.', + }, + ], [NoteType.RichText]: [], [NoteType.Markdown]: [], [NoteType.Task]: [], @@ -156,14 +162,6 @@ const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap [NoteType.Unknown]: [], } - if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) { - map[NoteType.Super].push({ - name: SuperEditorMetadata.name, - isEntitled: true, - noteType: NoteType.Super, - }) - } - return map }