You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-26 23:38:08 +02:00
Compare commits
7 Commits
smaller_se
...
close-stal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afd63aa3c8 | ||
|
|
04c3c218b9 | ||
|
|
088ae44c63 | ||
|
|
4aa9339fbb | ||
|
|
7edcbc5c27 | ||
|
|
f3650097a0 | ||
|
|
6a4326e2db |
@@ -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
|
||||
|
||||
1
.github/workflows/close-stale-issues.yml
vendored
1
.github/workflows/close-stale-issues.yml
vendored
@@ -15,7 +15,6 @@ jobs:
|
||||
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.'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
# ----------------------------------------------------------------------------------
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -121,35 +121,6 @@ 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