You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-02 00:08:04 +02:00
Compare commits
4 Commits
smaller_se
...
plugin_tas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1b38b936 | ||
|
|
a11402db6d | ||
|
|
c19d533d93 | ||
|
|
1a4ff7054a |
@@ -1,19 +1,12 @@
|
||||
_mydocs/
|
||||
_releases/
|
||||
.git/
|
||||
.yarn/cache/
|
||||
**/.DS_Store
|
||||
**/node_modules
|
||||
Assets/
|
||||
docs/
|
||||
lerna-debug.log
|
||||
packages/app-cli/
|
||||
packages/app-clipper/
|
||||
packages/app-desktop/
|
||||
packages/app-mobile/
|
||||
packages/generator-joplin/
|
||||
packages/plugin-repo-cli/
|
||||
.git/
|
||||
_releases/
|
||||
packages/app-desktop
|
||||
packages/app-cli
|
||||
packages/app-mobile
|
||||
packages/app-clipper
|
||||
packages/generator-joplin
|
||||
packages/plugin-repo-cli
|
||||
packages/server/db-*.sqlite
|
||||
packages/server/dist/
|
||||
packages/server/logs/
|
||||
packages/server/temp/
|
||||
packages/server/temp
|
||||
|
||||
@@ -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/
|
||||
|
||||
5
.github/scripts/run_ci.sh
vendored
5
.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
|
||||
# =============================================================================
|
||||
|
||||
25
.github/stale.yml
vendored
Normal file
25
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "good first issue"
|
||||
- "upstream"
|
||||
- "backlog"
|
||||
- "high"
|
||||
- "medium"
|
||||
- "spec"
|
||||
- "cannot reproduce"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs.
|
||||
You may comment on the issue and I will leave it open.
|
||||
Thank you for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.
|
||||
only: issues
|
||||
23
.github/workflows/close-stale-issues.yml
vendored
23
.github/workflows/close-stale-issues.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: 'Close stale issues'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * *'
|
||||
permissions:
|
||||
issues: write
|
||||
jobs:
|
||||
ProcessStaleIssues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
# Use this to do a dry run from a pull request
|
||||
# debug-only: true
|
||||
stale-issue-message: "Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may comment on the issue and I will leave it open. Thank you for your contributions."
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
operations-per-run: 1000
|
||||
exempt-issue-labels: 'good first issue,upstream,backlog,high,medium,spec,cannot reproduce'
|
||||
stale-issue-label: 'stale'
|
||||
close-issue-message: 'Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, feel free to create a new issue with up-to-date information.'
|
||||
# Don't process pull requests at all
|
||||
days-before-pr-stale: -1
|
||||
@@ -1,43 +0,0 @@
|
||||
diff --git a/index.js b/index.js
|
||||
index 85d89900d5fe575dd0c19430209fb1703b03554e..5fa68cc9a4bd2b21a7188bd263262fd9b1604ac6 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -145,7 +145,8 @@ module.exports = function multimd_table_plugin(md, options) {
|
||||
colspan, leftToken,
|
||||
rowspan, upTokens = [],
|
||||
tableLines, tgroupLines,
|
||||
- tag, text, range, r, c, b;
|
||||
+ tag, text, range, r, c, b, t,
|
||||
+ blockState;
|
||||
|
||||
if (startLine + 2 > endLine) { return false; }
|
||||
|
||||
@@ -315,18 +316,26 @@ module.exports = function multimd_table_plugin(md, options) {
|
||||
|
||||
/* Multiline. Join the text and feed into markdown-it blockParser. */
|
||||
if (options.multiline && trToken.meta.multiline && trToken.meta.mbounds) {
|
||||
- text = [ text.trimRight() ];
|
||||
+ // Pad the text with empty lines to ensure the line number mapping is correct
|
||||
+ text = new Array(trToken.map[0]).fill('').concat([ text.trimRight() ]);
|
||||
for (b = 1; b < trToken.meta.mbounds.length; b++) {
|
||||
/* Line with N bounds has cells indexed from 0 to N-2 */
|
||||
if (c > trToken.meta.mbounds[b].length - 2) { continue; }
|
||||
range = [ trToken.meta.mbounds[b][c] + 1, trToken.meta.mbounds[b][c + 1] ];
|
||||
text.push(state.src.slice.apply(state.src, range).trimRight());
|
||||
}
|
||||
- state.md.block.parse(text.join('\n'), state.md, state.env, state.tokens);
|
||||
+ blockState = new state.md.block.State(text.join('\n'), state.md, state.env, []);
|
||||
+ blockState.level = trToken.level + 1;
|
||||
+ // Start tokenizing from the actual content (trToken.map[0])
|
||||
+ state.md.block.tokenize(blockState, trToken.map[0], blockState.lineMax);
|
||||
+ for (t = 0; t < blockState.tokens.length; t++) {
|
||||
+ state.tokens.push(blockState.tokens[t]);
|
||||
+ }
|
||||
} else {
|
||||
token = state.push('inline', '', 0);
|
||||
token.content = text.trim();
|
||||
token.map = trToken.map;
|
||||
+ token.level = trToken.level + 1;
|
||||
token.children = [];
|
||||
}
|
||||
|
||||
@@ -728,23 +728,6 @@ footer .bottom-links-row p {
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
MEDIUM VIEW
|
||||
- Make menu bar elements smaller and closer to each others
|
||||
so that everything fit.
|
||||
*****************************************************************/
|
||||
|
||||
@media (max-width: 990px) {
|
||||
#nav-section > .container {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
#nav-section .button-link {
|
||||
padding: 4px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
NARROW VIEW
|
||||
- Top right menu is displayed
|
||||
@@ -757,23 +740,6 @@ footer .bottom-links-row p {
|
||||
padding-bottom: 130px;
|
||||
}
|
||||
|
||||
#menu-mobile .social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#menu-mobile .social-links a {
|
||||
margin-left: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#menu-mobile .social-links .social-link-mastodon,
|
||||
#menu-mobile .social-links .social-link-reddit,
|
||||
#menu-mobile .social-links .social-link-patreon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.front-page h1 {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
@@ -891,7 +857,7 @@ footer .bottom-links-row p {
|
||||
}
|
||||
|
||||
#menu-mobile .button-link {
|
||||
padding: 4px 12px;
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<footer class="darkblue-bg">
|
||||
<div class="container">
|
||||
{{> socialFeeds}}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-12 social-links">
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed"><i class="fab fa-twitter"></i></a>
|
||||
<a href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
|
||||
<a href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
|
||||
<a href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
|
||||
<a href="https://www.reddit.com/r/joplinapp/" title="Joplin Subreddit"><i class="fab fa-reddit"></i></a>
|
||||
<a href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row bottom-links-row">
|
||||
<div class="col-12 col-md-6">
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-9 text-right d-none d-md-block">
|
||||
<a href="https://twitter.com/joplinapp" title="Joplin Twitter feed" class="fw500"><i class="fab fa-twitter"></i></a>
|
||||
<a href="{{baseUrl}}/news/" class="fw500">News</a>
|
||||
<a href="{{baseUrl}}/news/" class="fw500">What's New</a>
|
||||
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500">Forum</a>
|
||||
{{#showJoplinCloudLinks}}
|
||||
@@ -44,7 +43,7 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center menu-mobile-top">
|
||||
<a href="{{baseUrl}}/news/" class="fw500 mobile-menu-link">News</a>
|
||||
<a href="{{baseUrl}}/news/" class="fw500 mobile-menu-link">What's New</a>
|
||||
<a href="{{baseUrl}}/help/" class="fw500 mobile-menu-link">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500 mobile-menu-link">Forum</a>
|
||||
</div>
|
||||
@@ -60,8 +59,6 @@
|
||||
{{#showToc}}
|
||||
<div id="toc-mobile">{{{tocHtml}}}</div>
|
||||
{{/showToc}}
|
||||
|
||||
{{> socialFeeds}}
|
||||
|
||||
<div>
|
||||
<p class="light-blue mobile-menu-link-bottom text-center">
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-12 social-links">
|
||||
<a class="social-link-twitter" href="https://twitter.com/joplinapp" title="Joplin Twitter feed"><i class="fab fa-twitter"></i></a>
|
||||
<a class="social-link-mastodon" href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
|
||||
<a class="social-link-patreon" href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
|
||||
<a class="social-link-discord" href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
|
||||
<a class="social-link-reddit" href="https://www.reddit.com/r/joplinapp/" title="Joplin Subreddit"><i class="fab fa-reddit"></i></a>
|
||||
<a class="social-link-github" href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
2
BUILD.md
2
BUILD.md
@@ -20,7 +20,7 @@ There are also a few forks of existing packages under the "fork-*" name.
|
||||
|
||||
- Install node 16+ - https://nodejs.org/en/
|
||||
- [Enable yarn](https://yarnpkg.com/getting-started/install): `corepack enable`
|
||||
- macOS: Install Cocoapods - `brew install cocoapods`. Apple Silicon [may require libvips](https://github.com/laurent22/joplin/pull/5966#issuecomment-1007158597) - `brew install vips`.
|
||||
- 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`
|
||||
|
||||
|
||||
@@ -8,36 +8,66 @@ RUN apt-get update \
|
||||
# Enables Yarn
|
||||
RUN corepack enable
|
||||
|
||||
RUN echo "Node: $(node --version)" \
|
||||
&& echo "Npm: $(npm --version)" \
|
||||
&& echo "Yarn: $(yarn --version)"
|
||||
RUN echo "Node: $(node --version)"
|
||||
RUN echo "Npm: $(npm --version)"
|
||||
RUN echo "Yarn: $(yarn --version)"
|
||||
|
||||
ARG user=joplin
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash $user
|
||||
USER $user
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV RUNNING_IN_DOCKER 1
|
||||
EXPOSE ${APP_PORT}
|
||||
ENV NODE_ENV development
|
||||
|
||||
WORKDIR /home/$user
|
||||
|
||||
RUN mkdir /home/$user/logs \
|
||||
&& mkdir /home/$user/.yarn
|
||||
RUN mkdir /home/$user/logs
|
||||
|
||||
COPY --chown=$user:$user .yarn/patches ./.yarn/patches
|
||||
COPY --chown=$user:$user .yarn/plugins ./.yarn/plugins
|
||||
COPY --chown=$user:$user .yarn/releases ./.yarn/releases
|
||||
COPY --chown=$user:$user package.json .
|
||||
# Install the root scripts but don't run postinstall (which would bootstrap
|
||||
# and build TypeScript files, but we don't have the TypeScript files at
|
||||
# this point)
|
||||
|
||||
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
|
||||
|
||||
# To take advantage of the Docker cache, we first copy all the package.json
|
||||
# and package-lock.json files, as they rarely change, and then bootstrap
|
||||
# all the packages.
|
||||
#
|
||||
# Note that bootstrapping the packages will run all the postinstall
|
||||
# scripts, which means that for packages that have such scripts, we need to
|
||||
# copy all the files.
|
||||
#
|
||||
# We can't run boostrap with "--ignore-scripts" because that would
|
||||
# prevent certain sub-packages, such as sqlite3, from being built
|
||||
|
||||
COPY --chown=$user:$user packages/fork-sax/package*.json ./packages/fork-sax/
|
||||
COPY --chown=$user:$user packages/fork-uslug/package*.json ./packages/fork-uslug/
|
||||
COPY --chown=$user:$user packages/htmlpack/package*.json ./packages/htmlpack/
|
||||
COPY --chown=$user:$user packages/renderer/package*.json ./packages/renderer/
|
||||
COPY --chown=$user:$user packages/tools/package*.json ./packages/tools/
|
||||
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
|
||||
COPY --chown=$user:$user tsconfig.json .
|
||||
|
||||
# The following have postinstall scripts so we need to copy all the files.
|
||||
# Since they should rarely change this is not an issue
|
||||
|
||||
COPY --chown=$user:$user packages/turndown ./packages/turndown
|
||||
COPY --chown=$user:$user packages/turndown-plugin-gfm ./packages/turndown-plugin-gfm
|
||||
COPY --chown=$user:$user packages/fork-htmlparser2 ./packages/fork-htmlparser2
|
||||
COPY --chown=$user:$user packages/server/package*.json ./packages/server/
|
||||
|
||||
# Then bootstrap only, without compiling the TypeScript files
|
||||
|
||||
RUN yarn install --inline-builds --mode=skip-build
|
||||
|
||||
# Now copy the source files. Put lib and server last as they are more likely to change.
|
||||
|
||||
COPY --chown=$user:$user packages/fork-sax ./packages/fork-sax
|
||||
COPY --chown=$user:$user packages/fork-uslug ./packages/fork-uslug
|
||||
COPY --chown=$user:$user packages/htmlpack ./packages/htmlpack
|
||||
@@ -46,23 +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
|
||||
|
||||
# For some reason there's both a .yarn/cache and .yarn/berry/cache that are
|
||||
# being generated, and both have the same content. Not clear why it does this
|
||||
# but we can delete it anyway. We can delete the cache because we use
|
||||
# `nodeLinker: node-modules`. If we ever implement Zero Install, we'll need to
|
||||
# keep the cache.
|
||||
#
|
||||
# Note that `yarn install` ignores `NODE_ENV=production` and will install dev
|
||||
# dependencies too, but this is fine because we need them to build the app.
|
||||
# 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 BUILD_SEQUENCIAL=1 yarn install --inline-builds \
|
||||
&& yarn cache clean \
|
||||
&& rm -rf .yarn/berry
|
||||
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds
|
||||
|
||||
# Call the command directly, without going via npm:
|
||||
# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#cmd
|
||||
WORKDIR "/home/$user/packages/server"
|
||||
CMD [ "node", "dist/app.js" ]
|
||||
ENV RUNNING_IN_DOCKER=1
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
# 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
|
||||
|
||||
@@ -300,6 +300,7 @@ To add a **Bucket Policy** from the AWS S3 Web Console, navigate to the **Permis
|
||||
{
|
||||
"Sid": "VisualEditor0",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": [
|
||||
"s3:ListBucket",
|
||||
"s3:GetBucketLocation",
|
||||
|
||||
@@ -78,8 +78,5 @@
|
||||
"node-gyp": "^8.4.1",
|
||||
"nodemon": "^2.0.9"
|
||||
},
|
||||
"packageManager": "yarn@3.1.1",
|
||||
"resolutions": {
|
||||
"markdown-it-multimd-table@4.1.1": "patch:markdown-it-multimd-table@npm:4.1.1#.yarn/patches/markdown-it-multimd-table-npm-4.1.1-47e334d4bd"
|
||||
}
|
||||
"packageManager": "yarn@3.1.1"
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
@@ -446,9 +446,8 @@ class Application extends BaseApplication {
|
||||
|
||||
await this.checkForLegacyTemplates();
|
||||
|
||||
// Note: Auto-update is a misnomer in the code.
|
||||
// The code below only checks, if a new version is available.
|
||||
// We only allow Windows and macOS users to automatically check for updates
|
||||
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
||||
// but then doesn't install it on exit.
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
const runAutoUpdateCheck = () => {
|
||||
if (Setting.value('autoUpdateEnabled')) {
|
||||
|
||||
@@ -30,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).
|
||||
|
||||
@@ -56,7 +56,7 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
|
||||
const containerStyle = Object.assign({}, theme.containerStyle, {
|
||||
overflowY: 'scroll',
|
||||
// padding: theme.configScreenPadding,
|
||||
padding: theme.configScreenPadding,
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
@@ -22,7 +22,6 @@ interface Props {
|
||||
export default function(props: Props) {
|
||||
const [folderTitle, setFolderTitle] = useState('');
|
||||
const [folderIcon, setFolderIcon] = useState<FolderIcon>();
|
||||
const titleInputRef = useRef(null);
|
||||
|
||||
const isNew = !props.folderId;
|
||||
|
||||
@@ -42,14 +41,6 @@ export default function(props: Props) {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
titleInputRef.current.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
titleInputRef.current.select();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
|
||||
if (event.buttonName === 'cancel') {
|
||||
onClose();
|
||||
@@ -99,7 +90,7 @@ export default function(props: Props) {
|
||||
<div className="form">
|
||||
<div className="form-input-group">
|
||||
<label>{_('Title')}</label>
|
||||
<StyledInput type="text" ref={titleInputRef} value={folderTitle} onChange={onFolderTitleChange}/>
|
||||
<StyledInput type="text" value={folderTitle} onChange={onFolderTitleChange}/>
|
||||
</div>
|
||||
|
||||
<div className="form-input-group">
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function styles(themeId: number) {
|
||||
return {
|
||||
container: {
|
||||
...theme.containerStyle,
|
||||
// padding: theme.configScreenPadding,
|
||||
padding: theme.configScreenPadding,
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
},
|
||||
actionsContainer: {
|
||||
|
||||
@@ -726,6 +726,13 @@ function useMenu(props: Props) {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'help:toggleTaskList',
|
||||
label: _('Open plugin task list'),
|
||||
click: () => {
|
||||
bridge().openProcessManager();
|
||||
},
|
||||
},
|
||||
|
||||
menuItemDic.toggleSafeMode,
|
||||
menuItemDic.openProfileDirectory,
|
||||
|
||||
@@ -67,7 +67,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
usePluginServiceRegistration(ref);
|
||||
|
||||
const { resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll, editor_resize, getLineScrollPercent,
|
||||
const { resetScroll, editor_scroll, setEditorPercentScroll, setViewerPercentScroll, editor_resize,
|
||||
} = useScrollHandler(editorRef, webviewRef, props.onScroll);
|
||||
|
||||
const codeMirror_change = useCallback((newBody: string) => {
|
||||
@@ -576,14 +576,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const { line, from, to } = shared.toggleCheckboxRange(msg, props.content);
|
||||
const newBody = shared.toggleCheckbox(msg, props.content);
|
||||
if (editorRef.current) {
|
||||
// To cancel CodeMirror's layout drift, the scroll position
|
||||
// is recorded before updated, and then it is restored.
|
||||
// Ref. https://github.com/laurent22/joplin/issues/5890
|
||||
const percent = getLineScrollPercent();
|
||||
editorRef.current.replaceRange(line, from, to);
|
||||
setEditorPercentScroll(percent);
|
||||
editorRef.current.updateBody(newBody);
|
||||
}
|
||||
} else if (msg === 'percentScroll') {
|
||||
const percent = arg0;
|
||||
|
||||
@@ -7,7 +7,6 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
const ignoreNextEditorScrollTime_ = useRef(Date.now());
|
||||
const ignoreNextEditorScrollEventCount_ = useRef(0);
|
||||
const delayedSetEditorPercentScrollTimeoutID_ = useRef(null);
|
||||
const lastResizeHeight_ = useRef(NaN);
|
||||
|
||||
// Ignores one next scroll event for a short time.
|
||||
const ignoreNextEditorScrollEvent = () => {
|
||||
@@ -91,7 +90,8 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
}, [scheduleOnScroll]);
|
||||
|
||||
const editor_scroll = useCallback(() => {
|
||||
const ignored = isNextEditorScrollEventIgnored();
|
||||
if (isNextEditorScrollEventIgnored()) return;
|
||||
|
||||
const cm = editorRef.current;
|
||||
if (isCodeMirrorReady(cm)) {
|
||||
const editorPercent = Math.max(0, Math.min(1, cm.getScrollPercent()));
|
||||
@@ -104,9 +104,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
// calculates GUI-independent line-based percent
|
||||
const percent = translateScrollPercentE2L(cm, editorPercent);
|
||||
scrollPercent_.current = percent;
|
||||
if (!ignored) {
|
||||
setViewerPercentScroll(percent);
|
||||
}
|
||||
setViewerPercentScroll(percent);
|
||||
}
|
||||
}
|
||||
}, [setViewerPercentScroll]);
|
||||
@@ -119,29 +117,13 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
}, []);
|
||||
|
||||
const editor_resize = useCallback((cm) => {
|
||||
if (isCodeMirrorReady(cm)) {
|
||||
// Only when resized, the scroll position is restored.
|
||||
const info = cm.getScrollInfo();
|
||||
const height = info.height - info.clientHeight;
|
||||
if (height !== lastResizeHeight_.current) {
|
||||
restoreEditorPercentScroll();
|
||||
lastResizeHeight_.current = height;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getLineScrollPercent = useCallback(() => {
|
||||
const cm = editorRef.current;
|
||||
if (isCodeMirrorReady(cm)) {
|
||||
const ePercent = cm.getScrollPercent();
|
||||
return translateScrollPercentE2L(cm, ePercent);
|
||||
} else {
|
||||
return scrollPercent_.current;
|
||||
if (cm) {
|
||||
restoreEditorPercentScroll();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll, editor_resize, getLineScrollPercent,
|
||||
resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll, editor_resize,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ export const Root = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 30px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const CommandService = require('@joplin/lib/services/CommandService').default;
|
||||
|
||||
class TagItemComponent extends React.Component {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = Object.assign({}, theme.tagStyle);
|
||||
const { title, id } = this.props;
|
||||
const title = this.props.title;
|
||||
|
||||
return <button style={style} onClick={() => CommandService.instance().execute('openTag', id)}>{title}</button>;
|
||||
return <span style={style}>{title}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ function TagList(props: Props) {
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const props = {
|
||||
title: tags[i].title,
|
||||
id: tags[i].id,
|
||||
key: tags[i].id,
|
||||
};
|
||||
output.push(<TagItem {...props} />);
|
||||
|
||||
@@ -151,7 +151,7 @@ General classes
|
||||
|
||||
body, button {
|
||||
color: var(--joplin-color);
|
||||
font-size: 13px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
div, span, a {
|
||||
@@ -159,7 +159,7 @@ div, span, a {
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-size: 24px;
|
||||
|
||||
&.-no-top-margin {
|
||||
margin-top: 0;
|
||||
@@ -193,7 +193,7 @@ div.form,
|
||||
|
||||
p {
|
||||
&.-small {
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
|
||||
# ./runForTesting.sh 1 createUsers,createData,reset,e2ee,sync && ./runForTesting.sh 2 reset,e2ee,sync && ./runForTesting.sh 1
|
||||
|
||||
# ----------------------------------------------------------------------------------
|
||||
# First user has E2EE, but second one doesn't:
|
||||
# ----------------------------------------------------------------------------------
|
||||
|
||||
# ./runForTesting.sh 1 createUsers,createData,reset,e2ee,sync && ./runForTesting.sh 2 reset,sync && ./runForTesting.sh 1
|
||||
|
||||
# ----------------------------------------------------------------------------------
|
||||
# Without E2EE:
|
||||
# ----------------------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
@@ -10,9 +10,6 @@
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"files": [
|
||||
"publish"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
"chalk": "^4.1.0",
|
||||
|
||||
@@ -98,14 +98,14 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
|
||||
// We could implement that here, but the above workaround saves some code.
|
||||
|
||||
static async checkConfig(options) {
|
||||
const fileApi = await SyncTargetAmazonS3.newFileApi_(SyncTargetAmazonS3.id(), options);
|
||||
fileApi.requestRepeatCount_ = 0;
|
||||
|
||||
const output = {
|
||||
ok: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
try {
|
||||
const fileApi = await SyncTargetAmazonS3.newFileApi_(SyncTargetAmazonS3.id(), options);
|
||||
fileApi.requestRepeatCount_ = 0;
|
||||
|
||||
const headBucketReq = new Promise((resolve, reject) => {
|
||||
fileApi.driver().api().send(
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const runtime = (): CommandRuntime => {
|
||||
return 'auth';
|
||||
}
|
||||
|
||||
reg.logger().error('Not authenticated with sync target - please check your credentials.');
|
||||
reg.logger().info('Not authentified with sync target - please check your credential.');
|
||||
return 'error';
|
||||
}
|
||||
|
||||
@@ -43,13 +43,8 @@ export const runtime = (): CommandRuntime => {
|
||||
try {
|
||||
sync = await reg.syncTarget().synchronizer();
|
||||
} catch (error) {
|
||||
reg.logger().error('Could not initialise synchroniser: ');
|
||||
reg.logger().error(error);
|
||||
error.message = `Could not initialise synchroniser: ${error.message}`;
|
||||
utils.store.dispatch({
|
||||
type: 'SYNC_REPORT_UPDATE',
|
||||
report: { errors: [error] },
|
||||
});
|
||||
reg.logger().info('Could not acquire synchroniser:');
|
||||
reg.logger().info(error);
|
||||
return 'error';
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ shared.toggleIsTodo_onPress = function(comp) {
|
||||
comp.setState(newState);
|
||||
};
|
||||
|
||||
function toggleCheckboxLine(ipcMessage, noteBody) {
|
||||
shared.toggleCheckbox = function(ipcMessage, noteBody) {
|
||||
const newBody = noteBody.split('\n');
|
||||
const p = ipcMessage.split(':');
|
||||
const lineIndex = Number(p[p.length - 1]);
|
||||
@@ -281,18 +281,7 @@ function toggleCheckboxLine(ipcMessage, noteBody) {
|
||||
} else {
|
||||
line = line.replace(/- \[x\] /i, '- [ ] ');
|
||||
}
|
||||
return [newBody, lineIndex, line];
|
||||
}
|
||||
|
||||
shared.toggleCheckboxRange = function(ipcMessage, noteBody) {
|
||||
const [lineIndex, line] = toggleCheckboxLine(ipcMessage, noteBody).slice(1);
|
||||
const from = { line: lineIndex, ch: 0 };
|
||||
const to = { line: lineIndex, ch: line.length };
|
||||
return { line, from, to };
|
||||
};
|
||||
|
||||
shared.toggleCheckbox = function(ipcMessage, noteBody) {
|
||||
const [newBody, lineIndex, line] = toggleCheckboxLine(ipcMessage, noteBody);
|
||||
newBody[lineIndex] = line;
|
||||
return newBody.join('\n');
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ shared.synchronize_press = async function(comp) {
|
||||
return 'auth';
|
||||
}
|
||||
|
||||
reg.logger().error('Not authenticated with sync target - please check your credentials.');
|
||||
reg.logger().info('Not authentified with sync target - please check your credential.');
|
||||
return 'error';
|
||||
}
|
||||
|
||||
@@ -113,13 +113,8 @@ shared.synchronize_press = async function(comp) {
|
||||
try {
|
||||
sync = await reg.syncTarget().synchronizer();
|
||||
} catch (error) {
|
||||
reg.logger().error('Could not initialise synchroniser: ');
|
||||
reg.logger().error(error);
|
||||
error.message = `Could not initialise synchroniser: ${error.message}`;
|
||||
comp.props.dispatch({
|
||||
type: 'SYNC_REPORT_UPDATE',
|
||||
report: { errors: [error] },
|
||||
});
|
||||
reg.logger().info('Could not acquire synchroniser:');
|
||||
reg.logger().info(error);
|
||||
return 'error';
|
||||
}
|
||||
|
||||
|
||||
@@ -1190,7 +1190,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
|
||||
|
||||
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
||||
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically update the application') },
|
||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
||||
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
||||
'sync.interval': {
|
||||
|
||||
@@ -89,7 +89,6 @@ export default class OneDriveApi {
|
||||
scope: 'files.readwrite offline_access sites.readwrite.all',
|
||||
response_type: 'code',
|
||||
redirect_uri: redirectUri,
|
||||
prompt: 'login',
|
||||
};
|
||||
return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${stringify(query)}`;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Command } from './types';
|
||||
*
|
||||
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
|
||||
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
|
||||
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts)
|
||||
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.ts)
|
||||
*
|
||||
* To view what arguments are supported, you can open any of these files
|
||||
* and look at the `execute()` command.
|
||||
|
||||
@@ -227,7 +227,6 @@ function addExtraStyles(style: any) {
|
||||
justifyContent: 'center',
|
||||
marginRight: 8,
|
||||
borderRadius: 100,
|
||||
borderWidth: 0,
|
||||
};
|
||||
|
||||
style.toolbarStyle = {
|
||||
|
||||
@@ -51,8 +51,6 @@ async function extractPluginFilesFromPackage(existingManifests: any, workDir: st
|
||||
|
||||
const pluginDir = resolveRelativePathWithinDir(workDir, 'node_modules', packageName, 'publish');
|
||||
|
||||
if (!(await fs.pathExists(pluginDir))) throw new Error(`Could not find publish directory at ${pluginDir}`);
|
||||
|
||||
const files = await fs.readdir(pluginDir);
|
||||
const manifestFilePath = path.resolve(pluginDir, files.find((f: any) => path.extname(f) === '.json'));
|
||||
const pluginFilePath = path.resolve(pluginDir, files.find((f: any) => path.extname(f) === '.jpl'));
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
# Individual Contributor License Agreement
|
||||
|
||||
Thank you for your interest in Joplin Server, owned by Cozic Ltd (the
|
||||
"Company"). In order to clarify the intellectual property license granted with
|
||||
Contributions from any person or entity, the Company must have a Contributor
|
||||
License Agreement ("CLA") on file that has been signed by each Contributor,
|
||||
indicating agreement to the license terms below. This license is for your
|
||||
protection as a Contributor as well as the protection of the Company and its
|
||||
users; it does not change your rights to use your own Contributions for any
|
||||
other purpose.
|
||||
|
||||
Please complete and sign this Agreement, and then email a copy to
|
||||
cla@joplinapp.org only (do not copy any other persons or lists). Read this
|
||||
document carefully before signing and keep a copy for your records.
|
||||
|
||||
- Full name: **FULL NAME**
|
||||
|
||||
- Postal Address: **POSTAL ADDRESS**
|
||||
|
||||
- Country: **COUNTRY**
|
||||
|
||||
- E-Mail: **EMAIL**
|
||||
|
||||
- GitHub username: **GITHUB USERNAME**
|
||||
|
||||
You accept and agree to the following terms and conditions for Your present and
|
||||
future Contributions submitted to the Company. In return, the Company shall not
|
||||
use Your Contributions in a way that is contrary to the public benefit or
|
||||
inconsistent with its bylaws in effect at the time of the Contribution. Except
|
||||
for the license granted herein to the Company and recipients of software
|
||||
distributed by the Company, You reserve all right, title, and interest in and to
|
||||
Your Contributions.
|
||||
|
||||
1. Definitions.
|
||||
|
||||
* "You" (or "Your")
|
||||
|
||||
"You" (or "Your") shall mean the copyright owner or legal entity authorized
|
||||
by the copyright owner that is making this Agreement with the Company. For
|
||||
legal entities, the entity making a Contribution and all other entities
|
||||
that control, are controlled by, or are under common control with that
|
||||
entity are considered to be a single Contributor. For the purposes of this
|
||||
definition, "control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or otherwise,
|
||||
or (ii) ownership of fifty percent (50%) or more of the outstanding shares,
|
||||
or (iii) beneficial ownership of such entity.
|
||||
|
||||
* "Contribution"
|
||||
|
||||
"Contribution" shall mean any original work of authorship, including any
|
||||
modifications or additions to an existing work, that is intentionally
|
||||
submitted by You to the Company for inclusion in, or documentation of, any
|
||||
of the products owned or managed by the Company (the "Work"). For the
|
||||
purposes of this definition, "submitted" means any form of electronic,
|
||||
verbal, or written communication sent to the Company or its
|
||||
representatives, including but not limited to communication on electronic
|
||||
mailing lists, source code control systems, and issue tracking systems that
|
||||
are managed by, or on behalf of, the Company for the purpose of discussing
|
||||
and improving the Work, but excluding communication that is conspicuously
|
||||
marked or otherwise designated in writing by You as "Not a Contribution."
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this
|
||||
Agreement, You hereby grant to the Company and to recipients of software
|
||||
distributed by the Company a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable copyright license to reproduce, prepare derivative
|
||||
works of, publicly display, publicly perform, sublicense, and distribute Your
|
||||
Contributions and such derivative works.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this
|
||||
Agreement, You hereby grant to the Company and to recipients of software
|
||||
distributed by the Company a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent license
|
||||
to make, have made, use, offer to sell, sell, import, and otherwise transfer
|
||||
the Work, where such license applies only to those patent claims licensable
|
||||
by You that are necessarily infringed by Your Contribution(s) alone or by
|
||||
combination of Your Contribution(s) with the Work to which such
|
||||
Contribution(s) was submitted. If any entity institutes patent litigation
|
||||
against You or any other entity (including a cross-claim or counterclaim in a
|
||||
lawsuit) alleging that your Contribution, or the Work to which you have
|
||||
contributed, constitutes direct or contributory patent infringement, then any
|
||||
patent licenses granted to that entity under this Agreement for that
|
||||
Contribution or Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. You represent that you are legally entitled to grant the above license. If
|
||||
your employer(s) has rights to intellectual property that you create that
|
||||
includes your Contributions, you represent that you have received permission
|
||||
to make Contributions on behalf of that employer, that your employer has
|
||||
waived such rights for your Contributions to the Company, or that your
|
||||
employer has executed a separate Corporate CLA with the Company.
|
||||
|
||||
5. You represent that each of Your Contributions is Your original creation (see
|
||||
section 7 for submissions on behalf of others). You represent that Your
|
||||
Contribution submissions include complete details of any third-party license
|
||||
or other restriction (including, but not limited to, related patents and
|
||||
trademarks) of which you are personally aware and which are associated with
|
||||
any part of Your Contributions.
|
||||
|
||||
6. You are not expected to provide support for Your Contributions, except to the
|
||||
extent You desire to provide support. You may provide support for free, for a
|
||||
fee, or not at all. Unless required by applicable law or agreed to in
|
||||
writing, You provide Your Contributions 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.
|
||||
|
||||
7. Should You wish to submit work that is not Your original creation, You may
|
||||
submit it to the Company separately from any Contribution, identifying the
|
||||
complete details of its source and of any license or other restriction
|
||||
(including, but not limited to, related patents, trademarks, and license
|
||||
agreements) of which you are personally aware, and conspicuously marking the
|
||||
work as "Submitted on behalf of a third-party: **NAME HERE**".
|
||||
|
||||
8. You agree to notify the Company of any facts or circumstances of which you
|
||||
become aware that would make these representations inaccurate in any respect.
|
||||
|
||||
Please sign: **SIGNATURE** Date: **DATE**
|
||||
@@ -24,7 +24,6 @@ import { RouteResponseFormat, routeResponseFormat } from './utils/routeUtils';
|
||||
import { parseEnv } from './env';
|
||||
import storageConnectionCheck from './utils/storageConnectionCheck';
|
||||
import { setLocale } from '@joplin/lib/locale';
|
||||
import checkAdminHandler from './middleware/checkAdminHandler';
|
||||
|
||||
interface Argv {
|
||||
env?: Env;
|
||||
@@ -197,7 +196,6 @@ async function main() {
|
||||
|
||||
app.use(apiVersionHandler);
|
||||
app.use(ownerHandler);
|
||||
app.use(checkAdminHandler);
|
||||
app.use(notificationHandler);
|
||||
app.use(clickJackingHandler);
|
||||
app.use(routeHandler);
|
||||
|
||||
@@ -66,7 +66,7 @@ function mailerConfigFromEnv(env: EnvVariables): MailerConfig {
|
||||
enabled: env.MAILER_ENABLED,
|
||||
host: env.MAILER_HOST,
|
||||
port: env.MAILER_PORT,
|
||||
security: env.MAILER_SECURITY,
|
||||
secure: env.MAILER_SECURE,
|
||||
authUser: env.MAILER_AUTH_USER,
|
||||
authPassword: env.MAILER_AUTH_PASSWORD,
|
||||
noReplyName: env.MAILER_NOREPLY_NAME,
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { afterAllTests, beforeAllDb, beforeEachDb, expectThrow } from './utils/testing/testUtils';
|
||||
import { parseEnv } from './env';
|
||||
|
||||
describe('env', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('env');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
it('should parse env values', async function() {
|
||||
const result = parseEnv({
|
||||
DB_CLIENT: 'pg',
|
||||
POSTGRES_PORT: '123',
|
||||
MAILER_ENABLED: 'true',
|
||||
SIGNUP_ENABLED: 'false',
|
||||
TERMS_ENABLED: '0',
|
||||
ACCOUNT_TYPES_ENABLED: '1',
|
||||
});
|
||||
|
||||
expect(result.DB_CLIENT).toBe('pg');
|
||||
expect(result.POSTGRES_PORT).toBe(123);
|
||||
expect(result.MAILER_ENABLED).toBe(true);
|
||||
expect(result.SIGNUP_ENABLED).toBe(false);
|
||||
expect(result.TERMS_ENABLED).toBe(false);
|
||||
expect(result.ACCOUNT_TYPES_ENABLED).toBe(true);
|
||||
});
|
||||
|
||||
it('should overrides default values', async function() {
|
||||
expect(parseEnv({}).POSTGRES_USER).toBe('joplin');
|
||||
expect(parseEnv({}, { POSTGRES_USER: 'other' }).POSTGRES_USER).toBe('other');
|
||||
});
|
||||
|
||||
it('should validate values', async function() {
|
||||
await expectThrow(async () => parseEnv({ POSTGRES_PORT: 'notanumber' }));
|
||||
await expectThrow(async () => parseEnv({ MAILER_ENABLED: 'TRUE' }));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,13 +1,7 @@
|
||||
// The possible env variables and their defaults are listed below.
|
||||
//
|
||||
// The env variables can be of type string, integer or boolean. When the type is
|
||||
// boolean, set the variable to "true", "false", "0" or "1" in your env file.
|
||||
|
||||
export enum MailerSecurity {
|
||||
None = 'none',
|
||||
Tls = 'tls',
|
||||
Starttls = 'starttls',
|
||||
}
|
||||
// boolean, set the variable to "0" or "1" in your env file.
|
||||
|
||||
const defaultEnvValues: EnvVariables = {
|
||||
// ==================================================
|
||||
@@ -72,8 +66,8 @@ const defaultEnvValues: EnvVariables = {
|
||||
|
||||
MAILER_ENABLED: false,
|
||||
MAILER_HOST: '',
|
||||
MAILER_PORT: 465,
|
||||
MAILER_SECURITY: MailerSecurity.Tls,
|
||||
MAILER_PORT: 587,
|
||||
MAILER_SECURE: true,
|
||||
MAILER_AUTH_USER: '',
|
||||
MAILER_AUTH_PASSWORD: '',
|
||||
MAILER_NOREPLY_NAME: '',
|
||||
@@ -126,7 +120,7 @@ export interface EnvVariables {
|
||||
MAILER_ENABLED: boolean;
|
||||
MAILER_HOST: string;
|
||||
MAILER_PORT: number;
|
||||
MAILER_SECURITY: MailerSecurity;
|
||||
MAILER_SECURE: boolean;
|
||||
MAILER_AUTH_USER: string;
|
||||
MAILER_AUTH_PASSWORD: string;
|
||||
MAILER_NOREPLY_NAME: string;
|
||||
@@ -140,13 +134,7 @@ export interface EnvVariables {
|
||||
STRIPE_WEBHOOK_SECRET: string;
|
||||
}
|
||||
|
||||
const parseBoolean = (s: string): boolean => {
|
||||
if (s === 'true' || s === '1') return true;
|
||||
if (s === 'false' || s === '0') return false;
|
||||
throw new Error(`Invalid boolean value: "${s}" (Must be one of "true", "false", "0, "1")`);
|
||||
};
|
||||
|
||||
export function parseEnv(rawEnv: Record<string, string>, defaultOverrides: any = null): EnvVariables {
|
||||
export function parseEnv(rawEnv: any, defaultOverrides: any = null): EnvVariables {
|
||||
const output: EnvVariables = {
|
||||
...defaultEnvValues,
|
||||
...defaultOverrides,
|
||||
@@ -157,21 +145,17 @@ export function parseEnv(rawEnv: Record<string, string>, defaultOverrides: any =
|
||||
|
||||
if (rawEnvValue === undefined) continue;
|
||||
|
||||
try {
|
||||
if (typeof value === 'number') {
|
||||
const v = Number(rawEnvValue);
|
||||
if (isNaN(v)) throw new Error(`Invalid number value "${rawEnvValue}"`);
|
||||
(output as any)[key] = v;
|
||||
} else if (typeof value === 'boolean') {
|
||||
(output as any)[key] = parseBoolean(rawEnvValue);
|
||||
} else if (typeof value === 'string') {
|
||||
(output as any)[key] = `${rawEnvValue}`;
|
||||
} else {
|
||||
throw new Error(`Invalid env default value type: ${typeof value}`);
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `Could not parse key "${key}": ${error.message}`;
|
||||
throw error;
|
||||
if (typeof value === 'number') {
|
||||
const v = Number(rawEnvValue);
|
||||
if (isNaN(v)) throw new Error(`Invalid number value for env variable ${key} = ${rawEnvValue}`);
|
||||
(output as any)[key] = v;
|
||||
} else if (typeof value === 'boolean') {
|
||||
if (rawEnvValue !== '0' && rawEnvValue !== '1') throw new Error(`Invalid boolean value for env variable ${key}: ${rawEnvValue} (Should be either "0" or "1")`);
|
||||
(output as any)[key] = rawEnvValue === '1';
|
||||
} else if (typeof value === 'string') {
|
||||
(output as any)[key] = `${rawEnvValue}`;
|
||||
} else {
|
||||
throw new Error(`Invalid env default value type: ${typeof value}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { ErrorForbidden } from '../utils/errors';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError, createUserAndSession } from '../utils/testing/testUtils';
|
||||
import checkAdminHandler from './checkAdminHandler';
|
||||
|
||||
describe('checkAdminHandler', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('checkAdminHandler');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should access /admin if the user is admin', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const context = await koaAppContext({
|
||||
sessionId: session.id,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/admin/organizations',
|
||||
},
|
||||
});
|
||||
|
||||
await expectNotThrow(async () => checkAdminHandler(context, koaNext));
|
||||
});
|
||||
|
||||
test('should not access /admin if the user is not admin', async function() {
|
||||
const { session } = await createUserAndSession(1);
|
||||
|
||||
const context = await koaAppContext({
|
||||
sessionId: session.id,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/admin/organizations',
|
||||
},
|
||||
});
|
||||
|
||||
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
test('should not access /admin if the user is not logged in', async function() {
|
||||
const context = await koaAppContext({
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/admin/organizations',
|
||||
},
|
||||
});
|
||||
|
||||
await expectHttpError(async () => checkAdminHandler(context, koaNext), ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { AppContext, KoaNext } from '../utils/types';
|
||||
import { isAdminRequest } from '../utils/requestUtils';
|
||||
import { ErrorForbidden } from '../utils/errors';
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
if (isAdminRequest(ctx)) {
|
||||
if (!ctx.joplin.owner) throw new ErrorForbidden();
|
||||
if (!ctx.joplin.owner.is_admin) throw new ErrorForbidden();
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
@@ -92,8 +92,7 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
return this.add(userId, NotificationKey.Any, NotificationLevel.Normal, message);
|
||||
}
|
||||
|
||||
public async addError(userId: Uuid, error: string | Error) {
|
||||
const message = typeof error === 'string' ? error : error.message;
|
||||
public async addError(userId: Uuid, message: string) {
|
||||
return this.add(userId, NotificationKey.Any, NotificationLevel.Error, message);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ router.get('api/users/:id/public_key', async (path: SubPath, ctx: AppContext) =>
|
||||
if (!user) return ''; // Don't throw an error to prevent polling the end point
|
||||
|
||||
const ppk = await ctx.joplin.models.user().publicPrivateKey(user.id);
|
||||
if (!ppk) return '';
|
||||
|
||||
return {
|
||||
id: ppk.id,
|
||||
|
||||
@@ -132,7 +132,7 @@ router.get('tasks', async (_path: SubPath, ctx: AppContext) => {
|
||||
postUrl: makeUrl(UrlType.Tasks),
|
||||
csrfTag: await createCsrfTag(ctx),
|
||||
},
|
||||
// cssFiles: ['index/tasks'],
|
||||
cssFiles: ['index/tasks'],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Email, EmailSender } from '../services/database/types';
|
||||
import { errorToString } from '../utils/errors';
|
||||
import EmailModel from '../models/EmailModel';
|
||||
import { markdownBodyToHtml, markdownBodyToPlainText } from './email/utils';
|
||||
import { MailerSecurity } from '../env';
|
||||
|
||||
const logger = Logger.create('EmailService');
|
||||
|
||||
@@ -26,16 +25,10 @@ export default class EmailService extends BaseService {
|
||||
if (!this.senderInfo(EmailSender.NoReply).email) {
|
||||
throw new Error('No-reply email must be set for email service to work (Set env variable MAILER_NOREPLY_EMAIL)');
|
||||
}
|
||||
|
||||
// NodeMailer's TLS options are weird:
|
||||
// https://nodemailer.com/smtp/#tls-options
|
||||
|
||||
const options: SMTPTransport.Options = {
|
||||
host: this.config.mailer.host,
|
||||
port: this.config.mailer.port,
|
||||
secure: this.config.mailer.security === MailerSecurity.Tls,
|
||||
ignoreTLS: this.config.mailer.security === MailerSecurity.None,
|
||||
requireTLS: this.config.mailer.security === MailerSecurity.Starttls,
|
||||
secure: this.config.mailer.secure,
|
||||
};
|
||||
if (this.config.mailer.authUser || this.config.mailer.authPassword) {
|
||||
options.auth = {
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface View {
|
||||
partials?: string[];
|
||||
cssFiles?: string[];
|
||||
jsFiles?: string[];
|
||||
strings?: Record<string, string>; // List of translatable strings
|
||||
}
|
||||
|
||||
interface GlobalParams {
|
||||
@@ -47,7 +46,6 @@ interface GlobalParams {
|
||||
isJoplinCloud?: boolean;
|
||||
impersonatorAdminSessionId?: string;
|
||||
csrfTag?: string;
|
||||
s?: Record<string, string>; // List of translatable strings
|
||||
}
|
||||
|
||||
export function isView(o: any): boolean {
|
||||
@@ -188,6 +186,12 @@ export default class MustacheService {
|
||||
...this.defaultLayoutOptions,
|
||||
...globalParams,
|
||||
userDisplayName: this.userDisplayName(globalParams ? globalParams.owner : null),
|
||||
};
|
||||
|
||||
const contentHtml = await this.renderFileContent(filePath, view, globalParams);
|
||||
|
||||
const layoutView: any = {
|
||||
global: globalParams,
|
||||
s: {
|
||||
home: _('Home'),
|
||||
users: _('Users'),
|
||||
@@ -197,12 +201,6 @@ export default class MustacheService {
|
||||
help: _('Help'),
|
||||
logout: _('Logout'),
|
||||
},
|
||||
};
|
||||
|
||||
const contentHtml = await this.renderFileContent(filePath, view, globalParams);
|
||||
|
||||
const layoutView: any = {
|
||||
global: globalParams,
|
||||
pageName: view.name,
|
||||
pageTitle: view.titleOverride ? view.title : `${config().appName} - ${view.title}`,
|
||||
contentHtml: contentHtml,
|
||||
|
||||
@@ -71,10 +71,6 @@ export function isApiRequest(ctx: AppContext): boolean {
|
||||
return ctx.path.indexOf('/api/') === 0;
|
||||
}
|
||||
|
||||
export function isAdminRequest(ctx: AppContext): boolean {
|
||||
return ctx.path.indexOf('/admin/') === 0;
|
||||
}
|
||||
|
||||
export function userIp(ctx: AppContext): string {
|
||||
if (ctx.headers['x-real-ip']) return ctx.headers['x-real-ip'];
|
||||
return ctx.ip;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findMatchingRoute, isValidOrigin, parseSubPath, splitItemPath } from './routeUtils';
|
||||
import { isValidOrigin, parseSubPath, splitItemPath } from './routeUtils';
|
||||
import { ItemAddressingType } from '../services/database/types';
|
||||
import { RouteType } from './types';
|
||||
|
||||
@@ -26,58 +26,6 @@ describe('routeUtils', function() {
|
||||
}
|
||||
});
|
||||
|
||||
it('should find a matching route', async function() {
|
||||
const testCases: any[] = [
|
||||
['/admin/organizations', {
|
||||
route: 1,
|
||||
basePath: 'admin/organizations',
|
||||
subPath: {
|
||||
id: '',
|
||||
link: '',
|
||||
addressingType: 1,
|
||||
raw: '',
|
||||
schema: 'admin/organizations',
|
||||
},
|
||||
}],
|
||||
|
||||
['/api/users/123', {
|
||||
route: 2,
|
||||
basePath: 'api/users',
|
||||
subPath: {
|
||||
id: '123',
|
||||
link: '',
|
||||
addressingType: 1,
|
||||
raw: '123',
|
||||
schema: 'api/users/:id',
|
||||
},
|
||||
}],
|
||||
|
||||
['/help', {
|
||||
route: 3,
|
||||
basePath: 'help',
|
||||
subPath: {
|
||||
id: '',
|
||||
link: '',
|
||||
addressingType: 1,
|
||||
raw: '',
|
||||
schema: 'help',
|
||||
},
|
||||
}],
|
||||
];
|
||||
|
||||
const routes: Record<string, any> = {
|
||||
'admin/organizations': 1,
|
||||
'api/users': 2,
|
||||
'help': 3,
|
||||
};
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const [path, expected] = testCase;
|
||||
const actual = findMatchingRoute(path, routes);
|
||||
expect(actual).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should split an item path', async function() {
|
||||
const testCases: any[] = [
|
||||
['root:/Documents/MyFile.md:', ['root', 'Documents', 'MyFile.md']],
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Account } from '../models/UserModel';
|
||||
import { Services } from '../services/types';
|
||||
import { Routers } from './routeUtils';
|
||||
import { DbConnection } from '../db';
|
||||
import { MailerSecurity } from '../env';
|
||||
|
||||
export enum Env {
|
||||
Dev = 'dev',
|
||||
@@ -75,7 +74,7 @@ export interface MailerConfig {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
security: MailerSecurity;
|
||||
secure: boolean;
|
||||
authUser: string;
|
||||
authPassword: string;
|
||||
noReplyName: string;
|
||||
|
||||
@@ -20,10 +20,6 @@ To switch between yearly and monthly payments, or to change from a Basic to Pro
|
||||
|
||||
Note that if you downgrade from Pro to Basic, new limitations will apply so for example you may have to delete some notes so that your account is below the required limit.
|
||||
|
||||
## Can my subscription be refunded?
|
||||
|
||||
We offer a 14 days trial when the subscription starts so that you can evaluate the service and potentially change your mind - if you cancel during that period you will not be charged. After 14 days your card will be charged and it will not be possible to issue a refund.
|
||||
|
||||
## How can I cancel my account?
|
||||
|
||||
Click on the [Profile button](#how-can-i-change-my-details), then scroll down and click on "Manage subscription".
|
||||
|
||||
@@ -10,27 +10,27 @@
|
||||
{{#global.owner}}
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/home">{{global.s.home}}</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/home">{{s.home}}</a>
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users">{{global.s.users}}</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users">{{s.users}}</a>
|
||||
{{/global.owner.is_admin}}
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/items">{{global.s.items}}</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/items">{{s.items}}</a>
|
||||
{{/global.owner.is_admin}}
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">{{global.s.log}}</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">{{s.log}}</a>
|
||||
{{/global.owner.is_admin}}
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/tasks">{{global.s.tasks}}</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/tasks">{{s.tasks}}</a>
|
||||
{{/global.owner.is_admin}}
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
{{#global.isJoplinCloud}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/help">{{global.s.help}}</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/help">{{s.help}}</a>
|
||||
{{/global.isJoplinCloud}}
|
||||
<div class="navbar-item">
|
||||
<form method="post" action="{{{global.baseUrl}}}/logout">
|
||||
<button class="button is-dark">{{global.s.logout}}</button>
|
||||
<button class="button is-dark">{{s.logout}}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
|
||||
@@ -347,7 +347,7 @@ function flagImageUrl(locale) {
|
||||
if (locale === 'sv') return `${baseUrl}/country-4x3/se.png`;
|
||||
if (locale === 'nb_NO') return `${baseUrl}/country-4x3/no.png`;
|
||||
if (locale === 'ro') return `${baseUrl}/country-4x3/ro.png`;
|
||||
if (locale === 'vi') return `${baseUrl}/country-4x3/vn.png`;
|
||||
if (locale === 'vi') return `${baseUrl}/country-4x3/vi.png`;
|
||||
if (locale === 'fa') return `${baseUrl}/country-4x3/ir.png`;
|
||||
if (locale === 'eo') return `${baseUrl}/esperanto.png`;
|
||||
return `${baseUrl}/country-4x3/${countryCodeOnly(locale).toLowerCase()}.png`;
|
||||
|
||||
@@ -19,7 +19,6 @@ async function main() {
|
||||
const argv = require('yargs').argv;
|
||||
if (!argv.tagName) throw new Error('--tag-name not provided');
|
||||
|
||||
const dryRun = !!argv.dryRun;
|
||||
const pushImages = !!argv.pushImages;
|
||||
const tagName = argv.tagName;
|
||||
const isPreRelease = getIsPreRelease(tagName);
|
||||
@@ -48,13 +47,7 @@ async function main() {
|
||||
console.info('isPreRelease:', isPreRelease);
|
||||
console.info('Docker tags:', dockerTags.join(', '));
|
||||
|
||||
const dockerCommand = `docker build --progress=plain -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`;
|
||||
if (dryRun) {
|
||||
console.info(dockerCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
await execCommand2(dockerCommand);
|
||||
await execCommand2(`docker build --progress=plain -t "joplin/server:${imageVersion}" ${buildArgs} -f Dockerfile.server .`);
|
||||
|
||||
for (const tag of dockerTags) {
|
||||
await execCommand2(`docker tag "joplin/server:${imageVersion}" "joplin/server:${tag}"`);
|
||||
|
||||
@@ -305,7 +305,7 @@ msgstr "Utseende"
|
||||
|
||||
#: packages/lib/models/Setting.ts:2146
|
||||
msgid "Application"
|
||||
msgstr "Program"
|
||||
msgstr "Avslutar programmet"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.tsx:33
|
||||
msgid "Apply"
|
||||
@@ -560,7 +560,7 @@ msgstr ""
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.ts:7
|
||||
msgid "Change application layout"
|
||||
msgstr "Ändra programmets layout"
|
||||
msgstr "Ändra applikationslayout"
|
||||
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.ts:189
|
||||
msgid "Change language"
|
||||
@@ -1001,7 +1001,7 @@ msgstr "Ta bort rad"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1186
|
||||
msgid "Delete local data and re-download from sync target"
|
||||
msgstr "Ta bort lokala data och hämta igen från synkroniseringsmålet"
|
||||
msgstr "Ta bort lokal data och hämta igen från synkroniseringsmålet"
|
||||
|
||||
#: packages/lib/models/Note.ts:760
|
||||
msgid "Delete note \"%s\"?"
|
||||
@@ -1576,7 +1576,7 @@ msgid ""
|
||||
"Fail-safe: Do not wipe out local data when sync target is empty (often the "
|
||||
"result of a misconfiguration or bug)"
|
||||
msgstr ""
|
||||
"Felsäkert: Rensa inte lokala data när synkroniseringsmålet är tomt (beror "
|
||||
"Felsäkert: Rensa inte lokal data när synkroniseringsmålet är tomt (beror "
|
||||
"oftast på felkonfigurering eller en bugg)"
|
||||
|
||||
#: packages/app-cli/app/main.js:95
|
||||
@@ -1888,7 +1888,7 @@ msgstr ""
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:142
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:129
|
||||
msgid "In order to use the web clipper, you need to do the following:"
|
||||
msgstr "För att kunna använda Web Clipper måste du göra följande:"
|
||||
msgstr "För att kunna använda web Clipper måste du göra följande:"
|
||||
|
||||
#: packages/lib/Synchronizer.ts:305
|
||||
msgid "In progress"
|
||||
@@ -2894,7 +2894,7 @@ msgstr "Omkryptering"
|
||||
|
||||
#: packages/lib/models/Setting.ts:1175
|
||||
msgid "Re-upload local data to sync target"
|
||||
msgstr "Ladda upp lokala data igen till synkroniseringsmålet"
|
||||
msgstr "Ladda upp lokal data igen för synkroniseringsmålet"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.tsx:439
|
||||
msgid "Read more about it"
|
||||
@@ -3584,7 +3584,7 @@ msgstr "Programmet har godkänts."
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx:619
|
||||
msgid "The application must be restarted for these changes to take effect."
|
||||
msgstr ""
|
||||
"Programmet måste startas om för att dessa ändringar ska träda i kraft."
|
||||
"Applikationen måste startas om för att dessa ändringar ska träda i kraft."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.tsx:511
|
||||
msgid ""
|
||||
@@ -3611,13 +3611,13 @@ msgid ""
|
||||
"is recommended that you apply it to your data."
|
||||
msgstr ""
|
||||
"Standard krypteringsmetod har ändrats till en säkrare och det är "
|
||||
"rekommenderat att du tillämpar den på dina data."
|
||||
"rekommenderat att du tillämpar den på din data."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.tsx:603
|
||||
msgid ""
|
||||
"The default encryption method has been changed, you should re-encrypt your "
|
||||
"data."
|
||||
msgstr "Standardkrypteringsmetoden har ändrats, du bör omkryptera dina data."
|
||||
msgstr "Standardkrypteringsmetoden har ändrats, du bör omkryptera din data."
|
||||
|
||||
#: packages/lib/models/Setting.ts:1228
|
||||
msgid ""
|
||||
@@ -3756,7 +3756,7 @@ msgstr "Web Clipper behöver din behörighet för att få åtkomst till dina dat
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:84
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:83
|
||||
msgid "The web clipper service is enabled and set to auto-start."
|
||||
msgstr "Web Clipper-tjänsten är aktiverad och inställd för automatisk start."
|
||||
msgstr "Web clipper-tjänsten är aktiverad och inställd för automatisk start."
|
||||
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.min.js:109
|
||||
#: packages/app-desktop/gui/ClipperConfigScreen.tsx:107
|
||||
@@ -3782,7 +3782,7 @@ msgstr ""
|
||||
|
||||
#: packages/lib/services/interop/InteropService_Exporter_Jex.ts:35
|
||||
msgid "There is no data to export."
|
||||
msgstr "Det finns inga data att exportera."
|
||||
msgstr "Det finns ingen data att exportera."
|
||||
|
||||
#: packages/lib/models/Resource.ts:412
|
||||
msgid ""
|
||||
@@ -4387,7 +4387,7 @@ msgid ""
|
||||
"You may use the tool below to re-encrypt your data, for example if you know "
|
||||
"that some of your notes are encrypted with an obsolete encryption method."
|
||||
msgstr ""
|
||||
"Du kan använda verktyget nedan för att omkryptera dina data, exempelvis om du "
|
||||
"Du kan använda verktyget nedan för att omkryptera din data, exempelvis om du "
|
||||
"vill veta om vissa av dina anteckningar är krypterade med en gammal "
|
||||
"krypteringsmetod."
|
||||
|
||||
@@ -4397,7 +4397,7 @@ msgstr "Ditt val: "
|
||||
|
||||
#: packages/lib/components/EncryptionConfigScreen/utils.ts:71
|
||||
msgid "Your data is going to be re-encrypted and synced again."
|
||||
msgstr "Dina data kommer att omkrypteras och synkroniseras igen."
|
||||
msgstr "Din data kommer att omkrypteras och synkroniseras igen."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:271
|
||||
msgid "Your master password is needed to decrypt some of your data."
|
||||
|
||||
@@ -147,7 +147,7 @@ export function execCommand(command: string, options: any = null): Promise<strin
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveRelativePathWithinDir(baseDir: string, ...relativePath: string[]): string {
|
||||
export function resolveRelativePathWithinDir(baseDir: string, ...relativePath: string[]) {
|
||||
const path = require('path');
|
||||
const resolvedBaseDir = path.resolve(baseDir);
|
||||
const resolvedPath = path.resolve(baseDir, ...relativePath);
|
||||
|
||||
@@ -167,10 +167,6 @@ function makeHomePageMd() {
|
||||
// while MarkdownIt doesn't and will in fact display the \. So we remove it here.
|
||||
md = md.replace(/\\\| bash/g, '| bash');
|
||||
|
||||
// We strip-off the donate links because they are added back (with proper
|
||||
// classes and CSS).
|
||||
md = md.replace(donateLinksRegex_, '');
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
@@ -216,19 +212,14 @@ async function main() {
|
||||
const assetUrls = await getAssetUrls();
|
||||
|
||||
const readmeMd = makeHomePageMd();
|
||||
const donateLinksMd = await getDonateLinks();
|
||||
|
||||
// await updateDownloadPage(readmeMd);
|
||||
|
||||
// =============================================================
|
||||
// HELP PAGE
|
||||
// =============================================================
|
||||
|
||||
renderPageToHtml(readmeMd, `${docDir}/help/index.html`, {
|
||||
sourceMarkdownFile: 'README.md',
|
||||
donateLinksMd,
|
||||
partials,
|
||||
sponsors,
|
||||
assetUrls,
|
||||
});
|
||||
renderPageToHtml(readmeMd, `${docDir}/help/index.html`, { sourceMarkdownFile: 'README.md', partials, sponsors, assetUrls });
|
||||
|
||||
// =============================================================
|
||||
// FRONT PAGE
|
||||
@@ -288,6 +279,7 @@ async function main() {
|
||||
|
||||
const mdFiles = glob.sync(`${readmeDir}/**/*.md`).map((f: string) => f.substr(rootDir.length + 1));
|
||||
const sources = [];
|
||||
const donateLinksMd = await getDonateLinks();
|
||||
|
||||
const makeTargetFilePath = (input: string): string => {
|
||||
if (isNewsFile(input)) {
|
||||
|
||||
@@ -21,3 +21,7 @@ If you exceed the storage space, you will not be able to upload new notes. You m
|
||||
We offer a 50% Education Discount for students and teachers. To claim it, please [contact us](mailto:support@joplincloud.com) from your university or school email address. You will then receive a URL you can use to subscribe to Joplin Cloud while benefiting from the 50% discount. This is valid for a whole year and can be renewed for as long as you are in education by contacting us again.
|
||||
|
||||
We may also offer bulk discounts for companies, associations and nonprofit organisations. Please [contact us](mailto:support@joplincloud.com) for more details.
|
||||
|
||||
## Can I get a refund for my subscription?
|
||||
|
||||
Unless you were accidentally charged due to an error, your subscription cannot be refunded. You can cancel it at any time however, which will stop any further charges. We also give a 14 days trial when the subscription starts so that you can evaluate the service and potentially change your mind - if you cancel during that period you will not be charged.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GSoC 2022 Ideas
|
||||
|
||||
2022 is Joplin third round at Google Summer of Code. Detailed information on how to get involved and apply are given in the [general Summer of Code introduction](https://joplinapp.org/gsoc2022/index/)
|
||||
2022 is Joplin second round at Google Summer of Code. Detailed information on how to get involved and apply are given in the [general Summer of Code introduction](https://joplinapp.org/gsoc2022/index/)
|
||||
|
||||
**These are all proposals! We are open to new ideas you might have!!** Do you have an awesome idea you want to work on with Joplin but that is not among the ideas below? That's cool. We love that! But please do us a favour: Get in touch with a mentor early on and make sure your project is realistic and within the scope of Joplin. Just make sure your idea is within this year's theme:
|
||||
|
||||
@@ -81,75 +81,6 @@ Difficulty level: High
|
||||
|
||||
Skills Required: Typescript, Javascript, CSS, HTML, Markdown rendering. You will also need to learn about TinyMCE if you're not already familiar with it.
|
||||
|
||||
## 7. Improve PDF export
|
||||
|
||||
Joplin uses Chrome's built-in print to PDF function which is very limited. This can be improved by using a 3rd party library to convert notes to PDF. Applies to desktop and CLI versions.
|
||||
|
||||
Potential benefits:
|
||||
* Export multiple notes as a single PDF
|
||||
* Embedding attachments (see https://github.com/laurent22/joplin/issues/5943)
|
||||
* Delay export until the note is fully rendered (https://discourse.joplinapp.org/t/ability-to-delay-pdf-export-to-allow-plugins-to-render/22159)
|
||||
|
||||
Difficulty level: Medium
|
||||
|
||||
Skills Required: Typescript, Javascript.
|
||||
|
||||
## 8. Replace built-in PDF renderer with a library
|
||||
|
||||
Just like with export, Joplin relies on the built-in PDF renderer to show PDF attachments. Replacing it with a 3rd-party library has a number of advantages:
|
||||
* Joplin can preserve PDF viewer state when a note is re-rendered. For instance currently after opening and closing settings, PDF are reset to the 1st page.
|
||||
* It may be possible to link to a specific page or even a location within a PDF document.
|
||||
* Annotate PDF documents from Joplin
|
||||
|
||||
Difficulty level: Medium
|
||||
|
||||
Skills Required: Typescript, Javascript.
|
||||
|
||||
## 9. Rebuild file system sync on Android
|
||||
|
||||
A recent update broke file system synchronization on Android, as applications are now required to use a new API to access storage. Currently there are no libraries that would proxy this API for React Native. If we want to get file system sync working again it has to be written from scratch.
|
||||
|
||||
Difficulty level: High
|
||||
|
||||
Skills Required: Android, Java/Kotlin, Typescript.
|
||||
|
||||
## 10. Tablet layout
|
||||
|
||||
On wide screens devices like tables Joplin could use a different layout, e.g. with note list always showing, or have both editor and viewer visible at the same time.
|
||||
|
||||
Difficulty Level: Medium
|
||||
|
||||
Skills Required: React, Typescript, CSS.
|
||||
|
||||
## 11. Improve plugin search and discoverability
|
||||
|
||||
As there are more and more plugins it would be good to improve how they are discovered, and to improve search - in particular improve search relevance. We are open to hear ideas about this, but a few things that could be done, for example are:
|
||||
|
||||
- Improve the [page that lists all the plugin](https://github.com/joplin/plugins#readme) by adding a download count (based on stats.json) and make the list sortable by download count.
|
||||
- In the app, use the info from stats.json to order the plugin - those with more downloads going on top for example
|
||||
- Create a dynamically generated page (using GitHub Actions) under joplinapp.org that shows some recommended plugins, trending plugins, etc. similar to [Add-ons for Firefox](https://addons.mozilla.org/en-GB/firefox/)
|
||||
|
||||
Those are just ideas and we're open to hearing more from you.
|
||||
|
||||
Difficulty Level: Medium
|
||||
|
||||
Skills Required: Typescript, CSS, GitHub Actions.
|
||||
|
||||
|
||||
## 12. Email plugin
|
||||
|
||||
Create a plugin to fetch mail via IMAP and convert messages to notes (including attachments). The plugin should be able to filter what messages it donwloads, e.g. based on the folder.
|
||||
|
||||
Additional features to consider:
|
||||
- support more than one account
|
||||
- convert HTML to Markdown
|
||||
- delete/move received emails
|
||||
|
||||
Difficulty Level: Medium
|
||||
|
||||
Skills Required: TypeScript, JavaScript.
|
||||
|
||||
|
||||
# More info
|
||||
|
||||
- Make sure you read the [Joplin Google Summer of Code Introduction](https://joplinapp.org/gsoc2022/index/)
|
||||
|
||||
Reference in New Issue
Block a user