You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-08 23:07:32 +02:00
Compare commits
49 Commits
plugin-gen
...
plugin_tas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1b38b936 | ||
|
|
a11402db6d | ||
|
|
c19d533d93 | ||
|
|
1a4ff7054a | ||
|
|
71140a638f | ||
|
|
6ba0fce237 | ||
|
|
c033a343c1 | ||
|
|
898c96a566 | ||
|
|
b83fa133b2 | ||
|
|
ec7fec8b59 | ||
|
|
b2fb4f2ea2 | ||
|
|
c74e51a58e | ||
|
|
7cba4be498 | ||
|
|
19b396f2ec | ||
|
|
4ed7c340a0 | ||
|
|
fe770917fd | ||
|
|
f2168d3bca | ||
|
|
5d4ebc6e14 | ||
|
|
3cf0841775 | ||
|
|
e5b6ecc50b | ||
|
|
f451633a51 | ||
|
|
e813d15ef2 | ||
|
|
b5b02d8d7b | ||
|
|
2660ff3af6 | ||
|
|
59cdcaf8d1 | ||
|
|
9426a2170c | ||
|
|
b47a541976 | ||
|
|
4df5ad5c7a | ||
|
|
6f0f3d586e | ||
|
|
863d894af1 | ||
|
|
a595b8f250 | ||
|
|
a6ff60aa99 | ||
|
|
aaf5d74b94 | ||
|
|
c7d0d659a0 | ||
|
|
57e46711f6 | ||
|
|
297b992944 | ||
|
|
3b05e7ec5f | ||
|
|
b98e64c881 | ||
|
|
46438a5888 | ||
|
|
d1e02fd5f0 | ||
|
|
b41a3d7f8d | ||
|
|
88b56a2abd | ||
|
|
c898214e7e | ||
|
|
852e6c141b | ||
|
|
fa868297a2 | ||
|
|
3744e08335 | ||
|
|
ea56d020e9 | ||
|
|
a6174c89a9 | ||
|
|
c423551723 |
@@ -52,6 +52,8 @@ packages/app-mobile/lib/rnInjectedJs/
|
||||
packages/app-mobile/locales
|
||||
packages/app-mobile/node_modules
|
||||
packages/app-mobile/pluginAssets/
|
||||
packages/electron-process-manager/dist/
|
||||
packages/electron-process-manager/src/ui/
|
||||
packages/fork-*
|
||||
packages/htmlpack/dist/
|
||||
packages/lib/assets/
|
||||
@@ -733,6 +735,9 @@ packages/app-desktop/services/commands/stateToWhenClauseContext.js.map
|
||||
packages/app-desktop/services/commands/types.d.ts
|
||||
packages/app-desktop/services/commands/types.js
|
||||
packages/app-desktop/services/commands/types.js.map
|
||||
packages/app-desktop/services/plugins/BackOffHandler.d.ts
|
||||
packages/app-desktop/services/plugins/BackOffHandler.js
|
||||
packages/app-desktop/services/plugins/BackOffHandler.js.map
|
||||
packages/app-desktop/services/plugins/PlatformImplementation.d.ts
|
||||
packages/app-desktop/services/plugins/PlatformImplementation.js
|
||||
packages/app-desktop/services/plugins/PlatformImplementation.js.map
|
||||
|
||||
6
.github/scripts/run_ci.sh
vendored
6
.github/scripts/run_ci.sh
vendored
@@ -1,5 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# We want the script to stop as soon as an error is found. Otherwise if there's
|
||||
# an error during `yarn install` for example, it's also going to throw millions
|
||||
# of errors in test units, which makes debugging difficult.
|
||||
set -e
|
||||
|
||||
# =============================================================================
|
||||
# Setup environment variables
|
||||
# =============================================================================
|
||||
@@ -37,6 +42,7 @@ echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
||||
echo "GITHUB_REF=$GITHUB_REF"
|
||||
echo "RUNNER_OS=$RUNNER_OS"
|
||||
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
|
||||
|
||||
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
|
||||
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
|
||||
|
||||
4
.github/workflows/github-actions-main.yml
vendored
4
.github/workflows/github-actions-main.yml
vendored
@@ -74,6 +74,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh"
|
||||
|
||||
@@ -84,6 +85,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
run: |
|
||||
yarn install
|
||||
cd packages/app-desktop
|
||||
@@ -122,6 +124,8 @@ jobs:
|
||||
corepack enable
|
||||
|
||||
- name: Build Docker Image
|
||||
env:
|
||||
BUILD_SEQUENCIAL: 1
|
||||
run: |
|
||||
yarn install
|
||||
yarn run buildServerDocker --tag-name server-v0.0.0
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -723,6 +723,9 @@ packages/app-desktop/services/commands/stateToWhenClauseContext.js.map
|
||||
packages/app-desktop/services/commands/types.d.ts
|
||||
packages/app-desktop/services/commands/types.js
|
||||
packages/app-desktop/services/commands/types.js.map
|
||||
packages/app-desktop/services/plugins/BackOffHandler.d.ts
|
||||
packages/app-desktop/services/plugins/BackOffHandler.js
|
||||
packages/app-desktop/services/plugins/BackOffHandler.js.map
|
||||
packages/app-desktop/services/plugins/PlatformImplementation.d.ts
|
||||
packages/app-desktop/services/plugins/PlatformImplementation.js
|
||||
packages/app-desktop/services/plugins/PlatformImplementation.js.map
|
||||
|
||||
3
BUILD.md
3
BUILD.md
@@ -19,6 +19,7 @@ There are also a few forks of existing packages under the "fork-*" name.
|
||||
## Required dependencies
|
||||
|
||||
- Install node 16+ - https://nodejs.org/en/
|
||||
- [Enable yarn](https://yarnpkg.com/getting-started/install): `corepack enable`
|
||||
- macOS: Install Cocoapods - `brew install cocoapods`
|
||||
- Windows: Install Windows Build Tools - `yarn install -g windows-build-tools --vs2015`
|
||||
- Linux: Install dependencies - `sudo apt install build-essential libnss3 libsecret-1-dev python rsync`
|
||||
@@ -59,7 +60,7 @@ Normally the **bundler** should start automatically with the application. If it
|
||||
## Building the clipper
|
||||
|
||||
cd packages/app-clipper/popup
|
||||
yarn run watch # To watch for changes
|
||||
npm run watch # To watch for changes
|
||||
|
||||
To test the extension please refer to the relevant pages for each browser: [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#Trying_it_out) / [Chrome](https://developer.chrome.com/docs/extensions/mv3/getstarted/). Please note that the extension in dev mode will only connect to a dev instance of the desktop app (and vice-versa).
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ Joplin is available in multiple languages thanks to the help of its users. You c
|
||||
|
||||
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
|
||||
|
||||
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should gives a clear overview of why you want to add this.
|
||||
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should give a clear overview of why you want to add this.
|
||||
- Bug fixes are always welcome. Start by reviewing the [list of bugs](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||
- A good way to easily start contributing is to pick and work on a [good first issue](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We try to make these issues as clear as possible and provide basic info on how the code should be changed, and if something is unclear feel free to ask for more information on the issue.
|
||||
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
|
||||
- **Changes that will consist in more than 50 lines of code should be discussed the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
|
||||
- All the applications share the same backend (database, synchronisation, settings, models, business logic, etc.) so if you change something in the backend in one app, makes sure it still work in the other apps. Usually it does, but keep this in mind.
|
||||
- **Changes that will consist of more than 50 lines of code should be discussed on the [Joplin Forum](https://discourse.joplinapp.org/)**, so that you don't spend too much time implementing something that might not be accepted.
|
||||
- All the applications share the same backend (database, synchronisation, settings, models, business logic, etc.) so if you change something in the backend in one app, make sure it still works in the other apps. Usually it does, but keep this in mind.
|
||||
- Pull requests that make many changes using an automated tool, like for spell fixing, styling, etc. will not be accepted. An exception would be if the changes have been discussed in the forum and someone has agreed to review **and test** the pull request.
|
||||
- Pull requests that make address multiple issues will most likely stall and eventually be closed. This is because we might be fine with one of the changes but not with others and untangling that kind of pull request is too much hassle both for maintainers and the person who submitted it. So most of the time someone gives up and the PR gets closed. So please keep the pull request focused on one issue.
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ COPY --chown=$user:$user package*.json ./
|
||||
COPY --chown=$user:$user .yarn ./.yarn
|
||||
COPY --chown=$user:$user .yarnrc.yml .
|
||||
COPY --chown=$user:$user yarn.lock .
|
||||
COPY --chown=$user:$user gulpfile.js .
|
||||
|
||||
RUN yarn install --inline-builds --mode=skip-build
|
||||
|
||||
@@ -75,14 +76,21 @@ COPY --chown=$user:$user packages/tools ./packages/tools
|
||||
COPY --chown=$user:$user packages/lib ./packages/lib
|
||||
COPY --chown=$user:$user packages/server ./packages/server
|
||||
|
||||
# Finally build everything, in particular the TypeScript files.
|
||||
# Finally build everything, in particular the TypeScript files. We can't just
|
||||
# run `yarn run build` because that wouldn't run the postinstall scripts in
|
||||
# dependencies (for example the sqlite3 native module would not be built). So
|
||||
# instead we run `yarn install`, which is going to install again all the
|
||||
# packages (but because it's already done it should be fast), and then run the
|
||||
# postinstall scripts, as well as build scripts.
|
||||
|
||||
RUN yarn run build
|
||||
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds
|
||||
|
||||
ENV RUNNING_IN_DOCKER=1
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
CMD [ "yarn", "--prefix", "packages/server", "start" ]
|
||||
# Not clear what's the equivalent of "--prefix" in Yarn 3, so keep using npm for
|
||||
# now.
|
||||
CMD [ "npm", "--prefix", "packages/server", "start" ]
|
||||
|
||||
# Build-time metadata
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
|
||||
@@ -219,6 +219,7 @@ then
|
||||
Type=Application
|
||||
Categories=Office;
|
||||
MimeType=x-scheme-handler/joplin;
|
||||
X-GNOME-SingleWindow=true
|
||||
EOF
|
||||
|
||||
# Update application icons
|
||||
|
||||
10
LICENSE
10
LICENSE
@@ -1,9 +1,9 @@
|
||||
All code in this repository is licensed under the MIT License **unless a
|
||||
directory contains a LICENSE file**, in which case that LICENSE file applies to
|
||||
the code in that sub-directory.
|
||||
directory contains a LICENSE or LICENSE.md file**, in which case that file
|
||||
applies to the code in that sub-directory.
|
||||
|
||||
For example, packages/fork-sax contains a ISC LICENSE file, thus all code under
|
||||
the packages/fork-sax directory is licensed under ISC.
|
||||
For example, packages/server contains a LICENSE.md file, thus all code under the
|
||||
packages/server directory is licensed under that license.
|
||||
|
||||
For example, packages/app-cli does NOT contain a LICENSE file, thus all code
|
||||
under that directory is licensed under the default license, which is MIT.
|
||||
@@ -20,7 +20,7 @@ icons please contact the author in order to get a permission.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2021 Laurent Cozic
|
||||
Copyright (c) 2016-2022 Laurent Cozic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -304,6 +304,7 @@ To add a **Bucket Policy** from the AWS S3 Web Console, navigate to the **Permis
|
||||
"Action": [
|
||||
"s3:ListBucket",
|
||||
"s3:GetBucketLocation",
|
||||
"s3:GetObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteObjectVersion",
|
||||
"s3:PutObject"
|
||||
|
||||
16
gulpfile.js
16
gulpfile.js
@@ -20,6 +20,22 @@ const tasks = {
|
||||
await utils.execCommandVerbose('git', ['push']);
|
||||
},
|
||||
},
|
||||
build: {
|
||||
fn: async () => {
|
||||
// Building everything in parallel seems to be unreliable on CI as
|
||||
// certain scripts randomly fail with missing files or folder, or
|
||||
// cannot delete certain directories (eg. copyPluginAssets or
|
||||
// copyApplicationAssets). Because of this, on CI, we run the build
|
||||
// sequencially. Locally we run it in parallel, which is much
|
||||
// faster, especially when having to rebuild after adding a
|
||||
// dependency.
|
||||
if (process.env.BUILD_SEQUENCIAL === '1') {
|
||||
await utils.execCommandVerbose('yarn', ['run', 'buildSequential']);
|
||||
} else {
|
||||
await utils.execCommandVerbose('yarn', ['run', 'buildParallel']);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn workspaces foreach --verbose --interlaced --parallel run build && yarn run tsc",
|
||||
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 run build && yarn run tsc",
|
||||
"buildSequential": "yarn workspaces foreach --verbose --interlaced run build && yarn run tsc",
|
||||
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildCommandIndex": "gulp buildCommandIndex",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
@@ -28,9 +29,9 @@
|
||||
"linter-ci": "eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"postinstall": "yarn run build",
|
||||
"publishAll": "git pull && yarn run build && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroid": "yarn run build && export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
|
||||
"postinstall": "gulp build",
|
||||
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroid": "yarn run buildParallel && export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
|
||||
"releaseAndroidClean": "node packages/tools/release-android.js",
|
||||
"releaseCli": "node packages/tools/release-cli.js",
|
||||
"releaseClipper": "node packages/tools/release-clipper.js",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Joplin Web Clipper</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
2
packages/app-desktop/.gitignore
vendored
2
packages/app-desktop/.gitignore
vendored
@@ -12,5 +12,5 @@ runForSharingCommands-*
|
||||
runForTestingCommands-*
|
||||
style.min.css
|
||||
build/lib/
|
||||
vendor/
|
||||
vendor/*
|
||||
!vendor/loadEmojiLib.js
|
||||
|
||||
@@ -9,6 +9,7 @@ const path = require('path');
|
||||
const { dirname } = require('@joplin/lib/path-utils');
|
||||
const fs = require('fs-extra');
|
||||
const { ipcMain } = require('electron');
|
||||
const { openProcessManager } = require('@joplin/electron-process-manager');
|
||||
|
||||
interface RendererProcessQuitReply {
|
||||
canClose: boolean;
|
||||
@@ -41,6 +42,10 @@ export default class ElectronAppWrapper {
|
||||
this.initialCallbackUrl_ = initialCallbackUrl;
|
||||
}
|
||||
|
||||
public openProcessManager() {
|
||||
openProcessManager(require('@electron/remote/main'));
|
||||
}
|
||||
|
||||
electronApp() {
|
||||
return this.electronApp_;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const fs = require('fs-extra');
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
@@ -63,6 +62,7 @@ import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import checkForUpdates from './checkForUpdates';
|
||||
import { AppState } from './app.reducer';
|
||||
import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
// import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
|
||||
|
||||
const pluginClasses = [
|
||||
@@ -234,23 +234,6 @@ class Application extends BaseApplication {
|
||||
});
|
||||
}
|
||||
|
||||
async loadCustomCss(filePath: string) {
|
||||
let cssString = '';
|
||||
if (await fs.pathExists(filePath)) {
|
||||
try {
|
||||
cssString = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
} catch (error) {
|
||||
let msg = error.message ? error.message : '';
|
||||
msg = `Could not load custom css from ${filePath}\n${msg}`;
|
||||
error.message = msg;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return cssString;
|
||||
}
|
||||
|
||||
private async checkForLegacyTemplates() {
|
||||
const templatesDir = `${Setting.value('profileDir')}/templates`;
|
||||
if (await shim.fsDriver().exists(templatesDir)) {
|
||||
@@ -523,6 +506,12 @@ class Application extends BaseApplication {
|
||||
|
||||
ResourceEditWatcher.instance().initialize(reg.logger(), (action: any) => { this.store().dispatch(action); }, (path: string) => bridge().openItem(path));
|
||||
|
||||
// Forwards the local event to the global event manager, so that it can
|
||||
// be picked up by the plugin manager.
|
||||
ResourceEditWatcher.instance().on('resourceChange', (event: any) => {
|
||||
eventManager.emit('resourceChange', event);
|
||||
});
|
||||
|
||||
RevisionService.instance().runInBackground();
|
||||
|
||||
// Make it available to the console window - useful to call revisionService.collectRevisions()
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { _, setLocale } from '@joplin/lib/locale';
|
||||
import { BrowserWindow, nativeTheme, nativeImage } from 'electron';
|
||||
const { dirname, toSystemSlashes } = require('@joplin/lib/path-utils');
|
||||
const { BrowserWindow, nativeTheme } = require('electron');
|
||||
|
||||
interface LastSelectedPath {
|
||||
file: string;
|
||||
directory: string;
|
||||
}
|
||||
|
||||
interface LastSelectedPaths {
|
||||
[key: string]: LastSelectedPath;
|
||||
}
|
||||
|
||||
export class Bridge {
|
||||
|
||||
private electronWrapper_: ElectronAppWrapper;
|
||||
private lastSelectedPaths_: LastSelectedPaths;
|
||||
private lastSelectedPaths_: LastSelectedPath;
|
||||
|
||||
constructor(electronWrapper: ElectronAppWrapper) {
|
||||
this.electronWrapper_ = electronWrapper;
|
||||
@@ -34,6 +30,10 @@ export class Bridge {
|
||||
return !this.electronApp().electronApp().isPackaged;
|
||||
}
|
||||
|
||||
public openProcessManager() {
|
||||
this.electronApp().openProcessManager();
|
||||
}
|
||||
|
||||
// The build directory contains additional external files that are going to
|
||||
// be packaged by Electron Builder. This is for files that need to be
|
||||
// accessed outside of the Electron app (for example the application icon).
|
||||
@@ -164,11 +164,11 @@ export class Bridge {
|
||||
if (!options) options = {};
|
||||
let fileType = 'file';
|
||||
if (options.properties && options.properties.includes('openDirectory')) fileType = 'directory';
|
||||
if (!('defaultPath' in options) && this.lastSelectedPaths_[fileType]) options.defaultPath = this.lastSelectedPaths_[fileType];
|
||||
if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType];
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const { filePaths } = await dialog.showOpenDialog(this.window(), options);
|
||||
if (filePaths && filePaths.length) {
|
||||
this.lastSelectedPaths_[fileType] = dirname(filePaths[0]);
|
||||
(this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]);
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
@@ -282,6 +282,10 @@ export class Bridge {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
public createImageFromPath(path: string) {
|
||||
return nativeImage.createFromPath(path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let bridge_: Bridge = null;
|
||||
|
||||
@@ -201,11 +201,11 @@ export default async function checkForUpdates(inBackground: boolean, parentWindo
|
||||
});
|
||||
|
||||
if (buttonIndex === 0) {
|
||||
bridge().openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
|
||||
void bridge().openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
|
||||
} else if (buttonIndex === 1) {
|
||||
await addSkippedVersion(release.version);
|
||||
} else if (buttonIndex === 2) {
|
||||
bridge().openExternal('https://joplinapp.org/changelog/');
|
||||
void bridge().openExternal('https://joplinapp.org/changelog/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
chromeButton_click() {
|
||||
bridge().openExternal('https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek');
|
||||
void bridge().openExternal('https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek');
|
||||
}
|
||||
|
||||
firefoxButton_click() {
|
||||
bridge().openExternal('https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/');
|
||||
void bridge().openExternal('https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/');
|
||||
}
|
||||
|
||||
copyToken_click() {
|
||||
|
||||
@@ -155,11 +155,11 @@ export default function(props: Props) {
|
||||
const onNameClick = useCallback(() => {
|
||||
const manifest = item.manifest;
|
||||
if (!manifest.homepage_url) return;
|
||||
bridge().openExternal(manifest.homepage_url);
|
||||
void bridge().openExternal(manifest.homepage_url);
|
||||
}, [item]);
|
||||
|
||||
const onRecommendedClick = useCallback(() => {
|
||||
bridge().openExternal('https://github.com/joplin/plugins/blob/master/readme/recommended.md#recommended-plugins');
|
||||
void bridge().openExternal('https://github.com/joplin/plugins/blob/master/readme/recommended.md#recommended-plugins');
|
||||
}, []);
|
||||
|
||||
// For plugins in dev mode things like enabling/disabling or
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function(props: Props) {
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onBrowsePlugins = useCallback(() => {
|
||||
bridge().openExternal('https://github.com/joplin/plugins/blob/master/README.md#plugins');
|
||||
void bridge().openExternal('https://github.com/joplin/plugins/blob/master/README.md#plugins');
|
||||
}, []);
|
||||
|
||||
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
|
||||
|
||||
@@ -8,25 +8,31 @@ import StyledInput from '../style/StyledInput';
|
||||
import { IconSelector, ChangeEvent } from './IconSelector';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { FolderIcon } from '@joplin/lib/services/database/types';
|
||||
import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
|
||||
import Button from '../Button/Button';
|
||||
import bridge from '../../services/bridge';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
folderId: string;
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [folderTitle, setFolderTitle] = useState('');
|
||||
const [folderIcon, setFolderIcon] = useState<FolderIcon>();
|
||||
|
||||
const isNew = !props.folderId;
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
if (isNew) return;
|
||||
|
||||
const folder = await Folder.load(props.folderId);
|
||||
if (event.cancelled) return;
|
||||
setFolderTitle(folder.title);
|
||||
setFolderIcon(Folder.unserializeIcon(folder.icon));
|
||||
}, [props.folderId]);
|
||||
}, [props.folderId, isNew]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
@@ -42,15 +48,29 @@ export default function(props: Props) {
|
||||
}
|
||||
|
||||
if (event.buttonName === 'ok') {
|
||||
await Folder.save({
|
||||
id: props.folderId,
|
||||
const folder: FolderEntity = {
|
||||
title: folderTitle,
|
||||
icon: Folder.serializeIcon(folderIcon),
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isNew) folder.id = props.folderId;
|
||||
if (props.parentId) folder.parent_id = props.parentId;
|
||||
|
||||
try {
|
||||
const savedFolder = await Folder.save(folder, { userSideValidation: true });
|
||||
onClose();
|
||||
|
||||
props.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: savedFolder.id,
|
||||
});
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}, [onClose, folderTitle, folderIcon, props.folderId]);
|
||||
}, [onClose, folderTitle, folderIcon, props.folderId, props.parentId]);
|
||||
|
||||
const onFolderTitleChange = useCallback((event: any) => {
|
||||
setFolderTitle(event.target.value);
|
||||
@@ -96,10 +116,12 @@ export default function(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const dialogTitle = isNew ? _('Create notebook') : _('Edit notebook');
|
||||
|
||||
function renderDialogWrapper() {
|
||||
return (
|
||||
<div className="dialog-root">
|
||||
<DialogTitle title={_('Edit notebook')}/>
|
||||
<DialogTitle title={dialogTitle}/>
|
||||
{renderContent()}
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import { Options } from './openFolderDialog';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'newFolder',
|
||||
@@ -9,35 +8,15 @@ export const declaration: CommandDeclaration = {
|
||||
iconName: 'fa-book',
|
||||
};
|
||||
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext, parentId: string = null) => {
|
||||
comp.setState({
|
||||
promptOptions: {
|
||||
label: _('Notebook title:'),
|
||||
onClose: async (answer: string) => {
|
||||
if (answer) {
|
||||
let folder = null;
|
||||
try {
|
||||
const toSave: any = { title: answer };
|
||||
if (parentId) toSave.parent_id = parentId;
|
||||
folder = await Folder.save(toSave, { userSideValidation: true });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
const options: Options = {
|
||||
isNew: true,
|
||||
parentId: parentId,
|
||||
};
|
||||
|
||||
if (folder) {
|
||||
comp.props.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
comp.setState({ promptOptions: null });
|
||||
},
|
||||
},
|
||||
});
|
||||
void CommandService.instance().execute('openFolderDialog', options);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export interface Options {
|
||||
isNew?: boolean;
|
||||
folderId?: string;
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openFolderDialog',
|
||||
label: () => _('Edit'),
|
||||
@@ -8,13 +14,22 @@ export const declaration: CommandDeclaration = {
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, folderId: string) => {
|
||||
execute: async (context: CommandContext, options: Options = null) => {
|
||||
options = {
|
||||
isNew: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (options.isNew && !('parentId' in options)) throw new Error('parentId mst be specified when creating a new folder');
|
||||
if (!options.isNew && !('folderId' in options)) throw new Error('folderId property is required');
|
||||
|
||||
context.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'editFolder',
|
||||
isOpen: true,
|
||||
props: {
|
||||
folderId,
|
||||
folderId: options.folderId,
|
||||
parentId: options.parentId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -27,9 +27,9 @@ export const runtime = (): CommandRuntime => {
|
||||
// but doesn't on macOS, so we need to convert it to a path
|
||||
// before passing it to openPath.
|
||||
const decodedPath = fileUriToPath(urlDecode(link), shim.platformName());
|
||||
require('electron').shell.openPath(decodedPath);
|
||||
void require('electron').shell.openPath(decodedPath);
|
||||
} else {
|
||||
require('electron').shell.openExternal(link);
|
||||
void require('electron').shell.openExternal(link);
|
||||
}
|
||||
} else {
|
||||
bridge().showErrorMessageBox(_('Unsupported link or message: %s', link));
|
||||
|
||||
@@ -19,7 +19,7 @@ export const runtime = (): CommandRuntime => {
|
||||
useSpellChecker = useSpellChecker === null ? context.state.settings['spellChecker.enabled'] : useSpellChecker;
|
||||
|
||||
const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguage, useSpellChecker);
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
const menu = Menu.buildFromTemplate(menuItems as any);
|
||||
menu.popup(bridge().window());
|
||||
},
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ interface Props {
|
||||
['spellChecker.language']: string;
|
||||
plugins: PluginStates;
|
||||
customCss: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const commandNames: string[] = menuCommandNames();
|
||||
@@ -249,7 +250,11 @@ function useMenu(props: Props) {
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
const pluginCommandNames = props.pluginMenuItems.map((view: any) => view.commandName);
|
||||
const menuItemDic = menuUtils.commandsToMenuItems(commandNames.concat(pluginCommandNames), (commandName: string) => onMenuItemClickRef.current(commandName));
|
||||
const menuItemDic = menuUtils.commandsToMenuItems(
|
||||
commandNames.concat(pluginCommandNames),
|
||||
(commandName: string) => onMenuItemClickRef.current(commandName),
|
||||
props.locale
|
||||
);
|
||||
|
||||
const quitMenuItem = {
|
||||
label: _('Quit'),
|
||||
@@ -697,13 +702,13 @@ function useMenu(props: Props) {
|
||||
submenu: [{
|
||||
label: _('Website and documentation'),
|
||||
accelerator: keymapService.getAccelerator('help'),
|
||||
click() { bridge().openExternal('https://joplinapp.org'); },
|
||||
click() { void bridge().openExternal('https://joplinapp.org'); },
|
||||
}, {
|
||||
label: _('Joplin Forum'),
|
||||
click() { bridge().openExternal('https://discourse.joplinapp.org'); },
|
||||
click() { void bridge().openExternal('https://discourse.joplinapp.org'); },
|
||||
}, {
|
||||
label: _('Make a donation'),
|
||||
click() { bridge().openExternal('https://joplinapp.org/donate/'); },
|
||||
click() { void bridge().openExternal('https://joplinapp.org/donate/'); },
|
||||
}, {
|
||||
label: _('Check for updates...'),
|
||||
visible: shim.isMac() ? false : true,
|
||||
@@ -721,6 +726,13 @@ function useMenu(props: Props) {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'help:toggleTaskList',
|
||||
label: _('Open plugin task list'),
|
||||
click: () => {
|
||||
bridge().openProcessManager();
|
||||
},
|
||||
},
|
||||
|
||||
menuItemDic.toggleSafeMode,
|
||||
menuItemDic.openProfileDirectory,
|
||||
@@ -816,7 +828,7 @@ function useMenu(props: Props) {
|
||||
menuItemDic.textCut,
|
||||
menuItemDic.textPaste,
|
||||
menuItemDic.textSelectAll,
|
||||
],
|
||||
] as any,
|
||||
},
|
||||
]));
|
||||
} else {
|
||||
@@ -830,7 +842,7 @@ function useMenu(props: Props) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
};
|
||||
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime, props['spellChecker.language'], props['spellChecker.enabled'], props.plugins, props.customCss]);
|
||||
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime, props['spellChecker.language'], props['spellChecker.enabled'], props.plugins, props.customCss, props.locale]);
|
||||
|
||||
useMenuStates(menu, props);
|
||||
|
||||
@@ -872,6 +884,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
|
||||
return {
|
||||
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(pluginCommandNames(state.pluginService.plugins)), whenClauseContext),
|
||||
locale: state.settings.locale,
|
||||
routeName: state.route.routeName,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
layoutButtonSequence: state.settings.layoutButtonSequence,
|
||||
|
||||
@@ -502,10 +502,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
padding-left: .2em;
|
||||
}
|
||||
|
||||
div.CodeMirror span.cm-strong {
|
||||
color: ${theme.colorBright};
|
||||
}
|
||||
|
||||
div.CodeMirror span.cm-hr {
|
||||
color: ${theme.dividerColor};
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (scrollTo) cm.scrollIntoView(match);
|
||||
if (scrollTo) cm.setSelection(match.from, match.to);
|
||||
return cm.markText(match.from, match.to, { className: 'cm-search-marker-selected' });
|
||||
}
|
||||
|
||||
@@ -107,7 +107,9 @@ export default function useEditorSearch(CodeMirror: any) {
|
||||
const searchTerm = getSearchTerm(keyword);
|
||||
|
||||
// We only want to scroll the first keyword into view in the case of a multi keyword search
|
||||
const scrollTo = i === 0 && (previousKeywordValue !== keyword.value || previousIndex !== options.selectedIndex);
|
||||
const scrollTo = i === 0 && (previousKeywordValue !== keyword.value || previousIndex !== options.selectedIndex ||
|
||||
// If there is only one choice, scrollTo should be true. The below is a dummy of nMatches === 1.
|
||||
options.selectedIndex === 0);
|
||||
|
||||
const match = highlightSearch(this, searchTerm, options.selectedIndex, scrollTo);
|
||||
if (match) marks.push(match);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { copyHtmlToClipboard } from './clipboardUtils';
|
||||
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import bridge from '../../../services/bridge';
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
@@ -131,6 +130,15 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
},
|
||||
copyImage: {
|
||||
label: _('Copy image'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
const { resourcePath } = await resourceInfo(options);
|
||||
const image = bridge().createImageFromPath(resourcePath);
|
||||
clipboard.writeImage(image);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image,
|
||||
},
|
||||
cut: {
|
||||
label: _('Cut'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import { SearchMarkers } from './useSearchMarkers';
|
||||
const CommandService = require('@joplin/lib/services/CommandService').default;
|
||||
|
||||
const logger = Logger.create('useNoteSearchBar');
|
||||
|
||||
@@ -70,6 +71,7 @@ export default function useNoteSearchBar() {
|
||||
const onClose = useCallback(() => {
|
||||
setShowLocalSearch(false);
|
||||
setLocalSearch(defaultLocalSearch());
|
||||
void CommandService.instance().execute('focusElementNoteBody');
|
||||
}, []);
|
||||
|
||||
const setResultCount = useCallback((count: number) => {
|
||||
|
||||
@@ -56,7 +56,15 @@ function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, s
|
||||
});
|
||||
}
|
||||
},
|
||||
enabledCondition: '!modalDialogVisible && markdownEditorPaneVisible && oneNoteSelected && noteIsMarkdown',
|
||||
|
||||
// We disable the editor commands whenever a modal dialog is visible,
|
||||
// otherwise the user might type something in a dialog and accidentally
|
||||
// change something in the editor. However, we still enable them for
|
||||
// GotoAnything so that it's possible to type eg `textBold` and bold the
|
||||
// currently selected text.
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/5707
|
||||
enabledCondition: '(!modalDialogVisible || gotoAnythingVisible) && markdownEditorPaneVisible && oneNoteSelected && noteIsMarkdown',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
);
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', itemId)));
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId })));
|
||||
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ export default function(props: Props) {
|
||||
}, [joplinCloudEmail, joplinCloudPassword, props.dispatch]);
|
||||
|
||||
const onJoplinCloudCreateAccountClick = useCallback(() => {
|
||||
bridge().openExternal('https://joplinapp.org/plans/');
|
||||
void bridge().openExternal('https://joplinapp.org/plans/');
|
||||
}, []);
|
||||
|
||||
function renderJoplinCloudLoginForm() {
|
||||
|
||||
@@ -134,9 +134,10 @@
|
||||
"7zip-bin-win": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@electron/remote": "2.0.1",
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@joeattardi/emoji-button": "^4.6.0",
|
||||
"@joplin/electron-process-manager": "workspace:^",
|
||||
"@joplin/lib": "~2.7",
|
||||
"@joplin/renderer": "~2.7",
|
||||
"async-mutex": "^0.1.3",
|
||||
@@ -147,7 +148,7 @@
|
||||
"debounce": "^1.2.0",
|
||||
"electron-window-state": "^4.1.1",
|
||||
"formatcoords": "^1.1.3",
|
||||
"fs-extra": "^5.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"highlight.js": "^10.2.1",
|
||||
"immer": "^7.0.5",
|
||||
"keytar": "^7.0.0",
|
||||
|
||||
@@ -496,10 +496,10 @@ class Dialog extends React.PureComponent<Props, State> {
|
||||
const isSelected = item.id === this.state.selectedItemId;
|
||||
const rowStyle = isSelected ? style.rowSelected : style.row;
|
||||
const titleHtml = item.fragments
|
||||
? `<span style="font-weight: bold; color: ${theme.colorBright};">${item.title}</span>`
|
||||
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>', { escapeHtml: true });
|
||||
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
|
||||
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
||||
|
||||
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>', { escapeHtml: true });
|
||||
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
||||
|
||||
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" />;
|
||||
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
||||
|
||||
@@ -41,6 +41,11 @@ if [ "$USER_NUM" = "1a" ]; then
|
||||
USER_PROFILE_NUM=1a
|
||||
fi
|
||||
|
||||
if [ "$USER_NUM" = "1b" ]; then
|
||||
USER_NUM=1
|
||||
USER_PROFILE_NUM=1b
|
||||
fi
|
||||
|
||||
COMMANDS=($(echo $2 | tr "," "\n"))
|
||||
PROFILE_DIR=~/.config/joplindev-desktop-$USER_PROFILE_NUM
|
||||
|
||||
@@ -54,6 +59,10 @@ do
|
||||
|
||||
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
elif [[ $CMD == "createUserDeletions" ]]; then
|
||||
|
||||
curl --data '{"action": "createUserDeletions"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
elif [[ $CMD == "createData" ]]; then
|
||||
|
||||
echo 'mkbook "shared"' >> "$CMD_FILE"
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function stateToWhenClauseContext(state: AppState, options: WhenC
|
||||
markdownEditorPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('editor'),
|
||||
markdownViewerPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('viewer'),
|
||||
modalDialogVisible: !!Object.keys(state.visibleDialogs).length,
|
||||
gotoAnythingVisible: !!state.visibleDialogs['gotoAnything'],
|
||||
sidebarVisible: !!state.mainLayout && layoutItemProp(state.mainLayout, 'sideBar', 'visible'),
|
||||
noteListHasNotes: !!state.notes.length,
|
||||
|
||||
|
||||
64
packages/app-desktop/services/plugins/BackOffHandler.ts
Normal file
64
packages/app-desktop/services/plugins/BackOffHandler.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import time from '@joplin/lib/time';
|
||||
|
||||
const logger = Logger.create('BackOffHandler');
|
||||
|
||||
// This handler performs two checks:
|
||||
//
|
||||
// 1. If the plugin makes many API calls one after the other, a delay is going
|
||||
// to be applied before responding. The delay is set using backOffIntervals_.
|
||||
// When a plugin needs to be throttled that way a warning is displayed so
|
||||
// that the author gets an opportunity to fix it.
|
||||
//
|
||||
// 2. If the plugin makes many simultaneous calls (over 100), the handler throws
|
||||
// an exception to stop the plugin. In that case the plugin will be broken,
|
||||
// but most plugins will not get this error anyway because call are usually
|
||||
// made in sequence. It might reveal a bug though - for example if the plugin
|
||||
// makes a call every 1 second, but does not wait for the response (or assume
|
||||
// the response will come in less than one second). In that case, the back
|
||||
// off intervals combined with the incorrect code will make the plugin fail.
|
||||
|
||||
export default class BackOffHandler {
|
||||
|
||||
private backOffIntervals_ = Array(100).fill(0).concat([0, 1, 1, 2, 3, 5, 8]);
|
||||
private lastRequestTime_ = 0;
|
||||
private pluginId_: string;
|
||||
private resetBackOffInterval_ = (this.backOffIntervals_[this.backOffIntervals_.length - 1] + 1) * 1000;
|
||||
private backOffIndex_ = 0;
|
||||
private waitCount_ = 0;
|
||||
private maxWaitCount_ = 100;
|
||||
|
||||
public constructor(pluginId: string) {
|
||||
this.pluginId_ = pluginId;
|
||||
}
|
||||
|
||||
private backOffInterval() {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - this.lastRequestTime_ > this.resetBackOffInterval_) {
|
||||
this.backOffIndex_ = 0;
|
||||
} else {
|
||||
this.backOffIndex_++;
|
||||
}
|
||||
|
||||
this.lastRequestTime_ = now;
|
||||
const effectiveIndex = this.backOffIndex_ >= this.backOffIntervals_.length ? this.backOffIntervals_.length - 1 : this.backOffIndex_;
|
||||
return this.backOffIntervals_[effectiveIndex];
|
||||
}
|
||||
|
||||
public async wait(path: string, args: any) {
|
||||
const interval = this.backOffInterval();
|
||||
if (!interval) return;
|
||||
|
||||
this.waitCount_++;
|
||||
|
||||
logger.warn(`Plugin ${this.pluginId_}: Applying a backoff of ${interval} seconds due to frequent plugin API calls. Consider reducing the number of calls, caching the data, or requesting more data per call. API call was: `, path, args, `[Wait count: ${this.waitCount_}]`);
|
||||
|
||||
if (this.waitCount_ > this.maxWaitCount_) throw new Error(`Plugin ${this.pluginId_}: More than ${this.maxWaitCount_} API alls are waiting - aborting. Please consider queuing the API calls in your plugins to reduce the load on the application.`);
|
||||
|
||||
await time.sleep(interval);
|
||||
|
||||
this.waitCount_--;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import { EventHandlers } from '@joplin/lib/services/plugins/utils/mapEventHandlersToIds';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import BackOffHandler from './BackOffHandler';
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
const logger = Logger.create('PluginRunner');
|
||||
@@ -83,8 +84,9 @@ function mapEventIdsToHandlers(pluginId: string, arg: any) {
|
||||
export default class PluginRunner extends BasePluginRunner {
|
||||
|
||||
protected eventHandlers_: EventHandlers = {};
|
||||
private backOffHandlers_: Record<string, BackOffHandler> = {};
|
||||
|
||||
constructor() {
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.eventHandler = this.eventHandler.bind(this);
|
||||
@@ -95,7 +97,14 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
return cb(...args);
|
||||
}
|
||||
|
||||
async run(plugin: Plugin, pluginApi: Global) {
|
||||
private backOffHandler(pluginId: string): BackOffHandler {
|
||||
if (!this.backOffHandlers_[pluginId]) {
|
||||
this.backOffHandlers_[pluginId] = new BackOffHandler(pluginId);
|
||||
}
|
||||
return this.backOffHandlers_[pluginId];
|
||||
}
|
||||
|
||||
public async run(plugin: Plugin, pluginApi: Global) {
|
||||
const scriptPath = `${Setting.value('tempDir')}/plugin_${plugin.id}.js`;
|
||||
await shim.fsDriver().writeFile(scriptPath, plugin.scriptText, 'utf8');
|
||||
|
||||
@@ -111,7 +120,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
|
||||
bridge().electronApp().registerPluginWindow(plugin.id, pluginWindow);
|
||||
|
||||
pluginWindow.loadURL(`${require('url').format({
|
||||
void pluginWindow.loadURL(`${require('url').format({
|
||||
pathname: require('path').join(__dirname, 'plugin_index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
@@ -148,6 +157,13 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
const debugMappedArgs = fullPath.includes('setHtml') ? '<hidden>' : mappedArgs;
|
||||
logger.debug(`Got message (3): ${fullPath}`, debugMappedArgs);
|
||||
|
||||
try {
|
||||
await this.backOffHandler(plugin.id).wait(fullPath, debugMappedArgs);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
let result: any = null;
|
||||
let error: any = null;
|
||||
try {
|
||||
|
||||
@@ -3,8 +3,62 @@ const glob = require('glob');
|
||||
const { resolve } = require('path');
|
||||
const { dirname } = require('@joplin/tools/gulp/utils');
|
||||
|
||||
const rootDir = resolve(__dirname, '../../..');
|
||||
const nodeModulesDir = resolve(__dirname, '../node_modules');
|
||||
|
||||
function stripOffRootDir(path) {
|
||||
if (path.startsWith(rootDir)) return path.substr(rootDir.length + 1);
|
||||
return path;
|
||||
}
|
||||
|
||||
const msleep = async (ms) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
};
|
||||
|
||||
// Running this script on CI is very unreliable. It fails with errors that don't
|
||||
// make much sense, such as:
|
||||
//
|
||||
// [Error: ENOENT: no such file or directory, copyfile
|
||||
// '/home/runner/work/joplin/joplin/Assets/TinyMCE/langs/ro_RO.js' ->
|
||||
// '/home/runner/work/joplin/joplin/packages/app-desktop/vendor/lib/tinymce/langs/ro_RO.js']
|
||||
//
|
||||
// (but "Assets/TinyMCE/langs/ro_RO.js" exists, since it's in the repo, and it's
|
||||
// normal that "tinymce/langs/ro_RO.js" doesn't exist since we want to create
|
||||
// it...)
|
||||
//
|
||||
// Another one, when trying to delete a directory:
|
||||
//
|
||||
// ENOTEMPTY: directory not empty
|
||||
//
|
||||
// (also makes no sense since the point of calling `remove()` is to remove a
|
||||
// directory that is not empty)
|
||||
//
|
||||
// Those errors are random - they may or may not happen on a CI run, and always
|
||||
// on different files. Since they don't make sense and are seemingly impossible
|
||||
// to fix, we instead implement a retry mechanism with exponential backoff. The
|
||||
// failures are relatively rare so 5 attempts should be enough to ensure all CI
|
||||
// runs succeed.
|
||||
//
|
||||
// It's possible the same technique should be added to copyPluginAssets too.
|
||||
|
||||
const withRetry = async (fn) => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
await fn();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn(`withRetry: Failed calling function - will retry (${i})`, error);
|
||||
await msleep(1000 + i * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('withRetry: Could not run function after multiple attempts');
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const langSourceDir = resolve(__dirname, '../../../Assets/TinyMCE/langs');
|
||||
const buildLibDir = resolve(__dirname, '../vendor/lib');
|
||||
@@ -50,11 +104,11 @@ async function main() {
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
await remove(destDir);
|
||||
await withRetry(() => remove(destDir));
|
||||
} else {
|
||||
console.info(`Copying ${sourceDir} => ${destDir}`);
|
||||
await mkdirp(destDir);
|
||||
await copy(sourceDir, destDir, { overwrite: true });
|
||||
console.info(`Copying ${stripOffRootDir(sourceDir)} => ${stripOffRootDir(destDir)}`);
|
||||
await withRetry(() => mkdirp(destDir));
|
||||
await withRetry(() => copy(sourceDir, destDir, { overwrite: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,10 +124,10 @@ async function main() {
|
||||
destFile = `${buildLibDir}/${file}`;
|
||||
}
|
||||
|
||||
await mkdirp(dirname(destFile));
|
||||
await withRetry(() => mkdirp(dirname(destFile)));
|
||||
|
||||
console.info(`Copying ${sourceFile} => ${destFile}`);
|
||||
await copy(sourceFile, destFile, { overwrite: true });
|
||||
console.info(`Copying ${stripOffRootDir(sourceFile)} => ${stripOffRootDir(destFile)}`);
|
||||
await withRetry(() => copy(sourceFile, destFile, { overwrite: true }));
|
||||
}
|
||||
|
||||
const supportedLocales = glob.sync(`${langSourceDir}/*.js`).map(s => {
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
const { copy, mkdirp, remove } = require('fs-extra');
|
||||
|
||||
const msleep = async (ms) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
};
|
||||
|
||||
// Same as copyApplicationAssets - probably both scripts should be merged in
|
||||
// one.
|
||||
const withRetry = async (fn) => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
await fn();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn(`withRetry: Failed calling function - will retry (${i})`, error);
|
||||
await msleep(1000 + i * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('withRetry: Could not run function after multiple attempts');
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const rootDir = `${__dirname}/..`;
|
||||
@@ -9,9 +33,16 @@ async function main() {
|
||||
`${rootDir}/pluginAssets`,
|
||||
];
|
||||
|
||||
for (const destDir of destDirs) {
|
||||
console.info(`Copying to ${destDir}`);
|
||||
await utils.copyDir(sourceDir, destDir);
|
||||
for (const action of ['delete', 'copy']) {
|
||||
for (const destDir of destDirs) {
|
||||
if (action === 'delete') {
|
||||
await withRetry(() => remove(destDir));
|
||||
} else {
|
||||
console.info(`Copying to ${destDir}`);
|
||||
await withRetry(() => mkdirp(destDir));
|
||||
await withRetry(() => copy(sourceDir, destDir, { overwrite: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
packages/app-desktop/vendor/loadEmojiLib.js
vendored
Normal file
2
packages/app-desktop/vendor/loadEmojiLib.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { EmojiButton } from './lib/@joeattardi/emoji-button/dist/index.js';
|
||||
window.EmojiButton = EmojiButton;
|
||||
4
packages/electron-process-manager/.babelrc
Normal file
4
packages/electron-process-manager/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets" : ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||
}
|
||||
BIN
packages/electron-process-manager/.github/screenshots/window.png
vendored
Normal file
BIN
packages/electron-process-manager/.github/screenshots/window.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
1
packages/electron-process-manager/.gitignore
vendored
Normal file
1
packages/electron-process-manager/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist/
|
||||
5
packages/electron-process-manager/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
packages/electron-process-manager/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
13
packages/electron-process-manager/.idea/electron-process-manager.iml
generated
Normal file
13
packages/electron-process-manager/.idea/electron-process-manager.iml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<content url="file://$MODULE_DIR$/../electron-process-reporter" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
81
packages/electron-process-manager/.idea/misc.xml
generated
Normal file
81
packages/electron-process-manager/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="JSX" />
|
||||
</component>
|
||||
<component name="MarkdownProjectSettings" wasCopied="true">
|
||||
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.5" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true" openRemoteLinks="true" replaceUnicodeEmoji="false" lastLayoutSetsDefault="false">
|
||||
<PanelProvider>
|
||||
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
|
||||
</PanelProvider>
|
||||
</PreviewSettings>
|
||||
<ParserSettings gitHubSyntaxChange="false" emojiShortcuts="0" emojiImages="0">
|
||||
<PegdownExtensions>
|
||||
<option name="ABBREVIATIONS" value="false" />
|
||||
<option name="ANCHORLINKS" value="true" />
|
||||
<option name="ASIDE" value="false" />
|
||||
<option name="ATXHEADERSPACE" value="true" />
|
||||
<option name="AUTOLINKS" value="true" />
|
||||
<option name="DEFINITIONS" value="false" />
|
||||
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
|
||||
<option name="FENCED_CODE_BLOCKS" value="true" />
|
||||
<option name="FOOTNOTES" value="false" />
|
||||
<option name="HARDWRAPS" value="false" />
|
||||
<option name="HTML_DEEP_PARSER" value="false" />
|
||||
<option name="INSERTED" value="false" />
|
||||
<option name="QUOTES" value="false" />
|
||||
<option name="RELAXEDHRULES" value="true" />
|
||||
<option name="SMARTS" value="false" />
|
||||
<option name="STRIKETHROUGH" value="true" />
|
||||
<option name="SUBSCRIPT" value="false" />
|
||||
<option name="SUPERSCRIPT" value="false" />
|
||||
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
|
||||
<option name="SUPPRESS_INLINE_HTML" value="false" />
|
||||
<option name="TABLES" value="true" />
|
||||
<option name="TASKLISTITEMS" value="true" />
|
||||
<option name="TOC" value="false" />
|
||||
<option name="WIKILINKS" value="true" />
|
||||
</PegdownExtensions>
|
||||
<ParserOptions>
|
||||
<option name="ADMONITION_EXT" value="false" />
|
||||
<option name="ATTRIBUTES_EXT" value="false" />
|
||||
<option name="COMMONMARK_LISTS" value="true" />
|
||||
<option name="DUMMY" value="false" />
|
||||
<option name="EMOJI_SHORTCUTS" value="true" />
|
||||
<option name="ENUMERATED_REFERENCES_EXT" value="false" />
|
||||
<option name="FLEXMARK_FRONT_MATTER" value="false" />
|
||||
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="false" />
|
||||
<option name="GFM_TABLE_RENDERING" value="true" />
|
||||
<option name="GITBOOK_URL_ENCODING" value="false" />
|
||||
<option name="GITHUB_LISTS" value="false" />
|
||||
<option name="GITHUB_WIKI_LINKS" value="true" />
|
||||
<option name="HEADER_ID_NO_DUPED_DASHES" value="false" />
|
||||
<option name="JEKYLL_FRONT_MATTER" value="false" />
|
||||
<option name="NO_TEXT_ATTRIBUTES" value="false" />
|
||||
<option name="PARSE_HTML_ANCHOR_ID" value="false" />
|
||||
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
|
||||
</ParserOptions>
|
||||
</ParserSettings>
|
||||
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true" embedImages="false" embedHttpImages="false" imageUriSerials="false">
|
||||
<GeneratorProvider>
|
||||
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
|
||||
</GeneratorProvider>
|
||||
<headerTop />
|
||||
<headerBottom />
|
||||
<bodyTop />
|
||||
<bodyBottom />
|
||||
</HtmlSettings>
|
||||
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssUriSerial="true" isCssTextEnabled="false" isDynamicPageWidth="true">
|
||||
<StylesheetProvider>
|
||||
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
|
||||
</StylesheetProvider>
|
||||
<ScriptProviders />
|
||||
<cssText />
|
||||
<cssUriHistory />
|
||||
</CssSettings>
|
||||
<HtmlExportSettings updateOnSave="false" parentDir="" targetDir="" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" linkFormatType="HTTP_ABSOLUTE" />
|
||||
<LinkMapSettings>
|
||||
<textMaps />
|
||||
</LinkMapSettings>
|
||||
</component>
|
||||
</project>
|
||||
8
packages/electron-process-manager/.idea/modules.xml
generated
Normal file
8
packages/electron-process-manager/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/electron-process-manager.iml" filepath="$PROJECT_DIR$/.idea/electron-process-manager.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
packages/electron-process-manager/.idea/vcs.xml
generated
Normal file
6
packages/electron-process-manager/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
107
packages/electron-process-manager/.idea/workspace.xml
generated
Normal file
107
packages/electron-process-manager/.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="b935834e-d3d3-420f-991d-e49e232dd71b" name="Default Changelist" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="FileColors">
|
||||
<fileColor scope="Non-Project Files (Material Default)" color="2E3C43" />
|
||||
<fileColor scope="Non-Project Files (Material Darker)" color="323232" />
|
||||
<fileColor scope="Non-Project Files (Material Lighter)" color="eae8e8" />
|
||||
<fileColor scope="Non-Project Files (Material Palenight)" color="2f2e43" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="PREVIOUS_COMMIT_AUTHORS">
|
||||
<list>
|
||||
<option value="Kris Dages <krisdages@git.whiteboxsoftware.net>" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="GitSEFilterConfiguration">
|
||||
<file-type-list>
|
||||
<filtered-out-file-type name="LOCAL_BRANCH" />
|
||||
<filtered-out-file-type name="REMOTE_BRANCH" />
|
||||
<filtered-out-file-type name="TAG" />
|
||||
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
|
||||
</file-type-list>
|
||||
</component>
|
||||
<component name="ProjectId" id="1hYhEkXEjWlOUW5DO64AadCwogW" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="autoscrollFromSource" value="true" />
|
||||
<option name="autoscrollToSource" value="true" />
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$/../electron-process-reporter" />
|
||||
<property name="node.js.detected.package.eslint" value="true" />
|
||||
<property name="node.js.detected.package.tslint" value="true" />
|
||||
<property name="node.js.selected.package.eslint" value="(autodetect)" />
|
||||
<property name="node.js.selected.package.tslint" value="(autodetect)" />
|
||||
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
|
||||
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
|
||||
<property name="nodejs_package_manager_path" value="yarn" />
|
||||
<property name="settings.editor.selected.configurable" value="web-ide.project.structure" />
|
||||
<property name="ts.external.directory.path" value="$APPLICATION_HOME_DIR$/plugins/JavaScriptLanguage/jsLanguageServicesImpl/external" />
|
||||
<property name="vue.rearranger.settings.migration" value="true" />
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="b935834e-d3d3-420f-991d-e49e232dd71b" name="Default Changelist" comment="" />
|
||||
<created>1600193824206</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1600193824206</updated>
|
||||
<workItem from="1600193825399" duration="438000" />
|
||||
<workItem from="1606177958157" duration="419000" />
|
||||
<workItem from="1616558380418" duration="1481000" />
|
||||
<workItem from="1627318845586" duration="3411000" />
|
||||
<workItem from="1627323366882" duration="72000" />
|
||||
<workItem from="1627489266598" duration="1192000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="(fix) Add `enableRemoteModule: true` to ProcessManagerWindow webPreferences Fixes broken UI in Electron 10">
|
||||
<created>1600193995342</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1600193995342</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="(breaking) Use `@electron/remote` instead of deprecated `remote` module Updated min electron version to 10. Bump version to 2.0.0">
|
||||
<created>1627322100905</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1627322100905</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="Update README.md for fork">
|
||||
<created>1627490446165</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1627490446165</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="(fix) Add `enableRemoteModule: true` to ProcessManagerWindow webPreferences Fixes broken UI in Electron 10" />
|
||||
<MESSAGE value="(breaking) Use `@electron/remote` instead of deprecated `remote` module Updated min electron version to 10. Bump version to 2.0.0" />
|
||||
<MESSAGE value="Update README.md for fork" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Update README.md for fork" />
|
||||
</component>
|
||||
</project>
|
||||
92
packages/electron-process-manager/README.md
Normal file
92
packages/electron-process-manager/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Process Manager UI for Electron Apps
|
||||
|
||||
* * *
|
||||
|
||||
2022-01-06: Forked from https://github.com/krisdages/electron-process-manager
|
||||
|
||||
* * *
|
||||
|
||||
## Fork using @electron/remote instead of builtin remote module
|
||||
* Minimum electron version is `10`
|
||||
* [@electron/remote](https://github.com/electron/remote) is a peerDependency. It needs to be initialized in the main process. Follow the instructions in the link.
|
||||
|
||||
## Original 1.0 Readme
|
||||
|
||||
This package provides a process manager UI for Electron applications.
|
||||
|
||||
It opens a window displaying a table of every processes run by the Electron application with information (type, URL for `webContents`, memory..).
|
||||
|
||||
[](https://badge.fury.io/js/electron-process-manager)
|
||||
|
||||

|
||||
|
||||
~~:warning: For `@electron>=3.0.0, <7.x`, use version `0.7.1` of this package.
|
||||
For versions `>=7.x`, use latest.~~
|
||||
|
||||
It can be useful to debug performance of an app with several `webview`.
|
||||
|
||||
It's inspired from Chrome's task manager.
|
||||
|
||||
## Features
|
||||
|
||||
- [ ] Memory reporting
|
||||
- [ ] Link memory data to web-contents (for electron >=1.7.1)
|
||||
- [x] Kill a process from the UI
|
||||
- [x] Open developer tools for a given process
|
||||
- [x] CPU metrics
|
||||
- [x] Sort by columns
|
||||
|
||||
⚠️ Unfortunately, memory info are no longer available in Electron>=4 (see [electron/electron#16179](https://github.com/electron/electron/issues/16179))
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install electron-process-manager
|
||||
```
|
||||
|
||||
## Usage
|
||||
```js
|
||||
const { openProcessManager } = require('electron-process-manager');
|
||||
|
||||
openProcessManager();
|
||||
```
|
||||
|
||||
## Options
|
||||
`openProcessManager` function can take options in paramters
|
||||
|
||||
#### options.defaultSorting
|
||||
**defaultSorting.how**: `'ascending' | 'descending'`
|
||||
|
||||
**defaultSorting.path**:
|
||||
|
||||
| Field name | path |
|
||||
|--------------------|----------------------------|
|
||||
| Pid | 'pid' |
|
||||
| WebContents Domain | 'webContents.0.URLDomain' |
|
||||
| Process Type | 'webContents.0.type' |
|
||||
| Private Memory | 'memory.privateBytes' |
|
||||
| Shared Memory | 'memory.sharedBytes' |
|
||||
| Working Set Size | 'memory.workingSetSize' |
|
||||
| % CPU | 'cpu.percentCPUUsage' |
|
||||
| Idle Wake Ups /s | 'cpu.idleWakeupsPerSecond' |
|
||||
| WebContents Id | 'webContents.0.id' |
|
||||
| WebContents Type | 'webContents.0.type' |
|
||||
| WebContents URL | 'webContents.0.URL' |
|
||||
|
||||
example:
|
||||
```js
|
||||
const { openProcessManager } = require('electron-process-manager');
|
||||
|
||||
openProcessManager({ how: 'descending', path: 'cpu.percentCPUUsage' });
|
||||
```
|
||||
|
||||
## Future
|
||||
|
||||
- Add physical memory (noted as "Memory" in Chrome's task manager)
|
||||
- Add networks metrics
|
||||
|
||||
Pull requests welcome :)
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
37
packages/electron-process-manager/package.json
Normal file
37
packages/electron-process-manager/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@joplin/electron-process-manager",
|
||||
"version": "2.0.1",
|
||||
"description": "Process manager UI for Electron applications - Fork with support for @electron/remote",
|
||||
"main": "src/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"bluebird": "^3.7.1",
|
||||
"bluebird-extra": "^2.0.0",
|
||||
"electron": "^10.4.7",
|
||||
"electron-default-menu": "1.0.1",
|
||||
"filesize": "^5.0.3",
|
||||
"format-number": "^3.0.0",
|
||||
"object-path": "^0.11.4",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"spectron": "^12.0.0",
|
||||
"webpack": "^2.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"electron": ">= 10"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-process-reporter": "^1.4.0"
|
||||
}
|
||||
}
|
||||
21
packages/electron-process-manager/process-manager.html
Normal file
21
packages/electron-process-manager/process-manager.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Process Manager</title>
|
||||
<link rel="stylesheet" href="vendor/photon.css">
|
||||
<style>
|
||||
.process-table {
|
||||
/*margin: 10px;*/
|
||||
}
|
||||
.process-table-container {
|
||||
overflow-x: scroll;
|
||||
flex: 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="window"></div>
|
||||
<script src="dist/ui-bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
64
packages/electron-process-manager/src/ProcessManager.js
Normal file
64
packages/electron-process-manager/src/ProcessManager.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { EventEmitter } = require('events');
|
||||
const process = require('process');
|
||||
const { webContents } = require('electron');
|
||||
|
||||
const ProcessManagerWindow = require('./ProcessManagerWindow.js');
|
||||
|
||||
const defaultOptions = { defaultSorting: { path: null, how: null } };
|
||||
|
||||
class ProcessManager extends EventEmitter {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// legacy
|
||||
this.openProcessManager = this.open.bind(this);
|
||||
}
|
||||
|
||||
// in case this isn't already done in the app.
|
||||
//
|
||||
// No longer needed because caller should setup electron/remote
|
||||
//
|
||||
// initializeElectronRemote() {
|
||||
// return require('@electron/remote/main').initialize();
|
||||
// }
|
||||
|
||||
// We pass the electron/remote/main instance to the manager to ensure it's
|
||||
// using the same as the main application.
|
||||
//
|
||||
// When using a peer dependency it seems the package ends up using its own
|
||||
// instance, which doesn't work.
|
||||
open(electronRemote, options = defaultOptions) {
|
||||
if (this.window) {
|
||||
this.window.focus();
|
||||
}
|
||||
|
||||
this.window = new ProcessManagerWindow(electronRemote);
|
||||
this.window.defaultSorting = options.defaultSorting || {};
|
||||
this.window.showWhenReady();
|
||||
this.window.on('kill-process', pid => this.killProcess(pid));
|
||||
this.window.on('open-dev-tools', webContentsId => this.openDevTools(webContentsId));
|
||||
this.window.on('closed', () => this.window = null);
|
||||
this.emit('open-window', this.window);
|
||||
|
||||
return this.window;
|
||||
}
|
||||
|
||||
killProcess(pid) {
|
||||
this.emit('will-kill-process', pid, this.window);
|
||||
process.kill(pid);
|
||||
this.emit('killed-process', pid, this.window);
|
||||
}
|
||||
|
||||
openDevTools(webContentsId) {
|
||||
this.emit('will-open-dev-tools', webContentsId, this.window);
|
||||
|
||||
const wc = webContents.fromId(webContentsId);
|
||||
wc.openDevTools({ mode: 'detach' });
|
||||
|
||||
this.emit('did-open-dev-tools', webContentsId, this.window);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ProcessManager;
|
||||
@@ -0,0 +1,78 @@
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const { onExtendedProcessMetrics } = require('electron-process-reporter');
|
||||
|
||||
class ProcessManagerWindow extends BrowserWindow {
|
||||
|
||||
constructor(electronRemote, options) {
|
||||
const winOptions = Object.assign({
|
||||
width: 800,
|
||||
height: 300,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
webviewTag: true,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
}, options || {});
|
||||
|
||||
super(winOptions);
|
||||
this.options = options;
|
||||
|
||||
this.attachProcessReporter();
|
||||
|
||||
const indexHtml = `file://${path.join(__dirname, '..', 'process-manager.html')}`;
|
||||
this.loadURL(indexHtml);
|
||||
|
||||
console.info('IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', electronRemote);
|
||||
electronRemote.enable(this.webContents);
|
||||
|
||||
setTimeout(() => {
|
||||
this.openDevTools();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
showWhenReady() {
|
||||
this.once('ready-to-show', () => {
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
sendStatsReport(reportData) {
|
||||
if (!this.webContents) return;
|
||||
this.webContents.send('process-manager:data', reportData);
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
this.webContents.openDevTools();
|
||||
}
|
||||
|
||||
attachProcessReporter() {
|
||||
this.subscription = onExtendedProcessMetrics(app)
|
||||
.subscribe(report => this.sendStatsReport(report));
|
||||
ipcMain.on('process-manager:kill-process', (e, pid) => {
|
||||
// ignore if not for us
|
||||
if (!this || this.isDestroyed()) return;
|
||||
if (e.sender !== this.webContents) return;
|
||||
|
||||
this.emit('kill-process', pid);
|
||||
});
|
||||
ipcMain.on('process-manager:open-dev-tools', (e, webContentsId) => {
|
||||
// ignore if not for us
|
||||
if (!this || this.isDestroyed()) return;
|
||||
if (e.sender !== this.webContents) return;
|
||||
|
||||
|
||||
this.emit('open-dev-tools', webContentsId);
|
||||
|
||||
});
|
||||
this.on('closed', () => {
|
||||
if (this.subscription) this.subscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProcessManagerWindow;
|
||||
4
packages/electron-process-manager/src/index.js
Normal file
4
packages/electron-process-manager/src/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const ProcessManager = require('./ProcessManager.js');
|
||||
|
||||
// singleton
|
||||
module.exports = new ProcessManager();
|
||||
106
packages/electron-process-manager/src/ui/ProcessManager.js
Normal file
106
packages/electron-process-manager/src/ui/ProcessManager.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import objectPath from 'object-path';
|
||||
import ProcessTable from './ProcessTable';
|
||||
import ToolBar from './ToolBar';
|
||||
|
||||
export default class ProcessManager extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
processData: null,
|
||||
selectedPid: null,
|
||||
sorting: {
|
||||
path: null,
|
||||
how: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
// TODO: disabled for now - the remote package would need to be passed to this script somehow.
|
||||
//
|
||||
// this.setState({ sorting: remote.getCurrentWindow().defaultSorting });
|
||||
ipcRenderer.on('process-manager:data', (_, data) => {
|
||||
this.setState({ processData: data });
|
||||
})
|
||||
}
|
||||
|
||||
canKill() {
|
||||
if (!this.state.selectedPid) return false;
|
||||
const pids = this.state.processData.map(p => p.pid);
|
||||
|
||||
// verify that select pid is in list of processes
|
||||
return pids.indexOf(this.state.selectedPid) !== -1;
|
||||
}
|
||||
|
||||
canOpenDevTool() {
|
||||
return this.canKill() && this.getWebContentsIdForSelectedProcess() !== null;
|
||||
}
|
||||
|
||||
getWebContentsIdForSelectedProcess() {
|
||||
const { processData, selectedPid } = this.state;
|
||||
if (!selectedPid) return null;
|
||||
|
||||
const process = processData.find(p => p.pid === selectedPid);
|
||||
if (!process || !process.webContents || process.webContents.length === 0) return null;
|
||||
|
||||
return process.webContents[0].id;
|
||||
}
|
||||
|
||||
handleKillProcess() {
|
||||
const pid = this.state.selectedPid;
|
||||
if (!pid) return;
|
||||
ipcRenderer.send('process-manager:kill-process', pid);
|
||||
}
|
||||
|
||||
handleOpenDevTool() {
|
||||
const webContentsId = this.getWebContentsIdForSelectedProcess();
|
||||
ipcRenderer.send('process-manager:open-dev-tools', webContentsId);
|
||||
}
|
||||
|
||||
getProcessData() {
|
||||
const { processData, sorting } = this.state;
|
||||
|
||||
if (!sorting.path || !sorting.how) return processData;
|
||||
|
||||
return processData.sort((p1, p2) => {
|
||||
const p1Metric = objectPath.get(p1, sorting.path);
|
||||
const p2Metric = objectPath.get(p2, sorting.path);
|
||||
|
||||
if (p1Metric === p2Metric) return 0;
|
||||
const comp = p1Metric < p2Metric ? -1 : 1;
|
||||
|
||||
return sorting.how == 'ascending' ? comp : -comp;
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const { processData } = this.state;
|
||||
if (!processData) return (<span>No data</span>);
|
||||
|
||||
return (
|
||||
<div className="window">
|
||||
<header className="toolbar toolbar-header">
|
||||
<ToolBar
|
||||
disableKill={!this.canKill()}
|
||||
onKillClick={this.handleKillProcess.bind(this)}
|
||||
disabelOpenDevTool={!this.canOpenDevTool()}
|
||||
onOpenDevToolClick={this.handleOpenDevTool.bind(this)}
|
||||
|
||||
/>
|
||||
</header>
|
||||
<div className="process-table-container">
|
||||
<ProcessTable
|
||||
processData={this.getProcessData()}
|
||||
selectedPid={this.state.selectedPid}
|
||||
sorting={this.state.sorting}
|
||||
onSortingChange={sorting => this.setState({ sorting })}
|
||||
onSelectedPidChange={pid => this.setState({ selectedPid: pid })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
75
packages/electron-process-manager/src/ui/ProcessRow.js
Normal file
75
packages/electron-process-manager/src/ui/ProcessRow.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import filesize from 'filesize';
|
||||
import format from 'format-number';
|
||||
|
||||
const KB = 1024;
|
||||
const formatPercentage = format({
|
||||
round: 1,
|
||||
padRight: 1
|
||||
});
|
||||
|
||||
export default class ProcessRow extends React.Component {
|
||||
static propTypes = {
|
||||
pid: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
memory: PropTypes.shape({
|
||||
peakWorkingSetSize: PropTypes.number,
|
||||
workingSetSize: PropTypes.number
|
||||
}),
|
||||
cpu: PropTypes.shape({
|
||||
percentCPUUsage: PropTypes.number,
|
||||
idleWakeupsPerSecond: PropTypes.number
|
||||
}),
|
||||
webContents: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
type: PropTypes.string,
|
||||
URL: PropTypes.string,
|
||||
URLDomain: PropTypes.string
|
||||
})),
|
||||
selected: PropTypes.bool,
|
||||
onSelect: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
const { webContents, memory } = this.props;
|
||||
if (!webContents || webContents.length === 0) {
|
||||
return (
|
||||
<tr
|
||||
className={this.props.selected ? 'selected': ''}
|
||||
onClick={this.props.onSelect}
|
||||
>
|
||||
<td>{this.props.pid}</td>
|
||||
<td></td>
|
||||
<td>{this.props.type}</td>
|
||||
<td>{memory ? filesize(memory.workingSetSize*KB) : 'N/A'}</td>
|
||||
<td>{formatPercentage(this.props.cpu.percentCPUUsage)}</td>
|
||||
<td>{this.props.cpu.idleWakeupsPerSecond}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
)
|
||||
} else {
|
||||
// FIX ME: we consider we have only have 1 webContents per process
|
||||
const wc = webContents[0];
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={this.props.selected ? 'selected': ''}
|
||||
onClick={this.props.onSelect}
|
||||
>
|
||||
<td>{this.props.pid}</td>
|
||||
<td>{wc.URLDomain}</td>
|
||||
<td>{this.props.type}</td>
|
||||
<td>{memory ? filesize(memory.workingSetSize*KB) : 'N/A'}</td>
|
||||
<td>{formatPercentage(this.props.cpu.percentCPUUsage)}</td>
|
||||
<td>{this.props.cpu.idleWakeupsPerSecond}</td>
|
||||
<td>{wc.id}</td>
|
||||
<td>{wc.type}</td>
|
||||
<td>{wc.URL}</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
packages/electron-process-manager/src/ui/ProcessTable.js
Normal file
94
packages/electron-process-manager/src/ui/ProcessTable.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ProcessRow from './ProcessRow';
|
||||
import ProcessTableHeader from './ProcessTableHeader';
|
||||
|
||||
export default class ProcessTable extends React.Component {
|
||||
static propTypes = {
|
||||
processData: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedPid: PropTypes.number,
|
||||
sorting: PropTypes.PropTypes.shape({
|
||||
path: PropTypes.string,
|
||||
how: PropTypes.string
|
||||
}),
|
||||
onSortingChange: PropTypes.func,
|
||||
onSelectedPidChange: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="process-table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<ProcessTableHeader
|
||||
path='pid'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>Pid</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='webContents.0.URLDomain'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>WebContents Domain</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='webContents.0.type'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>Process Type</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='memory.workingSetSize'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>Working Set Size</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='cpu.percentCPUUsage'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>% CPU</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='cpu.idleWakeupsPerSecond'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>Idle Wake Ups /s</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='webContents.0.id'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>WebContents Id</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='webContents.0.type'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>WebContents Type</ProcessTableHeader>
|
||||
|
||||
<ProcessTableHeader
|
||||
path='webContents.0.URL'
|
||||
sorting={this.props.sorting}
|
||||
onSortingChange={this.props.onSortingChange}
|
||||
>WebContents URL</ProcessTableHeader>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
this.props.processData.map(p =>
|
||||
<ProcessRow
|
||||
key={p.pid}
|
||||
{...p}
|
||||
onSelect={() => this.props.onSelectedPidChange(p.pid)}
|
||||
selected={this.props.selectedPid === p.pid}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
export default class ProcessTableHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
path: PropTypes.string.isRequired,
|
||||
sorting: PropTypes.PropTypes.shape({
|
||||
path: PropTypes.string,
|
||||
how: PropTypes.string
|
||||
}),
|
||||
onSortingChange: PropTypes.func
|
||||
}
|
||||
|
||||
getSortCharacter() {
|
||||
if (!this.sortHow) return (
|
||||
<span> </span>
|
||||
);
|
||||
return this.sortHow == 'ascending' ? '👆' : '👇'
|
||||
}
|
||||
|
||||
get sortHow() {
|
||||
if (!this.props.sorting) return null;
|
||||
|
||||
if (this.props.sorting.path == this.props.path){
|
||||
return this.props.sorting.how;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
let nextSortHow = null;
|
||||
if(this.sortHow === null) {
|
||||
nextSortHow = 'ascending';
|
||||
} else if (this.sortHow === 'ascending') {
|
||||
nextSortHow = 'descending';
|
||||
} else {
|
||||
nextSortHow = null;
|
||||
}
|
||||
this.props.onSortingChange({
|
||||
path: this.props.path,
|
||||
how: nextSortHow
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<th onClick={this.handleClick}>
|
||||
{this.props.children}
|
||||
|
||||
{this.getSortCharacter()}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
}
|
||||
35
packages/electron-process-manager/src/ui/ToolBar.js
Normal file
35
packages/electron-process-manager/src/ui/ToolBar.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class ToolBar extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
onKillClick: PropTypes.func,
|
||||
disableKill: PropTypes.bool,
|
||||
onOpenDevToolClick: PropTypes.func,
|
||||
disabelOpenDevTool: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="toolbar-actions">
|
||||
<div className="btn-group">
|
||||
<button
|
||||
className="btn btn-default"
|
||||
disabled={this.props.disableKill}
|
||||
onClick={this.props.onKillClick}
|
||||
>
|
||||
End process
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-default"
|
||||
disabled={this.props.disabelOpenDevTool}
|
||||
onClick={this.props.onOpenDevToolClick}
|
||||
>
|
||||
Open Dev Tool
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
6
packages/electron-process-manager/src/ui/index.js
Normal file
6
packages/electron-process-manager/src/ui/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import ProcessManager from './ProcessManager';
|
||||
|
||||
render(<ProcessManager/>, document.getElementById('app'));
|
||||
30
packages/electron-process-manager/tests/test.js
Normal file
30
packages/electron-process-manager/tests/test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const Application = require('spectron').Application;
|
||||
const { join } = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const app = new Application({
|
||||
env: { TEST_PROCESS_MANAGER: 1 },
|
||||
path: require(join(__dirname, '../node_modules/electron')),
|
||||
args: [join(__dirname, '../example/main.js')],
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await app.start();
|
||||
await app.client.waitUntilWindowLoaded();
|
||||
await app.electron.ipcRenderer.send('open-process-manager');
|
||||
// This looks to be incorrect signature for assert.
|
||||
// assert(app.client.getWindowCount(), 2);
|
||||
// There are 2 webviews on the index page. They are included in windowCount, so it's 4, not 2.
|
||||
assert.equal(await app.client.getWindowCount(), 4);
|
||||
await app.client.switchWindow(/process-manager\.html/);
|
||||
await (await app.client.$('#app .process-table')).waitForDisplayed({ timeout: 60000 });
|
||||
await app.stop();
|
||||
} catch (error) {
|
||||
console.error('Test failed', error);
|
||||
if (app && app.isRunning()) {
|
||||
await app.stop();
|
||||
process.exit(1);
|
||||
} else { process.exit(1); }
|
||||
}
|
||||
})();
|
||||
2341
packages/electron-process-manager/vendor/photon.css
vendored
Executable file
2341
packages/electron-process-manager/vendor/photon.css
vendored
Executable file
File diff suppressed because it is too large
Load Diff
27
packages/electron-process-manager/webpack.config.js
Normal file
27
packages/electron-process-manager/webpack.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
const BUILD_DIR = path.resolve(__dirname, 'dist');
|
||||
|
||||
const config = {
|
||||
entry: path.resolve(__dirname, 'src/ui/index.js'),
|
||||
// mode: 'development',
|
||||
devtool: 'eval-source-map',
|
||||
output: {
|
||||
path: BUILD_DIR,
|
||||
filename: 'ui-bundle.js',
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.(jsx|js)?$/,
|
||||
loader: 'babel-loader',
|
||||
include: path.resolve(__dirname, 'src/ui'),
|
||||
},
|
||||
],
|
||||
},
|
||||
target: 'electron-renderer',
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,5 +1,3 @@
|
||||
export { ModelType } from '../../../BaseModel';
|
||||
|
||||
// =================================================================
|
||||
// Command API types
|
||||
// =================================================================
|
||||
@@ -204,6 +202,25 @@ export interface Disposable {
|
||||
// dispose():void;
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
Note = 1,
|
||||
Folder = 2,
|
||||
Setting = 3,
|
||||
Resource = 4,
|
||||
Tag = 5,
|
||||
NoteTag = 6,
|
||||
Search = 7,
|
||||
Alarm = 8,
|
||||
MasterKey = 9,
|
||||
ItemChange = 10,
|
||||
NoteResource = 11,
|
||||
ResourceLocalState = 12,
|
||||
Revision = 13,
|
||||
Migration = 14,
|
||||
SmartFilter = 15,
|
||||
Command = 16,
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Menu types
|
||||
// =================================================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.3",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
@@ -34,4 +34,4 @@
|
||||
"repository": "https://github.com/laurent22/generator-joplin",
|
||||
"license": "MIT",
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
git pull
|
||||
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "There are changes in the repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CLI_DIR="$SCRIPT_DIR/../app-cli"
|
||||
LIB_DIR="$SCRIPT_DIR/../lib"
|
||||
@@ -19,11 +12,3 @@ rsync -a --delete "$LIB_DIR/plugin_types/services/plugins/api/" "$SCRIPT_DIR/gen
|
||||
cp "$LIB_DIR/services/plugins/api/types.ts" "$SCRIPT_DIR/generators/app/templates/api/"
|
||||
cp "$SCRIPT_DIR/generators/app/templates/api_index.ts" "$SCRIPT_DIR/generators/app/templates/api/index.ts"
|
||||
rm -f "$SCRIPT_DIR/generators/app/templates/api/types.d.ts"
|
||||
|
||||
yarn link
|
||||
|
||||
"$CLI_DIR/tests/support/plugins/updatePlugins.sh"
|
||||
|
||||
git add -A
|
||||
git c -m "Plugins: Updated types"
|
||||
git push
|
||||
|
||||
22
packages/generator-joplin/updateTypesAndDemos.sh
Executable file
22
packages/generator-joplin/updateTypesAndDemos.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
git pull
|
||||
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "There are changes in the repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CLI_DIR="$SCRIPT_DIR/../app-cli"
|
||||
LIB_DIR="$SCRIPT_DIR/../lib"
|
||||
|
||||
./updateTypes.sh
|
||||
|
||||
yarn link
|
||||
|
||||
"$CLI_DIR/tests/support/plugins/updatePlugins.sh"
|
||||
|
||||
git add -A
|
||||
git c -m "Plugins: Updated types"
|
||||
git push
|
||||
@@ -163,6 +163,7 @@ describe('services_CommandService', function() {
|
||||
it('should create menu items from commands', (async () => {
|
||||
const service = newService();
|
||||
const utils = new MenuUtils(service);
|
||||
const locale = 'fr_FR';
|
||||
|
||||
registerCommand(service, createCommand('test1', {
|
||||
execute: () => {},
|
||||
@@ -178,7 +179,7 @@ describe('services_CommandService', function() {
|
||||
clickedCommands.push(commandName);
|
||||
};
|
||||
|
||||
const menuItems = utils.commandsToMenuItems(['test1', 'test2'], onClick);
|
||||
const menuItems = utils.commandsToMenuItems(['test1', 'test2'], onClick, locale);
|
||||
|
||||
menuItems.test1.click();
|
||||
menuItems.test2.click();
|
||||
@@ -186,7 +187,10 @@ describe('services_CommandService', function() {
|
||||
expect(clickedCommands.join('_')).toBe('test1_test2');
|
||||
|
||||
// Also check that the same commands always return strictly the same menu
|
||||
expect(utils.commandsToMenuItems(['test1', 'test2'], onClick)).toBe(utils.commandsToMenuItems(['test1', 'test2'], onClick));
|
||||
expect(utils.commandsToMenuItems(['test1', 'test2'], onClick, locale)).toBe(utils.commandsToMenuItems(['test1', 'test2'], onClick, locale));
|
||||
|
||||
// And check that if the locale changes, new menu items are returned
|
||||
expect(utils.commandsToMenuItems(['test1', 'test2'], onClick, locale)).not.toBe(utils.commandsToMenuItems(['test1', 'test2'], onClick, 'en_GB'));
|
||||
}));
|
||||
|
||||
it('should give menu item props from state', (async () => {
|
||||
|
||||
@@ -97,8 +97,8 @@ export default class MenuUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public commandsToMenuItems(commandNames: string[], onClick: Function): MenuItems {
|
||||
const key: string = `${this.keymapService.lastSaveTime}_${commandNames.join('_')}`;
|
||||
public commandsToMenuItems(commandNames: string[], onClick: Function, locale: string): MenuItems {
|
||||
const key: string = `${this.keymapService.lastSaveTime}_${commandNames.join('_')}_${locale}`;
|
||||
if (this.menuItemCache_[key]) return this.menuItemCache_[key];
|
||||
|
||||
const output: MenuItems = {};
|
||||
@@ -107,7 +107,9 @@ export default class MenuUtils {
|
||||
output[commandName] = this.commandToMenuItem(commandName, onClick);
|
||||
}
|
||||
|
||||
this.menuItemCache_[key] = output;
|
||||
this.menuItemCache_ = {
|
||||
[key]: output,
|
||||
};
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,13 @@ interface SyncStartEvent {
|
||||
withErrors: boolean;
|
||||
}
|
||||
|
||||
interface ResourceChangeEvent {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ItemChangeHandler = (event: ItemChangeEvent)=> void;
|
||||
type SyncStartHandler = (event: SyncStartEvent)=> void;
|
||||
type ResourceChangeHandler = (event: ResourceChangeEvent)=> void;
|
||||
|
||||
/**
|
||||
* The workspace service provides access to all the parts of Joplin that
|
||||
@@ -54,7 +59,7 @@ export default class JoplinWorkspace {
|
||||
|
||||
private store: any;
|
||||
|
||||
constructor(store: any) {
|
||||
public constructor(store: any) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
@@ -98,6 +103,14 @@ export default class JoplinWorkspace {
|
||||
return makeListener(eventManager, 'itemChange', wrapperHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a resource is changed. Currently this handled will not be
|
||||
* called when a resource is added or deleted.
|
||||
*/
|
||||
public async onResourceChange(handler: ResourceChangeHandler): Promise<void> {
|
||||
makeListener(eventManager, 'resourceChange', handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an alarm associated with a to-do is triggered.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export { ModelType } from '../../../BaseModel';
|
||||
|
||||
// =================================================================
|
||||
// Command API types
|
||||
// =================================================================
|
||||
@@ -204,6 +202,25 @@ export interface Disposable {
|
||||
// dispose():void;
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
Note = 1,
|
||||
Folder = 2,
|
||||
Setting = 3,
|
||||
Resource = 4,
|
||||
Tag = 5,
|
||||
NoteTag = 6,
|
||||
Search = 7,
|
||||
Alarm = 8,
|
||||
MasterKey = 9,
|
||||
ItemChange = 10,
|
||||
NoteResource = 11,
|
||||
ResourceLocalState = 12,
|
||||
Revision = 13,
|
||||
Migration = 14,
|
||||
SmartFilter = 15,
|
||||
Command = 16,
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Menu types
|
||||
// =================================================================
|
||||
@@ -280,7 +297,7 @@ export interface MenuItem {
|
||||
/**
|
||||
* Set to "separator" to create a divider line
|
||||
*/
|
||||
type?: string;
|
||||
type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio');
|
||||
|
||||
/**
|
||||
* Accelerator associated with the menu item
|
||||
|
||||
@@ -15,7 +15,6 @@ const input: Theme = {
|
||||
colorWarn: 'rgb(228,86,0)',
|
||||
colorWarnUrl: '#155BDA',
|
||||
colorFaded: '#7C8B9E', // For less important text
|
||||
colorBright: '#000000', // For important text
|
||||
dividerColor: '#dddddd',
|
||||
selectedColor: '#e5e5e5',
|
||||
urlColor: '#155BDA',
|
||||
@@ -69,7 +68,6 @@ const expected = `
|
||||
--joplin-color-warn: rgb(228,86,0);
|
||||
--joplin-color-warn-url: #155BDA;
|
||||
--joplin-color-faded: #7C8B9E;
|
||||
--joplin-color-bright: #000000;
|
||||
--joplin-divider-color: #dddddd;
|
||||
--joplin-selected-color: #e5e5e5;
|
||||
--joplin-url-color: #155BDA;
|
||||
|
||||
@@ -11,7 +11,6 @@ const theme: Theme = {
|
||||
colorError: '#9a2f2f',
|
||||
colorWarn: '#d66500',
|
||||
colorFaded: '#666a73', // For less important text (e.g. not selected menu in settings)
|
||||
colorBright: '#d3dae3', // For important text; (e.g. bold)
|
||||
dividerColor: '#141a21', // Borders, I wish I could remove them
|
||||
selectedColor: '#2b5278', // Selected note
|
||||
urlColor: '#356693', // Links to external sites (e.g. in settings)
|
||||
|
||||
@@ -18,7 +18,6 @@ const theme: Theme = {
|
||||
colorWarn: '#9A5B00',
|
||||
colorWarnUrl: '#ffff82',
|
||||
colorFaded: '#999999', // For less important text
|
||||
colorBright: '#ffffff', // For important text
|
||||
dividerColor: '#555555',
|
||||
selectedColor: '#616161',
|
||||
urlColor: 'rgb(166,166,255)',
|
||||
|
||||
@@ -11,7 +11,6 @@ const theme: Theme = {
|
||||
colorError: '#ff5555',
|
||||
colorWarn: '#ffb86c',
|
||||
colorFaded: '#6272a4', // For less important text;
|
||||
colorBright: '#50fa7b', // For important text;
|
||||
dividerColor: '#bd93f9',
|
||||
selectedColor: '#44475a',
|
||||
urlColor: '#8be9fd',
|
||||
|
||||
@@ -15,7 +15,6 @@ const theme: Theme = {
|
||||
colorWarn: 'rgb(228,86,0)',
|
||||
colorWarnUrl: '#155BDA',
|
||||
colorFaded: '#7C8B9E', // For less important text
|
||||
colorBright: '#000000', // For important text
|
||||
dividerColor: '#dddddd',
|
||||
selectedColor: '#e5e5e5',
|
||||
urlColor: '#155BDA',
|
||||
|
||||
@@ -57,7 +57,6 @@ const theme: Theme = {
|
||||
colorError: nord[11],
|
||||
colorWarn: nord[12],
|
||||
colorFaded: nord[4], // For less important text;
|
||||
colorBright: nord[6], // For important text;
|
||||
dividerColor: nord[10],
|
||||
selectedColor: nord[9],
|
||||
urlColor: nord[8],
|
||||
|
||||
@@ -16,7 +16,6 @@ const theme: Theme = {
|
||||
codeBackgroundColor: 'rgb(47, 48, 49)',
|
||||
codeBorderColor: 'rgb(70, 70, 70)',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
colorBright: 'rgb(220,220,220)',
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
||||
@@ -11,7 +11,6 @@ const theme: Theme = {
|
||||
colorError: '#dc322f',
|
||||
colorWarn: '#cb4b16',
|
||||
colorFaded: '#657b83', // For less important text;
|
||||
colorBright: '#eee8d5', // For important text;
|
||||
dividerColor: '#586e75',
|
||||
selectedColor: '#073642',
|
||||
urlColor: '#268bd2',
|
||||
|
||||
@@ -11,7 +11,6 @@ const theme: Theme = {
|
||||
colorError: '#dc322f',
|
||||
colorWarn: '#cb4b16',
|
||||
colorFaded: '#839496', // For less important text;
|
||||
colorBright: '#073642', // For important text;
|
||||
dividerColor: '#eee8d5',
|
||||
selectedColor: '#eee8d5',
|
||||
urlColor: '#268bd2',
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface Theme {
|
||||
colorWarn: string;
|
||||
colorWarnUrl: string; // For URL displayed over a warningBackgroundColor
|
||||
colorFaded: string; // For less important text
|
||||
colorBright: string; // For important text
|
||||
dividerColor: string;
|
||||
selectedColor: string;
|
||||
urlColor: string;
|
||||
|
||||
@@ -4,7 +4,6 @@ module.exports = {
|
||||
lineHeight: '1.6em',
|
||||
backgroundColor: 'white',
|
||||
paddingBottom: 3,
|
||||
colorBright: '#000000', // For important text
|
||||
codeBorderColor: 'rgb(220, 220, 220)',
|
||||
codeBackgroundColor: 'rgb(243, 243, 243)',
|
||||
dividerColor: 'rgb(230,230,230)',
|
||||
|
||||
@@ -49,9 +49,6 @@ export default function(theme: any, options: Options = null) {
|
||||
padding-bottom: ${formatCssSize(theme.bodyPaddingBottom)};
|
||||
padding-top: ${formatCssSize(theme.bodyPaddingTop)};
|
||||
}
|
||||
strong {
|
||||
color: ${theme.colorBright};
|
||||
}
|
||||
kbd {
|
||||
border: 1px solid ${theme.codeBorderColor};
|
||||
box-shadow: inset 0 -1px 0 ${theme.codeBorderColor};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright (c) 2017-2021 Laurent Cozic
|
||||
|
||||
Personal Use License
|
||||
|
||||
Joplin Server is available for personal use only. For example you may host the software on your own server for non-commercial activity.
|
||||
|
||||
To obtain a license for commercial purposes, please contact us.
|
||||
282
packages/server/LICENSE.md
Normal file
282
packages/server/LICENSE.md
Normal file
@@ -0,0 +1,282 @@
|
||||
**JOPLIN SERVER PERSONAL USE LICENSE**
|
||||
|
||||
v.1: 21 December 2021
|
||||
|
||||
This Joplin Server Personal Use License (the "**License**") is a legally binding
|
||||
agreement between Cozic Ltd. registered under the laws of England and Wales
|
||||
(the "**Licensor**"), the owner of the server entitled ‘Joplin Server,’ the
|
||||
related software applications, and services (collectively, the "**Software**")
|
||||
and an individual user accessing the Software (the "**Licensee**") (each
|
||||
individually a "**Party**" and collectively, the "**Parties**").
|
||||
|
||||
# 1. Acceptance of the License
|
||||
|
||||
* **1.1** By accessing or otherwise using the Software, the Licensee confirms
|
||||
that the Licensee has read the License, accepts the terms of the License, and
|
||||
agrees to become legally bound by the License.
|
||||
|
||||
* **1.2** If the Licensee is entering into the License on behalf of a legal
|
||||
entity, the Licensee represents that the Licensee has the authority and the
|
||||
necessary capacity to bind such entity and its affiliates to the terms of the
|
||||
License.
|
||||
|
||||
* **1.3** If the Licensee does not have the authority specified in section 1.2
|
||||
or if the Licensee does not agree with one or more provisions of the License,
|
||||
the Licensee is not allowed to access the Software and the Licensee must not
|
||||
accept the License.
|
||||
|
||||
* **1.4** By using the Software, the Licensee acknowledges, agrees, and warrants
|
||||
that the Licensee:
|
||||
|
||||
* i. Shall comply with the terms of the License and all applicable local,
|
||||
state, national and foreign laws, treaties, and regulations;
|
||||
|
||||
* ii. Shall provide only true, accurate, complete, and up-to-date
|
||||
information; and
|
||||
|
||||
* iii. Has the capacity to conclude legally binding contracts with the
|
||||
Licensor.
|
||||
|
||||
# 2. Grant of RIGHTS
|
||||
|
||||
* **2.1** The Licensor hereby grants the Licensee a worldwide, non-exclusive,
|
||||
royalty free, and revocable license to access and use the Software in
|
||||
accordance with the terms of this License and the applicable laws.
|
||||
|
||||
* **2.2** Subject to the terms of the License, the Licensee is entitled to
|
||||
access and use the Software for non-commercial purposes for taking personal
|
||||
and professional notes, sharing notes with other users of the Software, and
|
||||
synchronising data.
|
||||
|
||||
* **2.3** Unless authorised by the Licensor in writing, the Software may be used
|
||||
for personal non-commercial purposes only. The Licensee is allowed to grant
|
||||
access to the Software to others for non-commercial purposes, provided that
|
||||
(i) the Licensee is not a business entity, (ii) the Licensee or the third
|
||||
party to which the access is granted does not use the Software to generate
|
||||
profits of any kind, and (iii) the Software is used for non-commercial
|
||||
purposes only. The Licensee is not allowed to: (i) use the Software for
|
||||
commercial purposes and (ii) grant others the right to use the Software for a
|
||||
fee or for any commercial purposes, including, without limitation, copying,
|
||||
reproducing, publishing, transmitting, transferring, selling, renting,
|
||||
modifying, creating derivative works from, distributing, reposting,
|
||||
performing, displaying, or in any other way commercially exploiting the
|
||||
Software without prior written authorisation from the Licensor. By way of
|
||||
illustration, the Licensee is not allowed to:
|
||||
|
||||
1. Install the Software on Licensee’s infrastructure and charge others for
|
||||
the use of the Software;
|
||||
|
||||
2. Install the Software behind a proxy and charge others for the use of the
|
||||
Software;
|
||||
|
||||
3. Install the Software on a Licensee’s or third party’s server and provide
|
||||
access to the Software to third parties (a) for a fee or (b) free of
|
||||
charge, if the said third parties may use the Software for commercial
|
||||
purposes;
|
||||
|
||||
4. Grant access to the Software to others for a fee or for any commercial
|
||||
purposes;
|
||||
|
||||
5. Grant access to the Software to others for a fee or free of charge if
|
||||
the Licensee is a legal or natural person engaged in commercial
|
||||
activities; or
|
||||
|
||||
6. Use the Software or grant other the right to use the Software in a way
|
||||
that generates income or commercial profits.
|
||||
|
||||
* **2.4** This License does not govern any commercial use of the Software, as
|
||||
authorised by the Licensor.
|
||||
|
||||
* **2.5** The Software is licensed and not sold. By accepting the License, the
|
||||
Licensee obtains the right to use the Software and not the ownership of the
|
||||
Software.
|
||||
|
||||
* **2.6** The Licensor reserves any rights not expressly granted to the
|
||||
Licensee under this License.
|
||||
|
||||
* **2.7** The Licensee is allowed to make a reasonable number of copies of the
|
||||
Software, as and if necessary for the purposes set forth herein, provided that
|
||||
only complete copies of the Software are made, including without limitation
|
||||
all ‘read me’ files, copyright notices, and other legal notices and terms
|
||||
included in the Software.
|
||||
|
||||
* **2.8** The Licensee is permitted to load and run the Software on any device,
|
||||
network or cloud virtual machines under Licensee’s control (collectively, the
|
||||
"**Devices**"), if such Devices are compatible with the Software. The Licensee
|
||||
is solely responsible for assessing the compatibility of the Devices to be
|
||||
used with the Software.
|
||||
|
||||
* **2.9** It is Licensee’s sole responsibility to verify and assess the
|
||||
suitability, validity and integrity of the Software prior to using it and to
|
||||
decide whether or not the Software fits for the intended use.
|
||||
|
||||
* **2.10** The Licensor reserves the right to grant the right to use the
|
||||
Software to third parties.
|
||||
|
||||
* **2.11** The Licensee acknowledges and agrees that any use of the Software
|
||||
that is prohibited by this License may be unlawful and may result in
|
||||
Licensee’s criminal liability.
|
||||
|
||||
# 3. Covered Software and services
|
||||
|
||||
* **3.1** The License applies only to the Software as provided to the Licensee
|
||||
by the Licensor. The License also applies to updates, supplements, and support
|
||||
services related to the Software, or any other services provided in relation
|
||||
to the Software, unless other terms and conditions have been provided thereto.
|
||||
|
||||
* **3.2** Any software or services that are not provided by the Licensor are
|
||||
not covered by this License. Such Software and services are subject to the
|
||||
terms and conditions set by the respective third party and the Licensee is
|
||||
solely responsible for obtaining, agreeing to, and complying with the
|
||||
respective terms and conditions at its own cost and expense.
|
||||
|
||||
* **3.3** The Licensor reserves the right, but is not under any obligation, to
|
||||
provide paid or free-of-charge updates and technical support services with
|
||||
regard to the Software, including fixing bugs and errors, and the possibility
|
||||
to use new versions of the Software.
|
||||
|
||||
# 4. Intellectual property and ownership
|
||||
|
||||
* **4.1** All title and copyright in and to the Software (including, but not
|
||||
limited to, any source code, images, graphics, photographs, animations, video,
|
||||
audio, music, text, and applets, incorporated in the Software) are owned by
|
||||
the Licensor. The Software is protected by the English copyright laws and
|
||||
international treaties. The Licensee is not allowed to incorporate any portion
|
||||
of the Software into other programs or compile any portion of it in
|
||||
combination with other programs, or otherwise copy (except to exercise rights
|
||||
granted in this License), modify, create derivative works of, distribute,
|
||||
assign any rights to, or license the Software in whole or in part.
|
||||
|
||||
* **4.2** The Licensee is not permitted to, without obtaining prior written
|
||||
authorisation from the Licensor, to use the trade names, trademarks, service
|
||||
marks or product names of the Licensor, except as required for the use of the
|
||||
Software.
|
||||
|
||||
* **4.3** The Licensor has made all efforts possible to avoid the Software
|
||||
being subject to the rights of third parties, in particular that its use does
|
||||
not infringe patents, copyrights or other intellectual property rights of
|
||||
third parties. However, the Licensor does not guarantee that the Software is
|
||||
not subject to the rights of third parties. The Licensee agrees to notify the
|
||||
Licensor immediately and in writing if any third party asserts an infringement
|
||||
claim against the Licensee in connection with the Software.
|
||||
|
||||
* **4.4** By submitting any content through the Software (the "Licensee’s
|
||||
Content"), the Licensee grants the Licensor unrestricted, sub-licensable,
|
||||
royalty-free, perpetual, and irrevocable rights to process the Licensee’s
|
||||
Content for the purposes of providing the Licensee with the Software and
|
||||
carrying out Licensor’s legitimate business interests.
|
||||
|
||||
* **4.5** The Licensee is not permitted to remove any copyright or other
|
||||
proprietary notices and legends.
|
||||
|
||||
# 5. PRIVACY AND DATA PROTECTION
|
||||
|
||||
* **5.1** The Licensor does not have access in any manner to the Licensee’s
|
||||
Content. Therefore, the Licensee is solely responsible for creating, keeping
|
||||
and maintaining any backup copies of any Licensee’s Content or other
|
||||
information submitted to, through, or in relation to the Software.
|
||||
|
||||
The Parties agree to individually comply with the applicable data protection
|
||||
laws pertaining to the Software.
|
||||
|
||||
# 6. AVAILABILITY
|
||||
|
||||
* **6.1** The availability of the Software may be affected by factors, which
|
||||
the Licensor cannot reasonably control, such as bandwidth problems, equipment
|
||||
failure, acts and omissions of our third-party service providers, or *force
|
||||
majeure* events. The Licensor takes no responsibility for the unavailability
|
||||
of the Software caused by such factors.
|
||||
|
||||
# 7. Limitation of liability and disclaimer of warranties
|
||||
|
||||
* **7.1** To the extent permitted by the applicable law, the Licensor expressly
|
||||
disclaims all warranties, express or implied, for the Software. The Licensor
|
||||
provides the Software on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
||||
ANY KIND, either express or implied, including, without limitation, any
|
||||
warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. The entire risk arising out of use or
|
||||
performance of the Software remains with the Licensee.
|
||||
|
||||
* **7.2** In no event shall the Licensor be liable for any damages whatsoever
|
||||
(including, without limitation, damages for loss of business profits, business
|
||||
interruption, loss of business information, or any other pecuniary loss)
|
||||
arising out of the Licensee’s Content, the use of or inability to use the
|
||||
Software, even if the Licensor has been advised of the possibility of such
|
||||
damages.
|
||||
|
||||
* **7.3** The Licensee is solely responsible for determining the
|
||||
appropriateness of the Software and assumes any risks associated with
|
||||
Licensee’s exercise of permissions under the License.
|
||||
|
||||
* **7.4** Third-party content or services are not covered by this License. The
|
||||
Licensee shall ensure Licensee’s compliance with any terms set forth by the
|
||||
respective third parties at its own risk, cost and expense. To the maximum
|
||||
extent permitted by law, the Licensor excludes any liability for any loss or
|
||||
damage resulting from the acts and omissions of such third-party service
|
||||
providers.
|
||||
|
||||
# 8. Governing Law AND DISPUTE RESOLUTION
|
||||
|
||||
* 8.1 This License and any disputes arising out of or in connection with the
|
||||
License and the Software shall be governed by and construed in accordance with
|
||||
the laws of England and Wales. Unless otherwise provided by the applicable
|
||||
law, all disputes arising out of or in connection with the License shall be
|
||||
submitted to the exclusive jurisdiction of the courts in London, the United
|
||||
Kingdom.
|
||||
|
||||
# 9. FINAL PROVISIONS
|
||||
|
||||
* **9.1** **Indemnification.** The Licensee shall indemnify the Licensor at
|
||||
Licensee’s expense if any claims are asserted by a third party against the
|
||||
Licensor by reason of Licensee’s misconduct or breach of any terms of the
|
||||
License, including failed adherence by the Licensee with any applicable laws,
|
||||
including, whether express or implied.
|
||||
|
||||
* **9.2** **Severability.** The unenforceability of any single provision of
|
||||
this License shall not affect any other provision hereof. Where such a
|
||||
provision is held to be unenforceable, the Parties shall use their best
|
||||
endeavours to negotiate and agree upon an enforceable provision, which
|
||||
achieves, to the greatest extent possible, the economic, legal and commercial
|
||||
objectives of the unenforceable provision.
|
||||
|
||||
* **9.3** **Waiver.** A failure of either Party to enforce strictly a provision
|
||||
of this License shall in no event be considered a waiver of any part of such
|
||||
provision. No waiver by either Party of any breach or default by the other
|
||||
party shall operate as a waiver of any succeeding breach or other default or
|
||||
breach by such other Party. No waiver shall have any effect unless it is
|
||||
specific, irrevocable and in writing.
|
||||
|
||||
* **9.4** **Term and termination.** The License shall commence upon Licensee’s
|
||||
access to the Software and continue until terminated by the Licensor. Upon
|
||||
termination of the License, the Licensee agrees to (i) stop all access and use
|
||||
of the Software and (ii) destroy all copies of the Software and all its
|
||||
component parts (if any) stored on the Devices. The provisions of the License
|
||||
that, by their nature, continue and survive will survive any termination of
|
||||
the License.
|
||||
|
||||
* **9.5** **Amendments.** The Licensor reserves the right, at its sole
|
||||
discretion, to change or modify this License at any time by sending a prior
|
||||
notification to the Licensee (if the contact details of the Licensee are
|
||||
available to the Licensor). Any modifications to the License shall become
|
||||
effective on the date indicated at the top of the amended License. By
|
||||
continuing to use the Software after the date on which the modifications were
|
||||
communicated, the Licensee agrees to be bound by the modified License. The
|
||||
Licensor reserves the right to change or discontinue the Software and any
|
||||
feature thereof with or without a prior notice to the Licensee.
|
||||
|
||||
* **9.6** **Entire agreement.** This License constitutes the entire
|
||||
understanding between the Parties with respect to the subject matter thereof
|
||||
and supersedes all prior agreements, negotiations and discussions between the
|
||||
Parties relating thereto.
|
||||
|
||||
* **9.7** **Transfer of rights.** The Licensee is not allowed to assign
|
||||
Licensee’s rights under the License. The Licensor is entitled to transfer its
|
||||
rights and obligations under the License entirely or partially to a third
|
||||
party by giving a prior notice to the Licensee. If the Licensee does not agree
|
||||
to the transfer, the Licensee can terminate this License by ceasing to use the
|
||||
Software.
|
||||
|
||||
* **9.8** **Contact.** For general enquiries and commercial licenses to use the
|
||||
Software, please contact the Licensor directly.
|
||||
|
||||
***
|
||||
@@ -157,10 +157,4 @@ From `packages/server`, run `npm run start-dev`
|
||||
|
||||
# License
|
||||
|
||||
Copyright (c) 2017-2021 Laurent Cozic
|
||||
|
||||
Personal Use License
|
||||
|
||||
Joplin Server is available for personal use only. For example you may host the software on your own server for non-commercial activity.
|
||||
|
||||
To obtain a license for commercial purposes, please contact us.
|
||||
See LICENSE.md in this directory
|
||||
|
||||
@@ -12,5 +12,8 @@ module.exports = {
|
||||
|
||||
slowTestThreshold: 60,
|
||||
|
||||
setupFilesAfterEnv: [`${__dirname}/jest.setup.js`],
|
||||
setupFilesAfterEnv: [
|
||||
'jest-expect-message',
|
||||
`${__dirname}/jest.setup.js`,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
@@ -47,6 +47,7 @@
|
||||
"nodemon": "^2.0.6",
|
||||
"pg": "^8.5.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"prettycron": "^0.10.0",
|
||||
"query-string": "^6.8.3",
|
||||
"rate-limiter-flexible": "^2.2.4",
|
||||
"raw-body": "^2.4.1",
|
||||
@@ -58,9 +59,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.7",
|
||||
"@rmp135/sql-ts": "^1.7.0",
|
||||
"@rmp135/sql-ts": "^1.12.1",
|
||||
"@types/fs-extra": "^8.0.0",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/jest-expect-message": "^1.0.3",
|
||||
"@types/jsdom": "^16.2.6",
|
||||
"@types/koa": "2.0.49",
|
||||
"@types/markdown-it": "^12.0.0",
|
||||
@@ -70,6 +72,7 @@
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"gulp": "^4.0.2",
|
||||
"jest": "^26.6.3",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"jsdom": "^16.4.0",
|
||||
"node-mocks-http": "^1.10.0",
|
||||
"source-map-support": "^0.5.13",
|
||||
|
||||
@@ -87,4 +87,9 @@ h4:hover a.heading-anchor,
|
||||
h5:hover a.heading-anchor,
|
||||
h6:hover a.heading-anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,74 +1,72 @@
|
||||
it('should pass', async function() {
|
||||
expect(true).toBe(true);
|
||||
import { afterAllTests, beforeAllDb, beforeEachDb, db } from './utils/testing/testUtils';
|
||||
import sqlts from '@rmp135/sql-ts';
|
||||
import { DbConnection, migrateDown, migrateUp, nextMigration } from './db';
|
||||
|
||||
async function dbSchemaSnapshot(db: DbConnection): Promise<any> {
|
||||
return sqlts.toTypeScript({}, db as any);
|
||||
}
|
||||
|
||||
describe('db', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('db', { autoMigrate: false });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
it('should allow upgrading and downgrading schema', async function() {
|
||||
// Migrations before that didn't have a down() step.
|
||||
const ignoreAllBefore = '20210819165350_user_flags';
|
||||
|
||||
// Some migrations produce no changes visible to sql-ts, in particular
|
||||
// when the migration only adds a constraint or an index. In this case
|
||||
// we skip the migration. Ideally we should test these too but for now
|
||||
// that will do.
|
||||
const doNoCheckUpgrade = [
|
||||
'20211030103016_item_owner_name_unique',
|
||||
'20211111134329_storage_index',
|
||||
];
|
||||
|
||||
let startProcessing = false;
|
||||
|
||||
while (true) {
|
||||
await migrateUp(db());
|
||||
|
||||
if (!startProcessing) {
|
||||
const next = await nextMigration(db());
|
||||
if (next === ignoreAllBefore) {
|
||||
startProcessing = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const next = await nextMigration(db());
|
||||
|
||||
if (!next) break;
|
||||
|
||||
const initialSchema = await dbSchemaSnapshot(db());
|
||||
|
||||
await migrateUp(db());
|
||||
|
||||
const afterUpgradeSchema = await dbSchemaSnapshot(db());
|
||||
|
||||
if (!doNoCheckUpgrade.includes(next)) {
|
||||
expect(initialSchema, `Schema upgrade did not produce a new schema. In migration: ${next}`).not.toEqual(afterUpgradeSchema);
|
||||
}
|
||||
|
||||
await migrateDown(db());
|
||||
|
||||
const afterRollbackSchema = await dbSchemaSnapshot(db());
|
||||
|
||||
expect(initialSchema, `Schema rollback did not produce previous schema. In migration: ${next}`).toEqual(afterRollbackSchema);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// import { afterAllTests, beforeAllDb, beforeEachDb, db } from "./utils/testing/testUtils";
|
||||
// import sqlts from '@rmp135/sql-ts';
|
||||
// import config from "./config";
|
||||
// import { connectDb, DbConnection, disconnectDb, migrateDown, migrateList, migrateUp, nextMigration } from "./services/database/types";
|
||||
|
||||
// async function dbSchemaSnapshot(db:DbConnection):Promise<any> {
|
||||
// return sqlts.toObject({
|
||||
// client: 'sqlite',
|
||||
// knex: db,
|
||||
// // 'connection': {
|
||||
// // 'filename': config().database.name,
|
||||
// // },
|
||||
// useNullAsDefault: true,
|
||||
// } as any)
|
||||
|
||||
// // return JSON.stringify(definitions);
|
||||
// }
|
||||
|
||||
// describe('db', function() {
|
||||
|
||||
// beforeAll(async () => {
|
||||
// await beforeAllDb('db', { autoMigrate: false });
|
||||
// });
|
||||
|
||||
// afterAll(async () => {
|
||||
// await afterAllTests();
|
||||
// });
|
||||
|
||||
// beforeEach(async () => {
|
||||
// await beforeEachDb();
|
||||
// });
|
||||
|
||||
// it('should allow downgrading schema', async function() {
|
||||
// const ignoreAllBefore = '20210819165350_user_flags';
|
||||
// let startProcessing = false;
|
||||
|
||||
// //console.info(await dbSchemaSnapshot());
|
||||
|
||||
// while (true) {
|
||||
// await migrateUp(db());
|
||||
|
||||
// if (!startProcessing) {
|
||||
// const next = await nextMigration(db());
|
||||
// if (next === ignoreAllBefore) {
|
||||
// startProcessing = true;
|
||||
// } else {
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!(await nextMigration(db()))) break;
|
||||
|
||||
// // await disconnectDb(db());
|
||||
// // const beforeSchema = await dbSchemaSnapshot(db());
|
||||
// // console.info(beforeSchema);
|
||||
// // await connectDb(db());
|
||||
|
||||
// // await migrateUp(db());
|
||||
// // await migrateDown(db());
|
||||
|
||||
// // const afterSchema = await dbSchemaSnapshot(db());
|
||||
|
||||
// // // console.info(beforeSchema);
|
||||
// // // console.info(afterSchema);
|
||||
|
||||
// // expect(beforeSchema).toEqual(afterSchema);
|
||||
// }
|
||||
// });
|
||||
|
||||
// });
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface DbConfigConnection {
|
||||
|
||||
export interface QueryContext {
|
||||
uniqueConstraintErrorLoggingDisabled?: boolean;
|
||||
noSuchTableErrorLoggingDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface KnexDatabaseConfig {
|
||||
@@ -227,6 +228,10 @@ export async function connectDb(dbConfig: DatabaseConfig): Promise<DbConnection>
|
||||
if (data.queryContext.uniqueConstraintErrorLoggingDisabled && isUniqueConstraintError(response)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.queryContext.noSuchTableErrorLoggingDisabled && isNoSuchTableError(response)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const msg: string[] = [];
|
||||
@@ -392,7 +397,8 @@ export function isUniqueConstraintError(error: any): boolean {
|
||||
|
||||
export async function latestMigration(db: DbConnection): Promise<Migration | null> {
|
||||
try {
|
||||
const result = await db('knex_migrations').select('name').orderBy('id', 'desc').first();
|
||||
const context: QueryContext = { noSuchTableErrorLoggingDisabled: true };
|
||||
const result = await db('knex_migrations').queryContext(context).select('name').orderBy('id', 'desc').first();
|
||||
return { name: result.name, done: true };
|
||||
} catch (error) {
|
||||
// If the database has never been initialized, we return null, so
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.createTable('user_deletions', (table: Knex.CreateTableBuilder) => {
|
||||
table.increments('id').unique().primary().notNullable();
|
||||
table.string('user_id', 32).notNullable();
|
||||
table.specificType('process_data', 'smallint').defaultTo(0).notNullable();
|
||||
table.specificType('process_account', 'smallint').defaultTo(0).notNullable();
|
||||
table.bigInteger('scheduled_time').notNullable();
|
||||
table.bigInteger('start_time').defaultTo(0).notNullable();
|
||||
table.bigInteger('end_time').defaultTo(0).notNullable();
|
||||
table.integer('success').defaultTo(0).notNullable();
|
||||
table.text('error', 'mediumtext').defaultTo('').notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.alterTable('user_deletions', (table: Knex.CreateTableBuilder) => {
|
||||
table.unique(['user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.dropTable('user_deletions');
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { Config } from '../utils/types';
|
||||
import personalizedUserContentBaseUrl from '@joplin/lib/services/joplinServer/personalizedUserContentBaseUrl';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import dbuuid from '../utils/dbuuid';
|
||||
import { defaultPagination, PaginatedResults, Pagination } from './utils/pagination';
|
||||
|
||||
const logger = Logger.create('BaseModel');
|
||||
|
||||
@@ -232,6 +233,28 @@ export default abstract class BaseModel<T> {
|
||||
return rows as T[];
|
||||
}
|
||||
|
||||
public async allPaginated(pagination: Pagination, options: LoadOptions = {}): Promise<PaginatedResults<T>> {
|
||||
pagination = {
|
||||
...defaultPagination(),
|
||||
...pagination,
|
||||
};
|
||||
|
||||
const itemCount = await this.count();
|
||||
|
||||
const items = await this
|
||||
.db(this.tableName)
|
||||
.select(this.selectFields(options))
|
||||
.orderBy(pagination.order[0].by, pagination.order[0].dir)
|
||||
.offset((pagination.page - 1) * pagination.limit)
|
||||
.limit(pagination.limit) as T[];
|
||||
|
||||
return {
|
||||
items,
|
||||
page_count: Math.ceil(itemCount / pagination.limit),
|
||||
has_more: items.length >= pagination.limit,
|
||||
};
|
||||
}
|
||||
|
||||
public async count(): Promise<number> {
|
||||
const r = await this
|
||||
.db(this.tableName)
|
||||
@@ -343,7 +366,7 @@ export default abstract class BaseModel<T> {
|
||||
return !!o;
|
||||
}
|
||||
|
||||
public async load(id: string, options: LoadOptions = {}): Promise<T> {
|
||||
public async load(id: Uuid | number, options: LoadOptions = {}): Promise<T> {
|
||||
if (!id) throw new Error('id cannot be empty');
|
||||
|
||||
return this.db(this.tableName).select(options.fields || this.defaultFields).where({ id: id }).first();
|
||||
|
||||
@@ -16,13 +16,9 @@ export interface DeltaChange extends Change {
|
||||
jop_updated_time?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedDeltaChanges extends PaginatedResults {
|
||||
items: DeltaChange[];
|
||||
}
|
||||
export type PaginatedDeltaChanges = PaginatedResults<DeltaChange>;
|
||||
|
||||
export interface PaginatedChanges extends PaginatedResults {
|
||||
items: Change[];
|
||||
}
|
||||
export type PaginatedChanges = PaginatedResults<Change>;
|
||||
|
||||
export interface ChangePagination {
|
||||
limit?: number;
|
||||
@@ -43,6 +39,15 @@ export function defaultDeltaPagination(): ChangePagination {
|
||||
};
|
||||
}
|
||||
|
||||
export function requestDeltaPagination(query: any): ChangePagination {
|
||||
if (!query) return defaultDeltaPagination();
|
||||
|
||||
const output: ChangePagination = {};
|
||||
if ('limit' in query) output.limit = query.limit;
|
||||
if ('cursor' in query) output.cursor = query.cursor;
|
||||
return output;
|
||||
}
|
||||
|
||||
export default class ChangeModel extends BaseModel<Change> {
|
||||
|
||||
public get tableName(): string {
|
||||
@@ -391,4 +396,12 @@ export default class ChangeModel extends BaseModel<Change> {
|
||||
return savedChange;
|
||||
}
|
||||
|
||||
public async deleteByItemIds(itemIds: Uuid[]) {
|
||||
if (!itemIds.length) return;
|
||||
|
||||
await this.db(this.tableName)
|
||||
.whereIn('item_id', itemIds)
|
||||
.delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, createItem, createItemTree, createResource, createNote, createFolder, createItemTree3, db, tempDir, expectNotThrow, expectHttpError } from '../utils/testing/testUtils';
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, createItemTree, createResource, createNote, createItemTree3, db, tempDir, expectNotThrow, expectHttpError } from '../utils/testing/testUtils';
|
||||
import { shareFolderWithUser } from '../utils/testing/shareApiUtils';
|
||||
import { resourceBlobPath } from '../utils/joplinUtils';
|
||||
import newModelFactory from './factory';
|
||||
@@ -23,64 +23,64 @@ describe('ItemModel', function() {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should find exclusively owned items 1', async function() {
|
||||
const { user: user1 } = await createUserAndSession(1, true);
|
||||
const { session: session2, user: user2 } = await createUserAndSession(2);
|
||||
// test('should find exclusively owned items 1', async function() {
|
||||
// const { user: user1 } = await createUserAndSession(1, true);
|
||||
// const { session: session2, user: user2 } = await createUserAndSession(2);
|
||||
|
||||
const tree: any = {
|
||||
'000000000000000000000000000000F1': {
|
||||
'00000000000000000000000000000001': null,
|
||||
},
|
||||
};
|
||||
// const tree: any = {
|
||||
// '000000000000000000000000000000F1': {
|
||||
// '00000000000000000000000000000001': null,
|
||||
// },
|
||||
// };
|
||||
|
||||
await createItemTree(user1.id, '', tree);
|
||||
await createItem(session2.id, 'root:/test.txt:', 'testing');
|
||||
// await createItemTree(user1.id, '', tree);
|
||||
// await createItem(session2.id, 'root:/test.txt:', 'testing');
|
||||
|
||||
{
|
||||
const itemIds = await models().item().exclusivelyOwnedItemIds(user1.id);
|
||||
expect(itemIds.length).toBe(2);
|
||||
// {
|
||||
// const itemIds = await models().item().exclusivelyOwnedItemIds(user1.id);
|
||||
// expect(itemIds.length).toBe(2);
|
||||
|
||||
const item1 = await models().item().load(itemIds[0]);
|
||||
const item2 = await models().item().load(itemIds[1]);
|
||||
// const item1 = await models().item().load(itemIds[0]);
|
||||
// const item2 = await models().item().load(itemIds[1]);
|
||||
|
||||
expect([item1.jop_id, item2.jop_id].sort()).toEqual(['000000000000000000000000000000F1', '00000000000000000000000000000001'].sort());
|
||||
}
|
||||
// expect([item1.jop_id, item2.jop_id].sort()).toEqual(['000000000000000000000000000000F1', '00000000000000000000000000000001'].sort());
|
||||
// }
|
||||
|
||||
{
|
||||
const itemIds = await models().item().exclusivelyOwnedItemIds(user2.id);
|
||||
expect(itemIds.length).toBe(1);
|
||||
}
|
||||
});
|
||||
// {
|
||||
// const itemIds = await models().item().exclusivelyOwnedItemIds(user2.id);
|
||||
// expect(itemIds.length).toBe(1);
|
||||
// }
|
||||
// });
|
||||
|
||||
test('should find exclusively owned items 2', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1, true);
|
||||
const { session: session2, user: user2 } = await createUserAndSession(2);
|
||||
// test('should find exclusively owned items 2', async function() {
|
||||
// const { session: session1, user: user1 } = await createUserAndSession(1, true);
|
||||
// const { session: session2, user: user2 } = await createUserAndSession(2);
|
||||
|
||||
await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
|
||||
'000000000000000000000000000000F1': {
|
||||
'00000000000000000000000000000001': null,
|
||||
},
|
||||
});
|
||||
// await shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', {
|
||||
// '000000000000000000000000000000F1': {
|
||||
// '00000000000000000000000000000001': null,
|
||||
// },
|
||||
// });
|
||||
|
||||
await createFolder(session2.id, { id: '000000000000000000000000000000F2' });
|
||||
// await createFolder(session2.id, { id: '000000000000000000000000000000F2' });
|
||||
|
||||
{
|
||||
const itemIds = await models().item().exclusivelyOwnedItemIds(user1.id);
|
||||
expect(itemIds.length).toBe(0);
|
||||
}
|
||||
// {
|
||||
// const itemIds = await models().item().exclusivelyOwnedItemIds(user1.id);
|
||||
// expect(itemIds.length).toBe(0);
|
||||
// }
|
||||
|
||||
{
|
||||
const itemIds = await models().item().exclusivelyOwnedItemIds(user2.id);
|
||||
expect(itemIds.length).toBe(1);
|
||||
}
|
||||
// {
|
||||
// const itemIds = await models().item().exclusivelyOwnedItemIds(user2.id);
|
||||
// expect(itemIds.length).toBe(1);
|
||||
// }
|
||||
|
||||
await models().user().delete(user2.id);
|
||||
// await models().user().delete(user2.id);
|
||||
|
||||
{
|
||||
const itemIds = await models().item().exclusivelyOwnedItemIds(user1.id);
|
||||
expect(itemIds.length).toBe(2);
|
||||
}
|
||||
});
|
||||
// {
|
||||
// const itemIds = await models().item().exclusivelyOwnedItemIds(user1.id);
|
||||
// expect(itemIds.length).toBe(2);
|
||||
// }
|
||||
// });
|
||||
|
||||
test('should find all items within a shared folder', async function() {
|
||||
const { user: user1, session: session1 } = await createUserAndSession(1);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user