Merge branch 'release/10.1.0'
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"parserOptions": {
|
||||
"project": "./app/assets/javascripts/tsconfig.json"
|
||||
},
|
||||
"ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js"],
|
||||
"ignorePatterns": [".eslintrc.js", "webpack.*.js", "webpack-defaults.js", "jest.config.js"],
|
||||
"rules": {
|
||||
"standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals
|
||||
"no-throw-literal": 0,
|
||||
|
||||
13
.github/workflows/git-sync.yml
vendored
Normal file
13
.github/workflows/git-sync.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
on: push
|
||||
jobs:
|
||||
git-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: git-sync
|
||||
uses: wei/git-sync@v3
|
||||
with:
|
||||
source_repo: "standardnotes/web"
|
||||
source_branch: "refs/remotes/source/*"
|
||||
destination_repo: "standardnotes/private-web"
|
||||
destination_branch: "refs/heads/*"
|
||||
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
|
||||
.eslintcache
|
||||
|
||||
# OS & IDE
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# npx pretty-quick --staged # trying lint-staged for now, it is slower but uses eslint.
|
||||
npx lint-staged
|
||||
4
app/assets/icons/ic-menu-close.svg
Normal file
4
app/assets/icons/ic-menu-close.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M17.5 13.0083L16.325 14.1667L12.15 10L16.325 5.83333L17.5 6.99167L14.5333 10L17.5 13.0083ZM2.5 5H13.3333V6.66667H2.5V5ZM2.5 10.8333V9.16667H10.8333V10.8333H2.5ZM2.5 15V13.3333H13.3333V15H2.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
6
app/assets/icons/ic-premium-feature.svg
Normal file
6
app/assets/icons/ic-premium-feature.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.36" x="1" y="1" width="18" height="18" rx="9" fill="#BBBEC4" />
|
||||
<path
|
||||
d="M6.6665 12H7.99984C7.99984 12.72 8.91317 13.3333 9.99984 13.3333C11.0865 13.3333 11.9998 12.72 11.9998 12C11.9998 11.2667 11.3065 11 9.83984 10.6467C8.4265 10.2933 6.6665 9.85333 6.6665 8C6.6665 6.80667 7.6465 5.79333 8.99984 5.45333V4H10.9998V5.45333C12.3532 5.79333 13.3332 6.80667 13.3332 8H11.9998C11.9998 7.28 11.0865 6.66667 9.99984 6.66667C8.91317 6.66667 7.99984 7.28 7.99984 8C7.99984 8.73333 8.69317 9 10.1598 9.35333C11.5732 9.70667 13.3332 10.1467 13.3332 12C13.3332 13.1933 12.3532 14.2067 10.9998 14.5467V16H8.99984V14.5467C7.6465 14.2067 6.6665 13.1933 6.6665 12Z"
|
||||
fill="#72767E" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 779 B |
@@ -78,8 +78,10 @@ import { PreferencesDirective } from './preferences';
|
||||
import { AppVersion, IsWebPlatform } from '@/version';
|
||||
import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
|
||||
import { PurchaseFlowDirective } from './purchaseFlow';
|
||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
|
||||
import { ComponentViewDirective } from '@/components/ComponentView';
|
||||
import { TagsListDirective } from '@/components/TagsList';
|
||||
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
@@ -181,8 +183,10 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('notesListOptionsMenu', NotesListOptionsDirective)
|
||||
.directive('icon', IconDirective)
|
||||
.directive('noteTagsContainer', NoteTagsContainerDirective)
|
||||
.directive('tags', TagsListDirective)
|
||||
.directive('preferences', PreferencesDirective)
|
||||
.directive('purchaseFlow', PurchaseFlowDirective);
|
||||
.directive('purchaseFlow', PurchaseFlowDirective)
|
||||
.directive('pinNoteButton', PinNoteButtonDirective);
|
||||
|
||||
// Filters
|
||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||
|
||||
@@ -34,7 +34,6 @@ const avoidFlickerTimeout = 7;
|
||||
export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
({
|
||||
application,
|
||||
appState,
|
||||
onLoad,
|
||||
componentUuid,
|
||||
templateComponent
|
||||
@@ -45,7 +44,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
const [isIssueOnLoading, setIsIssueOnLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
const [loadTimeout, setLoadTimeout] = useState<number | undefined>(undefined);
|
||||
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const [featureStatus, setFeatureStatus] = useState<FeatureStatus | undefined>(FeatureStatus.Entitled);
|
||||
const [isComponentValid, setIsComponentValid] = useState(true);
|
||||
const [error, setError] = useState<'offline-restricted' | 'url-missing' | undefined>(undefined);
|
||||
@@ -158,7 +157,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout && clearTimeout(loadTimeout);
|
||||
await application.componentManager.registerComponentWindow(
|
||||
component,
|
||||
iframe.contentWindow!
|
||||
@@ -194,6 +193,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
|
||||
useEffect(() => {
|
||||
reloadStatus();
|
||||
|
||||
if (!iframeRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -250,7 +250,7 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
||||
onVisibilityChange
|
||||
);
|
||||
};
|
||||
}, [appState, application, component, componentUuid, onVisibilityChange, reloadStatus, templateComponent]);
|
||||
}, [application, component, componentUuid, onVisibilityChange, templateComponent]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set/update `component` based on `componentUuid` prop.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PremiumFeatureIcon from '../../icons/ic-premium-feature.svg';
|
||||
import PencilOffIcon from '../../icons/ic-pencil-off.svg';
|
||||
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
|
||||
import RichTextIcon from '../../icons/ic-text-rich.svg';
|
||||
@@ -15,6 +16,7 @@ import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
||||
import MoreIcon from '../../icons/ic-more.svg';
|
||||
import TuneIcon from '../../icons/ic-tune.svg';
|
||||
import MenuArrowDownIcon from '../../icons/ic-menu-arrow-down.svg';
|
||||
import MenuCloseIcon from '../../icons/ic-menu-close.svg';
|
||||
import AuthenticatorIcon from '../../icons/ic-authenticator.svg';
|
||||
import SpreadsheetsIcon from '../../icons/ic-spreadsheets.svg';
|
||||
import TasksIcon from '../../icons/ic-tasks.svg';
|
||||
@@ -107,7 +109,9 @@ const ICONS = {
|
||||
'check-bold': CheckBoldIcon,
|
||||
'account-circle': AccountCircleIcon,
|
||||
'menu-arrow-down': MenuArrowDownIcon,
|
||||
window: WindowIcon
|
||||
'menu-close': MenuCloseIcon,
|
||||
window: WindowIcon,
|
||||
'premium-feature': PremiumFeatureIcon
|
||||
};
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
|
||||
@@ -4,6 +4,7 @@ import NotesIcon from '../../icons/il-notes.svg';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptionsPanel } from './NotesOptionsPanel';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PinNoteButton } from './PinNoteButton';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
@@ -17,13 +18,16 @@ const MultipleSelectedNotes = observer(({ application, appState }: Props) => {
|
||||
<div className="flex flex-col h-full items-center">
|
||||
<div className="flex items-center justify-between p-4 w-full">
|
||||
<h1 className="sk-h1 font-bold m-0">{count} selected notes</h1>
|
||||
<NotesOptionsPanel application={application} appState={appState} />
|
||||
<div className="flex">
|
||||
<div className="mr-3">
|
||||
<PinNoteButton appState={appState} />
|
||||
</div>
|
||||
<NotesOptionsPanel application={application} appState={appState} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col justify-center items-center w-full max-w-md">
|
||||
<NotesIcon className="block" />
|
||||
<h2 className="text-lg m-0 text-center mt-4">
|
||||
{count} selected notes
|
||||
</h2>
|
||||
<h2 className="text-lg m-0 text-center mt-4">{count} selected notes</h2>
|
||||
<p className="text-sm mt-2 text-center max-w-60">
|
||||
Actions will be performed on all selected notes.
|
||||
</p>
|
||||
|
||||
38
app/assets/javascripts/components/PinNoteButton.tsx
Normal file
38
app/assets/javascripts/components/PinNoteButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Icon } from './Icon';
|
||||
import { toDirective } from './utils';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const PinNoteButton: FunctionComponent<Props> = observer(
|
||||
({ appState, className = '' }) => {
|
||||
const notes = Object.values(appState.notes.selectedNotes);
|
||||
const pinned = notes.some((note) => note.pinned);
|
||||
|
||||
const togglePinned = () => {
|
||||
if (!pinned) {
|
||||
appState.notes.setPinSelectedNotes(true);
|
||||
} else {
|
||||
appState.notes.setPinSelectedNotes(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`sn-icon-button ${pinned ? 'toggled' : ''} ${className}`}
|
||||
onClick={togglePinned}
|
||||
>
|
||||
<VisuallyHidden>Pin selected notes</VisuallyHidden>
|
||||
<Icon type="pin" className="block" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const PinNoteButtonDirective = toDirective<Props>(PinNoteButton);
|
||||
75
app/assets/javascripts/components/PremiumFeaturesModal.tsx
Normal file
75
app/assets/javascripts/components/PremiumFeaturesModal.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { Icon } from './Icon';
|
||||
import PremiumIllustration from '../../svg/il-premium.svg';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
featureName: string;
|
||||
onClose: () => void;
|
||||
showModal: boolean;
|
||||
};
|
||||
|
||||
export const PremiumFeaturesModal: FunctionalComponent<Props> = ({
|
||||
featureName,
|
||||
onClose,
|
||||
showModal,
|
||||
}) => {
|
||||
const plansButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const onClickPlans = () => {
|
||||
if (window._plans_url) {
|
||||
window.location.assign(window._plans_url);
|
||||
}
|
||||
};
|
||||
|
||||
return showModal ? (
|
||||
<AlertDialog leastDestructiveRef={plansButtonRef}>
|
||||
<div tabIndex={-1} className="sn-component">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="max-w-89 bg-default rounded shadow-overlay p-4"
|
||||
>
|
||||
<AlertDialogLabel>
|
||||
<div className="flex justify-end p-1">
|
||||
<button
|
||||
className="flex p-0 cursor-pointer bg-transparent border-0"
|
||||
onClick={onClose}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<Icon className="color-neutral" type="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center p-1"
|
||||
aria-hidden={true}
|
||||
>
|
||||
<PremiumIllustration className="mb-2" />
|
||||
</div>
|
||||
<div className="text-lg text-center font-bold mb-1">
|
||||
Enable premium features
|
||||
</div>
|
||||
</AlertDialogLabel>
|
||||
<AlertDialogDescription className="text-sm text-center color-grey-1 px-4.5 mb-2">
|
||||
In order to use <span className="font-semibold">{featureName}</span>{' '}
|
||||
and other premium features, please purchase a subscription or
|
||||
upgrade your current plan.
|
||||
</AlertDialogDescription>
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={onClickPlans}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
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 { JSXInternal } from 'preact/src/jsx';
|
||||
import { Icon } from '../Icon';
|
||||
import { PremiumFeaturesModal } from '../PremiumFeaturesModal';
|
||||
import { Switch } from '../Switch';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
closeQuickSettingsMenu: () => void;
|
||||
focusModeEnabled: boolean;
|
||||
setFocusModeEnabled: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export const FocusModeSwitch: FunctionComponent<Props> = ({
|
||||
application,
|
||||
closeQuickSettingsMenu,
|
||||
focusModeEnabled,
|
||||
setFocusModeEnabled,
|
||||
}) => {
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
const isEntitledToFocusMode =
|
||||
application.getFeatureStatus(FeatureIdentifier.FocusMode) ===
|
||||
FeatureStatus.Entitled;
|
||||
|
||||
const toggleFocusMode = (
|
||||
e: JSXInternal.TargetedMouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
e.preventDefault();
|
||||
if (isEntitledToFocusMode) {
|
||||
setFocusModeEnabled(!focusModeEnabled);
|
||||
closeQuickSettingsMenu();
|
||||
} else {
|
||||
setShowUpgradeModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none justify-between"
|
||||
onClick={toggleFocusMode}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="menu-close" className="color-neutral mr-2" />
|
||||
Focused Writing
|
||||
</div>
|
||||
{isEntitledToFocusMode ? (
|
||||
<Switch className="px-0" checked={focusModeEnabled} />
|
||||
) : (
|
||||
<div title="Premium feature">
|
||||
<Icon type="premium-feature" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<PremiumFeaturesModal
|
||||
showModal={showUpgradeModal}
|
||||
featureName="Focus Mode"
|
||||
onClose={() => setShowUpgradeModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -15,81 +15,48 @@ import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { Icon } from './Icon';
|
||||
import { Switch } from './Switch';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
import { Icon } from '../Icon';
|
||||
import { Switch } from '../Switch';
|
||||
import { toDirective, useCloseOnBlur } from '../utils';
|
||||
import {
|
||||
quickSettingsKeyDownHandler,
|
||||
themesMenuKeyDownHandler,
|
||||
} from './eventHandlers';
|
||||
import { FocusModeSwitch } from './FocusModeSwitch';
|
||||
import { ThemesMenuButton } from './ThemesMenuButton';
|
||||
|
||||
const focusModeAnimationDuration = 1255;
|
||||
|
||||
const MENU_CLASSNAME =
|
||||
'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto';
|
||||
|
||||
type ThemeButtonProps = {
|
||||
theme: SNTheme;
|
||||
application: WebApplication;
|
||||
onBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
type MenuProps = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const ThemeButton: FunctionComponent<ThemeButtonProps> = ({
|
||||
application,
|
||||
theme,
|
||||
onBlur,
|
||||
}) => {
|
||||
const toggleTheme = (e: any) => {
|
||||
e.preventDefault();
|
||||
if (theme.isLayerable() || !theme.active) {
|
||||
application.toggleComponent(theme);
|
||||
const toggleFocusMode = (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
document.body.classList.add('focus-mode');
|
||||
} else {
|
||||
if (document.body.classList.contains('focus-mode')) {
|
||||
document.body.classList.add('disable-focus-mode');
|
||||
document.body.classList.remove('focus-mode');
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('disable-focus-mode');
|
||||
}, focusModeAnimationDuration);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${
|
||||
theme.isLayerable() ? `justify-start` : `justify-between`
|
||||
}`}
|
||||
onClick={toggleTheme}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{theme.isLayerable() ? (
|
||||
<>
|
||||
<Switch
|
||||
className="px-0 mr-2"
|
||||
checked={theme.active}
|
||||
onChange={toggleTheme}
|
||||
/>
|
||||
{theme.package_info.name}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`pseudo-radio-btn ${
|
||||
theme.active ? 'pseudo-radio-btn--checked' : ''
|
||||
} mr-2`}
|
||||
></div>
|
||||
<span className={theme.active ? 'font-semibold' : undefined}>
|
||||
{theme.package_info.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-5 h-5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: theme.package_info?.dock_icon?.background_color,
|
||||
}}
|
||||
></div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
({ application, appState }) => {
|
||||
const { closeQuickSettingsMenu, shouldAnimateCloseMenu } =
|
||||
appState.quickSettingsMenu;
|
||||
const {
|
||||
closeQuickSettingsMenu,
|
||||
shouldAnimateCloseMenu,
|
||||
focusModeEnabled,
|
||||
setFocusModeEnabled,
|
||||
} = appState.quickSettingsMenu;
|
||||
const [themes, setThemes] = useState<SNTheme[]>([]);
|
||||
const [toggleableComponents, setToggleableComponents] = useState<
|
||||
SNComponent[]
|
||||
@@ -104,6 +71,10 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
const quickSettingsMenuRef = useRef<HTMLDivElement>(null);
|
||||
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
toggleFocusMode(focusModeEnabled);
|
||||
}, [focusModeEnabled]);
|
||||
|
||||
const reloadThemes = useCallback(() => {
|
||||
application.streamItems(ContentType.Theme, () => {
|
||||
const themes = application.getDisplayableItems(
|
||||
@@ -157,12 +128,12 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
|
||||
useEffect(() => {
|
||||
if (themesMenuOpen) {
|
||||
defaultThemeButtonRef.current!.focus();
|
||||
defaultThemeButtonRef.current?.focus();
|
||||
}
|
||||
}, [themesMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
prefsButtonRef.current!.focus();
|
||||
prefsButtonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(
|
||||
@@ -171,9 +142,9 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
);
|
||||
|
||||
const toggleThemesMenu = () => {
|
||||
if (!themesMenuOpen) {
|
||||
if (!themesMenuOpen && themesButtonRef.current) {
|
||||
const themesButtonRect =
|
||||
themesButtonRef.current!.getBoundingClientRect();
|
||||
themesButtonRef.current.getBoundingClientRect();
|
||||
setThemesMenuPosition({
|
||||
left: themesButtonRect.right,
|
||||
bottom:
|
||||
@@ -200,7 +171,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
setThemesMenuOpen(false);
|
||||
themesButtonRef.current!.focus();
|
||||
themesButtonRef.current?.focus();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (!themesMenuOpen) {
|
||||
@@ -211,65 +182,23 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
|
||||
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> =
|
||||
(event) => {
|
||||
const items: NodeListOf<HTMLButtonElement> =
|
||||
quickSettingsMenuRef.current!.querySelectorAll(':scope > button');
|
||||
const currentFocusedIndex = Array.from(items).findIndex(
|
||||
(btn) => btn === document.activeElement
|
||||
quickSettingsKeyDownHandler(
|
||||
closeQuickSettingsMenu,
|
||||
event,
|
||||
quickSettingsMenuRef,
|
||||
themesMenuOpen
|
||||
);
|
||||
|
||||
if (!themesMenuOpen) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
closeQuickSettingsMenu();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (items[currentFocusedIndex + 1]) {
|
||||
items[currentFocusedIndex + 1].focus();
|
||||
} else {
|
||||
items[0].focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (items[currentFocusedIndex - 1]) {
|
||||
items[currentFocusedIndex - 1].focus();
|
||||
} else {
|
||||
items[items.length - 1].focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (
|
||||
event
|
||||
) => {
|
||||
const themes = themesMenuRef.current!.querySelectorAll('button');
|
||||
const currentFocusedIndex = Array.from(themes).findIndex(
|
||||
(themeBtn) => themeBtn === document.activeElement
|
||||
themesMenuKeyDownHandler(
|
||||
event,
|
||||
themesMenuRef,
|
||||
setThemesMenuOpen,
|
||||
themesButtonRef
|
||||
);
|
||||
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
case 'ArrowLeft':
|
||||
event.stopPropagation();
|
||||
setThemesMenuOpen(false);
|
||||
themesButtonRef.current!.focus();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (themes[currentFocusedIndex + 1]) {
|
||||
themes[currentFocusedIndex + 1].focus();
|
||||
} else {
|
||||
themes[0].focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (themes[currentFocusedIndex - 1]) {
|
||||
themes[currentFocusedIndex - 1].focus();
|
||||
} else {
|
||||
themes[themes.length - 1].focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDefaultTheme = () => {
|
||||
@@ -332,7 +261,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
Default
|
||||
</button>
|
||||
{themes.map((theme) => (
|
||||
<ThemeButton
|
||||
<ThemesMenuButton
|
||||
theme={theme}
|
||||
application={application}
|
||||
key={theme.uuid}
|
||||
@@ -341,7 +270,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
|
||||
{toggleableComponents.map((component) => (
|
||||
<Switch
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
@@ -356,10 +284,15 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(
|
||||
</div>
|
||||
</Switch>
|
||||
))}
|
||||
|
||||
<FocusModeSwitch
|
||||
application={application}
|
||||
closeQuickSettingsMenu={closeQuickSettingsMenu}
|
||||
focusModeEnabled={focusModeEnabled}
|
||||
setFocusModeEnabled={setFocusModeEnabled}
|
||||
/>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<button
|
||||
class="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
className="sn-dropdown-item focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={openPreferences}
|
||||
ref={prefsButtonRef}
|
||||
>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNTheme } from '@standardnotes/snjs';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { Switch } from '../Switch';
|
||||
|
||||
type Props = {
|
||||
theme: SNTheme;
|
||||
application: WebApplication;
|
||||
onBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
export const ThemesMenuButton: FunctionComponent<Props> = ({
|
||||
application,
|
||||
theme,
|
||||
onBlur,
|
||||
}) => {
|
||||
const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.preventDefault();
|
||||
if (theme.isLayerable() || !theme.active) {
|
||||
application.toggleComponent(theme);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`sn-dropdown-item focus:bg-info-backdrop focus:shadow-none ${
|
||||
theme.isLayerable() ? `justify-start` : `justify-between`
|
||||
}`}
|
||||
onClick={toggleTheme}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{theme.isLayerable() ? (
|
||||
<>
|
||||
<Switch className="px-0 mr-2" checked={theme.active} />
|
||||
{theme.package_info.name}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`pseudo-radio-btn ${
|
||||
theme.active ? 'pseudo-radio-btn--checked' : ''
|
||||
} mr-2`}
|
||||
></div>
|
||||
<span className={theme.active ? 'font-semibold' : undefined}>
|
||||
{theme.package_info.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-5 h-5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: theme.package_info?.dock_icon?.background_color,
|
||||
}}
|
||||
></div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { RefObject } from 'preact';
|
||||
import { StateUpdater } from 'preact/hooks';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
|
||||
export const quickSettingsKeyDownHandler = (
|
||||
closeQuickSettingsMenu: () => void,
|
||||
event: JSXInternal.TargetedKeyboardEvent<HTMLDivElement>,
|
||||
quickSettingsMenuRef: RefObject<HTMLDivElement>,
|
||||
themesMenuOpen: boolean
|
||||
) => {
|
||||
if (quickSettingsMenuRef?.current) {
|
||||
const items: NodeListOf<HTMLButtonElement> =
|
||||
quickSettingsMenuRef.current.querySelectorAll(':scope > button');
|
||||
const currentFocusedIndex = Array.from(items).findIndex(
|
||||
(btn) => btn === document.activeElement
|
||||
);
|
||||
|
||||
if (!themesMenuOpen) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
closeQuickSettingsMenu();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (items[currentFocusedIndex + 1]) {
|
||||
items[currentFocusedIndex + 1].focus();
|
||||
} else {
|
||||
items[0].focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (items[currentFocusedIndex - 1]) {
|
||||
items[currentFocusedIndex - 1].focus();
|
||||
} else {
|
||||
items[items.length - 1].focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const themesMenuKeyDownHandler = (
|
||||
event: React.KeyboardEvent<HTMLDivElement>,
|
||||
themesMenuRef: RefObject<HTMLDivElement>,
|
||||
setThemesMenuOpen: StateUpdater<boolean>,
|
||||
themesButtonRef: RefObject<HTMLButtonElement>
|
||||
) => {
|
||||
if (themesMenuRef?.current) {
|
||||
const themes = themesMenuRef.current.querySelectorAll('button');
|
||||
const currentFocusedIndex = Array.from(themes).findIndex(
|
||||
(themeBtn) => themeBtn === document.activeElement
|
||||
);
|
||||
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
case 'ArrowLeft':
|
||||
event.stopPropagation();
|
||||
setThemesMenuOpen(false);
|
||||
themesButtonRef.current?.focus();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (themes[currentFocusedIndex + 1]) {
|
||||
themes[currentFocusedIndex + 1].focus();
|
||||
} else {
|
||||
themes[0].focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (themes[currentFocusedIndex - 1]) {
|
||||
themes[currentFocusedIndex - 1].focus();
|
||||
} else {
|
||||
themes[themes.length - 1].focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -10,7 +10,8 @@ import '@reach/checkbox/styles.css';
|
||||
|
||||
export type SwitchProps = HTMLProps<HTMLInputElement> & {
|
||||
checked?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
// Optional in case it is wrapped in a button (e.g. a menu item)
|
||||
onChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
children?: ComponentChildren;
|
||||
role?: string;
|
||||
@@ -32,7 +33,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
checked={checked}
|
||||
onChange={(event) => {
|
||||
setChecked(event.target.checked);
|
||||
props.onChange(event.target.checked);
|
||||
props.onChange?.(event.target.checked);
|
||||
}}
|
||||
className={`sn-switch ${checked ? 'bg-info' : 'bg-neutral'}`}
|
||||
>
|
||||
|
||||
136
app/assets/javascripts/components/TagsList.tsx
Normal file
136
app/assets/javascripts/components/TagsList.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
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 { 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 { TagsListItem } from './TagsListItem';
|
||||
import { toDirective } from './utils';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const tagsWithOptionalTemplate = (
|
||||
template: SNTag | undefined,
|
||||
tags: SNTag[]
|
||||
): SNTag[] => {
|
||||
if (!template) {
|
||||
return tags;
|
||||
}
|
||||
return [template, ...tags];
|
||||
};
|
||||
|
||||
export const TagsList: FunctionComponent<Props> = observer(
|
||||
({ application, appState }) => {
|
||||
const templateTag = appState.templateTag;
|
||||
const tags = appState.tags.tags;
|
||||
const allTags = tagsWithOptionalTemplate(templateTag, tags);
|
||||
|
||||
const selectTag = useCallback(
|
||||
(tag: SNTag) => {
|
||||
appState.setSelectedTag(tag);
|
||||
},
|
||||
[appState]
|
||||
);
|
||||
|
||||
const saveTag = useCallback(
|
||||
async (tag: SNTag, newTitle: string) => {
|
||||
const templateTag = appState.templateTag;
|
||||
|
||||
const hasEmptyTitle = newTitle.length === 0;
|
||||
const hasNotChangedTitle = newTitle === tag.title;
|
||||
const isTemplateChange = templateTag && tag.uuid === templateTag.uuid;
|
||||
const hasDuplicatedTitle = !!application.findTagByTitle(newTitle);
|
||||
|
||||
runInAction(() => {
|
||||
appState.templateTag = undefined;
|
||||
appState.editingTag = undefined;
|
||||
});
|
||||
|
||||
if (hasEmptyTitle || hasNotChangedTitle) {
|
||||
if (isTemplateChange) {
|
||||
appState.undoCreateNewTag();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDuplicatedTitle) {
|
||||
if (isTemplateChange) {
|
||||
appState.undoCreateNewTag();
|
||||
}
|
||||
application.alertService?.alert(
|
||||
'A tag with this name already exists.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTemplateChange) {
|
||||
const insertedTag = await application.insertItem(templateTag);
|
||||
const changedTag = await application.changeItem<TagMutator>(
|
||||
insertedTag.uuid,
|
||||
(m) => {
|
||||
m.title = newTitle;
|
||||
}
|
||||
);
|
||||
|
||||
selectTag(changedTag as SNTag);
|
||||
await application.saveItem(insertedTag.uuid);
|
||||
} else {
|
||||
await application.changeAndSaveItem<TagMutator>(
|
||||
tag.uuid,
|
||||
(mutator) => {
|
||||
mutator.title = newTitle;
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[appState, application, selectTag]
|
||||
);
|
||||
|
||||
const removeTag = useCallback(
|
||||
async (tag: SNTag) => {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: STRING_DELETE_TAG,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
appState.removeTag(tag);
|
||||
}
|
||||
},
|
||||
[appState]
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const TagsListDirective = toDirective<Props>(TagsList);
|
||||
134
app/assets/javascripts/components/TagsListItem.tsx
Normal file
134
app/assets/javascripts/components/TagsListItem.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { SNTag } from '@standardnotes/snjs';
|
||||
import { runInAction } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent, JSX } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
tag: SNTag;
|
||||
selectTag: (tag: SNTag) => void;
|
||||
removeTag: (tag: SNTag) => void;
|
||||
saveTag: (tag: SNTag, newTitle: string) => void;
|
||||
appState: TagsListState;
|
||||
};
|
||||
|
||||
export type TagsListState = {
|
||||
readonly selectedTag: SNTag | undefined;
|
||||
editingTag: SNTag | undefined;
|
||||
};
|
||||
|
||||
export const TagsListItem: FunctionComponent<Props> = observer(
|
||||
({ tag, selectTag, saveTag, removeTag, appState }) => {
|
||||
const [title, setTitle] = useState(tag.title || '');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isSelected = appState.selectedTag === tag;
|
||||
const isEditing = appState.editingTag === tag;
|
||||
const noteCounts = tag.noteCount;
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(tag.title || '');
|
||||
}, [setTitle, tag]);
|
||||
|
||||
const selectCurrentTag = useCallback(() => {
|
||||
if (isEditing || isSelected) {
|
||||
return;
|
||||
}
|
||||
selectTag(tag);
|
||||
}, [isSelected, isEditing, selectTag, tag]);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
saveTag(tag, title);
|
||||
}, [tag, saveTag, title]);
|
||||
|
||||
const onInput = useCallback(
|
||||
(e: JSX.TargetedEvent<HTMLInputElement>) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.code === 'Enter') {
|
||||
inputRef.current?.blur();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[inputRef]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [inputRef, isEditing]);
|
||||
|
||||
const onClickRename = useCallback(() => {
|
||||
runInAction(() => {
|
||||
appState.editingTag = tag;
|
||||
});
|
||||
}, [appState, tag]);
|
||||
|
||||
const onClickSave = useCallback(() => {
|
||||
inputRef.current?.blur();
|
||||
}, [inputRef]);
|
||||
|
||||
const onClickDelete = useCallback(() => {
|
||||
removeTag(tag);
|
||||
}, [removeTag, tag]);
|
||||
|
||||
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}</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>
|
||||
)}
|
||||
{isEditing && (
|
||||
<a className="item" onClick={onClickSave}>
|
||||
Save
|
||||
</a>
|
||||
)}
|
||||
<a className="item" onClick={onClickDelete}>
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -5,7 +5,7 @@ import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* @returns a callback that will close a dropdown if none of its children has
|
||||
* focus. Must be set as the onBlur callback of children that need to be
|
||||
* focus. Use the returned function as the onBlur callback of children that need to be
|
||||
* monitored.
|
||||
*/
|
||||
export function useCloseOnBlur(
|
||||
@@ -36,13 +36,14 @@ export function useCloseOnClickOutside(
|
||||
container: { current: HTMLDivElement },
|
||||
setOpen: (open: boolean) => void
|
||||
): void {
|
||||
const closeOnClickOutside = useCallback((event: { target: EventTarget | null }) => {
|
||||
if (
|
||||
!container.current?.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [container, setOpen]);
|
||||
const closeOnClickOutside = useCallback(
|
||||
(event: { target: EventTarget | null }) => {
|
||||
if (!container.current?.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
[container, setOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', closeOnClickOutside);
|
||||
|
||||
@@ -35,8 +35,8 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
const editors = this.application
|
||||
.componentManager!.componentsForArea(ComponentArea.Editor)
|
||||
const editors = this.application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
|
||||
28
app/assets/javascripts/jest.config.js
Normal file
28
app/assets/javascripts/jest.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const pathsToModuleNameMapper = require('ts-jest/utils').pathsToModuleNameMapper;
|
||||
const tsConfig = require('./tsconfig.json');
|
||||
|
||||
const pathsFromTsconfig = tsConfig.compilerOptions.paths;
|
||||
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleNameMapper: {
|
||||
...pathsToModuleNameMapper(pathsFromTsconfig, {
|
||||
prefix: '<rootDir>',
|
||||
}),
|
||||
'^react$': ['preact/compat'],
|
||||
'^react-dom$': 'preact',
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||
},
|
||||
globals: {
|
||||
window: {},
|
||||
__VERSION__: '1.0.0',
|
||||
__DESKTOP__: false,
|
||||
__WEB__: true,
|
||||
self: {}, // fixes error happening on `import { SKAlert } from 'sn-stylekit'`
|
||||
},
|
||||
transform: {
|
||||
'\\.(pug)$': '../../../node_modules/jest-transform-pug',
|
||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||
'\\.svg$': 'svg-jest',
|
||||
},
|
||||
};
|
||||
@@ -96,8 +96,8 @@ export const OfflineSubscription: FunctionalComponent<IProps> = observer(({ appl
|
||||
)}
|
||||
</div>
|
||||
{(isSuccessfullyActivated || isSuccessfullyRemoved) && (
|
||||
<div className={'mt-3 mb-3 info font-bold'}>
|
||||
Successfully {isSuccessfullyActivated ? 'Activated' : 'Removed'}!
|
||||
<div className={'mt-3 mb-3 info'}>
|
||||
Your offline subscription code has been successfully {isSuccessfullyActivated ? 'activated' : 'removed'}.
|
||||
</div>
|
||||
)}
|
||||
{hasUserPreviouslyStoredCode && (
|
||||
|
||||
@@ -15,22 +15,53 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
const { userSubscription, userSubscriptionName } = subscriptionState;
|
||||
const expirationDate = new Date(
|
||||
convertTimestampToMilliseconds(userSubscription!.endsAt)
|
||||
).toLocaleString();
|
||||
);
|
||||
const expirationDateString = expirationDate.toLocaleString();
|
||||
const expired = expirationDate.getTime() < new Date().getTime();
|
||||
const canceled = userSubscription!.cancelled;
|
||||
|
||||
return userSubscription!.cancelled ? (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription has been{' '}
|
||||
<span className="font-bold">
|
||||
canceled but will remain valid until {expirationDate}
|
||||
</span>
|
||||
. You may resubscribe below if you wish.
|
||||
</Text>
|
||||
) : (
|
||||
if (canceled) {
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription has been canceled
|
||||
{' '}
|
||||
{expired ? (
|
||||
<span className="font-bold">
|
||||
and expired on {expirationDateString}
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-bold">
|
||||
but will remain valid until {expirationDateString}
|
||||
</span>
|
||||
)}
|
||||
. You may resubscribe below if you wish.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription {' '}
|
||||
<span className="font-bold">
|
||||
expired on {expirationDateString}
|
||||
</span>
|
||||
. You may resubscribe below if you wish.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
<span className="font-bold">
|
||||
@@ -38,7 +69,7 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription will be{' '}
|
||||
<span className="font-bold">renewed on {expirationDate}</span>.
|
||||
<span className="font-bold">renewed on {expirationDateString}</span>.
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,16 +5,16 @@ import {
|
||||
ApplicationService,
|
||||
SNTheme,
|
||||
removeFromArray,
|
||||
ApplicationEvent, ContentType
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
} from '@standardnotes/snjs';
|
||||
|
||||
const CACHED_THEMES_KEY = 'cachedThemes';
|
||||
|
||||
export class ThemeManager extends ApplicationService {
|
||||
|
||||
private activeThemes: string[] = []
|
||||
private unregisterDesktop!: () => void
|
||||
private unregisterStream!: () => void
|
||||
private activeThemes: string[] = [];
|
||||
private unregisterDesktop!: () => void;
|
||||
private unregisterStream!: () => void;
|
||||
|
||||
async onAppEvent(event: ApplicationEvent) {
|
||||
super.onAppEvent(event);
|
||||
@@ -54,7 +54,8 @@ export class ThemeManager extends ApplicationService {
|
||||
}
|
||||
|
||||
private registerObservers() {
|
||||
this.unregisterDesktop = this.webApplication.getDesktopService()
|
||||
this.unregisterDesktop = this.webApplication
|
||||
.getDesktopService()
|
||||
.registerUpdateObserver((component) => {
|
||||
if (component.active && component.isTheme()) {
|
||||
this.deactivateTheme(component.uuid);
|
||||
@@ -64,16 +65,21 @@ export class ThemeManager extends ApplicationService {
|
||||
}
|
||||
});
|
||||
|
||||
this.unregisterStream = this.application.streamItems(ContentType.Theme, (items) => {
|
||||
const themes = items as SNTheme[];
|
||||
for (const theme of themes) {
|
||||
if (theme.active) {
|
||||
this.activateTheme(theme);
|
||||
} else {
|
||||
this.deactivateTheme(theme.uuid);
|
||||
this.unregisterStream = this.application.streamItems(
|
||||
ContentType.Theme,
|
||||
() => {
|
||||
const themes = this.application.getDisplayableItems(
|
||||
ContentType.Theme
|
||||
) as SNTheme[];
|
||||
for (const theme of themes) {
|
||||
if (theme.active) {
|
||||
this.activateTheme(theme);
|
||||
} else {
|
||||
this.deactivateTheme(theme.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
private clearAppThemeState() {
|
||||
@@ -120,14 +126,17 @@ export class ThemeManager extends ApplicationService {
|
||||
|
||||
private async cacheThemes() {
|
||||
const themes = this.application!.getAll(this.activeThemes) as SNTheme[];
|
||||
const mapped = await Promise.all(themes.map(async (theme) => {
|
||||
const payload = theme.payloadRepresentation();
|
||||
const processedPayload = await this.application!.protocolService!.payloadByEncryptingPayload(
|
||||
payload,
|
||||
EncryptionIntent.LocalStorageDecrypted
|
||||
);
|
||||
return processedPayload;
|
||||
}));
|
||||
const mapped = await Promise.all(
|
||||
themes.map(async (theme) => {
|
||||
const payload = theme.payloadRepresentation();
|
||||
const processedPayload =
|
||||
await this.application!.protocolService!.payloadByEncryptingPayload(
|
||||
payload,
|
||||
EncryptionIntent.LocalStorageDecrypted
|
||||
);
|
||||
return processedPayload;
|
||||
})
|
||||
);
|
||||
return this.application!.setValue(
|
||||
CACHED_THEMES_KEY,
|
||||
mapped,
|
||||
@@ -145,15 +154,17 @@ export class ThemeManager extends ApplicationService {
|
||||
}
|
||||
|
||||
private async getCachedThemes() {
|
||||
const cachedThemes = await this.application!.getValue(
|
||||
const cachedThemes = (await this.application!.getValue(
|
||||
CACHED_THEMES_KEY,
|
||||
StorageValueModes.Nonwrapped
|
||||
) as SNTheme[];
|
||||
)) as SNTheme[];
|
||||
if (cachedThemes) {
|
||||
const themes = [];
|
||||
for (const cachedTheme of cachedThemes) {
|
||||
const payload = this.application!.createPayloadFromObject(cachedTheme);
|
||||
const theme = this.application!.createItemFromPayload(payload) as SNTheme;
|
||||
const theme = this.application!.createItemFromPayload(
|
||||
payload
|
||||
) as SNTheme;
|
||||
themes.push(theme);
|
||||
}
|
||||
return themes;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"strict": true,
|
||||
"isolatedModules": false,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"newLine": "lf",
|
||||
"declarationDir": "../../../dist/@types",
|
||||
@@ -15,13 +16,14 @@
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"typeRoots": ["./@types"],
|
||||
"types": ["@types/jest"],
|
||||
"paths": {
|
||||
"%/*": ["../templates/*"],
|
||||
"@/*": ["./*"],
|
||||
"@Controllers/*": ["./controllers/*"],
|
||||
"@Views/*": ["./views/*"],
|
||||
"@Services/*": ["./services/*"],
|
||||
"@node_modules/*": ["../../../node_modules/*"],
|
||||
"@node_modules/*": ["../../../node_modules/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import pull from 'lodash/pull';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
SNTag,
|
||||
SNNote,
|
||||
ContentType,
|
||||
PayloadSource,
|
||||
DeinitSource,
|
||||
PrefKey,
|
||||
} from '@standardnotes/snjs';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
import { Bridge } from '@/services/bridge';
|
||||
import { storage, StorageKey } from '@/services/localStorage';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ContentType,
|
||||
DeinitSource,
|
||||
PayloadSource,
|
||||
PrefKey,
|
||||
SNNote,
|
||||
SNTag,
|
||||
} from '@standardnotes/snjs';
|
||||
import pull from 'lodash/pull';
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
} from 'mobx';
|
||||
import { ActionsMenuState } from './actions_menu_state';
|
||||
import { NotesState } from './notes_state';
|
||||
import { NoteTagsState } from './note_tags_state';
|
||||
import { NoAccountWarningState } from './no_account_warning_state';
|
||||
import { SyncState } from './sync_state';
|
||||
import { SearchOptionsState } from './search_options_state';
|
||||
import { NotesState } from './notes_state';
|
||||
import { TagsState } from './tags_state';
|
||||
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
|
||||
import { PreferencesState } from './preferences_state';
|
||||
import { PurchaseFlowState } from './purchase_flow_state';
|
||||
import { QuickSettingsState } from './quick_settings_state';
|
||||
import { SearchOptionsState } from './search_options_state';
|
||||
import { SyncState } from './sync_state';
|
||||
import { TagsState } from './tags_state';
|
||||
|
||||
export enum AppStateEvent {
|
||||
TagChanged,
|
||||
@@ -34,7 +40,7 @@ export enum AppStateEvent {
|
||||
BeganBackupDownload,
|
||||
EndedBackupDownload,
|
||||
WindowDidFocus,
|
||||
WindowDidBlur
|
||||
WindowDidBlur,
|
||||
}
|
||||
|
||||
export type PanelResizedData = {
|
||||
@@ -62,8 +68,13 @@ export class AppState {
|
||||
rootScopeCleanup1: any;
|
||||
rootScopeCleanup2: any;
|
||||
onVisibilityChange: any;
|
||||
selectedTag?: SNTag;
|
||||
showBetaWarning: boolean;
|
||||
|
||||
selectedTag: SNTag | undefined;
|
||||
previouslySelectedTag: SNTag | undefined;
|
||||
editingTag: SNTag | undefined;
|
||||
_templateTag: SNTag | undefined;
|
||||
|
||||
readonly quickSettingsMenu = new QuickSettingsState();
|
||||
readonly accountMenu: AccountMenuState;
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
@@ -133,11 +144,25 @@ export class AppState {
|
||||
this.showBetaWarning = false;
|
||||
}
|
||||
|
||||
this.selectedTag = undefined;
|
||||
this.previouslySelectedTag = undefined;
|
||||
this.editingTag = undefined;
|
||||
this._templateTag = undefined;
|
||||
|
||||
makeObservable(this, {
|
||||
showBetaWarning: observable,
|
||||
isSessionsModalVisible: observable,
|
||||
preferences: observable,
|
||||
|
||||
selectedTag: observable,
|
||||
previouslySelectedTag: observable,
|
||||
_templateTag: observable,
|
||||
templateTag: computed,
|
||||
createNewTag: action,
|
||||
editingTag: observable,
|
||||
setSelectedTag: action,
|
||||
removeTag: action,
|
||||
|
||||
enableBetaWarning: action,
|
||||
disableBetaWarning: action,
|
||||
openSessionsModal: action,
|
||||
@@ -236,7 +261,7 @@ export class AppState {
|
||||
}
|
||||
|
||||
streamNotesAndTags() {
|
||||
this.application!.streamItems(
|
||||
this.application.streamItems(
|
||||
[ContentType.Note, ContentType.Tag],
|
||||
async (items, source) => {
|
||||
/** Close any editors for deleted/trashed/archived notes */
|
||||
@@ -269,10 +294,13 @@ export class AppState {
|
||||
}
|
||||
if (this.selectedTag) {
|
||||
const matchingTag = items.find(
|
||||
(candidate) => candidate.uuid === this.selectedTag!.uuid
|
||||
(candidate) =>
|
||||
this.selectedTag && candidate.uuid === this.selectedTag.uuid
|
||||
);
|
||||
if (matchingTag) {
|
||||
this.selectedTag = matchingTag as SNTag;
|
||||
runInAction(() => {
|
||||
this.selectedTag = matchingTag as SNTag;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,17 +371,69 @@ export class AppState {
|
||||
}
|
||||
|
||||
setSelectedTag(tag: SNTag) {
|
||||
if (tag.conflictOf) {
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.selectedTag === tag) {
|
||||
return;
|
||||
}
|
||||
const previousTag = this.selectedTag;
|
||||
|
||||
this.previouslySelectedTag = this.selectedTag;
|
||||
this.selectedTag = tag;
|
||||
|
||||
if (this.templateTag?.uuid === tag.uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifyEvent(AppStateEvent.TagChanged, {
|
||||
tag: tag,
|
||||
previousTag: previousTag,
|
||||
previousTag: this.previouslySelectedTag,
|
||||
});
|
||||
}
|
||||
|
||||
public getSelectedTag() {
|
||||
return this.selectedTag;
|
||||
}
|
||||
|
||||
public get templateTag(): SNTag | undefined {
|
||||
return this._templateTag;
|
||||
}
|
||||
|
||||
public set templateTag(tag: SNTag | undefined) {
|
||||
const previous = this._templateTag;
|
||||
this._templateTag = tag;
|
||||
|
||||
if (tag) {
|
||||
this.setSelectedTag(tag);
|
||||
this.editingTag = tag;
|
||||
} else if (previous) {
|
||||
this.selectedTag =
|
||||
previous === this.selectedTag ? undefined : this.selectedTag;
|
||||
this.editingTag =
|
||||
previous === this.editingTag ? undefined : this.editingTag;
|
||||
}
|
||||
}
|
||||
|
||||
public removeTag(tag: SNTag) {
|
||||
this.application.deleteItem(tag);
|
||||
this.setSelectedTag(this.tags.smartTags[0]);
|
||||
}
|
||||
|
||||
public async createNewTag() {
|
||||
const newTag = (await this.application.createTemplateItem(
|
||||
ContentType.Tag
|
||||
)) as SNTag;
|
||||
this.templateTag = newTag;
|
||||
}
|
||||
|
||||
public async undoCreateNewTag() {
|
||||
const previousTag = this.previouslySelectedTag || this.tags.smartTags[0];
|
||||
this.setSelectedTag(previousTag);
|
||||
}
|
||||
|
||||
/** Returns the tags that are referncing this note */
|
||||
public getNoteTags(note: SNNote) {
|
||||
return this.application.referencingForItem(note).filter((ref) => {
|
||||
@@ -361,10 +441,6 @@ export class AppState {
|
||||
}) as SNTag[];
|
||||
}
|
||||
|
||||
public getSelectedTag() {
|
||||
return this.selectedTag;
|
||||
}
|
||||
|
||||
panelDidResize(name: string, collapsed: boolean) {
|
||||
const data: PanelResizedData = {
|
||||
panel: name,
|
||||
|
||||
@@ -3,14 +3,17 @@ import { action, makeObservable, observable } from 'mobx';
|
||||
export class QuickSettingsState {
|
||||
open = false;
|
||||
shouldAnimateCloseMenu = false;
|
||||
focusModeEnabled = false;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
open: observable,
|
||||
shouldAnimateCloseMenu: observable,
|
||||
focusModeEnabled: observable,
|
||||
|
||||
setOpen: action,
|
||||
setShouldAnimateCloseMenu: action,
|
||||
setFocusModeEnabled: action,
|
||||
toggle: action,
|
||||
closeQuickSettingsMenu: action,
|
||||
});
|
||||
@@ -24,6 +27,10 @@ export class QuickSettingsState {
|
||||
this.shouldAnimateCloseMenu = shouldAnimate;
|
||||
};
|
||||
|
||||
setFocusModeEnabled = (enabled: boolean): void => {
|
||||
this.focusModeEnabled = enabled;
|
||||
};
|
||||
|
||||
toggle = (): void => {
|
||||
if (this.open) {
|
||||
this.closeQuickSettingsMenu();
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import { ComponentModalScope } from './../directives/views/componentModal';
|
||||
import { AccountSwitcherScope, PermissionsModalScope } from './../types';
|
||||
import { ComponentGroup } from './component_group';
|
||||
import { EditorGroup } from '@/ui_models/editor_group';
|
||||
import { InputModalScope } from '@/directives/views/inputModal';
|
||||
import { PasswordWizardType, PasswordWizardScope } from '@/types';
|
||||
import {
|
||||
SNApplication,
|
||||
SNComponent,
|
||||
PermissionDialog,
|
||||
DeinitSource,
|
||||
Platform,
|
||||
} from '@standardnotes/snjs';
|
||||
import angular from 'angular';
|
||||
import { WebDeviceInterface } from '@/web_device_interface';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { Bridge } from '@/services/bridge';
|
||||
import { WebCrypto } from '@/crypto';
|
||||
import { InputModalScope } from '@/directives/views/inputModal';
|
||||
import { AlertService } from '@/services/alertService';
|
||||
import { AutolockService } from '@/services/autolock_service';
|
||||
import { ArchiveManager } from '@/services/archiveManager';
|
||||
import { AutolockService } from '@/services/autolock_service';
|
||||
import { Bridge } from '@/services/bridge';
|
||||
import { DesktopManager } from '@/services/desktopManager';
|
||||
import { IOService } from '@/services/ioService';
|
||||
import { StatusManager } from '@/services/statusManager';
|
||||
import { ThemeManager } from '@/services/themeManager';
|
||||
import { PasswordWizardScope, PasswordWizardType } from '@/types';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { EditorGroup } from '@/ui_models/editor_group';
|
||||
import { AppVersion } from '@/version';
|
||||
import { isDev } from '@/utils';
|
||||
import { WebDeviceInterface } from '@/web_device_interface';
|
||||
import {
|
||||
DeinitSource,
|
||||
PermissionDialog,
|
||||
Platform,
|
||||
SNApplication,
|
||||
SNComponent,
|
||||
} from '@standardnotes/snjs';
|
||||
import angular from 'angular';
|
||||
import { ComponentModalScope } from './../directives/views/componentModal';
|
||||
import { AccountSwitcherScope, PermissionsModalScope } from './../types';
|
||||
import { ComponentGroup } from './component_group';
|
||||
|
||||
type WebServices = {
|
||||
appState: AppState;
|
||||
@@ -34,15 +33,14 @@ type WebServices = {
|
||||
statusManager: StatusManager;
|
||||
themeService: ThemeManager;
|
||||
io: IOService;
|
||||
}
|
||||
};
|
||||
|
||||
export class WebApplication extends SNApplication {
|
||||
|
||||
private scope?: angular.IScope
|
||||
private webServices!: WebServices
|
||||
private currentAuthenticationElement?: angular.IRootElementService
|
||||
public editorGroup: EditorGroup
|
||||
public componentGroup: ComponentGroup
|
||||
private scope?: angular.IScope;
|
||||
private webServices!: WebServices;
|
||||
private currentAuthenticationElement?: angular.IRootElementService;
|
||||
public editorGroup: EditorGroup;
|
||||
public componentGroup: ComponentGroup;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -54,7 +52,7 @@ export class WebApplication extends SNApplication {
|
||||
defaultSyncServerHost: string,
|
||||
public bridge: Bridge,
|
||||
enableUnfinishedFeatures: boolean,
|
||||
webSocketUrl: string,
|
||||
webSocketUrl: string
|
||||
) {
|
||||
super(
|
||||
bridge.environment,
|
||||
@@ -67,7 +65,7 @@ export class WebApplication extends SNApplication {
|
||||
defaultSyncServerHost,
|
||||
AppVersion,
|
||||
enableUnfinishedFeatures,
|
||||
webSocketUrl,
|
||||
webSocketUrl
|
||||
);
|
||||
this.$compile = $compile;
|
||||
this.scope = scope;
|
||||
@@ -108,7 +106,8 @@ export class WebApplication extends SNApplication {
|
||||
onStart(): void {
|
||||
super.onStart();
|
||||
this.componentManager!.openModalComponent = this.openModalComponent;
|
||||
this.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
|
||||
this.componentManager!.presentPermissionsDialog =
|
||||
this.presentPermissionsDialog;
|
||||
}
|
||||
|
||||
setWebServices(services: WebServices): void {
|
||||
@@ -176,8 +175,8 @@ export class WebApplication extends SNApplication {
|
||||
|
||||
presentPasswordModal(callback: () => void) {
|
||||
const scope = this.scope!.$new(true) as InputModalScope;
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
scope.type = 'password';
|
||||
scope.title = 'Decryption Assistance';
|
||||
scope.message = `Unable to decrypt this item with your current keys.
|
||||
Please enter your account password at the time of this revision.`;
|
||||
scope.callback = callback;
|
||||
@@ -205,8 +204,8 @@ export class WebApplication extends SNApplication {
|
||||
const scope = this.scope!.$new(true) as Partial<AccountSwitcherScope>;
|
||||
scope.application = this;
|
||||
const el = this.$compile!(
|
||||
"<account-switcher application='application' "
|
||||
+ "class='sk-modal'></account-switcher>"
|
||||
"<account-switcher application='application' " +
|
||||
"class='sk-modal'></account-switcher>"
|
||||
)(scope as any);
|
||||
this.applicationElement.append(el);
|
||||
}
|
||||
@@ -214,7 +213,7 @@ export class WebApplication extends SNApplication {
|
||||
async openModalComponent(component: SNComponent): Promise<void> {
|
||||
switch (component.package_info?.identifier) {
|
||||
case 'org.standardnotes.cloudlink':
|
||||
if (!await this.authorizeCloudLinkAccess()) {
|
||||
if (!(await this.authorizeCloudLinkAccess())) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -223,8 +222,8 @@ export class WebApplication extends SNApplication {
|
||||
scope.componentUuid = component.uuid;
|
||||
scope.application = this;
|
||||
const el = this.$compile!(
|
||||
"<component-modal application='application' component-uuid='componentUuid' "
|
||||
+ "class='sk-modal'></component-modal>"
|
||||
"<component-modal application='application' component-uuid='componentUuid' " +
|
||||
"class='sk-modal'></component-modal>"
|
||||
)(scope as any);
|
||||
this.applicationElement.append(el);
|
||||
}
|
||||
@@ -235,8 +234,8 @@ export class WebApplication extends SNApplication {
|
||||
scope.component = dialog.component;
|
||||
scope.callback = dialog.callback;
|
||||
const el = this.$compile!(
|
||||
"<permissions-modal component='component' permissions-string='permissionsString'"
|
||||
+ " callback='callback' class='sk-modal'></permissions-modal>"
|
||||
"<permissions-modal component='component' permissions-string='permissionsString'" +
|
||||
" callback='callback' class='sk-modal'></permissions-modal>"
|
||||
)(scope as any);
|
||||
this.applicationElement.append(el);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
|
||||
) {{self.state.noteStatus.message}}
|
||||
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
|
||||
pin-note-button(
|
||||
class='mr-3'
|
||||
app-state='self.appState',
|
||||
ng-if='self.appState.notes.selectedNotesCount > 0'
|
||||
)
|
||||
notes-options-panel(
|
||||
application='self.application',
|
||||
app-state='self.appState',
|
||||
|
||||
@@ -740,8 +740,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
|
||||
async reloadStackComponents() {
|
||||
const stackComponents = sortAlphabetically(
|
||||
this.application
|
||||
.componentManager!.componentsForArea(ComponentArea.EditorStack)
|
||||
this.application.componentManager
|
||||
.componentsForArea(ComponentArea.EditorStack)
|
||||
.filter((component) => component.active)
|
||||
);
|
||||
if (this.note) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#tags-column.sn-component.section.tags(aria-label='Tags')
|
||||
.component-view-container(ng-if='self.component && self.component.active')
|
||||
.component-view-container(ng-if='self.component')
|
||||
component-view.component-view(
|
||||
component-uuid='self.component.uuid',
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
#tags-content.content(ng-if='!(self.component && self.component.active)')
|
||||
#tags-content.content(ng-if='!(self.component)')
|
||||
.tags-title-section.section-title-bar
|
||||
.section-title-bar-header
|
||||
.sk-h3.title
|
||||
@@ -33,35 +33,10 @@
|
||||
.section-title-bar-header
|
||||
.sk-h3.title
|
||||
span.sk-bold Tags
|
||||
.tag(
|
||||
ng-class="{'selected' : self.state.selectedTag == tag}",
|
||||
ng-click='self.selectTag(tag)',
|
||||
ng-repeat='tag in self.state.tags track by tag.uuid'
|
||||
)
|
||||
.tag-info(ng-if="!tag.errorDecrypting")
|
||||
.tag-icon #
|
||||
input.title(
|
||||
ng-attr-id='tag-{{tag.uuid}}',
|
||||
ng-blur='self.saveTag($event, tag)'
|
||||
ng-change='self.onTagTitleChange(tag)',
|
||||
ng-model='self.titles[tag.uuid]',
|
||||
ng-class="{'editing' : self.state.editingTag == tag}",
|
||||
ng-click='self.selectTag(tag)',
|
||||
ng-keyup='$event.keyCode == 13 && $event.target.blur()',
|
||||
should-focus='self.state.templateTag || self.state.editingTag == tag',
|
||||
sn-autofocus='true',
|
||||
spellcheck='false'
|
||||
)
|
||||
.count {{self.state.noteCounts[tag.uuid]}}
|
||||
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.menu(ng-show='self.state.selectedTag == tag')
|
||||
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
|
||||
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
|
||||
a.item(ng-click='self.selectedDeleteTag(tag)') Delete
|
||||
.no-tags-placeholder(ng-show='self.state.tags.length == 0')
|
||||
| No tags. Create one using the add button above.
|
||||
tags(
|
||||
application='self.application',
|
||||
app-state='self.appState'
|
||||
)
|
||||
panel-resizer(
|
||||
collapsable='true',
|
||||
control='self.panelPuppet',
|
||||
|
||||
@@ -1,62 +1,46 @@
|
||||
import { PayloadContent } from '@standardnotes/snjs';
|
||||
import { WebDirective, PanelPuppet } from '@/types';
|
||||
import { PanelPuppet, WebDirective } from '@/types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import {
|
||||
SNTag,
|
||||
ContentType,
|
||||
ApplicationEvent,
|
||||
ComponentAction,
|
||||
SNSmartTag,
|
||||
ComponentArea,
|
||||
SNComponent,
|
||||
PrefKey,
|
||||
UuidString,
|
||||
TagMutator
|
||||
} from '@standardnotes/snjs';
|
||||
import template from './tags-view.pug';
|
||||
import { AppStateEvent } from '@/ui_models/app_state';
|
||||
import { PANEL_NAME_TAGS } from '@/views/constants';
|
||||
import { STRING_DELETE_TAG } from '@/strings';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ComponentAction,
|
||||
ComponentArea,
|
||||
ContentType,
|
||||
PrefKey,
|
||||
SNComponent,
|
||||
SNSmartTag,
|
||||
SNTag,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import template from './tags-view.pug';
|
||||
|
||||
type NoteCounts = Partial<Record<string, number>>
|
||||
type NoteCounts = Partial<Record<string, number>>;
|
||||
|
||||
type TagState = {
|
||||
tags: SNTag[]
|
||||
smartTags: SNSmartTag[]
|
||||
noteCounts: NoteCounts
|
||||
selectedTag?: SNTag
|
||||
/** If creating a new tag, the previously selected tag will be set here, so that if new
|
||||
* tag creation is canceled, the previous tag is re-selected */
|
||||
previousTag?: SNTag
|
||||
/** If a tag is in edit state, it will be set as the editingTag */
|
||||
editingTag?: SNTag
|
||||
/** If a tag is new and not yet saved, it will be set as the template tag */
|
||||
templateTag?: SNTag
|
||||
}
|
||||
smartTags: SNSmartTag[];
|
||||
noteCounts: NoteCounts;
|
||||
selectedTag?: SNTag;
|
||||
};
|
||||
|
||||
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
|
||||
/** Passed through template */
|
||||
readonly application!: WebApplication
|
||||
private readonly panelPuppet: PanelPuppet
|
||||
private unregisterComponent?: any
|
||||
component?: SNComponent
|
||||
readonly application!: WebApplication;
|
||||
private readonly panelPuppet: PanelPuppet;
|
||||
private unregisterComponent?: any;
|
||||
component?: SNComponent;
|
||||
/** The original name of the edtingTag before it began editing */
|
||||
private editingOriginalName?: string
|
||||
formData: { tagTitle?: string } = {}
|
||||
titles: Partial<Record<UuidString, string>> = {}
|
||||
private removeTagsObserver!: () => void
|
||||
private removeFoldersObserver!: () => void
|
||||
formData: { tagTitle?: string } = {};
|
||||
titles: Partial<Record<UuidString, string>> = {};
|
||||
private removeTagsObserver!: () => void;
|
||||
private removeFoldersObserver!: () => void;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
constructor($timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.panelPuppet = {
|
||||
onReady: () => this.loadPreferences()
|
||||
onReady: () => this.loadPreferences(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,16 +53,15 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
getInitialState(): TagState {
|
||||
return {
|
||||
tags: [],
|
||||
smartTags: [],
|
||||
noteCounts: {},
|
||||
};
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state as TagState;
|
||||
getState(): TagState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async onAppStart() {
|
||||
@@ -90,10 +73,9 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
super.onAppLaunch();
|
||||
this.loadPreferences();
|
||||
this.beginStreamingItems();
|
||||
|
||||
const smartTags = this.application.getSmartTags();
|
||||
this.setState({
|
||||
smartTags: smartTags,
|
||||
});
|
||||
this.setState({ smartTags });
|
||||
this.selectTag(smartTags[0]);
|
||||
}
|
||||
|
||||
@@ -114,28 +96,31 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
this.removeTagsObserver = this.application.streamItems(
|
||||
[ContentType.Tag, ContentType.SmartTag],
|
||||
async (items) => {
|
||||
const tags = items as Array<SNTag | SNSmartTag>;
|
||||
|
||||
await this.setState({
|
||||
tags: this.application.getDisplayableItems(ContentType.Tag) as SNTag[],
|
||||
smartTags: this.application.getSmartTags(),
|
||||
});
|
||||
|
||||
for (const tag of items as Array<SNTag | SNSmartTag>) {
|
||||
for (const tag of tags) {
|
||||
this.titles[tag.uuid] = tag.title;
|
||||
}
|
||||
|
||||
this.reloadNoteCounts();
|
||||
const selectedTag = this.state.selectedTag;
|
||||
|
||||
if (selectedTag) {
|
||||
/** If the selected tag has been deleted, revert to All view. */
|
||||
const matchingTag = items.find((tag) => {
|
||||
const matchingTag = tags.find((tag) => {
|
||||
return tag.uuid === selectedTag.uuid;
|
||||
}) as SNTag;
|
||||
});
|
||||
|
||||
if (matchingTag) {
|
||||
if (matchingTag.deleted) {
|
||||
this.selectTag(this.getState().smartTags[0]);
|
||||
} else {
|
||||
this.setState({
|
||||
selectedTag: matchingTag
|
||||
selectedTag: matchingTag,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -148,7 +133,7 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
onAppStateEvent(eventName: AppStateEvent) {
|
||||
if (eventName === AppStateEvent.TagChanged) {
|
||||
this.setState({
|
||||
selectedTag: this.application.getAppState().getSelectedTag()
|
||||
selectedTag: this.application.getAppState().getSelectedTag(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -167,37 +152,23 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
}
|
||||
|
||||
reloadNoteCounts() {
|
||||
let allTags: Array<SNTag | SNSmartTag> = [];
|
||||
if (this.getState().tags) {
|
||||
allTags = allTags.concat(this.getState().tags);
|
||||
}
|
||||
if (this.getState().smartTags) {
|
||||
allTags = allTags.concat(this.getState().smartTags);
|
||||
}
|
||||
const smartTags = this.state.smartTags;
|
||||
const noteCounts: NoteCounts = {};
|
||||
for (const tag of allTags) {
|
||||
if (tag === this.state.templateTag) {
|
||||
continue;
|
||||
}
|
||||
if (tag.isSmartTag) {
|
||||
/** Other smart tags do not contain counts */
|
||||
if (tag.isAllTag) {
|
||||
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag)
|
||||
.filter((note) => {
|
||||
return !note.archived && !note.trashed;
|
||||
});
|
||||
noteCounts[tag.uuid] = notes.length;
|
||||
}
|
||||
} else {
|
||||
const notes = this.application.referencesForItem(tag, ContentType.Note)
|
||||
|
||||
for (const tag of smartTags) {
|
||||
/** Other smart tags do not contain counts */
|
||||
if (tag.isAllTag) {
|
||||
const notes = this.application
|
||||
.notesMatchingSmartTag(tag as SNSmartTag)
|
||||
.filter((note) => {
|
||||
return !note.archived && !note.trashed;
|
||||
});
|
||||
noteCounts[tag.uuid] = notes.length;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
noteCounts: noteCounts
|
||||
noteCounts: noteCounts,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,14 +176,14 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
if (!this.panelPuppet.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.application.getPreference(PrefKey.TagsPanelWidth);
|
||||
if (width) {
|
||||
this.panelPuppet.setWidth!(width);
|
||||
if (this.panelPuppet.isCollapsed!()) {
|
||||
this.application.getAppState().panelDidResize(
|
||||
PANEL_NAME_TAGS,
|
||||
this.panelPuppet.isCollapsed!()
|
||||
);
|
||||
this.application
|
||||
.getAppState()
|
||||
.panelDidResize(PANEL_NAME_TAGS, this.panelPuppet.isCollapsed!());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,36 +194,39 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
_isAtMaxWidth: boolean,
|
||||
isCollapsed: boolean
|
||||
) => {
|
||||
this.application.setPreference(
|
||||
PrefKey.TagsPanelWidth,
|
||||
newWidth
|
||||
).then(() => this.application.sync());
|
||||
this.application.getAppState().panelDidResize(
|
||||
PANEL_NAME_TAGS,
|
||||
isCollapsed
|
||||
);
|
||||
}
|
||||
this.application
|
||||
.setPreference(PrefKey.TagsPanelWidth, newWidth)
|
||||
.then(() => this.application.sync());
|
||||
this.application.getAppState().panelDidResize(PANEL_NAME_TAGS, isCollapsed);
|
||||
};
|
||||
|
||||
registerComponentHandler() {
|
||||
this.unregisterComponent = this.application.componentManager!.registerHandler({
|
||||
identifier: 'tags',
|
||||
areas: [ComponentArea.TagsList],
|
||||
actionHandler: (_, action, data) => {
|
||||
if (action === ComponentAction.SelectItem) {
|
||||
if (data.item!.content_type === ContentType.Tag) {
|
||||
const tag = this.application.findItem(data.item!.uuid);
|
||||
if (tag) {
|
||||
this.selectTag(tag as SNTag);
|
||||
this.unregisterComponent =
|
||||
this.application.componentManager!.registerHandler({
|
||||
identifier: 'tags',
|
||||
areas: [ComponentArea.TagsList],
|
||||
actionHandler: (_, action, data) => {
|
||||
if (action === ComponentAction.SelectItem) {
|
||||
const item = data.item;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
} else if (data.item!.content_type === ContentType.SmartTag) {
|
||||
const matchingTag = this.getState().smartTags.find(t => t.uuid === data.item!.uuid);
|
||||
this.selectTag(matchingTag as SNSmartTag);
|
||||
|
||||
if (item.content_type === ContentType.SmartTag) {
|
||||
const matchingTag = this.getState().smartTags.find(
|
||||
(t) => t.uuid === item.uuid
|
||||
);
|
||||
|
||||
if (matchingTag) {
|
||||
this.selectTag(matchingTag);
|
||||
}
|
||||
}
|
||||
} else if (action === ComponentAction.ClearSelection) {
|
||||
this.selectTag(this.getState().smartTags[0]);
|
||||
}
|
||||
} else if (action === ComponentAction.ClearSelection) {
|
||||
this.selectTag(this.getState().smartTags[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async selectTag(tag: SNTag) {
|
||||
@@ -265,123 +239,11 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
}
|
||||
|
||||
async clickedAddNewTag() {
|
||||
if (this.getState().editingTag) {
|
||||
if (this.appState.templateTag) {
|
||||
return;
|
||||
}
|
||||
const newTag = await this.application.createTemplateItem(
|
||||
ContentType.Tag
|
||||
) as SNTag;
|
||||
this.setState({
|
||||
tags: [newTag].concat(this.getState().tags),
|
||||
previousTag: this.getState().selectedTag,
|
||||
selectedTag: newTag,
|
||||
editingTag: newTag,
|
||||
templateTag: newTag
|
||||
});
|
||||
}
|
||||
|
||||
onTagTitleChange(tag: SNTag | SNSmartTag) {
|
||||
this.setState({
|
||||
editingTag: tag
|
||||
});
|
||||
}
|
||||
|
||||
async saveTag($event: Event, tag: SNTag) {
|
||||
($event.target! as HTMLInputElement).blur();
|
||||
if (this.getState().templateTag) {
|
||||
if (!this.titles[tag.uuid]?.length) {
|
||||
return this.undoCreateTag(tag);
|
||||
}
|
||||
return this.saveNewTag();
|
||||
} else {
|
||||
return this.saveTagRename(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private async undoCreateTag(tag: SNTag) {
|
||||
await this.setState({
|
||||
templateTag: undefined,
|
||||
editingTag: undefined,
|
||||
selectedTag: this.appState.selectedTag,
|
||||
tags: this.state.tags.filter(existingTag => existingTag !== tag)
|
||||
});
|
||||
delete this.titles[tag.uuid];
|
||||
}
|
||||
|
||||
async saveTagRename(tag: SNTag) {
|
||||
const newTitle = this.titles[tag.uuid] || '';
|
||||
if (newTitle.length === 0) {
|
||||
this.titles[tag.uuid] = this.editingOriginalName;
|
||||
this.editingOriginalName = undefined;
|
||||
await this.setState({
|
||||
editingTag: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existingTag = this.application.findTagByTitle(newTitle);
|
||||
if (existingTag && existingTag.uuid !== tag.uuid) {
|
||||
this.application.alertService!.alert(
|
||||
"A tag with this name already exists."
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
|
||||
mutator.title = newTitle;
|
||||
});
|
||||
await this.setState({
|
||||
editingTag: undefined
|
||||
});
|
||||
}
|
||||
|
||||
async saveNewTag() {
|
||||
const newTag = this.getState().templateTag!;
|
||||
const newTitle = this.titles[newTag.uuid] || '';
|
||||
if (newTitle.length === 0) {
|
||||
await this.setState({
|
||||
templateTag: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
const existingTag = this.application.findTagByTitle(newTitle);
|
||||
if (existingTag) {
|
||||
this.application.alertService!.alert(
|
||||
"A tag with this name already exists."
|
||||
);
|
||||
this.undoCreateTag(newTag);
|
||||
return;
|
||||
}
|
||||
const insertedTag = await this.application.insertItem(newTag);
|
||||
const changedTag = await this.application.changeItem<TagMutator>(insertedTag.uuid, (m) => {
|
||||
m.title = newTitle;
|
||||
});
|
||||
await this.setState({
|
||||
templateTag: undefined,
|
||||
editingTag: undefined
|
||||
});
|
||||
this.selectTag(changedTag as SNTag);
|
||||
await this.application.saveItem(changedTag!.uuid);
|
||||
}
|
||||
|
||||
async selectedRenameTag(tag: SNTag) {
|
||||
this.editingOriginalName = tag.title;
|
||||
await this.setState({
|
||||
editingTag: tag
|
||||
});
|
||||
document.getElementById('tag-' + tag.uuid)!.focus();
|
||||
}
|
||||
|
||||
selectedDeleteTag(tag: SNTag) {
|
||||
this.removeTag(tag);
|
||||
}
|
||||
|
||||
async removeTag(tag: SNTag) {
|
||||
if (await confirmDialog({
|
||||
text: STRING_DELETE_TAG,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
this.application.deleteItem(tag);
|
||||
this.selectTag(this.getState().smartTags[0]);
|
||||
}
|
||||
this.appState.createNewTag();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +252,7 @@ export class TagsView extends WebDirective {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.scope = {
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
this.template = template;
|
||||
this.replace = true;
|
||||
|
||||
@@ -33,7 +33,7 @@ $heading-height: 75px;
|
||||
padding-top: 14px;
|
||||
padding-left: 14px;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 10px;
|
||||
padding-right: 14px;
|
||||
|
||||
border-bottom: none;
|
||||
z-index: $z-index-editor-title-bar;
|
||||
|
||||
85
app/assets/stylesheets/_focused.scss
Normal file
85
app/assets/stylesheets/_focused.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
.focus-mode {
|
||||
.mac-desktop #editor-column {
|
||||
// To offset colored circles in Mac
|
||||
padding-top: 35px;
|
||||
}
|
||||
|
||||
.mac-desktop #editor-column:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
#editor-title-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor-menu-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor-pane-component-stack {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#footer-bar {
|
||||
opacity: 0.08;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
#footer-bar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.section.tags,
|
||||
.section.notes {
|
||||
will-change: opacity;
|
||||
animation: fade-out 1.25s forwards;
|
||||
transition: width 1.25s;
|
||||
transition-delay: 0s;
|
||||
width: 0px !important;
|
||||
flex: none !important;
|
||||
}
|
||||
|
||||
.section.tags:hover {
|
||||
flex: initial;
|
||||
width: 0px !important;
|
||||
}
|
||||
|
||||
.section.notes:hover {
|
||||
flex: initial;
|
||||
width: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.disable-focus-mode {
|
||||
.section.tags,
|
||||
.section.notes {
|
||||
transition: width 1.25s;
|
||||
will-change: opacity;
|
||||
animation: fade-in 1.25s forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,10 @@
|
||||
max-width: 18rem;
|
||||
}
|
||||
|
||||
.max-w-89 {
|
||||
max-width: 22.25rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -300,10 +304,6 @@
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.max-w-89 {
|
||||
max-width: 22.25rem;
|
||||
}
|
||||
|
||||
.w-26 {
|
||||
width: 6.5rem;
|
||||
}
|
||||
@@ -461,6 +461,11 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.sn-component .px-4\.5,
|
||||
.sn-component .sk-panel .px-4\.5 {
|
||||
padding-left: 1.375rem;
|
||||
@@ -743,6 +748,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.border-info-contrast {
|
||||
border-color: var(--sn-stylekit-info-contrast-color);
|
||||
}
|
||||
|
||||
.sn-icon-button {
|
||||
&:focus {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
border-color: transparent;
|
||||
@extend .bg-info;
|
||||
@extend .color-info-contrast;
|
||||
|
||||
&:focus {
|
||||
background-color: var(--sn-stylekit-info-color) !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@extend .color-info;
|
||||
@extend .border-info;
|
||||
}
|
||||
|
||||
&:focus:hover {
|
||||
background-color: var(--sn-stylekit-contrast-background-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $screen-md-min) {
|
||||
.sn-component {
|
||||
.md\:hidden {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.tags {
|
||||
width: 180px;
|
||||
flex-grow: 0;
|
||||
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -85,18 +87,25 @@
|
||||
|
||||
// Required for Safari to avoid highlighting when dragging panel resizers
|
||||
// Make sure to undo if it's selected (for editing)
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
&.editing {
|
||||
-webkit-user-select: text !important;
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
-moz-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .count {
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
@import 'reach-sub';
|
||||
@import 'sessions-modal';
|
||||
@import 'preferences';
|
||||
@import 'focused';
|
||||
@import 'sn';
|
||||
|
||||
22
app/assets/svg/il-premium.svg
Normal file
22
app/assets/svg/il-premium.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="60" fill="#F4F5F7" />
|
||||
<g filter="url(#filter0_d_5_1809)">
|
||||
<rect x="31" y="31" width="58" height="58" rx="4" fill="white" />
|
||||
<path
|
||||
d="M49.9997 66H53.9997C53.9997 68.16 56.7397 70 59.9997 70C63.2596 70 65.9996 68.16 65.9996 66C65.9996 63.8 63.9197 63 59.5197 61.94C55.2797 60.88 49.9997 59.56 49.9997 54C49.9997 50.42 52.9397 47.38 56.9997 46.36V42H62.9996V46.36C67.0596 47.38 69.9996 50.42 69.9996 54H65.9996C65.9996 51.84 63.2596 50 59.9997 50C56.7397 50 53.9997 51.84 53.9997 54C53.9997 56.2 56.0796 57 60.4796 58.06C64.7196 59.12 69.9996 60.44 69.9996 66C69.9996 69.58 67.0596 72.62 62.9996 73.64V78H56.9997V73.64C52.9397 72.62 49.9997 69.58 49.9997 66Z"
|
||||
fill="#BBBEC4" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5_1809" x="19" y="23" width="82" height="82" filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha" />
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="6" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5_1809" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_1809" result="shape" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
29
package.json
29
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "standard-notes-web",
|
||||
"version": "3.9.5",
|
||||
"version": "3.9.6",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -16,7 +16,9 @@
|
||||
"bundle:desktop:beta": "webpack --config webpack.prod.js --env platform='desktop' --env public_beta='true'",
|
||||
"build": "bundle install && yarn install --frozen-lockfile && bundle exec rails assets:precompile && yarn bundle",
|
||||
"lint": "eslint --fix app/assets/javascripts",
|
||||
"tsc": "tsc --project app/assets/javascripts/tsconfig.json"
|
||||
"tsc": "tsc --project app/assets/javascripts/tsconfig.json",
|
||||
"test": "jest --config app/assets/javascripts/jest.config.js",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
@@ -27,6 +29,7 @@
|
||||
"@reach/visually-hidden": "^0.16.0",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@types/angular": "^1.8.3",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/lodash": "^4.14.176",
|
||||
"@types/pug": "^2.0.5",
|
||||
"@types/react": "^17.0.31",
|
||||
@@ -46,15 +49,25 @@
|
||||
"eslint-plugin-react-hooks": "^4.2.1-beta-149b420f6-20211119",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.4.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.3.1",
|
||||
"jest-transform-pug": "^0.1.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": ">=10",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.4.3",
|
||||
"ng-cache-loader": "0.0.26",
|
||||
"node-sass": "^6.0.1",
|
||||
"prettier": "^2.5.0",
|
||||
"pretty-quick": "^3.1.2",
|
||||
"pug": "^3.0.2",
|
||||
"pug-jest": "^1.0.1",
|
||||
"pug-loader": "^2.4.0",
|
||||
"sass-loader": "^12.2.0",
|
||||
"serve-static": "^1.14.1",
|
||||
"sn-stylekit": "5.2.15",
|
||||
"sn-stylekit": "5.2.17",
|
||||
"svg-jest": "^1.0.1",
|
||||
"ts-jest": "^27.0.7",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "4.4.4",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
@@ -70,12 +83,16 @@
|
||||
"@reach/checkbox": "^0.16.0",
|
||||
"@reach/dialog": "^0.16.2",
|
||||
"@reach/listbox": "^0.16.2",
|
||||
"@standardnotes/features": "1.9.0",
|
||||
"@standardnotes/features": "1.10.1",
|
||||
"@standardnotes/sncrypto-web": "1.5.3",
|
||||
"@standardnotes/snjs": "2.18.2",
|
||||
"@standardnotes/snjs": "2.19.6",
|
||||
"mobx": "^6.3.5",
|
||||
"mobx-react-lite": "^3.2.1",
|
||||
"mobx-react-lite": "^3.2.2",
|
||||
"preact": "^10.5.15",
|
||||
"qrcode.react": "^1.0.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,jsx,tsx}": "eslint --cache --fix",
|
||||
"*.{js,ts,jsx,tsx,css,md}": "prettier --write"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user