Merge branch 'release/3.6.8' into main
This commit is contained in:
@@ -59,6 +59,7 @@ import { SessionsModalDirective } from './components/SessionsModal';
|
|||||||
import { NoAccountWarningDirective } from './components/NoAccountWarning';
|
import { NoAccountWarningDirective } from './components/NoAccountWarning';
|
||||||
import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning';
|
import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning';
|
||||||
import { SearchOptionsDirective } from './components/SearchOptions';
|
import { SearchOptionsDirective } from './components/SearchOptions';
|
||||||
|
import { ConfirmSignoutDirective } from './components/ConfirmSignoutModal';
|
||||||
|
|
||||||
function reloadHiddenFirefoxTab(): boolean {
|
function reloadHiddenFirefoxTab(): boolean {
|
||||||
/**
|
/**
|
||||||
@@ -147,7 +148,8 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
.directive('sessionsModal', SessionsModalDirective)
|
.directive('sessionsModal', SessionsModalDirective)
|
||||||
.directive('noAccountWarning', NoAccountWarningDirective)
|
.directive('noAccountWarning', NoAccountWarningDirective)
|
||||||
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
|
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
|
||||||
.directive('searchOptions', SearchOptionsDirective);
|
.directive('searchOptions', SearchOptionsDirective)
|
||||||
|
.directive('confirmSignout', ConfirmSignoutDirective);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||||
|
|||||||
119
app/assets/javascripts/components/ConfirmSignoutModal.tsx
Normal file
119
app/assets/javascripts/components/ConfirmSignoutModal.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogLabel,
|
||||||
|
} from '@reach/alert-dialog';
|
||||||
|
import { STRING_SIGN_OUT_CONFIRMATION } from '@/strings';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { toDirective } from './utils';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
appState: AppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfirmSignoutContainer = observer((props: Props) => {
|
||||||
|
if (!props.appState.accountMenu.signingOut) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <ConfirmSignoutModal {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||||
|
const [deleteLocalBackups, setDeleteLocalBackups] = useState(
|
||||||
|
application.hasAccount()
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelRef = useRef<HTMLButtonElement>();
|
||||||
|
function close() {
|
||||||
|
appState.accountMenu.setSigningOut(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [localBackupsCount, setLocalBackupsCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
application.bridge.localBackupsCount().then(setLocalBackupsCount);
|
||||||
|
}, [appState.accountMenu.signingOut, application.bridge]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog onDismiss={close} leastDestructiveRef={cancelRef}>
|
||||||
|
<div className="sk-modal-content">
|
||||||
|
<div className="sn-component">
|
||||||
|
<div className="sk-panel">
|
||||||
|
<div className="sk-panel-content">
|
||||||
|
<div className="sk-panel-section">
|
||||||
|
<AlertDialogLabel className="sk-h3 sk-panel-section-title capitalize">
|
||||||
|
End your session?
|
||||||
|
</AlertDialogLabel>
|
||||||
|
<AlertDialogDescription className="sk-panel-row">
|
||||||
|
<p className="color-foreground">
|
||||||
|
{STRING_SIGN_OUT_CONFIRMATION}
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
{localBackupsCount > 0 && (
|
||||||
|
<div className="flex">
|
||||||
|
<div className="sk-panel-row"></div>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={deleteLocalBackups}
|
||||||
|
onChange={(event) => {
|
||||||
|
setDeleteLocalBackups(
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="ml-2">
|
||||||
|
Delete {localBackupsCount} local backup file
|
||||||
|
{localBackupsCount > 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="capitalize sk-a ml-1.5 p-0 rounded cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
application.bridge.viewlocalBackups();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View backup files
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex my-1 mt-4">
|
||||||
|
<button
|
||||||
|
className="sn-button neutral"
|
||||||
|
ref={cancelRef}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="sn-button danger ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (deleteLocalBackups) {
|
||||||
|
application.signOutAndDeleteLocalBackups();
|
||||||
|
} else {
|
||||||
|
application.signOut();
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{application.hasAccount()
|
||||||
|
? 'Sign Out'
|
||||||
|
: 'Clear Session Data'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ConfirmSignoutDirective = toDirective<Props>(
|
||||||
|
ConfirmSignoutContainer
|
||||||
|
);
|
||||||
@@ -88,6 +88,8 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
|||||||
private removeSyncObserver?: IReactionDisposer;
|
private removeSyncObserver?: IReactionDisposer;
|
||||||
private removeProtectionLengthObserver?: () => void;
|
private removeProtectionLengthObserver?: () => void;
|
||||||
|
|
||||||
|
public passcodeInput!: JQLite;
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($timeout: ng.ITimeoutService, appVersion: string) {
|
constructor($timeout: ng.ITimeoutService, appVersion: string) {
|
||||||
super($timeout);
|
super($timeout);
|
||||||
@@ -148,7 +150,7 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
|||||||
async $onInit() {
|
async $onInit() {
|
||||||
super.$onInit();
|
super.$onInit();
|
||||||
this.setState({
|
this.setState({
|
||||||
showSessions: await this.application.userCanManageSessions()
|
showSessions: await this.application.userCanManageSessions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const sync = this.appState.sync;
|
const sync = this.appState.sync;
|
||||||
@@ -385,15 +387,8 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
|||||||
this.appState.openSessionsModal();
|
this.appState.openSessionsModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroyLocalData() {
|
signOut() {
|
||||||
if (
|
this.appState.accountMenu.setSigningOut(true);
|
||||||
await confirmDialog({
|
|
||||||
text: STRING_SIGN_OUT_CONFIRMATION,
|
|
||||||
confirmButtonStyle: 'danger',
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
this.application.signOut();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showRegister() {
|
showRegister() {
|
||||||
@@ -523,17 +518,21 @@ class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
|||||||
async submitPasscodeForm() {
|
async submitPasscodeForm() {
|
||||||
const passcode = this.getState().formData.passcode!;
|
const passcode = this.getState().formData.passcode!;
|
||||||
if (passcode !== this.getState().formData.confirmPasscode!) {
|
if (passcode !== this.getState().formData.confirmPasscode!) {
|
||||||
this.application!.alertService!.alert(STRING_NON_MATCHING_PASSCODES);
|
await alertDialog({
|
||||||
|
text: STRING_NON_MATCHING_PASSCODES,
|
||||||
|
});
|
||||||
|
this.passcodeInput[0].focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await preventRefreshing(
|
await preventRefreshing(
|
||||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
||||||
async () => {
|
async () => {
|
||||||
if (this.application!.hasPasscode()) {
|
const successful = this.application.hasPasscode()
|
||||||
await this.application!.changePasscode(passcode);
|
? await this.application.changePasscode(passcode)
|
||||||
} else {
|
: await this.application.addPasscode(passcode);
|
||||||
await this.application!.addPasscode(passcode);
|
if (!successful) {
|
||||||
|
this.passcodeInput[0].focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -313,9 +313,7 @@ class PanelResizerCtrl implements PanelResizerScope {
|
|||||||
}
|
}
|
||||||
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
|
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
|
||||||
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
|
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
|
||||||
this.panel.style.flexBasis = `calc(100% - ${this.lastLeft}px)`;
|
|
||||||
} else {
|
} else {
|
||||||
this.panel.style.flexBasis = width + 'px';
|
|
||||||
this.panel.style.width = width + 'px';
|
this.panel.style.width = width + 'px';
|
||||||
}
|
}
|
||||||
this.lastWidth = width;
|
this.lastWidth = width;
|
||||||
@@ -344,8 +342,8 @@ class PanelResizerCtrl implements PanelResizerScope {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If an iframe is displayed adjacent to our panel, and the mouse exits over the iframe,
|
* If an iframe is displayed adjacent to our panel, and the mouse exits over the iframe,
|
||||||
* document[onmouseup] is not triggered because the document is no longer the same over
|
* document[onmouseup] is not triggered because the document is no longer the same over
|
||||||
* the iframe. We add an invisible overlay while resizing so that the mouse context
|
* the iframe. We add an invisible overlay while resizing so that the mouse context
|
||||||
* remains in our main document.
|
* remains in our main document.
|
||||||
*/
|
*/
|
||||||
addInvisibleOverlay() {
|
addInvisibleOverlay() {
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ export interface Bridge {
|
|||||||
environment: Environment;
|
environment: Environment;
|
||||||
|
|
||||||
getKeychainValue(): Promise<unknown>;
|
getKeychainValue(): Promise<unknown>;
|
||||||
setKeychainValue(value: any): Promise<void>;
|
setKeychainValue(value: unknown): Promise<void>;
|
||||||
clearKeychainValue(): Promise<void>;
|
clearKeychainValue(): Promise<void>;
|
||||||
|
|
||||||
|
localBackupsCount(): Promise<number>;
|
||||||
|
viewlocalBackups(): void;
|
||||||
|
deleteLocalBackups(): Promise<void>;
|
||||||
|
|
||||||
extensionsServerHost?: string;
|
extensionsServerHost?: string;
|
||||||
syncComponents(payloads: unknown[]): void;
|
syncComponents(payloads: unknown[]): void;
|
||||||
onMajorDataChange(): void;
|
onMajorDataChange(): void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bridge } from "./bridge";
|
import { Bridge } from './bridge';
|
||||||
import { Environment } from '@standardnotes/snjs';
|
import { Environment } from '@standardnotes/snjs';
|
||||||
|
|
||||||
const KEYCHAIN_STORAGE_KEY = 'keychain';
|
const KEYCHAIN_STORAGE_KEY = 'keychain';
|
||||||
@@ -14,7 +14,7 @@ export class BrowserBridge implements Bridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setKeychainValue(value: any): Promise<void> {
|
async setKeychainValue(value: unknown): Promise<void> {
|
||||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value));
|
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,9 +22,16 @@ export class BrowserBridge implements Bridge {
|
|||||||
localStorage.removeItem(KEYCHAIN_STORAGE_KEY);
|
localStorage.removeItem(KEYCHAIN_STORAGE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async localBackupsCount(): Promise<number> {
|
||||||
|
/** Browsers cannot save backups, only let you download one */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/** No-ops */
|
/** No-ops */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
|
async deleteLocalBackups(): Promise<void> {}
|
||||||
|
viewlocalBackups(): void {}
|
||||||
syncComponents(): void {}
|
syncComponents(): void {}
|
||||||
onMajorDataChange(): void {}
|
onMajorDataChange(): void {}
|
||||||
onInitialDataLoad(): void {}
|
onInitialDataLoad(): void {}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export const STRING_UNARCHIVE_LOCKED_ATTEMPT =
|
|||||||
"This note is locked. If you'd like to archive it, unlock it, and try again.";
|
"This note is locked. If you'd like to archive it, unlock it, and try again.";
|
||||||
export const STRING_DELETE_LOCKED_ATTEMPT =
|
export const STRING_DELETE_LOCKED_ATTEMPT =
|
||||||
"This note is locked. If you'd like to delete it, unlock it, and try again.";
|
"This note is locked. If you'd like to delete it, unlock it, and try again.";
|
||||||
|
export const STRING_EDIT_LOCKED_ATTEMPT =
|
||||||
|
"This note is locked. If you'd like to edit its options, unlock it, and try again.";
|
||||||
export function StringDeleteNote(title: string, permanently: boolean) {
|
export function StringDeleteNote(title: string, permanently: boolean) {
|
||||||
return permanently
|
return permanently
|
||||||
? `Are you sure you want to permanently delete ${title}?`
|
? `Are you sure you want to permanently delete ${title}?`
|
||||||
@@ -52,8 +54,7 @@ export function StringEmptyTrash(count: number) {
|
|||||||
/** @account */
|
/** @account */
|
||||||
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE =
|
export const STRING_ACCOUNT_MENU_UNCHECK_MERGE =
|
||||||
'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?';
|
'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?';
|
||||||
export const STRING_SIGN_OUT_CONFIRMATION =
|
export const STRING_SIGN_OUT_CONFIRMATION = 'This will delete all local items and extensions.';
|
||||||
'Are you sure you want to end your session? This will delete all local items and extensions.';
|
|
||||||
export const STRING_ERROR_DECRYPTING_IMPORT =
|
export const STRING_ERROR_DECRYPTING_IMPORT =
|
||||||
'There was an error decrypting your items. Make sure the password you entered is correct and try again.';
|
'There was an error decrypting your items. Make sure the password you entered is correct and try again.';
|
||||||
export const STRING_E2E_ENABLED =
|
export const STRING_E2E_ENABLED =
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import { action, makeObservable, observable } from "mobx";
|
|||||||
|
|
||||||
export class AccountMenuState {
|
export class AccountMenuState {
|
||||||
show = false;
|
show = false;
|
||||||
|
signingOut = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
show: observable,
|
show: observable,
|
||||||
|
signingOut: observable,
|
||||||
|
|
||||||
setShow: action,
|
setShow: action,
|
||||||
toggleShow: action,
|
toggleShow: action,
|
||||||
|
setSigningOut: action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +19,10 @@ export class AccountMenuState {
|
|||||||
this.show = show;
|
this.show = show;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSigningOut = (signingOut: boolean): void => {
|
||||||
|
this.signingOut = signingOut;
|
||||||
|
}
|
||||||
|
|
||||||
toggleShow = (): void => {
|
toggleShow = (): void => {
|
||||||
this.show = !this.show;
|
this.show = !this.show;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { WebApplication } from "../application";
|
|||||||
|
|
||||||
export class SearchOptionsState {
|
export class SearchOptionsState {
|
||||||
includeProtectedContents = false;
|
includeProtectedContents = false;
|
||||||
includeArchived = false;
|
includeArchived = true;
|
||||||
includeTrashed = false;
|
includeTrashed = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class WebApplication extends SNApplication {
|
|||||||
private $compile: angular.ICompileService,
|
private $compile: angular.ICompileService,
|
||||||
scope: angular.IScope,
|
scope: angular.IScope,
|
||||||
defaultSyncServerHost: string,
|
defaultSyncServerHost: string,
|
||||||
private bridge: Bridge,
|
public bridge: Bridge,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
bridge.environment,
|
bridge.environment,
|
||||||
@@ -169,6 +169,11 @@ export class WebApplication extends SNApplication {
|
|||||||
return angular.element(document.getElementById(this.identifier)!);
|
return angular.element(document.getElementById(this.identifier)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async signOutAndDeleteLocalBackups(): Promise<void> {
|
||||||
|
await this.bridge.deleteLocalBackups();
|
||||||
|
return this.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
presentPasswordModal(callback: () => void) {
|
presentPasswordModal(callback: () => void) {
|
||||||
const scope = this.scope!.$new(true) as InputModalScope;
|
const scope = this.scope!.$new(true) as InputModalScope;
|
||||||
scope.type = "password";
|
scope.type = "password";
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
STRING_ELLIPSES,
|
STRING_ELLIPSES,
|
||||||
STRING_DELETE_PLACEHOLDER_ATTEMPT,
|
STRING_DELETE_PLACEHOLDER_ATTEMPT,
|
||||||
STRING_DELETE_LOCKED_ATTEMPT,
|
STRING_DELETE_LOCKED_ATTEMPT,
|
||||||
|
STRING_EDIT_LOCKED_ATTEMPT,
|
||||||
StringDeleteNote,
|
StringDeleteNote,
|
||||||
StringEmptyTrash,
|
StringEmptyTrash,
|
||||||
} from '@/strings';
|
} from '@/strings';
|
||||||
@@ -88,11 +89,15 @@ type EditorState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type EditorValues = {
|
type EditorValues = {
|
||||||
title?: string;
|
title: string;
|
||||||
text?: string;
|
text: string;
|
||||||
tagsInputValue?: string;
|
tagsInputValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function copyEditorValues(values: EditorValues) {
|
||||||
|
return Object.assign({}, values);
|
||||||
|
}
|
||||||
|
|
||||||
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||||
return array.sort((a, b) =>
|
return array.sort((a, b) =>
|
||||||
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
||||||
@@ -110,7 +115,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
private saveTimeout?: ng.IPromise<void>;
|
private saveTimeout?: ng.IPromise<void>;
|
||||||
private statusTimeout?: ng.IPromise<void>;
|
private statusTimeout?: ng.IPromise<void>;
|
||||||
private lastEditorFocusEventSource?: EventSource;
|
private lastEditorFocusEventSource?: EventSource;
|
||||||
public editorValues: EditorValues = {};
|
public editorValues: EditorValues = { title: '', text: '' };
|
||||||
onEditorLoad?: () => void;
|
onEditorLoad?: () => void;
|
||||||
|
|
||||||
private tags: SNTag[] = [];
|
private tags: SNTag[] = [];
|
||||||
@@ -198,9 +203,12 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
if (!this.editorValues.text) {
|
if (!this.editorValues.text) {
|
||||||
this.editorValues.text = note.text;
|
this.editorValues.text = note.text;
|
||||||
}
|
}
|
||||||
if (note.lastSyncBegan) {
|
if (note.lastSyncBegan || note.dirty) {
|
||||||
if (note.lastSyncEnd) {
|
if (note.lastSyncEnd) {
|
||||||
if (note.lastSyncBegan!.getTime() > note.lastSyncEnd!.getTime()) {
|
if (
|
||||||
|
note.dirty ||
|
||||||
|
note.lastSyncBegan!.getTime() > note.lastSyncEnd!.getTime()
|
||||||
|
) {
|
||||||
this.showSavingStatus();
|
this.showSavingStatus();
|
||||||
} else if (
|
} else if (
|
||||||
note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime()
|
note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime()
|
||||||
@@ -301,6 +309,9 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
this.reloadPreferences();
|
this.reloadPreferences();
|
||||||
this.reloadStackComponents();
|
this.reloadStackComponents();
|
||||||
this.reloadNoteTagsComponent();
|
this.reloadNoteTagsComponent();
|
||||||
|
if (note.dirty) {
|
||||||
|
this.showSavingStatus();
|
||||||
|
}
|
||||||
if (note.safeText().length === 0 && !showProtectedWarning) {
|
if (note.safeText().length === 0 && !showProtectedWarning) {
|
||||||
this.focusTitle();
|
this.focusTitle();
|
||||||
}
|
}
|
||||||
@@ -408,6 +419,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
if (this.appState.getActiveEditor()?.isTemplateNote) {
|
if (this.appState.getActiveEditor()?.isTemplateNote) {
|
||||||
await this.appState.getActiveEditor().insertTemplatedNote();
|
await this.appState.getActiveEditor().insertTemplatedNote();
|
||||||
}
|
}
|
||||||
|
if (this.note.locked) {
|
||||||
|
this.application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!component) {
|
if (!component) {
|
||||||
if (!this.note.prefersPlainEditor) {
|
if (!this.note.prefersPlainEditor) {
|
||||||
await this.application.changeItem(this.note.uuid, (mutator) => {
|
await this.application.changeItem(this.note.uuid, (mutator) => {
|
||||||
@@ -464,26 +479,30 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
* close the editor (if we closed the editor before sync began, we'd get an exception,
|
* close the editor (if we closed the editor before sync began, we'd get an exception,
|
||||||
* since the debouncer will be triggered on a non-existent editor)
|
* since the debouncer will be triggered on a non-existent editor)
|
||||||
*/
|
*/
|
||||||
async saveNote(
|
async save(
|
||||||
|
note: SNNote,
|
||||||
|
editorValues: EditorValues,
|
||||||
bypassDebouncer = false,
|
bypassDebouncer = false,
|
||||||
isUserModified = false,
|
isUserModified = false,
|
||||||
dontUpdatePreviews = false,
|
dontUpdatePreviews = false,
|
||||||
customMutate?: (mutator: NoteMutator) => void,
|
customMutate?: (mutator: NoteMutator) => void,
|
||||||
closeAfterSync = false
|
closeAfterSync = false
|
||||||
) {
|
) {
|
||||||
|
const title = editorValues.title;
|
||||||
|
const text = editorValues.text;
|
||||||
|
const isTemplate = this.editor.isTemplateNote;
|
||||||
|
const selectedTag = this.appState.selectedTag;
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
this.application.alertService!.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN);
|
this.application.alertService.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const note = this.note;
|
|
||||||
if (note.deleted) {
|
if (note.deleted) {
|
||||||
this.application.alertService!.alert(STRING_DELETED_NOTE);
|
this.application.alertService.alert(STRING_DELETED_NOTE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.editor.isTemplateNote) {
|
if (isTemplate) {
|
||||||
await this.editor.insertTemplatedNote();
|
await this.editor.insertTemplatedNote();
|
||||||
}
|
}
|
||||||
const selectedTag = this.appState.selectedTag;
|
|
||||||
if (
|
if (
|
||||||
!selectedTag?.isSmartTag &&
|
!selectedTag?.isSmartTag &&
|
||||||
!selectedTag?.hasRelationshipWithItem(note)
|
!selectedTag?.hasRelationshipWithItem(note)
|
||||||
@@ -493,7 +512,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!this.application.findItem(note.uuid)) {
|
if (!this.application.findItem(note.uuid)) {
|
||||||
this.application.alertService!.alert(STRING_INVALID_NOTE);
|
this.application.alertService.alert(STRING_INVALID_NOTE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.application.changeItem(
|
await this.application.changeItem(
|
||||||
@@ -503,12 +522,12 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
if (customMutate) {
|
if (customMutate) {
|
||||||
customMutate(noteMutator);
|
customMutate(noteMutator);
|
||||||
}
|
}
|
||||||
noteMutator.title = this.editorValues.title!;
|
noteMutator.title = title;
|
||||||
noteMutator.text = this.editorValues.text!;
|
noteMutator.text = text;
|
||||||
if (!dontUpdatePreviews) {
|
if (!dontUpdatePreviews) {
|
||||||
const text = this.editorValues.text || '';
|
const noteText = text || '';
|
||||||
const truncate = text.length > NOTE_PREVIEW_CHAR_LIMIT;
|
const truncate = noteText.length > NOTE_PREVIEW_CHAR_LIMIT;
|
||||||
const substring = text.substring(0, NOTE_PREVIEW_CHAR_LIMIT);
|
const substring = noteText.substring(0, NOTE_PREVIEW_CHAR_LIMIT);
|
||||||
const previewPlain = substring + (truncate ? STRING_ELLIPSES : '');
|
const previewPlain = substring + (truncate ? STRING_ELLIPSES : '');
|
||||||
noteMutator.preview_plain = previewPlain;
|
noteMutator.preview_plain = previewPlain;
|
||||||
noteMutator.preview_html = undefined;
|
noteMutator.preview_html = undefined;
|
||||||
@@ -541,7 +560,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
syncTakingTooLong: false,
|
syncTakingTooLong: false,
|
||||||
});
|
});
|
||||||
this.setStatus({
|
this.setStatus({
|
||||||
message: 'All changes saved' + (this.application.noAccount() ? ' offline' : ''),
|
message:
|
||||||
|
'All changes saved' + (this.application.noAccount() ? ' offline' : ''),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +603,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentChanged() {
|
contentChanged() {
|
||||||
this.saveNote(false, true);
|
this.save(this.note, copyEditorValues(this.editorValues), false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTitleEnter($event: Event) {
|
onTitleEnter($event: Event) {
|
||||||
@@ -593,7 +613,13 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTitleChange() {
|
onTitleChange() {
|
||||||
this.saveNote(false, true, true);
|
this.save(
|
||||||
|
this.note,
|
||||||
|
copyEditorValues(this.editorValues),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
focusEditor() {
|
focusEditor() {
|
||||||
@@ -653,9 +679,16 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
if (permanently) {
|
if (permanently) {
|
||||||
this.performNoteDeletion(this.note);
|
this.performNoteDeletion(this.note);
|
||||||
} else {
|
} else {
|
||||||
this.saveNote(true, false, true, (mutator) => {
|
this.save(
|
||||||
mutator.trashed = true;
|
this.note,
|
||||||
});
|
copyEditorValues(this.editorValues),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
(mutator) => {
|
||||||
|
mutator.trashed = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,7 +698,9 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restoreTrashedNote() {
|
restoreTrashedNote() {
|
||||||
this.saveNote(
|
this.save(
|
||||||
|
this.note,
|
||||||
|
copyEditorValues(this.editorValues),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@@ -698,15 +733,31 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
togglePin() {
|
togglePin() {
|
||||||
this.saveNote(true, false, true, (mutator) => {
|
const note = this.note;
|
||||||
mutator.pinned = !this.note.pinned;
|
this.save(
|
||||||
});
|
note,
|
||||||
|
copyEditorValues(this.editorValues),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
(mutator) => {
|
||||||
|
mutator.pinned = !note.pinned;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLockNote() {
|
toggleLockNote() {
|
||||||
this.saveNote(true, false, true, (mutator) => {
|
const note = this.note;
|
||||||
mutator.locked = !this.note.locked;
|
this.save(
|
||||||
});
|
note,
|
||||||
|
copyEditorValues(this.editorValues),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
(mutator) => {
|
||||||
|
mutator.locked = !note.locked;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleProtectNote() {
|
async toggleProtectNote() {
|
||||||
@@ -723,29 +774,40 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleNotePreview() {
|
toggleNotePreview() {
|
||||||
this.saveNote(true, false, true, (mutator) => {
|
const note = this.note;
|
||||||
mutator.hidePreview = !this.note.hidePreview;
|
this.save(
|
||||||
});
|
note,
|
||||||
|
copyEditorValues(this.editorValues),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
(mutator) => {
|
||||||
|
mutator.hidePreview = !note.hidePreview;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleArchiveNote() {
|
toggleArchiveNote() {
|
||||||
if (this.note.locked) {
|
const note = this.note;
|
||||||
|
if (note.locked) {
|
||||||
alertDialog({
|
alertDialog({
|
||||||
text: this.note.archived
|
text: note.archived
|
||||||
? STRING_UNARCHIVE_LOCKED_ATTEMPT
|
? STRING_UNARCHIVE_LOCKED_ATTEMPT
|
||||||
: STRING_ARCHIVE_LOCKED_ATTEMPT,
|
: STRING_ARCHIVE_LOCKED_ATTEMPT,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saveNote(
|
this.save(
|
||||||
|
note,
|
||||||
|
copyEditorValues(this.editorValues),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
(mutator) => {
|
(mutator) => {
|
||||||
mutator.archived = !this.note.archived;
|
mutator.archived = !note.archived;
|
||||||
},
|
},
|
||||||
/** If we are unarchiving, and we are in the archived tag, close the editor */
|
/** If we are unarchiving, and we are in the archived tag, close the editor */
|
||||||
this.note.archived && this.appState.selectedTag?.isArchiveTag
|
note.archived && this.appState.selectedTag?.isArchiveTag
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,7 +1238,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
|||||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||||
}
|
}
|
||||||
this.editorValues.text = editor.value;
|
this.editorValues.text = editor.value;
|
||||||
this.saveNote(true);
|
this.save(this.note, copyEditorValues(this.editorValues), true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
.sk-button.contrast.wide(
|
.sk-button.contrast.wide(
|
||||||
ng-click='self.createNewNote()',
|
ng-click='self.createNewNote()',
|
||||||
title='Create a new note in the selected tag'
|
title='Create a new note in the selected tag'
|
||||||
)
|
aria-label="Create new note"
|
||||||
|
)
|
||||||
.sk-label
|
.sk-label
|
||||||
i.icon.ion-plus.add-button
|
i.icon.ion-plus.add-button
|
||||||
.filter-section(role='search')
|
.filter-section(role='search')
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ body {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: var(--sn-stylekit-base-font-size);
|
font-size: var(--sn-stylekit-base-font-size);
|
||||||
|
line-height: normal;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--sn-stylekit-foreground-color);
|
color: var(--sn-stylekit-foreground-color);
|
||||||
background-color: var(--sn-stylekit-background-color);
|
background-color: var(--sn-stylekit-background-color);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
right: 32px;
|
right: 36px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
transition: background-color 0.15s linear;
|
transition: background-color 0.15s linear;
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-foreground {
|
||||||
|
color: var(--sn-stylekit-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
.ring-info {
|
.ring-info {
|
||||||
box-shadow: 0 0 0 2px var(--sn-stylekit-info-color);
|
box-shadow: 0 0 0 2px var(--sn-stylekit-info-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,12 +200,13 @@
|
|||||||
)
|
)
|
||||||
.sk-panel-row
|
.sk-panel-row
|
||||||
input.sk-input.contrast(
|
input.sk-input.contrast(
|
||||||
ng-model='self.state.formData.passcode',
|
ng-ref='self.passcodeInput'
|
||||||
placeholder='Passcode',
|
ng-model='self.state.formData.passcode'
|
||||||
should-focus='true',
|
placeholder='Passcode'
|
||||||
sn-autofocus='true',
|
should-focus='true'
|
||||||
|
sn-autofocus='true'
|
||||||
type='password'
|
type='password'
|
||||||
)
|
)
|
||||||
input.sk-input.contrast(
|
input.sk-input.contrast(
|
||||||
ng-model='self.state.formData.confirmPasscode',
|
ng-model='self.state.formData.confirmPasscode',
|
||||||
placeholder='Confirm Passcode',
|
placeholder='Confirm Passcode',
|
||||||
@@ -297,6 +298,10 @@
|
|||||||
| {{ self.state.errorReportingEnabled ? 'Disable' : 'Enable'}} Error Reporting
|
| {{ self.state.errorReportingEnabled ? 'Disable' : 'Enable'}} Error Reporting
|
||||||
.sk-panel-row
|
.sk-panel-row
|
||||||
a(ng-click="self.openErrorReportingDialog()").sk-a What data is being sent?
|
a(ng-click="self.openErrorReportingDialog()").sk-a What data is being sent?
|
||||||
|
confirm-signout(
|
||||||
|
app-state='self.appState'
|
||||||
|
application='self.application'
|
||||||
|
)
|
||||||
.sk-panel-footer
|
.sk-panel-footer
|
||||||
.sk-panel-row
|
.sk-panel-row
|
||||||
.sk-p.left.neutral
|
.sk-p.left.neutral
|
||||||
@@ -311,7 +316,7 @@
|
|||||||
)
|
)
|
||||||
| Cancel
|
| Cancel
|
||||||
a.sk-a.right.danger.capitalize(
|
a.sk-a.right.danger.capitalize(
|
||||||
ng-click='self.destroyLocalData()',
|
ng-click='self.signOut()',
|
||||||
ng-if=`
|
ng-if=`
|
||||||
!self.state.formData.showLogin &&
|
!self.state.formData.showLogin &&
|
||||||
!self.state.formData.showRegister`
|
!self.state.formData.showRegister`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "standard-notes-web",
|
"name": "standard-notes-web",
|
||||||
"version": "3.6.7",
|
"version": "3.6.8",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"pug-loader": "^2.4.0",
|
"pug-loader": "^2.4.0",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"serve-static": "^1.14.1",
|
"serve-static": "^1.14.1",
|
||||||
"sn-stylekit": "github:standardnotes/StyleKit#c64573b229ac9154e480cde3b5fdab78c75b3530",
|
"sn-stylekit": "^4.0.3",
|
||||||
"ts-loader": "^8.0.17",
|
"ts-loader": "^8.0.17",
|
||||||
"typescript": "^4.1.5",
|
"typescript": "^4.1.5",
|
||||||
"typescript-eslint": "0.0.1-alpha.0",
|
"typescript-eslint": "0.0.1-alpha.0",
|
||||||
@@ -71,8 +71,9 @@
|
|||||||
"@reach/checkbox": "^0.13.2",
|
"@reach/checkbox": "^0.13.2",
|
||||||
"@reach/dialog": "^0.13.0",
|
"@reach/dialog": "^0.13.0",
|
||||||
"@standardnotes/sncrypto-web": "^1.2.10",
|
"@standardnotes/sncrypto-web": "^1.2.10",
|
||||||
"@standardnotes/snjs": "^2.0.74",
|
"@standardnotes/snjs": "^2.0.75",
|
||||||
"mobx": "^6.1.6",
|
"mobx": "^6.1.6",
|
||||||
|
"mobx-react-lite": "^3.2.0",
|
||||||
"preact": "^10.5.12"
|
"preact": "^10.5.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -1936,10 +1936,10 @@
|
|||||||
"@standardnotes/sncrypto-common" "^1.2.7"
|
"@standardnotes/sncrypto-common" "^1.2.7"
|
||||||
libsodium-wrappers "^0.7.8"
|
libsodium-wrappers "^0.7.8"
|
||||||
|
|
||||||
"@standardnotes/snjs@^2.0.74":
|
"@standardnotes/snjs@^2.0.75":
|
||||||
version "2.0.74"
|
version "2.0.75"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.0.74.tgz#042c3dcf5447006cdfc70b8cfcacf7ded92cea91"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.0.75.tgz#aeb0ead927da63dc85e28f78da2362126bb16602"
|
||||||
integrity sha512-2G0jw1n4GgOnSkpXDXvrJp9R8xsImVrfNRjKNBE7RqzBg2WFynF5xt0bfqzLdRFZyA5oeuEkTG1CAU4nNPy0xw==
|
integrity sha512-QL5YgDT0aN9t95gxgURqNudXr5dteVsc1ylsKKSw0DpEGiq0bACPxbI+sUFppoWTFmprxmDh3+vc+FFcFg7Lyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^2.0.0"
|
"@standardnotes/auth" "^2.0.0"
|
||||||
"@standardnotes/sncrypto-common" "^1.2.9"
|
"@standardnotes/sncrypto-common" "^1.2.9"
|
||||||
@@ -6094,6 +6094,11 @@ mixin-deep@^1.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
mobx-react-lite@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz#331d7365a6b053378dfe9c087315b4e41c5df69f"
|
||||||
|
integrity sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==
|
||||||
|
|
||||||
mobx@^6.1.6:
|
mobx@^6.1.6:
|
||||||
version "6.1.6"
|
version "6.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.1.6.tgz#ae75e57ec07d190ed187273864002163fa357224"
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.1.6.tgz#ae75e57ec07d190ed187273864002163fa357224"
|
||||||
@@ -7810,9 +7815,10 @@ slice-ansi@^4.0.0:
|
|||||||
astral-regex "^2.0.0"
|
astral-regex "^2.0.0"
|
||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
|
||||||
"sn-stylekit@github:standardnotes/StyleKit#c64573b229ac9154e480cde3b5fdab78c75b3530":
|
sn-stylekit@^4.0.3:
|
||||||
version "4.0.2"
|
version "4.0.3"
|
||||||
resolved "https://codeload.github.com/standardnotes/StyleKit/tar.gz/c64573b229ac9154e480cde3b5fdab78c75b3530"
|
resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-4.0.3.tgz#f8b86a68286bf5237dfc035c43a31464625b010c"
|
||||||
|
integrity sha512-cKsq3XndExpzJnP6qjd5khGbNhKlA724MLbxbMbgUv3aYQWZddHudzV84Lb5gx87mbUQJQc0lh0nPWZBpa2hUA==
|
||||||
|
|
||||||
snapdragon-node@^2.0.1:
|
snapdragon-node@^2.0.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user