mirror of
https://github.com/standardnotes/app
synced 2026-09-19 09:13:53 -04:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8755001f7e | ||
|
|
7c2e832065 | ||
|
|
096d82f7af | ||
|
|
8c8f045b9a | ||
|
|
b3140f1623 | ||
|
|
c5e104f90b | ||
|
|
09b8f00eed | ||
|
|
c39c72da7a | ||
|
|
7f28876047 | ||
|
|
24ff87c306 | ||
|
|
e8b758214d | ||
|
|
e58c563795 | ||
|
|
328bbb797d | ||
|
|
dab4f678f0 | ||
|
|
c847cc1d16 | ||
|
|
f49ba6bd4d |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.20.8",
|
||||
"version": "1.20.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
### Features
|
||||
|
||||
* ability to drag super list items; secure password generation blocks ([#2039](https://github.com/standardnotes/app/issues/2039)) ([c39c72d](https://github.com/standardnotes/app/commit/c39c72da7a4fb85f4da9aa4e6f8e9f7ba4486a94))
|
||||
|
||||
## [1.6.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improved error reporting in super; improved draggable block icon padding ([24ff87c](https://github.com/standardnotes/app/commit/24ff87c30625432393ffedeada14478ebb625292))
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue where converting empty plain note to super would result in error ([e58c563](https://github.com/standardnotes/app/commit/e58c5637951f928efcd8c8427f06d0b6c69b5e55))
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** enable block drag'n'drop in super editor ([#2029](https://github.com/standardnotes/app/issues/2029)) ([dab4f67](https://github.com/standardnotes/app/commit/dab4f678f04aee2c7dfbe7f416e7fc34e8d870cf))
|
||||
|
||||
## [1.5.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/blocks-editor",
|
||||
"version": "1.5.3",
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -31,8 +31,6 @@ import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugi
|
||||
import {truncateString} from './Utils';
|
||||
import {SuperEditorContentId} from './Constants';
|
||||
|
||||
const BlockDragEnabled = false;
|
||||
|
||||
type BlocksEditorProps = {
|
||||
onChange: (value: string, preview: string) => void;
|
||||
className?: string;
|
||||
@@ -69,8 +67,14 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
});
|
||||
previewText = truncateString(previewText, previewLength);
|
||||
|
||||
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
|
||||
onChange(stringifiedEditorState, previewText);
|
||||
try {
|
||||
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
|
||||
onChange(stringifiedEditorState, previewText);
|
||||
} catch (error) {
|
||||
window.alert(
|
||||
`An invalid change was made inside the Super editor. Your change was not saved. Please report this error to the team: ${error}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[onChange, didIgnoreFirstChange],
|
||||
@@ -94,7 +98,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
<div className="editor" ref={onRef}>
|
||||
<ContentEditable
|
||||
id={SuperEditorContentId}
|
||||
className={`ContentEditable__root ${className}`}
|
||||
className={`ContentEditable__root overflow-y-auto ${className}`}
|
||||
spellCheck={spellcheck}
|
||||
/>
|
||||
</div>
|
||||
@@ -130,11 +134,9 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
<>
|
||||
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
||||
<FloatingLinkEditorPlugin />
|
||||
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
||||
</>
|
||||
)}
|
||||
{floatingAnchorElem && BlockDragEnabled && (
|
||||
<>{<DraggableBlockPlugin anchorElem={floatingAnchorElem} />}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {BlockEditorNodes} from '../Lexical/Nodes/AllNodes';
|
||||
import {Klass, LexicalNode} from 'lexical';
|
||||
|
||||
type BlocksEditorComposerProps = {
|
||||
initialValue: string;
|
||||
initialValue: string | undefined;
|
||||
children: React.ReactNode;
|
||||
nodes?: Array<Klass<LexicalNode>>;
|
||||
readonly?: boolean;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
.draggable-block-menu {
|
||||
border-radius: 4px;
|
||||
padding: 2px 1px;
|
||||
padding: 3px 1px;
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
will-change: transform;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.draggable-block-menu .icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.3;
|
||||
background-image: url(#{$blocks-editor-icons-path}/draggable-block-menu.svg);
|
||||
width: 0.8rem;
|
||||
height: 1.1rem;
|
||||
opacity: 0.2;
|
||||
padding-left: 4.75px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.draggable-block-menu:active {
|
||||
@@ -21,16 +23,17 @@
|
||||
}
|
||||
|
||||
.draggable-block-menu:hover {
|
||||
background-color: #efefef;
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
}
|
||||
|
||||
.draggable-block-target-line {
|
||||
pointer-events: none;
|
||||
background: deepskyblue;
|
||||
height: 4px;
|
||||
background: var(--sn-stylekit-info-color);
|
||||
height: 0.25rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
will-change: transform;
|
||||
will-change: transform, opacity;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import {$createListNode, $isListNode} from '@lexical/list';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {eventFiles} from '@lexical/rich-text';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
@@ -19,16 +20,17 @@ import {
|
||||
} from 'lexical';
|
||||
import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
import {BlockIcon} from '@standardnotes/icons';
|
||||
|
||||
import {isHTMLElement} from '../../Utils/guard';
|
||||
import {Point} from '../../Utils/point';
|
||||
import {Rect} from '../../Utils/rect';
|
||||
import {ContainsPointReturn, Rect} from '../../Utils/rect';
|
||||
|
||||
const SPACE = 4;
|
||||
const TARGET_LINE_HALF_HEIGHT = 2;
|
||||
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
|
||||
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
|
||||
const TEXT_BOX_HORIZONTAL_PADDING = 28;
|
||||
const TEXT_BOX_HORIZONTAL_PADDING = 24;
|
||||
|
||||
const Downward = 1;
|
||||
const Upward = -1;
|
||||
@@ -52,12 +54,56 @@ function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
|
||||
return root ? root.__children : [];
|
||||
}
|
||||
|
||||
function elementContainingEventLocation(
|
||||
anchorElem: HTMLElement,
|
||||
element: HTMLElement,
|
||||
event: MouseEvent,
|
||||
): {contains: ContainsPointReturn; element: HTMLElement} {
|
||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
||||
|
||||
const eventLocation = new Point(event.x, event.y);
|
||||
const elementDomRect = Rect.fromDOM(element);
|
||||
const {marginTop, marginBottom} = window.getComputedStyle(element);
|
||||
|
||||
const rect = elementDomRect.generateNewRect({
|
||||
bottom: elementDomRect.bottom + parseFloat(marginBottom),
|
||||
left: anchorElementRect.left,
|
||||
right: anchorElementRect.right,
|
||||
top: elementDomRect.top - parseFloat(marginTop),
|
||||
});
|
||||
|
||||
const children = Array.from(element.children);
|
||||
|
||||
const shouldRecurseIntoChildren = ['UL', 'OL', 'LI'].includes(
|
||||
element.tagName,
|
||||
);
|
||||
|
||||
if (shouldRecurseIntoChildren) {
|
||||
for (const child of children) {
|
||||
const isLeaf = child.children.length === 0;
|
||||
if (isLeaf) {
|
||||
continue;
|
||||
}
|
||||
const childResult = elementContainingEventLocation(
|
||||
anchorElem,
|
||||
child as HTMLElement,
|
||||
event,
|
||||
);
|
||||
|
||||
if (childResult.contains.result) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {contains: rect.contains(eventLocation), element: element};
|
||||
}
|
||||
|
||||
function getBlockElement(
|
||||
anchorElem: HTMLElement,
|
||||
editor: LexicalEditor,
|
||||
event: MouseEvent,
|
||||
): HTMLElement | null {
|
||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
||||
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
|
||||
|
||||
let blockElem: HTMLElement | null = null;
|
||||
@@ -72,32 +118,22 @@ function getBlockElement(
|
||||
if (elem === null) {
|
||||
break;
|
||||
}
|
||||
const point = new Point(event.x, event.y);
|
||||
const domRect = Rect.fromDOM(elem);
|
||||
const {marginTop, marginBottom} = window.getComputedStyle(elem);
|
||||
const {contains, element} = elementContainingEventLocation(
|
||||
anchorElem,
|
||||
elem,
|
||||
event,
|
||||
);
|
||||
|
||||
const rect = domRect.generateNewRect({
|
||||
bottom: domRect.bottom + parseFloat(marginBottom),
|
||||
left: anchorElementRect.left,
|
||||
right: anchorElementRect.right,
|
||||
top: domRect.top - parseFloat(marginTop),
|
||||
});
|
||||
|
||||
const {
|
||||
result,
|
||||
reason: {isOnTopSide, isOnBottomSide},
|
||||
} = rect.contains(point);
|
||||
|
||||
if (result) {
|
||||
blockElem = elem;
|
||||
if (contains.result) {
|
||||
blockElem = element;
|
||||
prevIndex = index;
|
||||
break;
|
||||
}
|
||||
|
||||
if (direction === Indeterminate) {
|
||||
if (isOnTopSide) {
|
||||
if (contains.reason.isOnTopSide) {
|
||||
direction = Upward;
|
||||
} else if (isOnBottomSide) {
|
||||
} else if (contains.reason.isOnBottomSide) {
|
||||
direction = Downward;
|
||||
} else {
|
||||
// stop search block element
|
||||
@@ -123,7 +159,6 @@ function setMenuPosition(
|
||||
) {
|
||||
if (!targetElem) {
|
||||
floatingElem.style.opacity = '0';
|
||||
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,13 +220,12 @@ function setTargetLine(
|
||||
targetLineElem.style.width = `${
|
||||
anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2
|
||||
}px`;
|
||||
targetLineElem.style.opacity = '.4';
|
||||
targetLineElem.style.opacity = '.6';
|
||||
}
|
||||
|
||||
function hideTargetLine(targetLineElem: HTMLElement | null) {
|
||||
if (targetLineElem) {
|
||||
targetLineElem.style.opacity = '0';
|
||||
targetLineElem.style.transform = 'translate(-10000px, -10000px)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,18 +317,30 @@ function useDraggableBlockMenu(
|
||||
return false;
|
||||
}
|
||||
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
||||
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
if (targetNode === draggedNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let nodeToInsert = draggedNode;
|
||||
const targetParent = targetNode.getParent();
|
||||
const sourceParent = draggedNode.getParent();
|
||||
|
||||
if ($isListNode(sourceParent) && !$isListNode(targetParent)) {
|
||||
const newList = $createListNode(sourceParent.getListType());
|
||||
newList.append(draggedNode);
|
||||
nodeToInsert = newList;
|
||||
}
|
||||
|
||||
const {top, height} = targetBlockElem.getBoundingClientRect();
|
||||
const shouldInsertAfter = pageY - top > height / 2;
|
||||
if (shouldInsertAfter) {
|
||||
targetNode.insertAfter(draggedNode);
|
||||
targetNode.insertAfter(nodeToInsert);
|
||||
} else {
|
||||
targetNode.insertBefore(draggedNode);
|
||||
targetNode.insertBefore(nodeToInsert);
|
||||
}
|
||||
setDraggableBlockElem(null);
|
||||
|
||||
@@ -347,7 +393,9 @@ function useDraggableBlockMenu(
|
||||
draggable={true}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}>
|
||||
<div className={isEditable ? 'icon' : ''} />
|
||||
<div className={isEditable ? 'icon' : ''}>
|
||||
<BlockIcon className="text-text pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="draggable-block-target-line" ref={targetLineRef} />
|
||||
</>,
|
||||
|
||||
@@ -110,8 +110,8 @@ export function InsertTableDialog({
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput label="No of rows" onChange={setRows} value={rows} />
|
||||
<TextInput label="No of columns" onChange={setColumns} value={columns} />
|
||||
<TextInput label="Number of rows" onChange={setRows} value={rows} />
|
||||
<TextInput label="Number of columns" onChange={setColumns} value={columns} />
|
||||
<DialogActions data-test-id="table-model-confirm-insert">
|
||||
<Button onClick={onClick}>Confirm</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
border: 0px;
|
||||
background-color: #eee;
|
||||
border-radius: 5px;
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.Button__root:hover {
|
||||
background-color: #ddd;
|
||||
background-color: var(--sn-stylekit-info-color);
|
||||
color: var(--sn-stylekit-info-contrast-color);
|
||||
}
|
||||
.Button__small {
|
||||
padding-top: 5px;
|
||||
@@ -32,5 +32,5 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.Button__disabled:hover {
|
||||
background-color: #eee;
|
||||
background-color: var(--sn-stylekit-secondary-background-color);
|
||||
}
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
color: #666;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.Input__input {
|
||||
display: flex;
|
||||
flex: 2;
|
||||
border: 1px solid #999;
|
||||
border: 1px solid var(--sn-stylekit-contrast-border-color);
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
background-color: rgba(40, 40, 40, 0.6);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
flex-grow: 0px;
|
||||
flex-shrink: 1px;
|
||||
z-index: 100;
|
||||
@@ -28,22 +28,23 @@
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-grow: 0px;
|
||||
background-color: #fff;
|
||||
background-color: var(--sn-stylekit-background-color);
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-shadow: 0 0 20px 0 #444;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0px 0 var(--sn-stylekit-shadow-color);
|
||||
border-radius: 0px;
|
||||
}
|
||||
.Modal__title {
|
||||
color: #444;
|
||||
color:var(--sn-stylekit-foreground-color);
|
||||
margin: 0px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--sn-stylekit-border-color);
|
||||
}
|
||||
.Modal__closeButton {
|
||||
border: 0px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 15px;
|
||||
border-radius: 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -52,10 +53,11 @@
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #eee;
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
}
|
||||
.Modal__closeButton:hover {
|
||||
background-color: #ddd;
|
||||
background-color: var(--sn-stylekit-info-color);
|
||||
color: var(--sn-stylekit-info-contrast-color);
|
||||
}
|
||||
.Modal__content {
|
||||
padding-top: 20px;
|
||||
|
||||
@@ -73,7 +73,7 @@ function PortalImpl({
|
||||
aria-label="Close modal"
|
||||
type="button"
|
||||
onClick={onClose}>
|
||||
X
|
||||
✕
|
||||
</button>
|
||||
<div className="Modal__content">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import {isPoint, Point} from './point';
|
||||
|
||||
type ContainsPointReturn = {
|
||||
export type ContainsPointReturn = {
|
||||
result: boolean;
|
||||
reason: {
|
||||
isOnTopSide: boolean;
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.101.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
### Features
|
||||
|
||||
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
|
||||
|
||||
## [3.100.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.100.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.100.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.100.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.100.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.100.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.100.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { FileBackupRecord, FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { AppState } from 'app/AppState'
|
||||
import { shell } from 'electron'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
@@ -120,6 +120,10 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
return this.defaultMappingFileValue()
|
||||
}
|
||||
|
||||
for (const entry of Object.values(data.files)) {
|
||||
entry.backedUpOn = new Date(entry.backedUpOn)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -129,6 +133,10 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
void shell.openPath(location)
|
||||
}
|
||||
|
||||
async openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||
void shell.openPath(record.absolutePath)
|
||||
}
|
||||
|
||||
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
|
||||
await writeJSONFile(this.getMappingFileLocation(), file)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { StoreKeys } from '../Store/StoreKeys'
|
||||
const path = require('path')
|
||||
const rendererPath = path.join('file://', __dirname, '/renderer.js')
|
||||
|
||||
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { FileBackupsDevice, FileBackupsMapping, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
|
||||
import { KeychainInterface } from '../Keychain/KeychainInterface'
|
||||
@@ -64,6 +64,7 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
|
||||
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
|
||||
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
|
||||
openFileBackup: this.openFileBackup.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,4 +203,8 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
public openFilesBackupsLocation(): Promise<void> {
|
||||
return this.fileBackups.openFilesBackupsLocation()
|
||||
}
|
||||
|
||||
public openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||
return this.fileBackups.openFileBackup(record)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Environment,
|
||||
FileBackupsMapping,
|
||||
RawKeychainValue,
|
||||
FileBackupRecord,
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
|
||||
import { Component } from '../Main/Packages/PackageManagerInterface'
|
||||
@@ -132,6 +133,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
||||
return this.remoteBridge.openFilesBackupsLocation()
|
||||
}
|
||||
|
||||
openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||
return this.remoteBridge.openFileBackup(record)
|
||||
}
|
||||
|
||||
async saveFilesBackupsFile(
|
||||
uuid: string,
|
||||
metaFile: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.100.12",
|
||||
"version": "3.101.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.19.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.19.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.19.16",
|
||||
"version": "1.19.17",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.26.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
### Features
|
||||
|
||||
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
|
||||
|
||||
## [1.25.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.25.16",
|
||||
"version": "1.26.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Web filepicker for Standard Notes projects",
|
||||
"main": "dist/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"types": "./src/index.ts",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prestart": "yarn clean",
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"build": "echo 'Empty build script required for yarn topological install'",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "jest"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
|
||||
"extends": "../../UILib.tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
### Features
|
||||
|
||||
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
|
||||
|
||||
## [1.12.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.12.16",
|
||||
"version": "1.13.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Client-side files library",
|
||||
"main": "dist/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prestart": "yarn clean",
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"build": "echo 'Empty build script required for yarn topological install'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { FileBackupsMapping } from './FileBackupsMapping'
|
||||
import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping'
|
||||
|
||||
export interface FileBackupsDevice {
|
||||
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
|
||||
@@ -18,4 +18,5 @@ export interface FileBackupsDevice {
|
||||
changeFilesBackupsLocation(): Promise<string | undefined>
|
||||
getFilesBackupsLocation(): Promise<string>
|
||||
openFilesBackupsLocation(): Promise<void>
|
||||
openFileBackup(record: FileBackupRecord): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'
|
||||
|
||||
export type FileBackupRecord = {
|
||||
backedUpOn: Date
|
||||
absolutePath: string
|
||||
relativePath: string
|
||||
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
|
||||
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
|
||||
version: typeof FileBackupsConstantsV1.Version
|
||||
}
|
||||
|
||||
export interface FileBackupsMapping {
|
||||
version: typeof FileBackupsConstantsV1.Version
|
||||
files: Record<
|
||||
Uuid,
|
||||
{
|
||||
backedUpOn: Date
|
||||
absolutePath: string
|
||||
relativePath: string
|
||||
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
|
||||
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
|
||||
version: typeof FileBackupsConstantsV1.Version
|
||||
}
|
||||
>
|
||||
files: Record<Uuid, FileBackupRecord>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
|
||||
"extends": "../../UILib.tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
### Features
|
||||
|
||||
* ability to drag super list items; secure password generation blocks ([#2039](https://github.com/standardnotes/app/issues/2039)) ([c39c72d](https://github.com/standardnotes/app/commit/c39c72da7a4fb85f4da9aa4e6f8e9f7ba4486a94))
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** enable block drag'n'drop in super editor ([#2029](https://github.com/standardnotes/app/issues/2029)) ([dab4f67](https://github.com/standardnotes/app/commit/dab4f678f04aee2c7dfbe7f416e7fc34e8d870cf))
|
||||
|
||||
## [1.4.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/icons
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/icons",
|
||||
"version": "1.4.10",
|
||||
"version": "1.6.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="9px" height="14px" viewBox="0 0 9 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-2" fill="currentColor" fill-rule="nonzero">
|
||||
<path d="M1.6666,3.33333 C1.2246,3.33333 0.8007,3.15774 0.4881,2.84518 C0.1755,2.53262 0,2.1087 0,1.66667 C0,1.22464 0.1755,0.80072 0.4881,0.48816 C0.8007,0.1756 1.2246,0 1.6666,0 C2.1086,0 2.5326,0.1756 2.8451,0.48816 C3.1577,0.80072 3.3333,1.22464 3.3333,1.66667 C3.3333,2.1087 3.1577,2.53262 2.8451,2.84518 C2.5326,3.15774 2.1086,3.33333 1.6666,3.33333 Z M1.6666,8.33333 C1.2246,8.33333 0.8007,8.15773 0.4881,7.84513 C0.1755,7.53263 0,7.10873 0,6.66663 C0,6.22463 0.1755,5.80073 0.4881,5.48813 C0.8007,5.17563 1.2246,5.00003 1.6666,5.00003 C2.1086,5.00003 2.5326,5.17563 2.8451,5.48813 C3.1577,5.80073 3.3333,6.22463 3.3333,6.66663 C3.3333,7.10873 3.1577,7.53263 2.8451,7.84513 C2.5326,8.15773 2.1086,8.33333 1.6666,8.33333 Z M1.6666,13.33333 C1.2246,13.33333 0.8007,13.15773 0.4881,12.84513 C0.1755,12.53263 0,12.10873 0,11.66663 C0,11.22463 0.1755,10.80073 0.4881,10.48813 C0.8007,10.17563 1.2246,10.00003 1.6666,10.00003 C2.1086,10.00003 2.5326,10.17563 2.8451,10.48813 C3.1577,10.80073 3.3333,11.22463 3.3333,11.66663 C3.3333,12.10873 3.1577,12.53263 2.8451,12.84513 C2.5326,13.15773 2.1086,13.33333 1.6666,13.33333 L1.6666,13.33333 Z M6.6666,3.33333 C6.2246,3.33333 5.8007,3.15774 5.4881,2.84518 C5.1755,2.53262 5,2.1087 5,1.66667 C5,1.22464 5.1755,0.80072 5.4881,0.48816 C5.8007,0.1756 6.2246,0 6.6666,0 C7.1086,0 7.5326,0.1756 7.8451,0.48816 C8.1577,0.80072 8.3333,1.22464 8.3333,1.66667 C8.3333,2.1087 8.1577,2.53262 7.8451,2.84518 C7.5326,3.15774 7.1086,3.33333 6.6666,3.33333 Z M6.6666,8.33333 C6.2246,8.33333 5.8007,8.15773 5.4881,7.84513 C5.1755,7.53263 5,7.10873 5,6.66663 C5,6.22463 5.1755,5.80073 5.4881,5.48813 C5.8007,5.17563 6.2246,5.00003 6.6666,5.00003 C7.1086,5.00003 7.5326,5.17563 7.8451,5.48813 C8.1577,5.80073 8.3333,6.22463 8.3333,6.66663 C8.3333,7.10873 8.1577,7.53263 7.8451,7.84513 C7.5326,8.15773 7.1086,8.33333 6.6666,8.33333 Z M6.6666,13.33333 C6.2246,13.33333 5.8007,13.15773 5.4881,12.84513 C5.1755,12.53263 5,12.10873 5,11.66663 C5,11.22463 5.1755,10.80073 5.4881,10.48813 C5.8007,10.17563 6.2246,10.00003 6.6666,10.00003 C7.1086,10.00003 7.5326,10.17563 7.8451,10.48813 C8.1577,10.80073 8.3333,11.22463 8.3333,11.66663 C8.3333,12.10873 8.1577,12.53263 7.8451,12.84513 C7.5326,13.15773 7.1086,13.33333 6.6666,13.33333 L6.6666,13.33333 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -21,6 +21,7 @@ import AttachmentFileIcon from './ic-attachment-file.svg'
|
||||
import AuthenticatorIcon from './ic-authenticator.svg'
|
||||
import AuthenticatorVariantIcon from './ic-authenticator-variant.svg'
|
||||
import BackIosIcon from './ic-back-ios.svg'
|
||||
import BlockIcon from './ic-block.svg'
|
||||
import BlueDotIcon from './blue-dot.svg'
|
||||
import BoldIcon from './ic-bold.svg'
|
||||
import BoxFilledIcon from './ic-box-filled.svg'
|
||||
@@ -224,6 +225,7 @@ export {
|
||||
AuthenticatorIcon,
|
||||
AuthenticatorVariantIcon,
|
||||
BackIosIcon,
|
||||
BlockIcon,
|
||||
BlueDotIcon,
|
||||
BoldIcon,
|
||||
BoxFilledIcon,
|
||||
|
||||
@@ -27,6 +27,7 @@ import TypeSubscript from './type-subscript.svg'
|
||||
import TypeSuperscript from './type-superscript.svg'
|
||||
import TypeUnderline from './type-underline.svg'
|
||||
import LexicalPencilFill from './pencil-fill.svg'
|
||||
import LexicalDraggableBlockMenu from './draggable-block-menu.svg'
|
||||
|
||||
export {
|
||||
LexicalCaretRightFill,
|
||||
@@ -58,4 +59,5 @@ export {
|
||||
TypeSuperscript,
|
||||
TypeUnderline,
|
||||
LexicalPencilFill,
|
||||
LexicalDraggableBlockMenu,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.46.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.46.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.46.22",
|
||||
"version": "3.46.29",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.37.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.37.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.37.5",
|
||||
"version": "1.37.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.255](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.254](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.253](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.252](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.251](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.250](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.249](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.248](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.3.248",
|
||||
"version": "1.3.255",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.46.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
### Features
|
||||
|
||||
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
|
||||
|
||||
## [1.45.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.45.6",
|
||||
"version": "1.46.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,13 @@ import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files'
|
||||
import {
|
||||
FilesApiInterface,
|
||||
FileBackupMetadataFile,
|
||||
FileBackupsDevice,
|
||||
FileBackupsMapping,
|
||||
FileBackupRecord,
|
||||
} from '@standardnotes/files'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
@@ -11,6 +17,7 @@ import { StatusServiceInterface } from '../Status/StatusServiceInterface'
|
||||
export class FilesBackupService extends AbstractService {
|
||||
private itemsObserverDisposer: () => void
|
||||
private pendingFiles = new Set<Uuid>()
|
||||
private mappingCache?: FileBackupsMapping['files']
|
||||
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
@@ -75,8 +82,30 @@ export class FilesBackupService extends AbstractService {
|
||||
return this.device.openFilesBackupsLocation()
|
||||
}
|
||||
|
||||
private async getBackupsMapping(): Promise<FileBackupsMapping['files']> {
|
||||
return (await this.device.getFilesBackupsMappingFile()).files
|
||||
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files']> {
|
||||
const result = (await this.device.getFilesBackupsMappingFile()).files
|
||||
|
||||
this.mappingCache = result
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private invalidateMappingCache(): void {
|
||||
this.mappingCache = undefined
|
||||
}
|
||||
|
||||
private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files']> {
|
||||
return this.mappingCache ?? (await this.getBackupsMappingFromDisk())
|
||||
}
|
||||
|
||||
public async getFileBackupInfo(file: FileItem): Promise<FileBackupRecord | undefined> {
|
||||
const mapping = await this.getBackupsMappingFromCache()
|
||||
const record = mapping[file.uuid]
|
||||
return record
|
||||
}
|
||||
|
||||
public async openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||
await this.device.openFileBackup(record)
|
||||
}
|
||||
|
||||
private async handleChangedFiles(files: FileItem[]): Promise<void> {
|
||||
@@ -88,7 +117,7 @@ export class FilesBackupService extends AbstractService {
|
||||
return
|
||||
}
|
||||
|
||||
const mapping = await this.getBackupsMapping()
|
||||
const mapping = await this.getBackupsMappingFromDisk()
|
||||
|
||||
for (const file of files) {
|
||||
if (this.pendingFiles.has(file.uuid)) {
|
||||
@@ -105,6 +134,8 @@ export class FilesBackupService extends AbstractService {
|
||||
this.pendingFiles.delete(file.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
this.invalidateMappingCache()
|
||||
}
|
||||
|
||||
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.151.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.151.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** add handling errors on creating websocket connection ([c5e104f](https://github.com/standardnotes/app/commit/c5e104f90bcd0d494b9d4adc0b27f2f942dfe71c))
|
||||
|
||||
## [2.151.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -70,6 +70,11 @@ export class SNWebSocketsService extends AbstractService<WebSocketsServiceEvent,
|
||||
private async createWebSocketConnectionToken(): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await this.webSocketApiService.createConnectionToken()
|
||||
if (response.data.error) {
|
||||
console.error(response.data.error)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
return response.data.token
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.151.7",
|
||||
"version": "2.151.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
## [1.3.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
## [1.3.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/toast",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.11",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.14.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.14.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
* keyboard shortcuts for primary actions ([#2030](https://github.com/standardnotes/app/issues/2030)) ([f49ba6b](https://github.com/standardnotes/app/commit/f49ba6bd4d38e4026bd88630014126a6e0086351))
|
||||
|
||||
## [1.13.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.13.18",
|
||||
"version": "1.14.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
|
||||
export enum KeyboardKey {
|
||||
Tab = 'Tab',
|
||||
Backspace = 'Backspace',
|
||||
Up = 'ArrowUp',
|
||||
Down = 'ArrowDown',
|
||||
Left = 'ArrowLeft',
|
||||
Right = 'ArrowRight',
|
||||
Enter = 'Enter',
|
||||
Escape = 'Escape',
|
||||
Home = 'Home',
|
||||
End = 'End',
|
||||
}
|
||||
|
||||
export enum KeyboardModifier {
|
||||
Shift = 'Shift',
|
||||
Ctrl = 'Control',
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta = 'Meta',
|
||||
Alt = 'Alt',
|
||||
}
|
||||
|
||||
enum KeyboardKeyEvent {
|
||||
Down = 'KeyEventDown',
|
||||
Up = 'KeyEventUp',
|
||||
}
|
||||
|
||||
type KeyboardObserver = {
|
||||
key?: KeyboardKey | string
|
||||
modifiers?: KeyboardModifier[]
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onKeyUp?: (event: KeyboardEvent) => void
|
||||
element?: HTMLElement
|
||||
elements?: HTMLElement[]
|
||||
notElement?: HTMLElement
|
||||
notElementIds?: string[]
|
||||
notTags?: string[]
|
||||
}
|
||||
|
||||
export class IOService {
|
||||
readonly activeModifiers = new Set<KeyboardModifier>()
|
||||
private observers: KeyboardObserver[] = []
|
||||
|
||||
constructor(private isMac: boolean) {
|
||||
window.addEventListener('keydown', this.handleKeyDown)
|
||||
window.addEventListener('keyup', this.handleKeyUp)
|
||||
window.addEventListener('blur', this.handleWindowBlur)
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.observers.length = 0
|
||||
window.removeEventListener('keydown', this.handleKeyDown)
|
||||
window.removeEventListener('keyup', this.handleKeyUp)
|
||||
window.removeEventListener('blur', this.handleWindowBlur)
|
||||
;(this.handleKeyDown as unknown) = undefined
|
||||
;(this.handleKeyUp as unknown) = undefined
|
||||
;(this.handleWindowBlur as unknown) = undefined
|
||||
}
|
||||
|
||||
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (modifier) {
|
||||
case KeyboardModifier.Meta: {
|
||||
if (this.isMac) {
|
||||
this.activeModifiers.add(modifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
case KeyboardModifier.Ctrl: {
|
||||
if (!this.isMac) {
|
||||
this.activeModifiers.add(modifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
this.activeModifiers.add(modifier)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return
|
||||
}
|
||||
|
||||
this.activeModifiers.delete(modifier)
|
||||
}
|
||||
|
||||
public cancelAllKeyboardModifiers = (): void => {
|
||||
this.activeModifiers.clear()
|
||||
}
|
||||
|
||||
public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.addActiveModifier(modifier)
|
||||
}
|
||||
|
||||
public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.removeActiveModifier(modifier)
|
||||
}
|
||||
|
||||
private handleKeyDown = (event: KeyboardEvent): void => {
|
||||
this.updateAllModifiersFromEvent(event)
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Down)
|
||||
}
|
||||
|
||||
private handleKeyUp = (event: KeyboardEvent): void => {
|
||||
this.updateAllModifiersFromEvent(event)
|
||||
this.notifyObserver(event, KeyboardKeyEvent.Up)
|
||||
}
|
||||
|
||||
private updateAllModifiersFromEvent(event: KeyboardEvent): void {
|
||||
for (const modifier of Object.values(KeyboardModifier)) {
|
||||
if (event.getModifierState(modifier)) {
|
||||
this.addActiveModifier(modifier)
|
||||
} else {
|
||||
this.removeActiveModifier(modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowBlur = (): void => {
|
||||
for (const modifier of this.activeModifiers) {
|
||||
this.activeModifiers.delete(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] {
|
||||
const allModifiers = Object.values(KeyboardModifier)
|
||||
const eventModifiers = allModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
const matches =
|
||||
((event.ctrlKey || event.key === KeyboardModifier.Ctrl) && modifier === KeyboardModifier.Ctrl) ||
|
||||
((event.metaKey || event.key === KeyboardModifier.Meta) && modifier === KeyboardModifier.Meta) ||
|
||||
((event.altKey || event.key === KeyboardModifier.Alt) && modifier === KeyboardModifier.Alt) ||
|
||||
((event.shiftKey || event.key === KeyboardModifier.Shift) && modifier === KeyboardModifier.Shift)
|
||||
|
||||
return matches
|
||||
})
|
||||
|
||||
return eventModifiers
|
||||
}
|
||||
|
||||
private eventMatchesKeyAndModifiers(
|
||||
event: KeyboardEvent,
|
||||
key: KeyboardKey | string | undefined,
|
||||
modifiers: KeyboardModifier[] = [],
|
||||
): boolean {
|
||||
const eventModifiers = this.modifiersForEvent(event)
|
||||
if (eventModifiers.length !== modifiers.length) {
|
||||
return false
|
||||
}
|
||||
for (const modifier of modifiers) {
|
||||
if (!eventModifiers.includes(modifier)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Modifers match, check key
|
||||
if (!key) {
|
||||
return true
|
||||
}
|
||||
// In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F'
|
||||
// In our case we don't differentiate between the two.
|
||||
return key.toLowerCase() === event.key.toLowerCase()
|
||||
}
|
||||
|
||||
notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
const target = event.target as HTMLElement
|
||||
for (const observer of this.observers) {
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notTags && observer.notTags.includes(target.tagName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
|
||||
const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp
|
||||
if (callback) {
|
||||
callback(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeyObserver(observer: KeyboardObserver): () => void {
|
||||
this.observers.push(observer)
|
||||
|
||||
const thislessObservers = this.observers
|
||||
return () => {
|
||||
removeFromArray(thislessObservers, observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { KeyboardCommand } from './KeyboardCommands'
|
||||
|
||||
export type KeyboardCommandHandler = {
|
||||
command: KeyboardCommand
|
||||
onKeyDown?: (event: KeyboardEvent) => boolean | void
|
||||
onKeyUp?: (event: KeyboardEvent) => boolean | void
|
||||
element?: HTMLElement
|
||||
elements?: HTMLElement[]
|
||||
notElement?: HTMLElement
|
||||
notElementIds?: string[]
|
||||
notTags?: string[]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export type KeyboardCommand = symbol
|
||||
|
||||
function createKeyboardCommand(type: string): KeyboardCommand {
|
||||
return Symbol(type)
|
||||
}
|
||||
|
||||
export const TOGGLE_LIST_PANE_KEYBOARD_COMMAND = createKeyboardCommand('TOGGLE_LIST_PANE_KEYBOARD_COMMAND')
|
||||
export const TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND = createKeyboardCommand('TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND')
|
||||
export const CREATE_NEW_NOTE_KEYBOARD_COMMAND = createKeyboardCommand('CREATE_NEW_NOTE_KEYBOARD_COMMAND')
|
||||
export const NEXT_LIST_ITEM_KEYBOARD_COMMAND = createKeyboardCommand('NEXT_LIST_ITEM_KEYBOARD_COMMAND')
|
||||
export const PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND = createKeyboardCommand('PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND')
|
||||
export const SEARCH_KEYBOARD_COMMAND = createKeyboardCommand('SEARCH_KEYBOARD_COMMAND')
|
||||
export const CANCEL_SEARCH_COMMAND = createKeyboardCommand('CANCEL_SEARCH_COMMAND')
|
||||
export const SELECT_ALL_ITEMS_KEYBOARD_COMMAND = createKeyboardCommand('SELECT_ALL_ITEMS_KEYBOARD_COMMAND')
|
||||
export const SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND = createKeyboardCommand('SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND')
|
||||
export const DELETE_NOTE_KEYBOARD_COMMAND = createKeyboardCommand('DELETE_NOTE_KEYBOARD_COMMAND')
|
||||
export const TAB_COMMAND = createKeyboardCommand('PLAIN_EDITOR_INSERT_TAB_KEYBOARD_COMMAND')
|
||||
export const ESCAPE_COMMAND = createKeyboardCommand('ESCAPE_COMMAND')
|
||||
export const TOGGLE_FOCUS_MODE_COMMAND = createKeyboardCommand('TOGGLE_FOCUS_MODE_COMMAND')
|
||||
export const CHANGE_EDITOR_COMMAND = createKeyboardCommand('CHANGE_EDITOR_COMMAND')
|
||||
export const FOCUS_TAGS_INPUT_COMMAND = createKeyboardCommand('FOCUS_TAGS_INPUT_COMMAND')
|
||||
export const CREATE_NEW_TAG_COMMAND = createKeyboardCommand('CREATE_NEW_TAG_COMMAND')
|
||||
export const OPEN_NOTE_HISTORY_COMMAND = createKeyboardCommand('OPEN_NOTE_HISTORY_COMMAND')
|
||||
export const CAPTURE_SAVE_COMMAND = createKeyboardCommand('CAPTURE_SAVE_COMMAND')
|
||||
export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND')
|
||||
export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND')
|
||||
@@ -0,0 +1,12 @@
|
||||
export enum KeyboardKey {
|
||||
Tab = 'Tab',
|
||||
Backspace = 'Backspace',
|
||||
Up = 'ArrowUp',
|
||||
Down = 'ArrowDown',
|
||||
Left = 'ArrowLeft',
|
||||
Right = 'ArrowRight',
|
||||
Enter = 'Enter',
|
||||
Escape = 'Escape',
|
||||
Home = 'Home',
|
||||
End = 'End',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum KeyboardKeyEvent {
|
||||
Down = 'KeyEventDown',
|
||||
Up = 'KeyEventUp',
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum KeyboardModifier {
|
||||
Shift = 'Shift',
|
||||
Ctrl = 'Control',
|
||||
/** ⌘ key on Mac, ⊞ key on Windows */
|
||||
Meta = 'Meta',
|
||||
Alt = 'Alt',
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Environment, Platform } from '@standardnotes/snjs'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
|
||||
import { KeyboardCommand } from './KeyboardCommands'
|
||||
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
import { KeyboardCommandHandler } from './KeyboardCommandHandler'
|
||||
import { KeyboardShortcut, PlatformedKeyboardShortcut } from './KeyboardShortcut'
|
||||
import { getKeyboardShortcuts } from './getKeyboardShortcuts'
|
||||
|
||||
export class KeyboardService {
|
||||
readonly activeModifiers = new Set<KeyboardModifier>()
|
||||
private commandHandlers: KeyboardCommandHandler[] = []
|
||||
private commandMap = new Map<KeyboardCommand, KeyboardShortcut>()
|
||||
|
||||
constructor(private platform: Platform, environment: Environment) {
|
||||
window.addEventListener('keydown', this.handleKeyDown)
|
||||
window.addEventListener('keyup', this.handleKeyUp)
|
||||
window.addEventListener('blur', this.handleWindowBlur)
|
||||
|
||||
const shortcuts = getKeyboardShortcuts(platform, environment)
|
||||
for (const shortcut of shortcuts) {
|
||||
this.registerShortcut(shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
get isMac() {
|
||||
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.commandHandlers.length = 0
|
||||
window.removeEventListener('keydown', this.handleKeyDown)
|
||||
window.removeEventListener('keyup', this.handleKeyUp)
|
||||
window.removeEventListener('blur', this.handleWindowBlur)
|
||||
;(this.handleKeyDown as unknown) = undefined
|
||||
;(this.handleKeyUp as unknown) = undefined
|
||||
;(this.handleWindowBlur as unknown) = undefined
|
||||
}
|
||||
|
||||
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (modifier) {
|
||||
case KeyboardModifier.Meta: {
|
||||
if (this.isMac) {
|
||||
this.activeModifiers.add(modifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
case KeyboardModifier.Ctrl: {
|
||||
if (!this.isMac) {
|
||||
this.activeModifiers.add(modifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
this.activeModifiers.add(modifier)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return
|
||||
}
|
||||
|
||||
this.activeModifiers.delete(modifier)
|
||||
}
|
||||
|
||||
public cancelAllKeyboardModifiers = (): void => {
|
||||
this.activeModifiers.clear()
|
||||
}
|
||||
|
||||
public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.addActiveModifier(modifier)
|
||||
}
|
||||
|
||||
public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.removeActiveModifier(modifier)
|
||||
}
|
||||
|
||||
private handleKeyDown = (event: KeyboardEvent): void => {
|
||||
this.updateAllModifiersFromEvent(event)
|
||||
|
||||
this.handleKeyboardEvent(event, KeyboardKeyEvent.Down)
|
||||
}
|
||||
|
||||
private handleKeyUp = (event: KeyboardEvent): void => {
|
||||
this.updateAllModifiersFromEvent(event)
|
||||
|
||||
this.handleKeyboardEvent(event, KeyboardKeyEvent.Up)
|
||||
}
|
||||
|
||||
private updateAllModifiersFromEvent(event: KeyboardEvent): void {
|
||||
for (const modifier of Object.values(KeyboardModifier)) {
|
||||
if (event.getModifierState(modifier)) {
|
||||
this.addActiveModifier(modifier)
|
||||
} else {
|
||||
this.removeActiveModifier(modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowBlur = (): void => {
|
||||
for (const modifier of this.activeModifiers) {
|
||||
this.activeModifiers.delete(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
for (const command of this.commandMap.keys()) {
|
||||
const shortcut = this.commandMap.get(command)
|
||||
if (!shortcut) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (eventMatchesKeyAndModifiers(event, shortcut)) {
|
||||
if (shortcut.preventDefault) {
|
||||
event.preventDefault()
|
||||
}
|
||||
this.handleCommand(command, event, keyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleCommand(command: KeyboardCommand, event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
for (const observer of this.commandHandlers) {
|
||||
if (observer.command !== command) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notTags && observer.notTags.includes(target.tagName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp
|
||||
if (callback) {
|
||||
const exclusive = callback(event)
|
||||
if (exclusive) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerShortcut(shortcut: KeyboardShortcut): void {
|
||||
this.commandMap.set(shortcut.command, shortcut)
|
||||
}
|
||||
|
||||
addCommandHandler(observer: KeyboardCommandHandler): () => void {
|
||||
this.commandHandlers.push(observer)
|
||||
|
||||
const thislessObservers = this.commandHandlers
|
||||
return () => {
|
||||
observer.onKeyDown = undefined
|
||||
observer.onKeyDown = undefined
|
||||
removeFromArray(thislessObservers, observer)
|
||||
}
|
||||
}
|
||||
|
||||
addCommandHandlers(handlers: KeyboardCommandHandler[]): () => void {
|
||||
const disposers = handlers.map((handler) => this.addCommandHandler(handler))
|
||||
return () => {
|
||||
for (const disposer of disposers) {
|
||||
disposer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keyboardShortcutForCommand(command: KeyboardCommand): PlatformedKeyboardShortcut | undefined {
|
||||
const shortcut = this.commandMap.get(command)
|
||||
if (!shortcut) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
platform: this.platform,
|
||||
...shortcut,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Platform } from '@standardnotes/snjs'
|
||||
import { KeyboardCommand } from './KeyboardCommands'
|
||||
import { KeyboardKey } from './KeyboardKey'
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
|
||||
export type KeyboardShortcut = {
|
||||
command: KeyboardCommand
|
||||
modifiers?: KeyboardModifier[]
|
||||
key?: KeyboardKey | string
|
||||
/**
|
||||
* Alternative to using key, if the key can be affected by alt + shift. For example, if you want alt + shift + n,
|
||||
* use code 'KeyN' instead of key 'n', as the modifiers would turn n into '˜' on Mac.
|
||||
*/
|
||||
code?: string
|
||||
preventDefault?: boolean
|
||||
}
|
||||
|
||||
export type PlatformedKeyboardShortcut = KeyboardShortcut & {
|
||||
platform: Platform
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { KeyboardShortcut } from './KeyboardShortcut'
|
||||
import { modifiersForEvent } from './modifiersForEvent'
|
||||
|
||||
export function eventMatchesKeyAndModifiers(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean {
|
||||
const eventModifiers = modifiersForEvent(event)
|
||||
const shortcutModifiers = shortcut.modifiers ?? []
|
||||
|
||||
if (eventModifiers.length !== shortcutModifiers.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const modifier of shortcutModifiers) {
|
||||
if (!eventModifiers.includes(modifier)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (!shortcut.key && !shortcut.code) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (shortcut.key) {
|
||||
return shortcut.key.toLowerCase() === event.key.toLowerCase()
|
||||
} else {
|
||||
return shortcut.code === event.code
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Environment, Platform } from '@standardnotes/snjs'
|
||||
import { isMacPlatform } from './platformCheck'
|
||||
import {
|
||||
CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
SEARCH_KEYBOARD_COMMAND,
|
||||
SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
|
||||
SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
|
||||
DELETE_NOTE_KEYBOARD_COMMAND,
|
||||
TAB_COMMAND,
|
||||
ESCAPE_COMMAND,
|
||||
CANCEL_SEARCH_COMMAND,
|
||||
TOGGLE_FOCUS_MODE_COMMAND,
|
||||
CHANGE_EDITOR_COMMAND,
|
||||
FOCUS_TAGS_INPUT_COMMAND,
|
||||
CREATE_NEW_TAG_COMMAND,
|
||||
OPEN_NOTE_HISTORY_COMMAND,
|
||||
CAPTURE_SAVE_COMMAND,
|
||||
STAR_NOTE_COMMAND,
|
||||
PIN_NOTE_COMMAND,
|
||||
} from './KeyboardCommands'
|
||||
import { KeyboardKey } from './KeyboardKey'
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
import { KeyboardShortcut } from './KeyboardShortcut'
|
||||
|
||||
export function getKeyboardShortcuts(platform: Platform, _environment: Environment): KeyboardShortcut[] {
|
||||
const isMac = isMacPlatform(platform)
|
||||
|
||||
const primaryModifier = isMac ? KeyboardModifier.Meta : KeyboardModifier.Ctrl
|
||||
|
||||
return [
|
||||
{
|
||||
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
||||
key: 'l',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
},
|
||||
{
|
||||
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
||||
key: 'e',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
},
|
||||
{
|
||||
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||
code: 'KeyN',
|
||||
modifiers: [KeyboardModifier.Alt, KeyboardModifier.Shift],
|
||||
},
|
||||
{
|
||||
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
key: KeyboardKey.Down,
|
||||
},
|
||||
{
|
||||
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
key: KeyboardKey.Up,
|
||||
},
|
||||
{
|
||||
command: SEARCH_KEYBOARD_COMMAND,
|
||||
code: 'KeyF',
|
||||
modifiers: [KeyboardModifier.Alt, KeyboardModifier.Shift],
|
||||
},
|
||||
{
|
||||
command: CANCEL_SEARCH_COMMAND,
|
||||
key: KeyboardKey.Escape,
|
||||
},
|
||||
{
|
||||
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
|
||||
key: 'a',
|
||||
modifiers: [primaryModifier],
|
||||
},
|
||||
{
|
||||
command: SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
|
||||
modifiers: [KeyboardModifier.Alt],
|
||||
},
|
||||
{
|
||||
command: DELETE_NOTE_KEYBOARD_COMMAND,
|
||||
key: KeyboardKey.Backspace,
|
||||
modifiers: [primaryModifier],
|
||||
},
|
||||
{
|
||||
command: TAB_COMMAND,
|
||||
key: KeyboardKey.Tab,
|
||||
},
|
||||
{
|
||||
command: ESCAPE_COMMAND,
|
||||
key: KeyboardKey.Escape,
|
||||
},
|
||||
{
|
||||
command: TOGGLE_FOCUS_MODE_COMMAND,
|
||||
key: 'f',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
},
|
||||
{
|
||||
command: CHANGE_EDITOR_COMMAND,
|
||||
key: '/',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
command: FOCUS_TAGS_INPUT_COMMAND,
|
||||
code: 'KeyT',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Alt],
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
command: CREATE_NEW_TAG_COMMAND,
|
||||
code: 'KeyN',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Alt],
|
||||
},
|
||||
{
|
||||
command: OPEN_NOTE_HISTORY_COMMAND,
|
||||
key: 'h',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
command: CAPTURE_SAVE_COMMAND,
|
||||
key: 's',
|
||||
modifiers: [primaryModifier],
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
command: STAR_NOTE_COMMAND,
|
||||
key: 's',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
command: PIN_NOTE_COMMAND,
|
||||
key: 'p',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
preventDefault: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Platform } from '@standardnotes/snjs'
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
|
||||
function isMacPlatform(platform: Platform) {
|
||||
return platform === Platform.MacDesktop || platform === Platform.MacWeb
|
||||
}
|
||||
|
||||
export function keyboardCharacterForModifier(modifier: KeyboardModifier, platform: Platform) {
|
||||
const isMac = isMacPlatform(platform)
|
||||
|
||||
if (modifier === KeyboardModifier.Meta) {
|
||||
return isMac ? '⌘' : '⊞'
|
||||
} else if (modifier === KeyboardModifier.Ctrl) {
|
||||
return isMac ? '⌃' : 'Ctrl'
|
||||
} else if (modifier === KeyboardModifier.Alt) {
|
||||
return isMac ? '⌥' : 'Alt'
|
||||
} else if (modifier === KeyboardModifier.Shift) {
|
||||
return isMac ? '⇧' : 'Shift'
|
||||
} else {
|
||||
return KeyboardModifier[modifier]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { isMacPlatform } from '@standardnotes/ui-services'
|
||||
import { keyboardCharacterForModifier } from './keyboardCharacterForModifier'
|
||||
import { PlatformedKeyboardShortcut } from './KeyboardShortcut'
|
||||
|
||||
function stringForCode(code = ''): string {
|
||||
return code.replace('Key', '').replace('Digit', '')
|
||||
}
|
||||
|
||||
export function keyboardStringForShortcut(shortcut: PlatformedKeyboardShortcut | undefined) {
|
||||
if (!shortcut) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const key = shortcut.key?.toUpperCase() || stringForCode(shortcut.code)
|
||||
|
||||
if (!shortcut.modifiers || shortcut.modifiers.length === 0) {
|
||||
return key
|
||||
}
|
||||
|
||||
const modifierCharacters = shortcut.modifiers.map((modifier) =>
|
||||
keyboardCharacterForModifier(modifier, shortcut.platform),
|
||||
)
|
||||
|
||||
if (isMacPlatform(shortcut.platform)) {
|
||||
return `${modifierCharacters.join('')}${key}`
|
||||
} else {
|
||||
return `${modifierCharacters.join('+')}+${key}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
|
||||
export function modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] {
|
||||
const allModifiers = Object.values(KeyboardModifier)
|
||||
const eventModifiers = allModifiers.filter((modifier) => {
|
||||
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
|
||||
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
|
||||
const matches =
|
||||
((event.ctrlKey || event.key === KeyboardModifier.Ctrl) && modifier === KeyboardModifier.Ctrl) ||
|
||||
((event.metaKey || event.key === KeyboardModifier.Meta) && modifier === KeyboardModifier.Meta) ||
|
||||
((event.altKey || event.key === KeyboardModifier.Alt) && modifier === KeyboardModifier.Alt) ||
|
||||
((event.shiftKey || event.key === KeyboardModifier.Shift) && modifier === KeyboardModifier.Shift)
|
||||
|
||||
return matches
|
||||
})
|
||||
|
||||
return eventModifiers
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Platform } from '@standardnotes/snjs'
|
||||
|
||||
export function isMacPlatform(platform: Platform) {
|
||||
return platform === Platform.MacDesktop || platform === Platform.MacWeb
|
||||
}
|
||||
|
||||
export function isWindowsPlatform(platform: Platform) {
|
||||
return platform === Platform.WindowsDesktop || platform === Platform.WindowsWeb
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
export * from './Alert/Functions'
|
||||
export * from './Alert/WebAlertService'
|
||||
export * from './Archive/ArchiveManager'
|
||||
export * from './IO/IOService'
|
||||
export * from './Keyboard/KeyboardService'
|
||||
export * from './Keyboard/KeyboardShortcut'
|
||||
export * from './Keyboard/KeyboardCommands'
|
||||
export * from './Keyboard/platformCheck'
|
||||
export * from './Keyboard/KeyboardKey'
|
||||
export * from './Keyboard/KeyboardModifier'
|
||||
export * from './Keyboard/keyboardCharacterForModifier'
|
||||
export * from './Keyboard/keyboardStringForShortcut'
|
||||
export * from './Preferences/PreferenceId'
|
||||
export * from './Route/Params/DemoParams'
|
||||
export * from './Route/Params/OnboardingParams'
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
### Features
|
||||
|
||||
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
|
||||
|
||||
## [1.11.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/utils
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/utils",
|
||||
"version": "1.11.3",
|
||||
"version": "1.12.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -41,6 +41,7 @@
|
||||
"eslint-plugin-prettier": "*",
|
||||
"jest": "^29.3.1",
|
||||
"jsdom": "^20.0.2",
|
||||
"ts-jest": "^29.0.3"
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,51 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.105.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* lazy load embedded files in super editor ([#2043](https://github.com/standardnotes/app/issues/2043)) ([096d82f](https://github.com/standardnotes/app/commit/096d82f7af73613a3d8bc994bec2ec10a92a527c))
|
||||
* super editor popover menus ([#2041](https://github.com/standardnotes/app/issues/2041)) ([8c8f045](https://github.com/standardnotes/app/commit/8c8f045b9a53d910bcbf1dc5ee19cf92addf252e))
|
||||
|
||||
### Features
|
||||
|
||||
* display file backup status in file context menu ([#2044](https://github.com/standardnotes/app/issues/2044)) (skip e2e) ([7c2e832](https://github.com/standardnotes/app/commit/7c2e832065d45676f1b69b7c386e789e2f76775e))
|
||||
|
||||
## [3.104.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.104.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
### Features
|
||||
|
||||
* ability to drag super list items; secure password generation blocks ([#2039](https://github.com/standardnotes/app/issues/2039)) ([c39c72d](https://github.com/standardnotes/app/commit/c39c72da7a4fb85f4da9aa4e6f8e9f7ba4486a94))
|
||||
|
||||
## [3.103.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improved error reporting in super; improved draggable block icon padding ([24ff87c](https://github.com/standardnotes/app/commit/24ff87c30625432393ffedeada14478ebb625292))
|
||||
|
||||
## [3.103.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue where converting empty plain note to super would result in error ([e58c563](https://github.com/standardnotes/app/commit/e58c5637951f928efcd8c8427f06d0b6c69b5e55))
|
||||
|
||||
# [3.103.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** enable block drag'n'drop in super editor ([#2029](https://github.com/standardnotes/app/issues/2029)) ([dab4f67](https://github.com/standardnotes/app/commit/dab4f678f04aee2c7dfbe7f416e7fc34e8d870cf))
|
||||
|
||||
# [3.102.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
* keyboard shortcuts for primary actions ([#2030](https://github.com/standardnotes/app/issues/2030)) ([f49ba6b](https://github.com/standardnotes/app/commit/f49ba6bd4d38e4026bd88630014126a6e0086351))
|
||||
|
||||
## [3.101.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.101.2",
|
||||
"version": "3.105.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
@@ -99,9 +99,6 @@
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dnd-touch-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
"sass-loader": "*",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
--sn-stylekit-accessory-tint-color-1: #941648;
|
||||
|
||||
--sn-stylekit-shadow-color: var(--background-2);
|
||||
--sn-stylekit-shadow-color: #c4b89f;
|
||||
--sn-stylekit-background-color: var(--background-1);
|
||||
--sn-stylekit-foreground-color: var(--foreground-color);
|
||||
--sn-stylekit-border-color: var(--border-color);
|
||||
|
||||
@@ -29,7 +29,8 @@ import { DesktopManager } from './Device/DesktopManager'
|
||||
import {
|
||||
ArchiveManager,
|
||||
AutolockService,
|
||||
IOService,
|
||||
KeyboardService,
|
||||
PreferenceId,
|
||||
RouteService,
|
||||
RouteServiceInterface,
|
||||
ThemeManager,
|
||||
@@ -85,19 +86,17 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
this.itemControllerGroup = new ItemGroupController(this)
|
||||
this.routeService = new RouteService(this, internalEventBus)
|
||||
|
||||
const viewControllerManager = new ViewControllerManager(this, deviceInterface)
|
||||
const archiveService = new ArchiveManager(this)
|
||||
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
|
||||
const themeService = new ThemeManager(this, internalEventBus)
|
||||
|
||||
this.setWebServices({
|
||||
viewControllerManager,
|
||||
archiveService,
|
||||
desktopService: isDesktopDevice(deviceInterface) ? new DesktopManager(this, deviceInterface) : undefined,
|
||||
io,
|
||||
autolockService: this.isNativeMobileWeb() ? undefined : new AutolockService(this, internalEventBus),
|
||||
themeService,
|
||||
})
|
||||
this.webServices = {} as WebServices
|
||||
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
|
||||
this.webServices.archiveService = new ArchiveManager(this)
|
||||
this.webServices.themeService = new ThemeManager(this, internalEventBus)
|
||||
this.webServices.autolockService = this.isNativeMobileWeb()
|
||||
? undefined
|
||||
: new AutolockService(this, internalEventBus)
|
||||
this.webServices.desktopService = isDesktopDevice(deviceInterface)
|
||||
? new DesktopManager(this, deviceInterface)
|
||||
: undefined
|
||||
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
|
||||
|
||||
if (this.isNativeMobileWeb()) {
|
||||
this.mobileWebReceiver = new MobileWebReceiver(this)
|
||||
@@ -152,10 +151,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
}
|
||||
}
|
||||
|
||||
setWebServices(services: WebServices): void {
|
||||
this.webServices = services
|
||||
}
|
||||
|
||||
public addWebEventObserver(observer: WebEventObserver): () => void {
|
||||
this.webEventObservers.push(observer)
|
||||
|
||||
@@ -194,6 +189,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return this.webServices.archiveService
|
||||
}
|
||||
|
||||
public get paneController() {
|
||||
return this.webServices.viewControllerManager.paneController
|
||||
}
|
||||
|
||||
public get desktopDevice(): DesktopDeviceInterface | undefined {
|
||||
if (isDesktopDevice(this.deviceInterface)) {
|
||||
return this.deviceInterface
|
||||
@@ -221,8 +220,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return this.webServices.themeService
|
||||
}
|
||||
|
||||
public get io() {
|
||||
return this.webServices.io
|
||||
public get keyboardService() {
|
||||
return this.webServices.keyboardService
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
@@ -389,4 +388,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
FeatureIdentifier.PlainEditor
|
||||
)
|
||||
}
|
||||
|
||||
openPreferences(pane?: PreferenceId): void {
|
||||
this.getViewControllerManager().preferencesController.openPreferences()
|
||||
if (pane) {
|
||||
this.getViewControllerManager().preferencesController.setCurrentPane(pane)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ export {
|
||||
DesktopClientRequiresWebMethods,
|
||||
FileBackupsMapping,
|
||||
FileBackupsDevice,
|
||||
FileBackupRecord,
|
||||
} from '@standardnotes/snjs'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { DesktopManager } from './Device/DesktopManager'
|
||||
import { ArchiveManager, AutolockService, IOService, ThemeManager } from '@standardnotes/ui-services'
|
||||
import { ArchiveManager, AutolockService, KeyboardService, ThemeManager } from '@standardnotes/ui-services'
|
||||
|
||||
export type WebServices = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -8,5 +8,5 @@ export type WebServices = {
|
||||
autolockService?: AutolockService
|
||||
archiveService: ArchiveManager
|
||||
themeService: ThemeManager
|
||||
io: IOService
|
||||
keyboardService: KeyboardService
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModa
|
||||
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
|
||||
import ApplicationProvider from './ApplicationProvider'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import CommandProvider from './CommandProvider'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -196,85 +197,90 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
|
||||
return (
|
||||
<ApplicationProvider application={application}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<DarkModeHandler application={application} />
|
||||
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||
<PremiumModalProvider application={application} featuresController={viewControllerManager.featuresController}>
|
||||
<div className={platformString + ' main-ui-view sn-component h-full'}>
|
||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
<CommandProvider service={application.keyboardService}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<DarkModeHandler application={application} />
|
||||
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||
<PremiumModalProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
>
|
||||
<div className={platformString + ' main-ui-view sn-component h-full'}>
|
||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<NoteGroupView application={application} />
|
||||
</ErrorBoundary>
|
||||
</FileDragNDropProvider>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<NoteGroupView application={application} />
|
||||
</ErrorBoundary>
|
||||
</FileDragNDropProvider>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
</CommandProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ReactNode, createContext, useContext, memo } from 'react'
|
||||
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { KeyboardService } from '@standardnotes/ui-services'
|
||||
|
||||
const CommandServiceContext = createContext<KeyboardService | undefined>(undefined)
|
||||
|
||||
export const useCommandService = () => {
|
||||
const value = useContext(CommandServiceContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error('Component must be a child of <CommandServiceProvider />')
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
type ChildrenProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
type ProviderProps = {
|
||||
service: KeyboardService
|
||||
} & ChildrenProps
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||
|
||||
const CommandServiceProvider = ({ service, children }: ProviderProps) => {
|
||||
return (
|
||||
<CommandServiceContext.Provider value={service}>
|
||||
<MemoizedChildren children={children} />
|
||||
</CommandServiceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(CommandServiceProvider)
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const RoundIconButton = forwardRef(
|
||||
({ onClick, className, icon: iconType, iconClassName, id }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
({ onClick, className, icon: iconType, iconClassName, id, label }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
const click: MouseEventHandler = (e) => {
|
||||
e.preventDefault()
|
||||
onClick()
|
||||
@@ -29,6 +29,8 @@ const RoundIconButton = forwardRef(
|
||||
onClick={click}
|
||||
ref={ref}
|
||||
id={id}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon type={iconType} className={iconClassName} />
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ChangeEditorMenu from './ChangeEditorMenu'
|
||||
import Popover from '../Popover/Popover'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
|
||||
import { CHANGE_EDITOR_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -40,10 +41,24 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
|
||||
setIsClickOutsideDisabled(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: CHANGE_EDITOR_COMMAND,
|
||||
onKeyDown: () => {
|
||||
void toggleMenu()
|
||||
},
|
||||
})
|
||||
}, [application, toggleMenu])
|
||||
|
||||
const shortcut = useMemo(
|
||||
() => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_COMMAND),
|
||||
[application],
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<RoundIconButton
|
||||
label="Change note type"
|
||||
label={`Change note type (${shortcut && keyboardStringForShortcut(shortcut)})`}
|
||||
onClick={toggleMenu}
|
||||
ref={buttonRef}
|
||||
icon={selectedEditorIcon}
|
||||
|
||||
@@ -143,10 +143,10 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
const removeActionObserver = componentViewer.addActionObserver((action, data) => {
|
||||
switch (action) {
|
||||
case ComponentAction.KeyDown:
|
||||
application.io.handleComponentKeyDown(data.keyboardModifier)
|
||||
application.keyboardService.handleComponentKeyDown(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.KeyUp:
|
||||
application.io.handleComponentKeyUp(data.keyboardModifier)
|
||||
application.keyboardService.handleComponentKeyUp(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.Click:
|
||||
application.getViewControllerManager().notesController.setContextMenuOpen(false)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
|
||||
import {
|
||||
CANCEL_SEARCH_COMMAND,
|
||||
CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||
keyboardStringForShortcut,
|
||||
NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
SEARCH_KEYBOARD_COMMAND,
|
||||
SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||
import { FileItem, PrefKey } from '@standardnotes/snjs'
|
||||
@@ -12,7 +20,7 @@ import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import ContentListHeader from './Header/ContentListHeader'
|
||||
@@ -110,7 +118,6 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
panelWidth,
|
||||
renderedItems,
|
||||
items,
|
||||
searchBarElement,
|
||||
isCurrentNoteTemplate,
|
||||
} = itemListController
|
||||
|
||||
@@ -142,80 +149,69 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
|
||||
|
||||
useEffect(() => {
|
||||
const searchBarElement = document.getElementById(ElementIds.SearchBar)
|
||||
/**
|
||||
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
|
||||
* use Control modifier as well. These rules don't apply to desktop, but
|
||||
* probably better to be consistent.
|
||||
*/
|
||||
const disposeNewNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: 'n',
|
||||
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
void addNewItem()
|
||||
return application.keyboardService.addCommandHandlers([
|
||||
{
|
||||
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
void addNewItem()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const disposeNextNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: KeyboardKey.Down,
|
||||
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
|
||||
onKeyDown: () => {
|
||||
if (searchBarElement === document.activeElement) {
|
||||
searchBarElement?.blur()
|
||||
}
|
||||
selectNextItem()
|
||||
{
|
||||
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
|
||||
onKeyDown: () => {
|
||||
if (searchBarElement === document.activeElement) {
|
||||
searchBarElement?.blur()
|
||||
}
|
||||
selectNextItem()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const disposePreviousNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: KeyboardKey.Up,
|
||||
element: document.body,
|
||||
onKeyDown: () => {
|
||||
selectPreviousItem()
|
||||
{
|
||||
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
element: document.body,
|
||||
onKeyDown: () => {
|
||||
selectPreviousItem()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const disposeSearchKeyObserver = application.io.addKeyObserver({
|
||||
key: 'f',
|
||||
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift],
|
||||
onKeyDown: () => {
|
||||
if (searchBarElement) {
|
||||
searchBarElement.focus()
|
||||
}
|
||||
{
|
||||
command: SEARCH_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
if (searchBarElement) {
|
||||
event.preventDefault()
|
||||
searchBarElement.focus()
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const disposeSelectAllKeyObserver = application.io.addKeyObserver({
|
||||
key: 'a',
|
||||
modifiers: [KeyboardModifier.Ctrl],
|
||||
onKeyDown: (event) => {
|
||||
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
|
||||
|
||||
if (!isTargetInsideContentList) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
selectionController.selectAll()
|
||||
{
|
||||
command: CANCEL_SEARCH_COMMAND,
|
||||
onKeyDown: () => {
|
||||
if (searchBarElement) {
|
||||
searchBarElement.blur()
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
{
|
||||
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
|
||||
onKeyDown: (event) => {
|
||||
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
|
||||
|
||||
return () => {
|
||||
disposeNewNoteKeyObserver()
|
||||
disposeNextNoteKeyObserver()
|
||||
disposePreviousNoteKeyObserver()
|
||||
disposeSearchKeyObserver()
|
||||
disposeSelectAllKeyObserver()
|
||||
}
|
||||
}, [
|
||||
addNewItem,
|
||||
application.io,
|
||||
createNewNote,
|
||||
searchBarElement,
|
||||
selectNextItem,
|
||||
selectPreviousItem,
|
||||
selectionController,
|
||||
])
|
||||
if (!isTargetInsideContentList) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
selectionController.selectAll()
|
||||
},
|
||||
},
|
||||
])
|
||||
}, [addNewItem, application.keyboardService, createNewNote, selectNextItem, selectPreviousItem, selectionController])
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
||||
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
||||
@@ -229,11 +225,17 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
[application, selectedAsTag, navigationController],
|
||||
)
|
||||
|
||||
const addButtonLabel = useMemo(
|
||||
() => (isFilesSmartView ? 'Upload file' : 'Create a new note in the selected tag'),
|
||||
[isFilesSmartView],
|
||||
const shortcutForCreate = useMemo(
|
||||
() => application.keyboardService.keyboardShortcutForCommand(CREATE_NEW_NOTE_KEYBOARD_COMMAND),
|
||||
[application],
|
||||
)
|
||||
|
||||
const addButtonLabel = useMemo(() => {
|
||||
return isFilesSmartView
|
||||
? 'Upload file'
|
||||
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
|
||||
}, [isFilesSmartView, shortcutForCreate])
|
||||
|
||||
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
||||
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FileItem, FileBackupRecord } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getFileIconComponent } from '../FilePreview/getFileIconComponent'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemTags from './ListItemTags'
|
||||
@@ -12,19 +12,28 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
filesController,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
hideTags,
|
||||
item,
|
||||
item: file,
|
||||
onSelect,
|
||||
selected,
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
const application = useApplication()
|
||||
|
||||
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
|
||||
}, [application, file])
|
||||
|
||||
const listItemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -45,7 +54,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
let shouldOpenContextMenu = selected
|
||||
|
||||
if (!selected) {
|
||||
const { didSelect } = await onSelect(item)
|
||||
const { didSelect } = await onSelect(file)
|
||||
if (didSelect) {
|
||||
shouldOpenContextMenu = true
|
||||
}
|
||||
@@ -55,18 +64,18 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
openFileContextMenu(posX, posY)
|
||||
}
|
||||
},
|
||||
[selected, onSelect, item, openFileContextMenu],
|
||||
[selected, onSelect, file, openFileContextMenu],
|
||||
)
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
const { didSelect } = await onSelect(item, true)
|
||||
const { didSelect } = await onSelect(file, true)
|
||||
if (didSelect) {
|
||||
toggleAppPane(AppPaneId.Editor)
|
||||
}
|
||||
}, [item, onSelect, toggleAppPane])
|
||||
}, [file, onSelect, toggleAppPane])
|
||||
|
||||
const IconComponent = () =>
|
||||
getFileIconComponent(getIconForFileType((item as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
|
||||
getFileIconComponent(getIconForFileType((file as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
|
||||
|
||||
useContextMenuEvent(listItemRef, openContextMenu)
|
||||
|
||||
@@ -74,7 +83,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
<div
|
||||
ref={listItemRef}
|
||||
className={classNames('flex max-h-[300px] w-[190px] cursor-pointer px-1 pt-2 text-text md:w-[200px]')}
|
||||
id={item.uuid}
|
||||
id={file.uuid}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
@@ -92,11 +101,11 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
)}
|
||||
<div className="min-w-0 flex-grow py-4 px-0">
|
||||
<div className="line-clamp-2 overflow-hidden text-editor font-semibold">
|
||||
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{item.title}</div>
|
||||
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{file.title}</div>
|
||||
</div>
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemMetadata item={file} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
<ListItemConflictIndicator item={file} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -105,7 +114,14 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
selected ? 'bg-info text-info-contrast' : 'bg-passive-4 text-neutral',
|
||||
)}
|
||||
>
|
||||
{formatSizeToReadableString(item.decryptedSize)}
|
||||
<div className="flex justify-between">
|
||||
{formatSizeToReadableString(file.decryptedSize)}
|
||||
{backupInfo && (
|
||||
<div title="File is backed up locally">
|
||||
<Icon type="check-circle" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs'
|
||||
import { ListableContentItem } from './ListableContentItem'
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, s
|
||||
open={showFileContextMenu}
|
||||
anchorPoint={fileContextMenuLocation}
|
||||
togglePopover={() => setShowFileContextMenu(!showFileContextMenu)}
|
||||
side="right"
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||
import { FileBackupRecord, FileItem } from '@standardnotes/snjs'
|
||||
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
||||
|
||||
export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }> = ({ file }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
|
||||
}, [application, file])
|
||||
|
||||
const openFileBackup = useCallback(() => {
|
||||
if (backupInfo) {
|
||||
void application.fileBackups?.openFileBackup(backupInfo)
|
||||
}
|
||||
}, [backupInfo, application])
|
||||
|
||||
const configureFileBackups = useCallback(() => {
|
||||
application.openPreferences('backups')
|
||||
}, [application])
|
||||
|
||||
return (
|
||||
<>
|
||||
{backupInfo && (
|
||||
<MenuItem
|
||||
icon={'check-circle'}
|
||||
iconClassName={'text-success mt-1'}
|
||||
className={'items-start'}
|
||||
onClick={openFileBackup}
|
||||
>
|
||||
<div className="ml-2">
|
||||
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
|
||||
<div className="text-xs text-neutral">{backupInfo.absolutePath}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{!backupInfo && application.fileBackups && (
|
||||
<MenuItem
|
||||
icon={'safe-square'}
|
||||
className={'items-start'}
|
||||
iconClassName={'text-neutral mt-1'}
|
||||
onClick={configureFileBackups}
|
||||
>
|
||||
<div className="ml-2">
|
||||
<div>Configure file backups</div>
|
||||
<div className="text-xs text-neutral">File not backed up locally</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import { MenuItemType } from '../Menu/MenuItemType'
|
||||
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
|
||||
|
||||
type Props = {
|
||||
closeMenu: () => void
|
||||
@@ -120,6 +121,9 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</MenuItem>
|
||||
|
||||
<FileContextMenuBackupOption file={selectedFiles[0]} />
|
||||
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="px-3 pt-1 pb-0.5 text-xs font-medium text-neutral">
|
||||
{!hasSelectedMultipleFiles && (
|
||||
|
||||
@@ -6,7 +6,8 @@ import Spinner from '@/Components/Spinner/Spinner'
|
||||
import FilePreviewError from './FilePreviewError'
|
||||
import { isFileTypePreviewable } from './isFilePreviewable'
|
||||
import PreviewComponent from './PreviewComponent'
|
||||
import ProtectedItemOverlay from '../ProtectedItemOverlay/ProtectedItemOverlay'
|
||||
import Button from '../Button/Button'
|
||||
import { ProtectedIllustration } from '@standardnotes/icons'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -77,13 +78,28 @@ const FilePreview = ({ file, application }: Props) => {
|
||||
}, [application.files, downloadedBytes, file, isFilePreviewable, isAuthorized])
|
||||
|
||||
if (!isAuthorized) {
|
||||
const hasProtectionSources = application.hasProtectionSources()
|
||||
|
||||
return (
|
||||
<ProtectedItemOverlay
|
||||
showAccountMenu={application.showAccountMenu}
|
||||
itemType={'file'}
|
||||
onViewItem={() => application.protections.authorizeItemAccess(file)}
|
||||
hasProtectionSources={application.hasProtectionSources()}
|
||||
/>
|
||||
<div className="flex flex-grow flex-col items-center justify-center">
|
||||
<ProtectedIllustration className="mb-4 h-30 w-30" />
|
||||
<div className="mb-2 text-base font-bold">This file is protected.</div>
|
||||
<p className="max-w-[35ch] text-center text-sm text-passive-0">
|
||||
{hasProtectionSources
|
||||
? 'Authenticate to view this file.'
|
||||
: 'Add a passcode or create an account to require authentication to view this file.'}
|
||||
</p>
|
||||
<div className="mt-3 flex gap-3">
|
||||
{!hasProtectionSources && (
|
||||
<Button primary small onClick={() => application.showAccountMenu()}>
|
||||
Open account menu
|
||||
</Button>
|
||||
)}
|
||||
<Button primary onClick={() => application.protections.authorizeItemAccess(file)}>
|
||||
{hasProtectionSources ? 'Authenticate' : 'View file'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCa
|
||||
<div className="mb-2 text-base font-bold">This file can't be previewed.</div>
|
||||
{isFilePreviewable ? (
|
||||
<>
|
||||
<div className="max-w-35ch mb-4 text-center text-sm text-passive-0">
|
||||
<div className="mb-4 max-w-[35ch] text-center text-sm text-passive-0">
|
||||
There was an error loading the file. Try again, or download the file and open it using another application.
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@@ -41,7 +41,7 @@ const FilePreviewError = ({ file, filesController, isFilePreviewable, tryAgainCa
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="max-w-35ch mb-4 text-center text-sm text-passive-0">
|
||||
<div className="mb-4 max-w-[35ch] text-center text-sm text-passive-0">
|
||||
To view this file, download it and open it using another application.
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -40,7 +40,14 @@ const AccountMenuButton = ({
|
||||
</div>
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover anchorElement={buttonRef.current} open={isOpen} togglePopover={toggleMenu} side="top" className="py-2">
|
||||
<Popover
|
||||
anchorElement={buttonRef.current}
|
||||
open={isOpen}
|
||||
togglePopover={toggleMenu}
|
||||
side="top"
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
<AccountMenu
|
||||
onClickOutside={onClickOutside}
|
||||
viewControllerManager={viewControllerManager}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const IconNameToSvgMapping = {
|
||||
'star-circle-filled': icons.StarCircleFilled,
|
||||
'star-filled': icons.StarFilledIcon,
|
||||
'star-variant-filled': icons.StarVariantFilledIcon,
|
||||
'safe-square': icons.SafeSquareIcon,
|
||||
'trash-filled': icons.TrashFilledIcon,
|
||||
'trash-sweep': icons.TrashSweepIcon,
|
||||
'user-add': icons.UserAddIcon,
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { PlatformedKeyboardShortcut, keyboardCharacterForModifier, isMacPlatform } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
shortcut: PlatformedKeyboardShortcut
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const KeyboardShortcutIndicator = ({ shortcut, className }: Props) => {
|
||||
const modifiers = shortcut.modifiers || []
|
||||
const primaryKey = (shortcut.key || '').toUpperCase()
|
||||
const addPluses = !isMacPlatform(shortcut.platform)
|
||||
const spacingClass = addPluses ? '' : 'ml-0.5'
|
||||
|
||||
const keys: string[] = []
|
||||
modifiers.forEach((modifier, index) => {
|
||||
keys.push(keyboardCharacterForModifier(modifier, shortcut.platform))
|
||||
|
||||
if (addPluses && (primaryKey || index !== modifiers.length - 1)) {
|
||||
keys.push('+')
|
||||
}
|
||||
})
|
||||
|
||||
if (primaryKey) {
|
||||
keys.push(primaryKey)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`keyboard-shortcut-indicator flex opacity-[0.35] ${className}`}>
|
||||
{keys.map((key, index) => {
|
||||
return (
|
||||
<div className={index !== 0 ? spacingClass : ''} key={index}>
|
||||
{key}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -26,9 +26,16 @@ type Props = {
|
||||
focusPreviousItem: () => void
|
||||
focusedId: string | undefined
|
||||
setFocusedId: (id: string) => void
|
||||
hoverLabel?: string
|
||||
}
|
||||
|
||||
const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focusedId, setFocusedId }: Props) => {
|
||||
const ItemLinkAutocompleteInput = ({
|
||||
linkingController,
|
||||
focusPreviousItem,
|
||||
focusedId,
|
||||
setFocusedId,
|
||||
hoverLabel,
|
||||
}: Props) => {
|
||||
const application = useApplication()
|
||||
const { tags, linkItemToSelectedItem, createAndAddNewTag, isEntitledToNoteLinking, activeItem } = linkingController
|
||||
|
||||
@@ -128,6 +135,8 @@ const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focus
|
||||
onKeyDown={onKeyDown}
|
||||
id={ElementIds.ItemLinkAutocompleteInput}
|
||||
autoComplete="off"
|
||||
title={hoverLabel}
|
||||
aria-label={hoverLabel}
|
||||
/>
|
||||
{areSearchResultsVisible && (
|
||||
<DisclosurePanel
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FilesController } from '@/Controllers/FilesController'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { useState } from 'react'
|
||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { FileContextMenuBackupOption } from '../FileContextMenu/FileContextMenuBackupOption'
|
||||
import Icon from '../Icon/Icon'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import Switch from '../Switch/Switch'
|
||||
@@ -91,6 +92,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</button>
|
||||
|
||||
<FileContextMenuBackupOption file={file} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user