feat: add Super note type to list of note types (#2086)

This commit is contained in:
Mo
2022-12-05 09:38:42 -06:00
committed by GitHub
parent 1d22365086
commit caf2c4a876
20 changed files with 148 additions and 87 deletions

View File

@@ -51,4 +51,4 @@ export enum FeatureIdentifier {
*/
export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe'
export const ExperimentalFeatures = [FeatureIdentifier.SuperEditor]
export const ExperimentalFeatures = []

View File

@@ -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',

View File

@@ -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 []
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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": {

View File

@@ -28,6 +28,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
})
const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType(
note?.noteType || selectedEditor?.package_info.note_type,
true,
)
const [isClickOutsideDisabled, setIsClickOutsideDisabled] = useState(false)

View File

@@ -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<ChangeEditorMenuProps> = ({
const onClickEditorItem = () => {
selectItem(item).catch(console.error)
}
return (
<MenuRadioButtonItem
key={item.name}
onClick={onClickEditorItem}
className={'flex-row-reverse py-2'}
className={'flex-row-reversed py-2'}
checked={item.isEntitled ? isSelected(item) : false}
info={item.description}
>
<div className="flex flex-grow items-center justify-between">
<div className={`flex items-center ${group.featured ? 'font-bold' : ''}`}>
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
{item.name}
{item.isLabs && (
<Pill className="py-0.5 px-1.5" style="success">
Labs
</Pill>
)}
</div>
{!item.isEntitled && (
<Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />

View File

@@ -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<DisplayOptionsMenuProps> = ({
<div className="flex flex-col pr-5">
<div className="flex flex-row items-center">
<div className="text-base font-semibold uppercase text-text lg:text-xs">Daily Notebook</div>
<div className="ml-2 rounded bg-warning px-1.5 py-[1px] text-[10px] font-bold text-warning-contrast">
<Pill className="py-0 px-1.5" style="success">
Labs
</div>
</Pill>
</div>
<div className="mt-1">Capture new notes daily with a calendar-based layout</div>
</div>

View File

@@ -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<HTMLDivElement> = useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
setMobileVisible(!mobileVisible)
},
[mobileVisible],
)
return (
<div className="relative">
<div className={classNames('peer flex h-5 w-5 items-center justify-center rounded-full')} onClick={onClickMobile}>
<Icon type={'help'} className="text-neutral" size="large" />
<span className="sr-only">Note sync status</span>
</div>
<div
className={classNames(
'hidden',
'absolute top-full right-0 w-60 translate-x-2 translate-y-1 select-none rounded border border-border shadow-main',
'bg-default py-1.5 px-3 text-left peer-hover:block peer-focus:block',
)}
>
{text}
</div>
</div>
)
}
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<HTMLButtonElement>,
) => {
return (
@@ -37,6 +77,7 @@ const MenuRadioButtonItem = forwardRef(
{shortcut && <KeyboardShortcutIndicator className="mr-2" shortcut={shortcut} />}
<RadioIndicator disabled={disabled} checked={checked} className="flex-shrink-0" />
{children}
{info && <Tooltip text={info} />}
</button>
</MenuListItem>
)

View File

@@ -5,4 +5,6 @@ export type EditorMenuItem = {
component?: SNComponent
isEntitled: boolean
noteType: NoteType
isLabs?: boolean
description?: string
}

View File

@@ -24,8 +24,8 @@ const General: FunctionComponent<Props> = ({ viewControllerManager, application,
<Defaults application={application} />
<Tools application={application} />
<SmartViews application={application} featuresController={viewControllerManager.featuresController} />
<LabsPane application={application} />
<Moments application={application} />
<LabsPane application={application} />
<Advanced
application={application}
viewControllerManager={viewControllerManager}

View File

@@ -80,7 +80,7 @@ const Moments: FunctionComponent<Props> = ({ application }: Props) => {
<div className="flex items-center justify-between">
<div className="flex items-start">
<Title>Moments</Title>
<Pill style={'warning'}>Labs</Pill>
<Pill style={'success'}>Labs</Pill>
<Pill style={'info'}>Professional</Pill>
</div>
<Switch onChange={toggle} checked={momentsEnabled} />

View File

@@ -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 (
<>
<PreferencesGroup>
@@ -248,25 +254,22 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => {
<Title>Autolock</Title>
<Text className="mb-3">The autolock timer begins when the window or tab loses focus.</Text>
<div className="flex flex-row items-center">
{application
.getAutolockService()!
.getAutoLockIntervalOptions()
.map((option) => {
return (
<a
key={option.value}
className={classNames(
'mr-3 cursor-pointer rounded',
option.value === selectedAutoLockInterval
? 'bg-info px-1.5 py-0.5 text-info-contrast'
: 'text-info',
)}
onClick={() => selectDesktopAutoLockInterval(option.value)}
>
{option.label}
</a>
)
})}
{autolockService.getAutoLockIntervalOptions().map((option) => {
return (
<a
key={option.value}
className={classNames(
'mr-3 cursor-pointer rounded',
option.value === selectedAutoLockInterval
? 'bg-info px-1.5 py-0.5 text-info-contrast'
: 'text-info',
)}
onClick={() => selectDesktopAutoLockInterval(option.value)}
>
{option.label}
</a>
)
})}
</div>
</PreferencesSegment>
</PreferencesGroup>

View File

@@ -31,7 +31,7 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
return (
<AlertDialog leastDestructiveRef={ctaButtonRef} className="p-0">
<div tabIndex={-1} className="sn-component">
<div tabIndex={-1} className="sn-component bg-default">
<div tabIndex={0} className="max-w-89 rounded bg-default p-4 shadow-main">
{type === PremiumFeatureModalType.UpgradePrompt && (
<UpgradePrompt

View File

@@ -29,6 +29,7 @@ export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
type EditorMetadata = {
name: string
icon: IconType
subtleIcon?: IconType
iconClassName: string
iconTintNumber: number
}
@@ -36,8 +37,9 @@ type EditorMetadata = {
export const SuperEditorMetadata: EditorMetadata = {
name: 'Super',
icon: 'file-doc',
subtleIcon: 'format-align-left',
iconClassName: 'text-accessory-tint-4',
iconTintNumber: 4,
iconTintNumber: 1,
}
export const PlainEditorMetadata: EditorMetadata = {

View File

@@ -4,7 +4,7 @@ export enum FeatureTrunkName {
Super,
}
export const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
[FeatureTrunkName.Super]: isDev && true,
}

View File

@@ -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

View File

@@ -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]
}

View File

@@ -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
}