You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
22 Commits
docker_ser
...
v1.7.2
Author | SHA1 | Date | |
---|---|---|---|
|
ad34cee234 | ||
|
ddf3e16ff0 | ||
|
4e2e26f033 | ||
|
f37d37e613 | ||
|
63e30f6ccb | ||
|
9c718baf61 | ||
|
e8ae29adf2 | ||
|
0c67805626 | ||
|
e91c94baca | ||
|
aa74d05b24 | ||
|
9a1f7f227d | ||
|
40779d09c0 | ||
|
351d8d94c5 | ||
|
6addd52ba4 | ||
|
ea5849855f | ||
|
c81529dc45 | ||
|
b6d7971691 | ||
|
446db2d688 | ||
|
52da072f9a | ||
|
0d2bf6d787 | ||
|
59fe4a2193 | ||
|
df6d146c84 |
@@ -6,4 +6,5 @@ packages/app-desktop
|
||||
packages/app-cli
|
||||
packages/app-mobile
|
||||
packages/app-clipper
|
||||
packages/generator-joplin
|
||||
packages/generator-joplin
|
||||
packages/plugin-repo-cli
|
29
.env-sample
29
.env-sample
@@ -1,9 +1,26 @@
|
||||
# Example of local config, for development:
|
||||
# =============================================================================
|
||||
# PRODUCTION CONFIG EXAMPLE
|
||||
# -----------------------------------------------------------------------------
|
||||
# By default it will use SQLite, but that's mostly to test and evaluate the
|
||||
# server. So you'll want to specify db connection settings to use Postgres.
|
||||
# =============================================================================
|
||||
#
|
||||
# JOPLIN_BASE_URL=http://localhost:22300
|
||||
# JOPLIN_PORT=22300
|
||||
# APP_BASE_URL=https://example.com/joplin
|
||||
# APP_PORT=22300
|
||||
#
|
||||
# DB_CLIENT=pg
|
||||
# POSTGRES_PASSWORD=joplin
|
||||
# POSTGRES_DATABASE=joplin
|
||||
# POSTGRES_USER=joplin
|
||||
# POSTGRES_PORT=5432
|
||||
# POSTGRES_HOST=localhost
|
||||
|
||||
# Example of config for production:
|
||||
# =============================================================================
|
||||
# DEV CONFIG EXAMPLE
|
||||
# -----------------------------------------------------------------------------
|
||||
# Example of local config, for development. In dev mode, you would usually use
|
||||
# SQLite so database settings are not needed.
|
||||
# =============================================================================
|
||||
#
|
||||
# JOPLIN_BASE_URL=https://example.com/joplin
|
||||
# JOPLIN_PORT=22300
|
||||
# APP_BASE_URL=http://localhost:22300
|
||||
# APP_PORT=22300
|
||||
|
@@ -111,6 +111,9 @@ packages/app-cli/tests/models_Note.js.map
|
||||
packages/app-cli/tests/models_Setting.d.ts
|
||||
packages/app-cli/tests/models_Setting.js
|
||||
packages/app-cli/tests/models_Setting.js.map
|
||||
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
|
||||
packages/app-cli/tests/services/plugins/RepositoryApi.js
|
||||
packages/app-cli/tests/services/plugins/RepositoryApi.js.map
|
||||
packages/app-cli/tests/services/plugins/api/JoplinSettings.d.ts
|
||||
packages/app-cli/tests/services/plugins/api/JoplinSettings.js
|
||||
packages/app-cli/tests/services/plugins/api/JoplinSettings.js.map
|
||||
@@ -1371,6 +1374,12 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/plugin-repo-cli/dummy.test.d.ts
|
||||
packages/plugin-repo-cli/dummy.test.js
|
||||
packages/plugin-repo-cli/dummy.test.js.map
|
||||
packages/plugin-repo-cli/index.d.ts
|
||||
packages/plugin-repo-cli/index.js
|
||||
packages/plugin-repo-cli/index.js.map
|
||||
packages/plugins/ToggleSidebars/api/index.d.ts
|
||||
packages/plugins/ToggleSidebars/api/index.js
|
||||
packages/plugins/ToggleSidebars/api/index.js.map
|
||||
@@ -1461,21 +1470,6 @@ packages/renderer/utils.js.map
|
||||
packages/server/src/app.d.ts
|
||||
packages/server/src/app.js
|
||||
packages/server/src/app.js.map
|
||||
packages/server/src/config-base.d.ts
|
||||
packages/server/src/config-base.js
|
||||
packages/server/src/config-base.js.map
|
||||
packages/server/src/config-buildTypes.d.ts
|
||||
packages/server/src/config-buildTypes.js
|
||||
packages/server/src/config-buildTypes.js.map
|
||||
packages/server/src/config-dev.d.ts
|
||||
packages/server/src/config-dev.js
|
||||
packages/server/src/config-dev.js.map
|
||||
packages/server/src/config-prod.d.ts
|
||||
packages/server/src/config-prod.js
|
||||
packages/server/src/config-prod.js.map
|
||||
packages/server/src/config-tests.d.ts
|
||||
packages/server/src/config-tests.js
|
||||
packages/server/src/config-tests.js.map
|
||||
packages/server/src/config.d.ts
|
||||
packages/server/src/config.js
|
||||
packages/server/src/config.js.map
|
||||
@@ -1686,10 +1680,13 @@ packages/server/src/utils/urlUtils.js.map
|
||||
packages/server/src/utils/uuidgen.d.ts
|
||||
packages/server/src/utils/uuidgen.js
|
||||
packages/server/src/utils/uuidgen.js.map
|
||||
packages/tools/build-plugin-repository.d.ts
|
||||
packages/tools/build-plugin-repository.js
|
||||
packages/tools/build-plugin-repository.js.map
|
||||
packages/tools/lerna-add.d.ts
|
||||
packages/tools/lerna-add.js
|
||||
packages/tools/lerna-add.js.map
|
||||
packages/tools/release-server.d.ts
|
||||
packages/tools/release-server.js
|
||||
packages/tools/release-server.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
|
33
.gitignore
vendored
33
.gitignore
vendored
@@ -99,6 +99,9 @@ packages/app-cli/tests/models_Note.js.map
|
||||
packages/app-cli/tests/models_Setting.d.ts
|
||||
packages/app-cli/tests/models_Setting.js
|
||||
packages/app-cli/tests/models_Setting.js.map
|
||||
packages/app-cli/tests/services/plugins/RepositoryApi.d.ts
|
||||
packages/app-cli/tests/services/plugins/RepositoryApi.js
|
||||
packages/app-cli/tests/services/plugins/RepositoryApi.js.map
|
||||
packages/app-cli/tests/services/plugins/api/JoplinSettings.d.ts
|
||||
packages/app-cli/tests/services/plugins/api/JoplinSettings.js
|
||||
packages/app-cli/tests/services/plugins/api/JoplinSettings.js.map
|
||||
@@ -1359,6 +1362,12 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/plugin-repo-cli/dummy.test.d.ts
|
||||
packages/plugin-repo-cli/dummy.test.js
|
||||
packages/plugin-repo-cli/dummy.test.js.map
|
||||
packages/plugin-repo-cli/index.d.ts
|
||||
packages/plugin-repo-cli/index.js
|
||||
packages/plugin-repo-cli/index.js.map
|
||||
packages/plugins/ToggleSidebars/api/index.d.ts
|
||||
packages/plugins/ToggleSidebars/api/index.js
|
||||
packages/plugins/ToggleSidebars/api/index.js.map
|
||||
@@ -1449,21 +1458,6 @@ packages/renderer/utils.js.map
|
||||
packages/server/src/app.d.ts
|
||||
packages/server/src/app.js
|
||||
packages/server/src/app.js.map
|
||||
packages/server/src/config-base.d.ts
|
||||
packages/server/src/config-base.js
|
||||
packages/server/src/config-base.js.map
|
||||
packages/server/src/config-buildTypes.d.ts
|
||||
packages/server/src/config-buildTypes.js
|
||||
packages/server/src/config-buildTypes.js.map
|
||||
packages/server/src/config-dev.d.ts
|
||||
packages/server/src/config-dev.js
|
||||
packages/server/src/config-dev.js.map
|
||||
packages/server/src/config-prod.d.ts
|
||||
packages/server/src/config-prod.js
|
||||
packages/server/src/config-prod.js.map
|
||||
packages/server/src/config-tests.d.ts
|
||||
packages/server/src/config-tests.js
|
||||
packages/server/src/config-tests.js.map
|
||||
packages/server/src/config.d.ts
|
||||
packages/server/src/config.js
|
||||
packages/server/src/config.js.map
|
||||
@@ -1674,10 +1668,13 @@ packages/server/src/utils/urlUtils.js.map
|
||||
packages/server/src/utils/uuidgen.d.ts
|
||||
packages/server/src/utils/uuidgen.js
|
||||
packages/server/src/utils/uuidgen.js.map
|
||||
packages/tools/build-plugin-repository.d.ts
|
||||
packages/tools/build-plugin-repository.js
|
||||
packages/tools/build-plugin-repository.js.map
|
||||
packages/tools/lerna-add.d.ts
|
||||
packages/tools/lerna-add.js
|
||||
packages/tools/lerna-add.js.map
|
||||
packages/tools/release-server.d.ts
|
||||
packages/tools/release-server.js
|
||||
packages/tools/release-server.js.map
|
||||
packages/tools/tool-utils.d.ts
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/tool-utils.js.map
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
|
@@ -1,3 +0,0 @@
|
||||
FROM postgres:13.1
|
||||
|
||||
EXPOSE 5432
|
@@ -16,8 +16,15 @@ WORKDIR /home/$user
|
||||
|
||||
RUN mkdir /home/$user/logs
|
||||
|
||||
# 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 ./
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
# 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
|
||||
# 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
|
||||
@@ -27,19 +34,10 @@ RUN mkdir /home/$user/logs
|
||||
# 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 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)
|
||||
|
||||
RUN npm install --ignore-scripts
|
||||
|
||||
COPY --chown=$user:$user packages/fork-sax/package*.json ./packages/fork-sax/
|
||||
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
|
||||
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/server/package*.json ./packages/server/
|
||||
COPY --chown=$user:$user packages/lib/package*.json ./packages/lib/
|
||||
COPY --chown=$user:$user lerna.json .
|
||||
COPY --chown=$user:$user tsconfig.json .
|
||||
|
||||
@@ -50,22 +48,29 @@ 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
|
||||
|
||||
RUN ls -la /home/$user
|
||||
|
||||
# Then bootstrap only, without compiling the TypeScript files
|
||||
|
||||
RUN npm run bootstrap
|
||||
|
||||
# We have a separate step for the server files because they are more likely to
|
||||
# change.
|
||||
|
||||
COPY --chown=$user:$user packages/server/package*.json ./packages/server/
|
||||
RUN npm run bootstrapServerOnly
|
||||
|
||||
# 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/lib ./packages/lib
|
||||
COPY --chown=$user:$user packages/renderer ./packages/renderer
|
||||
COPY --chown=$user:$user packages/tools ./packages/tools
|
||||
COPY --chown=$user:$user packages/lib ./packages/lib
|
||||
COPY --chown=$user:$user packages/server ./packages/server
|
||||
|
||||
# Finally build everything, in particular the TypeScript files.
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE ${JOPLIN_PORT}
|
||||
ENV RUNNING_IN_DOCKER=1
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
CMD [ "npm", "--prefix", "packages/server", "start" ]
|
||||
|
15
docker-compose.db-dev.yml
Normal file
15
docker-compose.db-dev.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# For development this compose file starts the database only. The app can then
|
||||
# be started using `npm run start-dev`, which is useful for development, because
|
||||
# it means the app Docker file doesn't have to be rebuilt on each change.
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:13.1
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=joplin
|
||||
- POSTGRES_USER=joplin
|
||||
- POSTGRES_DB=joplin
|
@@ -1,28 +1,27 @@
|
||||
# For development, the easiest might be to only start the Postgres container and
|
||||
# run the app directly with `npm start`. Or use sqlite3.
|
||||
# This compose file can be used in development to run both the database and app
|
||||
# within Docker.
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
# app:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile.server-dev
|
||||
# ports:
|
||||
# - "22300:22300"
|
||||
# # volumes:
|
||||
# # - ./packages/server/:/var/www/joplin/packages/server/
|
||||
# # - /var/www/joplin/packages/server/node_modules/
|
||||
db:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.db
|
||||
dockerfile: Dockerfile.server
|
||||
ports:
|
||||
- "22300:22300"
|
||||
environment:
|
||||
- DB_CLIENT=pg
|
||||
- POSTGRES_PASSWORD=joplin
|
||||
- POSTGRES_DATABASE=joplin
|
||||
- POSTGRES_USER=joplin
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_HOST=localhost
|
||||
db:
|
||||
image: postgres:13.1
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
# TODO: Considering the database is only exposed to the
|
||||
# application, and not to the outside world, is there a need to
|
||||
# pick a secure password?
|
||||
- POSTGRES_PASSWORD=joplin
|
||||
- POSTGRES_USER=joplin
|
||||
- POSTGRES_DB=joplin
|
||||
- POSTGRES_DB=joplin
|
||||
|
@@ -1,40 +1,34 @@
|
||||
# This is a sample docker-compose file that can be used to run Joplin Server
|
||||
# along with a PostgreSQL server.
|
||||
#
|
||||
# All environment variables are optional. If you don't set them, you will get a
|
||||
# warning from docker-compose, however the app should use working defaults.
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
- JOPLIN_BASE_URL=${JOPLIN_BASE_URL}
|
||||
- JOPLIN_PORT=${JOPLIN_PORT}
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.server
|
||||
ports:
|
||||
- "${JOPLIN_PORT}:${JOPLIN_PORT}"
|
||||
# volumes:
|
||||
# # Mount the server directory so that it's possible to edit file
|
||||
# # while the container is running. However don't mount the
|
||||
# # node_modules directory which will be specific to the Docker
|
||||
# # image (eg native modules will be built for Ubuntu, while the
|
||||
# # container might be running in Windows)
|
||||
# # https://stackoverflow.com/a/37898591/561309
|
||||
# - ./packages/server:/home/joplin/packages/server
|
||||
# - /home/joplin/packages/server/node_modules/
|
||||
db:
|
||||
restart: unless-stopped
|
||||
# By default, the Postgres image saves the data to a Docker volume,
|
||||
# so it persists whenever the server is restarted using
|
||||
# `docker-compose up`. Note that it would however be deleted when
|
||||
# running `docker-compose down`.
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.db
|
||||
image: postgres:13.1
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# TODO: Considering the database is only exposed to the
|
||||
# application, and not to the outside world, is there a need to
|
||||
# pick a secure password?
|
||||
- POSTGRES_PASSWORD=joplin
|
||||
- POSTGRES_USER=joplin
|
||||
- POSTGRES_DB=joplin
|
||||
- APP_PORT=22300
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_DB=${POSTGRES_DATABASE}
|
||||
app:
|
||||
image: joplin/server:latest
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "22300:22300"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_BASE_URL=${APP_BASE_URL}
|
||||
- DB_CLIENT=pg
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DATABASE=${POSTGRES_DATABASE}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT}
|
||||
- POSTGRES_HOST=db
|
@@ -8,12 +8,12 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap --no-ci",
|
||||
"bootstrapServerOnly": "lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/server",
|
||||
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
|
||||
"build": "lerna run build && npm run tsc",
|
||||
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildDoc": "./packages/tools/build-all.sh",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
"buildPluginRepo": "node packages/tools/build-plugin-repository.js",
|
||||
"buildTranslations": "npm run tsc && node packages/tools/build-translation.js",
|
||||
"buildTranslationsNoTsc": "node packages/tools/build-translation.js",
|
||||
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc",
|
||||
@@ -25,7 +25,7 @@
|
||||
"linter-precommit": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"postinstall": "npm run bootstrap --no-ci && npm run build",
|
||||
"publishAll": "git pull && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"publishAll": "git pull && npm run build && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroidClean": "node packages/tools/release-android.js",
|
||||
"releaseAndroid": "export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
|
||||
"releaseCli": "node packages/tools/release-cli.js",
|
||||
|
@@ -1,22 +1,8 @@
|
||||
const gulp = require('gulp');
|
||||
const fs = require('fs-extra');
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
const { setPackagePrivateField } = require('@joplin/tools/tool-utils');
|
||||
const tasks = {
|
||||
// compileExtensions: {
|
||||
// fn: require('../Tools/gulp/tasks/compileExtensions.js'),
|
||||
// },
|
||||
// copyLib: require('../Tools/gulp/tasks/copyLib'),
|
||||
// tsc: require('../Tools/gulp/tasks/tsc'),
|
||||
// updateIgnoredTypeScriptBuild: require('../Tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
};
|
||||
|
||||
// async function makePackagePublic(filePath) {
|
||||
// const text = await fs.readFile(filePath, 'utf8');
|
||||
// const obj = JSON.parse(text);
|
||||
// delete obj.private;
|
||||
// await fs.writeFile(filePath, JSON.stringify(obj), 'utf8');
|
||||
// }
|
||||
const tasks = {};
|
||||
|
||||
tasks.prepareBuild = {
|
||||
fn: async () => {
|
||||
@@ -26,8 +12,7 @@ tasks.prepareBuild = {
|
||||
});
|
||||
|
||||
await utils.copyFile(`${__dirname}/package.json`, `${buildDir}/package.json`);
|
||||
// await makePackagePublic(`${buildDir}/package.json`);
|
||||
await setPackagePrivateField(`${buildDir}/package.json`, false);
|
||||
await utils.setPackagePrivateField(`${buildDir}/package.json`, false);
|
||||
|
||||
await utils.copyFile(`${__dirname}/package-lock.json`, `${buildDir}/package-lock.json`);
|
||||
await utils.copyFile(`${__dirname}/gulpfile.js`, `${buildDir}/gulpfile.js`);
|
||||
@@ -50,14 +35,6 @@ tasks.prepareTestBuild = {
|
||||
],
|
||||
});
|
||||
|
||||
// const rootDir = utils.rootDir();
|
||||
|
||||
// await utils.copyDir(`${rootDir}/packages/app-mobile/lib`, `${testBuildDir}/lib`, {
|
||||
// excluded: [
|
||||
// `${rootDir}/packages/renderer/node_modules`,
|
||||
// ],
|
||||
// });
|
||||
// await utils.copyDir(`${rootDir}/packages/app-mobile/locales`, `${testBuildDir}/locales`);
|
||||
await fs.mkdirp(`${testBuildDir}/data`);
|
||||
},
|
||||
};
|
||||
@@ -67,12 +44,4 @@ utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
gulp.task('build', gulp.series([
|
||||
'prepareBuild',
|
||||
// 'compileExtensions',
|
||||
// 'copyLib',
|
||||
]));
|
||||
|
||||
// gulp.task('buildTests', gulp.series([
|
||||
// // 'prepareTestBuild',
|
||||
// // 'compileExtensions',
|
||||
// // 'copyLib',
|
||||
// ]));
|
||||
|
@@ -27,17 +27,22 @@ describe('InMemoryCache', function() {
|
||||
await time.msleep(510);
|
||||
expect(cache.value('test')).toBe(undefined);
|
||||
|
||||
// Check that the TTL is reset every time setValue is called
|
||||
cache.setValue('test', 'something', 300);
|
||||
await time.msleep(100);
|
||||
cache.setValue('test', 'something', 300);
|
||||
await time.msleep(100);
|
||||
cache.setValue('test', 'something', 300);
|
||||
await time.msleep(100);
|
||||
cache.setValue('test', 'something', 300);
|
||||
await time.msleep(100);
|
||||
// This test can sometimes fail in some cases, probably because it
|
||||
// sleeps for more than 100ms (when the computer is slow). Changing this
|
||||
// to use higher values would slow down the test unit too much, so let's
|
||||
// disable it for now.
|
||||
|
||||
expect(cache.value('test')).toBe('something');
|
||||
// Check that the TTL is reset every time setValue is called
|
||||
// cache.setValue('test', 'something', 300);
|
||||
// await time.msleep(100);
|
||||
// cache.setValue('test', 'something', 300);
|
||||
// await time.msleep(100);
|
||||
// cache.setValue('test', 'something', 300);
|
||||
// await time.msleep(100);
|
||||
// cache.setValue('test', 'something', 300);
|
||||
// await time.msleep(100);
|
||||
|
||||
// expect(cache.value('test')).toBe('something');
|
||||
});
|
||||
|
||||
it('should delete old records', async () => {
|
||||
|
56
packages/app-cli/tests/services/plugins/RepositoryApi.ts
Normal file
56
packages/app-cli/tests/services/plugins/RepositoryApi.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, supportDir, createTempDir } from '../../test-utils';
|
||||
|
||||
async function newRepoApi(): Promise<RepositoryApi> {
|
||||
const repo = new RepositoryApi(`${supportDir}/pluginRepo`, await createTempDir());
|
||||
await repo.loadManifests();
|
||||
return repo;
|
||||
}
|
||||
|
||||
describe('services_plugins_RepositoryApi', function() {
|
||||
|
||||
beforeEach(async (done: Function) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should get the manifests', (async () => {
|
||||
const api = await newRepoApi();
|
||||
const manifests = await api.manifests();
|
||||
expect(!!manifests.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
|
||||
expect(!!manifests.find(m => m.id === 'org.joplinapp.plugins.ToggleSidebars')).toBe(true);
|
||||
}));
|
||||
|
||||
it('should search', (async () => {
|
||||
const api = await newRepoApi();
|
||||
|
||||
{
|
||||
const results = await api.search('to');
|
||||
expect(results.length).toBe(2);
|
||||
expect(!!results.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
|
||||
expect(!!results.find(m => m.id === 'org.joplinapp.plugins.ToggleSidebars')).toBe(true);
|
||||
}
|
||||
|
||||
{
|
||||
const results = await api.search('backlink');
|
||||
expect(results.length).toBe(1);
|
||||
expect(!!results.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should download a plugin', (async () => {
|
||||
const api = await newRepoApi();
|
||||
const pluginPath = await api.downloadPlugin('org.joplinapp.plugins.ToggleSidebars');
|
||||
expect(await shim.fsDriver().exists(pluginPath)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should tell if a plugin can be updated', (async () => {
|
||||
const api = await newRepoApi();
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.0')).toBe(true);
|
||||
expect(await api.pluginCanBeUpdated('org.joplinapp.plugins.ToggleSidebars', '1.0.2')).toBe(false);
|
||||
expect(await api.pluginCanBeUpdated('does.not.exist', '1.0.0')).toBe(false);
|
||||
}));
|
||||
|
||||
});
|
25
packages/app-cli/tests/support/pluginRepo/README.md
Normal file
25
packages/app-cli/tests/support/pluginRepo/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Joplin Plugin Repository
|
||||
|
||||
This is the official Joplin Plugin Repository
|
||||
|
||||
## Installation
|
||||
|
||||
To install any of these plugins, open the desktop application, then go to the "Plugins" section in the Configuration screen. You can then search for any plugin and install it from there.
|
||||
|
||||
## Plugins
|
||||
|
||||
This repository contains the following plugins:
|
||||
|
||||
<!-- PLUGIN_LIST -->
|
||||
| Name | Version | Description | Author
|
||||
--- | --- | --- | --- | ---
|
||||
[🏠](https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632) | Backlinks to note | 1.0.4 | Creates backlinks to opened note | a
|
||||
[🏠](https://github.com/JackGruber/joplin-plugin-combine-notes) | Combine notes | 0.2.1 | Combine one or more notes | JackGruber
|
||||
[🏠](https://github.com/JackGruber/joplin-plugin-copytags) | Copy Tags | 0.3.2 | Plugin to extend the Joplin tagging menu with a coppy all tags and tagging list with more control. | JackGruber
|
||||
[🏠](https://discourse.joplinapp.org/t/go-to-note-tag-or-notebook-via-highlighting-text-in-editor/12731) | Create and go to #tags and @notebooks | 1.3.4 | Go to tag,notebook or note via links or via text | a
|
||||
[🏠](https://github.com/benji300/joplin-favorites) | Favorites | 1.0.0 | Save any notebook, note, to-do, tag, or search as favorite in an extra panel view for quick access. (v1.0.0) | Benji300
|
||||
[🏠](https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars) | Note list and side bar toggle buttons | 1.0.2 | Adds buttons to toggle note list and sidebar | Laurent Cozic
|
||||
[🏠](https://github.com/JackGruber/joplin-plugin-note-overview) | Note overview | 1.0.0 | A note overview is created based on the defined search and the specified fields | JackGruber
|
||||
[🏠](https://github.com/benji300/joplin-note-tabs) | Note Tabs | 1.1.1 | Allows to open several notes at once in tabs and pin them. (v1.1.1) | Benji300
|
||||
[🏠](https://github.com/JackGruber/joplin-plugin-backup) | Simple Backup | 0.3.0 | Plugin to create manual and automatic backups | JackGruber
|
||||
<!-- PLUGIN_LIST -->
|
29
packages/app-cli/tests/support/pluginRepo/manifests.json
Normal file
29
packages/app-cli/tests/support/pluginRepo/manifests.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"joplin.plugin.ambrt.backlinksToNote": {
|
||||
"manifest_version": 1,
|
||||
"id": "joplin.plugin.ambrt.backlinksToNote",
|
||||
"app_min_version": "1.5",
|
||||
"version": "1.0.4",
|
||||
"name": "Backlinks to note",
|
||||
"description": "Creates backlinks to opened note",
|
||||
"author": "a",
|
||||
"homepage_url": "https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632",
|
||||
"_publish_hash": "sha256:ab9c9bc776e167a3b1a9ec40a4927d93da14514c0508f46dd6e5ea3f8a3a6c3b",
|
||||
"_publish_commit": "master:5b74f93c687463572c46200292fa911e0ba96033",
|
||||
"_npm_package_name": "joplin-plugin-backlinks"
|
||||
},
|
||||
"org.joplinapp.plugins.ToggleSidebars": {
|
||||
"manifest_version": 1,
|
||||
"id": "org.joplinapp.plugins.ToggleSidebars",
|
||||
"app_min_version": "1.6",
|
||||
"version": "1.0.2",
|
||||
"name": "Note list and side bar toggle buttons",
|
||||
"description": "Adds buttons to toggle note list and sidebar",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
|
||||
"repository_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
|
||||
"_publish_hash": "sha256:e0d833b7ef1bb8f02ee4cb861ef1989621358c45a5614911071302dc0527a3b4",
|
||||
"_publish_commit": "dev:1b5b2342fc25717b77ad9f1627c1a334e5bbae54",
|
||||
"_npm_package_name": "@joplin/joplin-plugin-toggle-sidebars"
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "joplin.plugin.ambrt.backlinksToNote",
|
||||
"app_min_version": "1.5",
|
||||
"version": "1.0.4",
|
||||
"name": "Backlinks to note",
|
||||
"description": "Creates backlinks to opened note",
|
||||
"author": "a",
|
||||
"homepage_url": "https://discourse.joplinapp.org/t/insert-referencing-notes-backlinks-plugin/13632",
|
||||
"_publish_hash": "sha256:ab9c9bc776e167a3b1a9ec40a4927d93da14514c0508f46dd6e5ea3f8a3a6c3b",
|
||||
"_publish_commit": "master:5b74f93c687463572c46200292fa911e0ba96033",
|
||||
"_npm_package_name": "joplin-plugin-backlinks"
|
||||
}
|
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "org.joplinapp.plugins.ToggleSidebars",
|
||||
"app_min_version": "1.6",
|
||||
"version": "1.0.2",
|
||||
"name": "Note list and side bar toggle buttons",
|
||||
"description": "Adds buttons to toggle note list and sidebar",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
|
||||
"repository_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",
|
||||
"_publish_hash": "sha256:e0d833b7ef1bb8f02ee4cb861ef1989621358c45a5614911071302dc0527a3b4",
|
||||
"_publish_commit": "dev:1b5b2342fc25717b77ad9f1627c1a334e5bbae54",
|
||||
"_npm_package_name": "@joplin/joplin-plugin-toggle-sidebars"
|
||||
}
|
Binary file not shown.
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"app_min_version": "1.4",
|
||||
"name": "Register Command Test",
|
||||
"description": "To test registering commands",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage_url": "https://joplinapp.org"
|
||||
}
|
||||
|
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# - Update src/manifest.json with the new version number
|
||||
# - Run the below command
|
||||
# - Then the file /manifests.json also needs to be updated with the new manifest file
|
||||
|
||||
npm run dist && cp publish/org.joplinapp.plugins.RegisterCommandDemo.jpl ~/src/joplin-plugins-test/plugins/org.joplinapp.plugins.RegisterCommandDemo/plugin.jpl && cp publish/org.joplinapp.plugins.RegisterCommandDemo.json ~/src/joplin-plugins-test/plugins/org.joplinapp.plugins.RegisterCommandDemo/plugin.json
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -233,7 +233,7 @@ function main(processArgv) {
|
||||
const configs = {
|
||||
// Builds the main src/index.ts and copy the extra content from /src to
|
||||
// /dist including scripts, CSS and any other asset.
|
||||
buildMain: pluginConfig,
|
||||
buildMain: [pluginConfig],
|
||||
|
||||
// Builds the extra scripts as defined in plugin.config.json. When doing
|
||||
// so, some JavaScript files that were copied in the previous might be
|
||||
@@ -247,7 +247,7 @@ function main(processArgv) {
|
||||
// run without this. So we give it an entry that we know is going to
|
||||
// exist and output in the publish dir. Then the plugin will delete this
|
||||
// temporary file before packaging the plugin.
|
||||
createArchive: createArchiveConfig,
|
||||
createArchive: [createArchiveConfig],
|
||||
};
|
||||
|
||||
// If we are running the first config step, we clean up and create the build
|
||||
@@ -270,4 +270,10 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@@ -104,6 +104,7 @@ FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
const logDir = `${__dirname}/../tests/logs`;
|
||||
const baseTempDir = `${__dirname}/../tests/tmp/${suiteName_}`;
|
||||
const supportDir = `${__dirname}/support`;
|
||||
|
||||
// We add a space in the data directory path as that will help uncover
|
||||
// various space-in-path issues.
|
||||
@@ -180,6 +181,7 @@ BaseItem.loadClass('Revision', Revision);
|
||||
Setting.setConstant('appId', 'net.cozic.joplintest-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
Setting.setConstant('tempDir', baseTempDir);
|
||||
Setting.setConstant('cacheDir', baseTempDir);
|
||||
Setting.setConstant('env', 'dev');
|
||||
|
||||
BaseService.logger_ = logger;
|
||||
@@ -864,4 +866,4 @@ class TestApp extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { waitForFolderCount, afterAllCleanUp, exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
|
||||
export { supportDir, waitForFolderCount, afterAllCleanUp, exportDir, newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
|
||||
|
@@ -78,7 +78,7 @@ const commands = [
|
||||
require('./gui/NoteEditor/commands/showRevisions'),
|
||||
require('./gui/NoteList/commands/focusElementNoteList'),
|
||||
require('./gui/NoteListControls/commands/focusSearch'),
|
||||
require('./gui/SideBar/commands/focusElementSideBar'),
|
||||
require('./gui/Sidebar/commands/focusElementSideBar'),
|
||||
];
|
||||
|
||||
// Commands that are not tied to any particular component.
|
||||
@@ -527,7 +527,7 @@ class Application extends BaseApplication {
|
||||
// time, however we only effectively uninstall the plugin the next
|
||||
// time the app is started. What plugin should be uninstalled is
|
||||
// stored in the settings.
|
||||
const newSettings = await service.uninstallPlugins(pluginSettings);
|
||||
const newSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings));
|
||||
Setting.setValue('plugins.states', newSettings);
|
||||
|
||||
try {
|
||||
|
@@ -7,6 +7,7 @@ export enum ButtonLevel {
|
||||
Secondary = 'secondary',
|
||||
Tertiary = 'tertiary',
|
||||
SidebarSecondary = 'sidebarSecondary',
|
||||
Recommended = 'recommended',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -121,6 +122,20 @@ const StyledButtonTertiary = styled(StyledButtonBase)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonRecommended = styled(StyledButtonBase)`
|
||||
border: 1px solid ${(props: any) => props.theme.borderColor4};
|
||||
background-color: ${(props: any) => props.theme.warningBackgroundColor};
|
||||
|
||||
${StyledIcon} {
|
||||
color: ${(props: any) => props.theme.color};
|
||||
}
|
||||
|
||||
${StyledTitle} {
|
||||
color: ${(props: any) => props.theme.color};
|
||||
opacity: 0.9;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonSidebarSecondary = styled(StyledButtonBase)`
|
||||
background: none;
|
||||
border-color: ${(props: any) => props.theme.color2};
|
||||
@@ -167,10 +182,11 @@ function buttonClass(level: ButtonLevel) {
|
||||
if (level === ButtonLevel.Primary) return StyledButtonPrimary;
|
||||
if (level === ButtonLevel.Tertiary) return StyledButtonTertiary;
|
||||
if (level === ButtonLevel.SidebarSecondary) return StyledButtonSidebarSecondary;
|
||||
if (level === ButtonLevel.Recommended) return StyledButtonRecommended;
|
||||
return StyledButtonSecondary;
|
||||
}
|
||||
|
||||
export default function Button(props: Props) {
|
||||
function Button(props: Props) {
|
||||
const iconOnly = props.iconName && !props.title;
|
||||
|
||||
const StyledButton = buttonClass(props.level);
|
||||
@@ -197,3 +213,5 @@ export default function Button(props: Props) {
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(Button)`${space}`;
|
||||
|
@@ -148,6 +148,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
|
||||
const createSettingComponents = (advanced: boolean) => {
|
||||
const output = [];
|
||||
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
if (!!md.advanced !== advanced) continue;
|
||||
@@ -160,8 +161,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
const settingComps = createSettingComponents(false);
|
||||
const advancedSettingComps = createSettingComponents(true);
|
||||
|
||||
const sectionWidths: Record<string, number> = {
|
||||
plugins: 900,
|
||||
const sectionWidths: Record<string, any> = {
|
||||
plugins: '100%',
|
||||
};
|
||||
|
||||
const sectionStyle: any = {
|
||||
@@ -305,7 +306,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
);
|
||||
}
|
||||
|
||||
private renderHeader(themeId: number, label: string) {
|
||||
private renderHeader(themeId: number, label: string, style: any = null) {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const labelStyle = Object.assign({}, theme.textStyle, {
|
||||
@@ -314,6 +315,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
fontSize: theme.fontSize * 1.25,
|
||||
fontWeight: 500,
|
||||
marginBottom: theme.mainPadding,
|
||||
...style,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -457,7 +459,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
// There's probably a better way to do this but can't figure it out.
|
||||
|
||||
return (
|
||||
<div key={key + value.toString()} style={rowStyle}>
|
||||
<div key={key + (`${value}`).toString()} style={rowStyle}>
|
||||
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
id={`setting_checkbox_${key}`}
|
||||
@@ -564,7 +566,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
value={cmd[1]}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div style={{ width: inputStyle.width }}>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
@@ -593,7 +595,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div style={{ width: inputStyle.width }}>
|
||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -11,34 +11,47 @@ export enum InstallState {
|
||||
Installed = 3,
|
||||
}
|
||||
|
||||
export enum UpdateState {
|
||||
Idle = 1,
|
||||
CanUpdate = 2,
|
||||
Updating = 3,
|
||||
HasBeenUpdated = 4,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
item?: PluginItem;
|
||||
manifest?: PluginManifest;
|
||||
installState?: InstallState;
|
||||
updateState?: UpdateState;
|
||||
themeId: number;
|
||||
onToggle?: Function;
|
||||
onDelete?: Function;
|
||||
onInstall?: Function;
|
||||
onUpdate?: Function;
|
||||
}
|
||||
|
||||
function manifestToItem(manifest: PluginManifest): PluginItem {
|
||||
return {
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
description: manifest.description,
|
||||
enabled: true,
|
||||
deleted: false,
|
||||
devMode: false,
|
||||
hasBeenUpdated: false,
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
deleted: boolean;
|
||||
devMode: boolean;
|
||||
hasBeenUpdated: boolean;
|
||||
}
|
||||
|
||||
const CellRoot = styled.div`
|
||||
@@ -50,7 +63,7 @@ const CellRoot = styled.div`
|
||||
padding: 15px;
|
||||
border: 1px solid ${props => props.theme.dividerColor};
|
||||
border-radius: 6px;
|
||||
width: 250px;
|
||||
width: 320px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
|
||||
@@ -90,6 +103,12 @@ const StyledName = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const StyledVersion = styled.span`
|
||||
margin-left: 5px;
|
||||
color: ${props => props.theme.colorFaded};
|
||||
font-size: ${props => props.theme.fontSize * 0.9}px;
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
font-family: ${props => props.theme.fontFamily};
|
||||
color: ${props => props.theme.colorFaded};
|
||||
@@ -138,6 +157,23 @@ export default function(props: Props) {
|
||||
/>;
|
||||
}
|
||||
|
||||
function renderUpdateButton() {
|
||||
if (!props.onUpdate) return null;
|
||||
|
||||
let title = _('Update');
|
||||
if (props.updateState === UpdateState.Updating) title = _('Updating...');
|
||||
if (props.updateState === UpdateState.Idle) title = _('Updated');
|
||||
if (props.updateState === UpdateState.HasBeenUpdated) title = _('Updated');
|
||||
|
||||
return <Button
|
||||
ml={1}
|
||||
level={ButtonLevel.Recommended}
|
||||
onClick={() => props.onUpdate({ item })}
|
||||
title={title}
|
||||
disabled={props.updateState === UpdateState.HasBeenUpdated}
|
||||
/>;
|
||||
}
|
||||
|
||||
function renderFooter() {
|
||||
if (item.devMode) return null;
|
||||
|
||||
@@ -145,6 +181,7 @@ export default function(props: Props) {
|
||||
<CellFooter>
|
||||
{renderDeleteButton()}
|
||||
{renderInstallButton()}
|
||||
{renderUpdateButton()}
|
||||
<div style={{ display: 'flex', flex: 1 }}/>
|
||||
</CellFooter>
|
||||
);
|
||||
@@ -153,7 +190,7 @@ export default function(props: Props) {
|
||||
return (
|
||||
<CellRoot>
|
||||
<CellTop>
|
||||
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''}</StyledName>
|
||||
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''} <StyledVersion>v{item.version}</StyledVersion></StyledName>
|
||||
{renderToggleButton()}
|
||||
</CellTop>
|
||||
<CellContent>
|
||||
|
@@ -1,18 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import styled from 'styled-components';
|
||||
import SearchPlugins from './SearchPlugins';
|
||||
import PluginBox from './PluginBox';
|
||||
import PluginBox, { UpdateState } from './PluginBox';
|
||||
import Button, { ButtonLevel } from '../../../Button/Button';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import produce from 'immer';
|
||||
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
||||
import { PluginItem } from './PluginBox';
|
||||
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import useOnInstallHandler, { OnPluginSettingChangeEvent } from './useOnInstallHandler';
|
||||
const { space } = require('styled-system');
|
||||
|
||||
const maxWidth: number = 250;
|
||||
const maxWidth: number = 320;
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
@@ -26,7 +29,7 @@ const UserPluginsRoot = styled.div`
|
||||
`;
|
||||
|
||||
const ToolsButton = styled(Button)`
|
||||
margin-right: 2px;
|
||||
margin-right: 6px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@@ -38,6 +41,15 @@ interface Props {
|
||||
renderHeader: Function;
|
||||
}
|
||||
|
||||
let repoApi_: RepositoryApi = null;
|
||||
|
||||
function repoApi(): RepositoryApi {
|
||||
if (repoApi_) return repoApi_;
|
||||
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
|
||||
// repoApi_ = new RepositoryApi('/Users/laurent/src/joplin-plugins-test', Setting.value('tempDir'));
|
||||
return repoApi_;
|
||||
}
|
||||
|
||||
function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] {
|
||||
return useMemo(() => {
|
||||
const output: PluginItem[] = [];
|
||||
@@ -53,10 +65,12 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[
|
||||
output.push({
|
||||
id: pluginId,
|
||||
name: plugin.manifest.name,
|
||||
version: plugin.manifest.version,
|
||||
description: plugin.manifest.description,
|
||||
enabled: setting.enabled,
|
||||
deleted: setting.deleted,
|
||||
devMode: plugin.devMode,
|
||||
hasBeenUpdated: setting.hasBeenUpdated,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +84,9 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[
|
||||
|
||||
export default function(props: Props) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [manifestsLoaded, setManifestsLoaded] = useState<boolean>(false);
|
||||
const [updatingPluginsIds, setUpdatingPluginIds] = useState<Record<string, boolean>>({});
|
||||
const [canBeUpdatedPluginIds, setCanBeUpdatedPluginIds] = useState<Record<string, boolean>>({});
|
||||
|
||||
const pluginService = PluginService.instance();
|
||||
|
||||
@@ -77,6 +94,43 @@ export default function(props: Props) {
|
||||
return pluginService.unserializePluginSettings(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function fetchManifests() {
|
||||
await repoApi().loadManifests();
|
||||
if (cancelled) return;
|
||||
setManifestsLoaded(true);
|
||||
}
|
||||
|
||||
void fetchManifests();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manifestsLoaded) return () => {};
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function fetchPluginIds() {
|
||||
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems as any);
|
||||
if (cancelled) return;
|
||||
const conv: Record<string, boolean> = {};
|
||||
pluginIds.forEach(id => conv[id] = true);
|
||||
setCanBeUpdatedPluginIds(conv);
|
||||
}
|
||||
|
||||
void fetchPluginIds();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [manifestsLoaded, pluginItems]);
|
||||
|
||||
const onDelete = useCallback(async (event: any) => {
|
||||
const item: PluginItem = event.item;
|
||||
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.name));
|
||||
@@ -118,6 +172,12 @@ export default function(props: Props) {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
}, []);
|
||||
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
|
||||
|
||||
const onToolsClick = useCallback(async () => {
|
||||
const template = [];
|
||||
|
||||
@@ -144,12 +204,22 @@ export default function(props: Props) {
|
||||
for (const item of items) {
|
||||
if (item.deleted) continue;
|
||||
|
||||
const isUpdating = updatingPluginsIds[item.id];
|
||||
const onUpdateHandler = canBeUpdatedPluginIds[item.id] ? onUpdate : null;
|
||||
|
||||
let updateState = UpdateState.Idle;
|
||||
if (onUpdateHandler) updateState = UpdateState.CanUpdate;
|
||||
if (isUpdating) updateState = UpdateState.Updating;
|
||||
if (item.hasBeenUpdated) updateState = UpdateState.HasBeenUpdated;
|
||||
|
||||
output.push(<PluginBox
|
||||
key={item.id}
|
||||
item={item}
|
||||
themeId={props.themeId}
|
||||
updateState={updateState}
|
||||
onDelete={onDelete}
|
||||
onToggle={onToggle}
|
||||
onUpdate={onUpdateHandler}
|
||||
/>);
|
||||
}
|
||||
|
||||
@@ -174,13 +244,11 @@ export default function(props: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
function renderSearchArea() {
|
||||
return (
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
{props.renderHeader(props.themeId, _('Search for plugins'))}
|
||||
<SearchPlugins
|
||||
disabled={!manifestsLoaded}
|
||||
maxWidth={maxWidth}
|
||||
themeId={props.themeId}
|
||||
searchQuery={searchQuery}
|
||||
@@ -188,18 +256,32 @@ export default function(props: Props) {
|
||||
onSearchQueryChange={onSearchQueryChange}
|
||||
onPluginSettingsChange={onSearchPluginSettingsChange}
|
||||
renderDescription={props.renderDescription}
|
||||
repoApi={repoApi}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBottomArea() {
|
||||
if (searchQuery) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
|
||||
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
{props.renderHeader(props.themeId, _('Manage your plugins'))}
|
||||
</div>
|
||||
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Primary} onClick={onToolsClick}/>
|
||||
</div>
|
||||
{renderUserPlugins(pluginItems)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Root>
|
||||
{renderSearchArea()}
|
||||
{renderBottomArea()}
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import PluginBox, { InstallState } from './PluginBox';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useOnInstallHandler from './useOnInstallHandler';
|
||||
@@ -27,24 +26,18 @@ interface Props {
|
||||
onPluginSettingsChange(event: any): void;
|
||||
renderDescription: Function;
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
let repoApi_: RepositoryApi = null;
|
||||
|
||||
function repoApi(): RepositoryApi {
|
||||
if (repoApi_) return repoApi_;
|
||||
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
|
||||
return repoApi_;
|
||||
repoApi(): RepositoryApi;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const [searchStarted, setSearchStarted] = useState(false);
|
||||
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
||||
const asyncSearchQueue = useRef(new AsyncActionQueue(200));
|
||||
const asyncSearchQueue = useRef(new AsyncActionQueue(10));
|
||||
const [installingPluginsIds, setInstallingPluginIds] = useState<Record<string, boolean>>({});
|
||||
const [searchResultCount, setSearchResultCount] = useState(null);
|
||||
|
||||
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, repoApi, props.onPluginSettingsChange);
|
||||
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, props.repoApi, props.onPluginSettingsChange, false);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchResultCount(null);
|
||||
@@ -53,7 +46,7 @@ export default function(props: Props) {
|
||||
setManifests([]);
|
||||
setSearchResultCount(null);
|
||||
} else {
|
||||
const r = await repoApi().search(props.searchQuery);
|
||||
const r = await props.repoApi().search(props.searchQuery);
|
||||
setManifests(r);
|
||||
setSearchResultCount(r.length);
|
||||
}
|
||||
@@ -107,6 +100,8 @@ export default function(props: Props) {
|
||||
onChange={onChange}
|
||||
onSearchButtonClick={onSearchButtonClick}
|
||||
searchStarted={searchStarted}
|
||||
placeholder={_('Search for plugins...')}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@@ -6,7 +6,13 @@ import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('useOnInstallHandler');
|
||||
|
||||
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: Function) {
|
||||
export interface OnPluginSettingChangeEvent {
|
||||
value: PluginSettings;
|
||||
}
|
||||
|
||||
type OnPluginSettingChangeHandler = (event: OnPluginSettingChangeEvent)=> void;
|
||||
|
||||
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: OnPluginSettingChangeHandler, isUpdate: boolean) {
|
||||
return useCallback(async (event: any) => {
|
||||
const pluginId = event.item.id;
|
||||
|
||||
@@ -19,7 +25,11 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
|
||||
let installError = null;
|
||||
|
||||
try {
|
||||
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
|
||||
if (isUpdate) {
|
||||
await PluginService.instance().updatePluginFromRepo(repoApi(), pluginId);
|
||||
} else {
|
||||
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
|
||||
}
|
||||
} catch (error) {
|
||||
installError = error;
|
||||
logger.error(error);
|
||||
@@ -28,6 +38,7 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
|
||||
if (!installError) {
|
||||
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||
draft[pluginId] = defaultPluginSetting();
|
||||
if (isUpdate) draft[pluginId].hasBeenUpdated = true;
|
||||
});
|
||||
|
||||
onPluginSettingsChange({ value: newSettings });
|
||||
|
@@ -41,6 +41,8 @@ interface Props {
|
||||
onKeyDown?: Function;
|
||||
onSearchButtonClick: Function;
|
||||
searchStarted: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface OnChangeEvent {
|
||||
@@ -60,12 +62,13 @@ export default function(props: Props) {
|
||||
ref={props.inputRef}
|
||||
value={props.value}
|
||||
type="text"
|
||||
placeholder={_('Search...')}
|
||||
placeholder={props.placeholder || _('Search...')}
|
||||
onChange={onChange}
|
||||
onFocus={props.onFocus}
|
||||
onBlur={props.onBlur}
|
||||
onKeyDown={props.onKeyDown}
|
||||
spellCheck={false}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<SearchButton onClick={props.onSearchButtonClick}>
|
||||
<SearchButtonIcon className={iconName}/>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
const gulp = require('gulp');
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const tasks = {
|
||||
compileScripts: {
|
||||
@@ -18,55 +17,18 @@ const tasks = {
|
||||
electronRebuild: {
|
||||
fn: require('./tools/electronRebuild.js'),
|
||||
},
|
||||
// compileExtensions: {
|
||||
// fn: require('@joplin/tools/gulp/tasks/compileExtensions.js'),
|
||||
// },
|
||||
// copyLib: require('@joplin/tools/gulp/tasks/copyLib'),
|
||||
tsc: require('@joplin/tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
|
||||
linkReact: {
|
||||
fn: async () => {
|
||||
// React is a dependency of both the lib and app-desktop
|
||||
// packages, which cause a duplicate React issue. To go around
|
||||
// this, one way is to manually link the package.
|
||||
//
|
||||
// Note that React must also be unlinked in preinstall step
|
||||
// otherwise there will be permission errors when running
|
||||
// `lerna bootstrap`
|
||||
//
|
||||
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
|
||||
process.chdir(__dirname);
|
||||
await fs.remove('./node_modules/react');
|
||||
await fs.remove('./node_modules/react-dom');
|
||||
await utils.execCommand('npm link ../lib/node_modules/react');
|
||||
await utils.execCommand('npm link ../lib/node_modules/react-dom');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
// const buildSeries = [
|
||||
// // 'compileExtensions',
|
||||
// // 'copyLib',
|
||||
// ];
|
||||
|
||||
// On Windows also run tsc because `npm run watch` locks some folders
|
||||
// which makes the copyPluginAssets command fail. For that reason,
|
||||
// it's not possible to run watch on Windows while testing the desktop app.
|
||||
// if (require('os').platform() === 'win32') {
|
||||
// buildSeries.push('tsc');
|
||||
// }
|
||||
|
||||
const buildParallel = [
|
||||
// gulp.series(...buildSeries),
|
||||
'compileScripts',
|
||||
'compilePackageInfo',
|
||||
'copyPluginAssets',
|
||||
'copyTinyMceLangs',
|
||||
'updateIgnoredTypeScriptBuild',
|
||||
// 'linkReact',
|
||||
];
|
||||
|
||||
gulp.task('build', gulp.parallel(...buildParallel));
|
||||
|
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.2",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -635,8 +635,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
let resource = Resource.new();
|
||||
resource.id = uuid.create();
|
||||
resource.mime = mimeType;
|
||||
resource.title = pickerResponse.fileName ? pickerResponse.fileName : '';
|
||||
resource.file_extension = safeFileExtension(fileExtension(pickerResponse.fileName ? pickerResponse.fileName : localFilePath));
|
||||
resource.title = pickerResponse.name ? pickerResponse.name : '';
|
||||
resource.file_extension = safeFileExtension(fileExtension(pickerResponse.name ? pickerResponse.name : localFilePath));
|
||||
|
||||
if (!resource.mime) resource.mime = 'application/octet-stream';
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
const gulp = require('gulp');
|
||||
const fs = require('fs-extra');
|
||||
const utils = require('@joplin/tools/gulp/utils');
|
||||
|
||||
const tasks = {
|
||||
@@ -12,23 +11,6 @@ const tasks = {
|
||||
podInstall: {
|
||||
fn: require('./tools/podInstall'),
|
||||
},
|
||||
prepareRelease: {
|
||||
fn: require('./tools/prepareRelease'),
|
||||
},
|
||||
// clean: {
|
||||
// fn: require('./tools/clean'),
|
||||
// },
|
||||
linkReact: {
|
||||
fn: async () => {
|
||||
// React is a dependency of both the lib and app-desktop
|
||||
// packages, which cause a duplicate React issue. To go around
|
||||
// this, one way is to manually link the package.
|
||||
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
|
||||
process.chdir(__dirname);
|
||||
await fs.remove('./node_modules/react');
|
||||
await utils.execCommand('npm link ../lib/node_modules/react');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
@@ -36,6 +18,5 @@ utils.registerGulpTasks(gulp, tasks);
|
||||
gulp.task('build', gulp.series(
|
||||
'buildReactNativeInjectedJs',
|
||||
'encodeAssets',
|
||||
// 'linkReact',
|
||||
'podInstall'
|
||||
));
|
||||
|
@@ -1,32 +0,0 @@
|
||||
// This is to replace the symlinks inside node_modules with the actual packages
|
||||
// as I assumed it was needed to build the final release. However it seems
|
||||
// Android `assembleRelease` handles symlinks properly so maybe this is not
|
||||
// needed after all ¯\_(ツ)_/¯
|
||||
|
||||
const { copyDir } = require('@joplin/tools/gulp/utils');
|
||||
const { rootDir, deleteLink, toSystemSlashes } = require('@joplin/tools/tool-utils');
|
||||
const mobileDir = `${rootDir}/packages/app-mobile`;
|
||||
|
||||
module.exports = async function() {
|
||||
const dirsToCopy = [
|
||||
'fork-htmlparser2',
|
||||
'fork-sax',
|
||||
'lib',
|
||||
'renderer',
|
||||
];
|
||||
|
||||
const destDir = `${mobileDir}/node_modules/@joplin`;
|
||||
|
||||
for (const dir of dirsToCopy) {
|
||||
const destPath = toSystemSlashes(`${destDir}/${dir}`);
|
||||
const sourcePath = toSystemSlashes(`${rootDir}/packages/${dir}`);
|
||||
|
||||
console.info(`Copying ${sourcePath} => ${destPath}`);
|
||||
|
||||
// TODO: copy symlink so that it can be restored
|
||||
await deleteLink(destPath);
|
||||
await copyDir(sourcePath, destPath, {
|
||||
excluded: ['node_modules'],
|
||||
});
|
||||
}
|
||||
};
|
@@ -1,31 +0,0 @@
|
||||
// const fs = require('fs');
|
||||
// const { execSync } = require("child_process");
|
||||
|
||||
// const isWindows = process.platform === "win32";
|
||||
|
||||
// function toSystemSlashes(path) {
|
||||
// const os = process.platform;
|
||||
// if (os === 'win32') return path.replace(/\//g, '\\');
|
||||
// return path.replace(/\\/g, '/');
|
||||
// }
|
||||
|
||||
// const nodeModulesPath = `${__dirname}/../node_modules`;
|
||||
|
||||
// function deleteLink(path) {
|
||||
// if (isWindows) {
|
||||
// try {
|
||||
// execSync(`rmdir "${toSystemSlashes(path)}"`, { stdio : 'pipe' });
|
||||
// } catch (error) {
|
||||
// // console.info('Error: ' + error.message);
|
||||
// }
|
||||
// } else {
|
||||
// try {
|
||||
// fs.unlinkSync(toSystemSlashes(path));
|
||||
// } catch (error) {
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// deleteLink(nodeModulesPath + '/react');
|
||||
// deleteLink(nodeModulesPath + '/react-dom');
|
2
packages/fork-htmlparser2/package-lock.json
generated
2
packages/fork-htmlparser2/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"version": "4.1.15",
|
||||
"version": "4.1.19",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"description": "Fast & forgiving HTML/XML/RSS parser",
|
||||
"version": "4.1.15",
|
||||
"version": "4.1.19",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -52,7 +52,7 @@
|
||||
"coveralls": "^3.0.1",
|
||||
"eslint": "^6.0.0",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"jest": "^24.8.0",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^1.18.2",
|
||||
"ts-jest": "^24.0.2",
|
||||
"typescript": "^3.5.3"
|
||||
|
2
packages/fork-sax/package-lock.json
generated
2
packages/fork-sax/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-sax",
|
||||
"version": "1.2.19",
|
||||
"version": "1.2.23",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/fork-sax",
|
||||
"description": "An evented streaming XML parser in JavaScript",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||
"version": "1.2.19",
|
||||
"version": "1.2.23",
|
||||
"main": "lib/sax.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@@ -34,4 +34,4 @@
|
||||
"repository": "https://github.com/laurent22/generator-joplin",
|
||||
"license": "MIT",
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
|
@@ -667,6 +667,7 @@ export default class BaseApplication {
|
||||
const resourceDirName = 'resources';
|
||||
const resourceDir = `${profileDir}/${resourceDirName}`;
|
||||
const tempDir = `${profileDir}/tmp`;
|
||||
const cacheDir = `${profileDir}/cache`;
|
||||
|
||||
Setting.setConstant('env', initArgs.env);
|
||||
Setting.setConstant('profileDir', profileDir);
|
||||
@@ -674,6 +675,7 @@ export default class BaseApplication {
|
||||
Setting.setConstant('resourceDirName', resourceDirName);
|
||||
Setting.setConstant('resourceDir', resourceDir);
|
||||
Setting.setConstant('tempDir', tempDir);
|
||||
Setting.setConstant('cacheDir', cacheDir);
|
||||
Setting.setConstant('pluginDir', `${profileDir}/plugins`);
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
@@ -695,6 +697,7 @@ export default class BaseApplication {
|
||||
await fs.mkdirp(profileDir, 0o755);
|
||||
await fs.mkdirp(resourceDir, 0o755);
|
||||
await fs.mkdirp(tempDir, 0o755);
|
||||
await fs.mkdirp(cacheDir, 0o755);
|
||||
|
||||
// Clean up any remaining watched files (they start with "edit-")
|
||||
await shim.fsDriver().removeAllThatStartWith(profileDir, 'edit-');
|
||||
|
@@ -1659,6 +1659,7 @@ Setting.constants_ = {
|
||||
profileDir: '',
|
||||
templateDir: '',
|
||||
tempDir: '',
|
||||
cacheDir: '',
|
||||
pluginDir: '',
|
||||
flagOpenDevTools: false,
|
||||
syncVersion: 2,
|
||||
|
2
packages/lib/package-lock.json
generated
2
packages/lib/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "1.0.16",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "1.0.16",
|
||||
"version": "1.7.2",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
@@ -22,11 +22,11 @@
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.15",
|
||||
"@joplin/fork-sax": "^1.2.19",
|
||||
"@joplin/renderer": "^1.0.24",
|
||||
"@joplin/turndown": "^4.0.37",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.19",
|
||||
"@joplin/fork-htmlparser2": "^4.1.19",
|
||||
"@joplin/fork-sax": "^1.2.23",
|
||||
"@joplin/renderer": "^1.7.2",
|
||||
"@joplin/turndown": "^4.0.41",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.23",
|
||||
"async-mutex": "^0.1.3",
|
||||
"aws-sdk": "^2.588.0",
|
||||
"base-64": "^0.1.0",
|
||||
|
@@ -8,6 +8,7 @@ import { filename, dirname, rtrimSlashes } from '../../path-utils';
|
||||
import Setting from '../../models/Setting';
|
||||
import Logger from '../../Logger';
|
||||
import RepositoryApi from './RepositoryApi';
|
||||
import produce from 'immer';
|
||||
const compareVersions = require('compare-versions');
|
||||
const uslug = require('uslug');
|
||||
const md5File = require('md5-file/promise');
|
||||
@@ -31,12 +32,19 @@ export interface Plugins {
|
||||
export interface PluginSetting {
|
||||
enabled: boolean;
|
||||
deleted: boolean;
|
||||
|
||||
// After a plugin has been updated, the user needs to restart the app before
|
||||
// loading the new version. In the meantime, we set this property to `true`
|
||||
// so that we know the plugin has been updated. It is used for example to
|
||||
// disable the Update button.
|
||||
hasBeenUpdated: boolean;
|
||||
}
|
||||
|
||||
export function defaultPluginSetting(): PluginSetting {
|
||||
return {
|
||||
enabled: true,
|
||||
deleted: false,
|
||||
hasBeenUpdated: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,6 +100,10 @@ export default class PluginService extends BaseService {
|
||||
delete this.plugins_[pluginId];
|
||||
}
|
||||
|
||||
private async deletePluginFiles(plugin: Plugin) {
|
||||
await shim.fsDriver().remove(plugin.baseDir);
|
||||
}
|
||||
|
||||
public pluginById(id: string): Plugin {
|
||||
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
|
||||
|
||||
@@ -173,7 +185,7 @@ export default class PluginService extends BaseService {
|
||||
const fname = filename(path);
|
||||
const hash = await md5File(path);
|
||||
|
||||
const unpackDir = `${Setting.value('tempDir')}/${fname}`;
|
||||
const unpackDir = `${Setting.value('cacheDir')}/${fname}`;
|
||||
const manifestFilePath = `${unpackDir}/manifest.json`;
|
||||
|
||||
let manifest: any = await this.loadManifestToObject(manifestFilePath);
|
||||
@@ -194,7 +206,7 @@ export default class PluginService extends BaseService {
|
||||
|
||||
manifest._package_hash = hash;
|
||||
|
||||
await shim.fsDriver().writeFile(manifestFilePath, JSON.stringify(manifest), 'utf8');
|
||||
await shim.fsDriver().writeFile(manifestFilePath, JSON.stringify(manifest, null, '\t'), 'utf8');
|
||||
}
|
||||
|
||||
return this.loadPluginFromPath(unpackDir);
|
||||
@@ -345,6 +357,10 @@ export default class PluginService extends BaseService {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public async updatePluginFromRepo(repoApi: RepositoryApi, pluginId: string): Promise<Plugin> {
|
||||
return this.installPluginFromRepo(repoApi, pluginId);
|
||||
}
|
||||
|
||||
public async installPlugin(jplPath: string): Promise<Plugin> {
|
||||
logger.info(`Installing plugin: "${jplPath}"`);
|
||||
|
||||
@@ -352,6 +368,7 @@ export default class PluginService extends BaseService {
|
||||
// from where it is now to check that it is valid and to retrieve
|
||||
// the plugin ID.
|
||||
const preloadedPlugin = await this.loadPluginFromPath(jplPath);
|
||||
await this.deletePluginFiles(preloadedPlugin);
|
||||
|
||||
const destPath = `${Setting.value('pluginDir')}/${preloadedPlugin.id}.jpl`;
|
||||
await shim.fsDriver().copy(jplPath, destPath);
|
||||
@@ -402,6 +419,16 @@ export default class PluginService extends BaseService {
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
// On startup the "hasBeenUpdated" prop can be cleared since the new version
|
||||
// of the plugin has now been loaded.
|
||||
public clearUpdateState(settings: PluginSettings): PluginSettings {
|
||||
return produce(settings, (draft: PluginSettings) => {
|
||||
for (const pluginId in draft) {
|
||||
if (draft[pluginId].hasBeenUpdated) draft[pluginId].hasBeenUpdated = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
await this.runner_.waitForSandboxCalls();
|
||||
}
|
||||
|
@@ -1,11 +1,15 @@
|
||||
import shim from '../../shim';
|
||||
import { PluginManifest } from './utils/types';
|
||||
const md5 = require('md5');
|
||||
const compareVersions = require('compare-versions');
|
||||
|
||||
export default class RepositoryApi {
|
||||
|
||||
// For now, it's assumed that the baseUrl is a GitHub repo URL, such as
|
||||
// https://github.com/joplin/plugins
|
||||
// As a base URL, this class can support either a remote repository or a
|
||||
// local one (a directory path), which is useful for testing.
|
||||
//
|
||||
// For now, if the baseUrl is an actual URL it's assumed it's a GitHub repo
|
||||
// URL, such as https://github.com/joplin/plugins
|
||||
//
|
||||
// Later on, other repo types could be supported.
|
||||
private baseUrl_: string;
|
||||
@@ -17,8 +21,29 @@ export default class RepositoryApi {
|
||||
this.tempDir_ = tempDir;
|
||||
}
|
||||
|
||||
public async loadManifests() {
|
||||
const manifestsText = await this.fetchText('manifests.json');
|
||||
try {
|
||||
const manifests = JSON.parse(manifestsText);
|
||||
if (!manifests) throw new Error('Invalid or missing JSON');
|
||||
this.manifests_ = Object.keys(manifests).map(id => {
|
||||
return manifests[id];
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Could not parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private get isLocalRepo(): boolean {
|
||||
return this.baseUrl_.indexOf('http') !== 0;
|
||||
}
|
||||
|
||||
private get contentBaseUrl(): string {
|
||||
return `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
|
||||
if (this.isLocalRepo) {
|
||||
return this.baseUrl_;
|
||||
} else {
|
||||
return `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
|
||||
}
|
||||
}
|
||||
|
||||
private fileUrl(relativePath: string): string {
|
||||
@@ -26,7 +51,11 @@ export default class RepositoryApi {
|
||||
}
|
||||
|
||||
private async fetchText(path: string): Promise<string> {
|
||||
return shim.fetchText(this.fileUrl(path));
|
||||
if (this.isLocalRepo) {
|
||||
return shim.fsDriver().readFile(this.fileUrl(path), 'utf8');
|
||||
} else {
|
||||
return shim.fetchText(this.fileUrl(path));
|
||||
}
|
||||
}
|
||||
|
||||
public async search(query: string): Promise<PluginManifest[]> {
|
||||
@@ -60,27 +89,40 @@ export default class RepositoryApi {
|
||||
const fileUrl = this.fileUrl(`plugins/${manifest.id}/plugin.jpl`);
|
||||
const hash = md5(Date.now() + Math.random());
|
||||
const targetPath = `${this.tempDir_}/${hash}_${manifest.id}.jpl`;
|
||||
const response = await shim.fetchBlob(fileUrl, {
|
||||
path: targetPath,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Could not download plugin "${pluginId}" from "${fileUrl}"`);
|
||||
if (this.isLocalRepo) {
|
||||
await shim.fsDriver().copy(fileUrl, targetPath);
|
||||
} else {
|
||||
const response = await shim.fetchBlob(fileUrl, {
|
||||
path: targetPath,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Could not download plugin "${pluginId}" from "${fileUrl}"`);
|
||||
}
|
||||
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
private async manifests(): Promise<PluginManifest[]> {
|
||||
if (this.manifests_) return this.manifests_;
|
||||
const manifestsText = await this.fetchText('manifests.json');
|
||||
try {
|
||||
const manifests = JSON.parse(manifestsText);
|
||||
if (!manifests) throw new Error('Invalid or missing JSON');
|
||||
this.manifests_ = Object.keys(manifests).map(id => {
|
||||
return manifests[id];
|
||||
});
|
||||
return this.manifests_;
|
||||
} catch (error) {
|
||||
throw new Error(`Could not parse JSON: ${error.message}`);
|
||||
public async manifests(): Promise<PluginManifest[]> {
|
||||
if (!this.manifests_) throw new Error('Manifests have no been loaded!');
|
||||
return this.manifests_;
|
||||
}
|
||||
|
||||
public async canBeUpdatedPlugins(installedManifests: PluginManifest[]): Promise<string[]> {
|
||||
const output = [];
|
||||
|
||||
for (const manifest of installedManifests) {
|
||||
const canBe = await this.pluginCanBeUpdated(manifest.id, manifest.version);
|
||||
if (canBe) output.push(manifest.id);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async pluginCanBeUpdated(pluginId: string, installedVersion: string): Promise<boolean> {
|
||||
const manifest = (await this.manifests()).find(m => m.id === pluginId);
|
||||
if (!manifest) return false;
|
||||
return compareVersions(installedVersion, manifest.version) < 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
9
packages/plugin-repo-cli/dummy.test.ts
Normal file
9
packages/plugin-repo-cli/dummy.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Dummy test because the Jest setup is done but there's for now no test.
|
||||
|
||||
describe('dummy', () => {
|
||||
|
||||
it('should pass', () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
|
||||
});
|
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as process from 'process';
|
||||
import validatePluginId from '@joplin/lib/services/plugins/utils/validatePluginId';
|
||||
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '@joplin/lib/markdownUtils';
|
||||
const { execCommand, execCommandVerbose, rootDir, resolveRelativePathWithinDir, gitPullTry } = require('./tool-utils.js');
|
||||
import { execCommand2, resolveRelativePathWithinDir, gitPullTry, gitRepoCleanTry, gitRepoClean } from '@joplin/tools/tool-utils.js';
|
||||
|
||||
interface NpmPackage {
|
||||
name: string;
|
||||
@@ -45,6 +47,7 @@ async function checkPluginRepository(dirPath: string) {
|
||||
|
||||
const previousDir = process.cwd();
|
||||
process.chdir(dirPath);
|
||||
await gitRepoCleanTry();
|
||||
await gitPullTry();
|
||||
process.chdir(previousDir);
|
||||
}
|
||||
@@ -70,13 +73,13 @@ async function extractPluginFilesFromPackage(existingManifests: any, workDir: st
|
||||
const previousDir = process.cwd();
|
||||
process.chdir(workDir);
|
||||
|
||||
await execCommandVerbose('npm', ['install', packageName, '--save', '--ignore-scripts']);
|
||||
await execCommand2(`npm install ${packageName} --save --ignore-scripts`, { showOutput: false });
|
||||
|
||||
const pluginDir = resolveRelativePathWithinDir(workDir, 'node_modules', packageName, 'publish');
|
||||
|
||||
const files = await fs.readdir(pluginDir);
|
||||
const manifestFilePath = path.resolve(pluginDir, files.find(f => path.extname(f) === '.json'));
|
||||
const pluginFilePath = path.resolve(pluginDir, files.find(f => path.extname(f) === '.jpl'));
|
||||
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'));
|
||||
|
||||
if (!(await fs.pathExists(manifestFilePath))) throw new Error(`Could not find manifest file at ${manifestFilePath}`);
|
||||
if (!(await fs.pathExists(pluginFilePath))) throw new Error(`Could not find plugin file at ${pluginFilePath}`);
|
||||
@@ -155,23 +158,41 @@ async function updateReadme(readmePath: string, manifests: any) {
|
||||
|
||||
const tableRegex = /<!-- PLUGIN_LIST -->([^]*)<!-- PLUGIN_LIST -->/;
|
||||
|
||||
const content = await fs.readFile(readmePath, 'utf8');
|
||||
const content = await fs.pathExists(readmePath) ? await fs.readFile(readmePath, 'utf8') : '<!-- PLUGIN_LIST -->\n<!-- PLUGIN_LIST -->';
|
||||
const newContent = content.replace(tableRegex, `<!-- PLUGIN_LIST -->\n${mdTable}\n<!-- PLUGIN_LIST -->`);
|
||||
|
||||
await fs.writeFile(readmePath, newContent, 'utf8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// We assume that the repository is located in a directory next to the main
|
||||
// Joplin monorepo.
|
||||
const repoDir = path.resolve(path.dirname(rootDir), 'joplin-plugins');
|
||||
interface CommandBuildArgs {
|
||||
pluginRepoDir: string;
|
||||
}
|
||||
|
||||
enum ProcessingActionType {
|
||||
Add = 1,
|
||||
Update = 2,
|
||||
}
|
||||
|
||||
function commitMessage(actionType: ProcessingActionType, npmPackage: NpmPackage): string {
|
||||
const output: string[] = [];
|
||||
|
||||
if (actionType === ProcessingActionType.Add) {
|
||||
output.push('New');
|
||||
} else {
|
||||
output.push('Update');
|
||||
}
|
||||
|
||||
output.push(`${npmPackage.name}@${npmPackage.version}`);
|
||||
|
||||
return output.join(': ');
|
||||
}
|
||||
|
||||
async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
|
||||
const tempDir = `${repoDir}/temp`;
|
||||
const pluginManifestsPath = path.resolve(repoDir, 'manifests.json');
|
||||
const obsoleteManifestsPath = path.resolve(repoDir, 'obsoletes.json');
|
||||
const errorsPath = path.resolve(repoDir, 'errors.json');
|
||||
|
||||
await checkPluginRepository(repoDir);
|
||||
|
||||
await fs.mkdirp(tempDir);
|
||||
|
||||
const originalPluginManifests = await readJsonFile(pluginManifestsPath, {});
|
||||
@@ -181,31 +202,31 @@ async function main() {
|
||||
...obsoleteManifests,
|
||||
};
|
||||
|
||||
const searchResults = (await execCommand('npm search joplin-plugin --searchlimit 5000 --json')).trim();
|
||||
const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults));
|
||||
|
||||
const packageTempDir = `${tempDir}/packages`;
|
||||
|
||||
await fs.mkdirp(packageTempDir);
|
||||
const previousDir = process.cwd();
|
||||
process.chdir(packageTempDir);
|
||||
await execCommand('npm init --yes --loglevel silent');
|
||||
await execCommand2('npm init --yes --loglevel silent', { quiet: true });
|
||||
|
||||
const errors: any[] = [];
|
||||
const errors: any = await readJsonFile(errorsPath, {});
|
||||
delete errors[npmPackage.name];
|
||||
|
||||
let actionType: ProcessingActionType = ProcessingActionType.Update;
|
||||
let manifests: any = {};
|
||||
|
||||
// TODO: validate plugin ID when publishing
|
||||
try {
|
||||
const destDir = `${repoDir}/plugins/`;
|
||||
const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, npmPackage.name, destDir);
|
||||
|
||||
for (const npmPackage of npmPackages) {
|
||||
try {
|
||||
const packageName = npmPackage.name;
|
||||
const destDir = `${repoDir}/plugins/`;
|
||||
const manifest = await extractPluginFilesFromPackage(existingManifests, packageTempDir, packageName, destDir);
|
||||
if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errors.push(error);
|
||||
if (!existingManifests[manifest.id]) {
|
||||
actionType = ProcessingActionType.Add;
|
||||
}
|
||||
|
||||
if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errors[npmPackage.name] = error.message || '';
|
||||
}
|
||||
|
||||
// We preserve the original manifests so that if a plugin has been removed
|
||||
@@ -219,20 +240,91 @@ async function main() {
|
||||
|
||||
await fs.writeFile(pluginManifestsPath, JSON.stringify(manifests, null, '\t'), 'utf8');
|
||||
|
||||
if (errors.length) {
|
||||
const toWrite = errors.map((e: any) => {
|
||||
return {
|
||||
message: e.message || '',
|
||||
};
|
||||
});
|
||||
await fs.writeFile(errorsPath, JSON.stringify(toWrite, null, '\t'), 'utf8');
|
||||
if (Object.keys(errors).length) {
|
||||
await fs.writeFile(errorsPath, JSON.stringify(errors, null, '\t'), 'utf8');
|
||||
} else {
|
||||
await fs.remove(errorsPath);
|
||||
}
|
||||
|
||||
await updateReadme(`${repoDir}/README.md`, manifests);
|
||||
|
||||
process.chdir(previousDir);
|
||||
await fs.remove(tempDir);
|
||||
|
||||
process.chdir(repoDir);
|
||||
|
||||
if (!(await gitRepoClean())) {
|
||||
await execCommand2('git add -A', { showOutput: false });
|
||||
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, npmPackage)], { showOutput: false });
|
||||
} else {
|
||||
console.info('Nothing to commit');
|
||||
}
|
||||
}
|
||||
|
||||
async function commandBuild(args: CommandBuildArgs) {
|
||||
console.info(new Date(), 'Building repository...');
|
||||
|
||||
const repoDir = args.pluginRepoDir;
|
||||
await checkPluginRepository(repoDir);
|
||||
|
||||
const searchResults = (await execCommand2('npm search joplin-plugin --searchlimit 5000 --json', { showOutput: false })).trim();
|
||||
const npmPackages = pluginInfoFromSearchResults(JSON.parse(searchResults));
|
||||
|
||||
for (const npmPackage of npmPackages) {
|
||||
await processNpmPackage(npmPackage, repoDir);
|
||||
}
|
||||
|
||||
await execCommand2('git push');
|
||||
}
|
||||
|
||||
async function commandVersion() {
|
||||
const p = await readJsonFile(path.resolve(__dirname, 'package.json'));
|
||||
console.info(`Version ${p.version}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const scriptName: string = 'plugin-repo-cli';
|
||||
|
||||
const commands: Record<string, Function> = {
|
||||
build: commandBuild,
|
||||
version: commandVersion,
|
||||
};
|
||||
|
||||
let selectedCommand: string = '';
|
||||
let selectedCommandArgs: string = '';
|
||||
|
||||
function setSelectedCommand(name: string, args: any) {
|
||||
selectedCommand = name;
|
||||
selectedCommandArgs = args;
|
||||
}
|
||||
|
||||
require('yargs')
|
||||
.scriptName(scriptName)
|
||||
.usage('$0 <cmd> [args]')
|
||||
|
||||
.command('build <plugin-repo-dir>', 'Build the plugin repository', (yargs: any) => {
|
||||
yargs.positional('plugin-repo-dir', {
|
||||
type: 'string',
|
||||
describe: 'Directory where the plugin repository is located',
|
||||
});
|
||||
}, (args: any) => setSelectedCommand('build', args))
|
||||
|
||||
.command('version', 'Gives version info', () => {}, (args: any) => setSelectedCommand('version', args))
|
||||
|
||||
.help()
|
||||
.argv;
|
||||
|
||||
if (!selectedCommand) {
|
||||
console.error(`Please provide a command name or type \`${scriptName} --help\` for help`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!commands[selectedCommand]) {
|
||||
console.error(`No such command: ${selectedCommand}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await commands[selectedCommand](selectedCommandArgs);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
7815
packages/plugin-repo-cli/package-lock.json
generated
Normal file
7815
packages/plugin-repo-cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
packages/plugin-repo-cli/package.json
Normal file
33
packages/plugin-repo-cli/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@joplin/plugin-repo-cli",
|
||||
"version": "1.7.4",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"plugin-repo-cli": "./index.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc --project tsconfig.json",
|
||||
"watch": "tsc --watch --project tsconfig.json",
|
||||
"test": "jest",
|
||||
"test-ci": "npm run test"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^1.7.2",
|
||||
"@joplin/tools": "^1.7.2",
|
||||
"fs-extra": "^9.0.1",
|
||||
"yargs": "^16.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
"jest": "^26.6.3",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
10
packages/plugin-repo-cli/tsconfig.json
Normal file
10
packages/plugin-repo-cli/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
],
|
||||
}
|
2
packages/renderer/package-lock.json
generated
2
packages/renderer/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "1.0.24",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "1.0.24",
|
||||
"version": "1.7.2",
|
||||
"description": "The Joplin note renderer, used the mobile and desktop application",
|
||||
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
|
||||
"main": "index.js",
|
||||
@@ -24,7 +24,7 @@
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/fork-htmlparser2": "^4.1.15",
|
||||
"@joplin/fork-htmlparser2": "^4.1.19",
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "^10.2.1",
|
||||
|
@@ -4,64 +4,86 @@
|
||||
|
||||
First copy `.env-sample` to `.env` and edit the values in there:
|
||||
|
||||
- `JOPLIN_BASE_URL`: This is the base public URL where the service will be running. For example, if you want it to run from `https://example.com/joplin`, this is what you should set the URL to. The base URL can include the port.
|
||||
- `JOPLIN_PORT`: The local port on which the Docker container will listen. You would typically map this port to 443 (TLS) with a reverse proxy.
|
||||
- `APP_BASE_URL`: This is the base public URL where the service will be running. For example, if you want it to run from `https://example.com/joplin`, this is what you should set the URL to. The base URL can include the port.
|
||||
- `APP_PORT`: The local port on which the Docker container will listen. You would typically map this port to 443 (TLS) with a reverse proxy.
|
||||
|
||||
## Install application
|
||||
## Running the server
|
||||
|
||||
To start the server with default configuration, run:
|
||||
|
||||
```shell
|
||||
wget https://github.com/laurent22/joplin/archive/server-v1.6.4.tar.gz
|
||||
tar xzvf server-v1.6.4.tar.gz
|
||||
mv joplin-server-v1.6.4 joplin-server
|
||||
cd joplin-server
|
||||
docker-compose --file docker-compose.server.yml up --detach
|
||||
docker run --env-file .env -p 22300:22300 joplin/server:latest
|
||||
```
|
||||
|
||||
This will start the server, which will listen on port **22300** on **localhost**.
|
||||
This will start the server, which will listen on port **22300** on **localhost**. By default it will use SQLite, which allows you to test the app without setting up a database. To run it for production though, you'll want to connect the container to a database, as described below.
|
||||
|
||||
Due to the restart policy defined in the docker-compose file, the server will be restarted automatically whenever the host reboots.
|
||||
## Setup the database
|
||||
|
||||
You can setup the container to either use an existing PostgreSQL server, or connect it to a new one using docker-compose
|
||||
|
||||
### Using an existing PostgreSQL server
|
||||
|
||||
To use an existing PostgresSQL server, set the following environment variables in the .env file:
|
||||
|
||||
```conf
|
||||
DB_CLIENT=pg
|
||||
POSTGRES_PASSWORD=joplin
|
||||
POSTGRES_DATABASE=joplin
|
||||
POSTGRES_USER=joplin
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_HOST=localhost
|
||||
```
|
||||
|
||||
Make sure that the provided database and user exist as the server will not create them.
|
||||
|
||||
### Using docker-compose
|
||||
|
||||
A [sample docker-compose file](https://github.com/laurent22/joplin/blob/dev/docker-compose.server.yml
|
||||
) is available to show how to use Docker to install both the database and server and connect them:
|
||||
|
||||
## Setup reverse proxy
|
||||
|
||||
You will then need to expose this server to the internet by setting up a reverse proxy, and that will depend on how your server is currently configured, and whether you already have Nginx or Apache running:
|
||||
Once Joplin Server is running, you will then need to expose it to the internet by setting up a reverse proxy, and that will depend on how your server is currently configured, and whether you already have Nginx or Apache running:
|
||||
|
||||
- [Apache Reverse Proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html)
|
||||
- [Nginx Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
|
||||
|
||||
## Setup admin user
|
||||
## Setup the website
|
||||
|
||||
For the following instructions, we'll assume that the Joplin server is running on `https://example.com/joplin`.
|
||||
Once the server is exposed to the internet, you can open the admin UI and get it ready for synchronisation. For the following instructions, we'll assume that the Joplin server is running on `https://example.com/joplin`.
|
||||
|
||||
By default, the instance will be setup with an admin user with email **admin@localhost** and password **admin** and you should change this by opening the admin UI. To do so, open `https://example.com/joplin/login`. From there, go to Profile and change the admin password.
|
||||
### Secure the admin user
|
||||
|
||||
## Setup a user for sync
|
||||
By default, the instance will be setup with an admin user with email **admin@localhost** and password **admin** and you should change this. To do so, open `https://example.com/joplin/login` and login as admin. Then go to the Profile section and change the admin password.
|
||||
|
||||
While the admin user can be used for synchronisation, it is recommended to create a separate non-admin user for it. To do, open the admin UI and navigate to the Users page - from there you can create a new user.
|
||||
### Create a user for sync
|
||||
|
||||
Once this is done, you can use the email and password you specified to sync this user account with your Joplin clients.
|
||||
While the admin user can be used for synchronisation, it is recommended to create a separate non-admin user for it. To do so, navigate to the Users page - from there you can create a new user. Once this is done, you can use the email and password you specified to sync this user account with your Joplin clients.
|
||||
|
||||
## Checking the logs
|
||||
|
||||
Checking the log can be done the standard Docker way:
|
||||
|
||||
```shell
|
||||
```bash
|
||||
# With Docker:
|
||||
docker logs --follow CONTAINER
|
||||
|
||||
# With docker-compose:
|
||||
docker-compose --file docker-compose.server.yml logs
|
||||
```
|
||||
|
||||
# Set up for development
|
||||
# Setup for development
|
||||
|
||||
## Setting up the database
|
||||
## Setup up the database
|
||||
|
||||
### SQLite
|
||||
|
||||
The server supports SQLite for development and test units. To use it, open `src/config-dev.ts` and uncomment the sqlite3 config.
|
||||
By default the server supports SQLite for development, so nothing needs to be setup.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
It's best to use PostgreSQL as this is what is used in production, however it requires Docker.
|
||||
|
||||
To use it, from the monorepo root, run `docker-compose --file docker-compose.server-dev.yml up`, which will start the PostgreSQL database.
|
||||
To use Postgres, from the monorepo root, run `docker-compose --file docker-compose.server-dev.yml up`, which will start the PostgreSQL database.
|
||||
|
||||
## Starting the server
|
||||
|
||||
From `packages/server`, run `npm run start-dev`
|
||||
From `packages/server`, run `npm run start-dev`
|
||||
|
7
packages/server/package-lock.json
generated
7
packages/server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -5938,6 +5938,11 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-env-file": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/node-env-file/-/node-env-file-0.1.8.tgz",
|
||||
"integrity": "sha1-/Mt7BQ9zW1oz2p65N89vGrRX+2k="
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json dist/app.js --env dev",
|
||||
@@ -26,6 +26,7 @@
|
||||
"markdown-it": "^12.0.4",
|
||||
"mustache": "^3.1.0",
|
||||
"nanoid": "^2.1.1",
|
||||
"node-env-file": "^0.1.8",
|
||||
"nodemon": "^2.0.6",
|
||||
"pg": "^8.5.1",
|
||||
"query-string": "^6.8.3",
|
||||
|
@@ -5,32 +5,30 @@ import * as Koa from 'koa';
|
||||
import * as fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
||||
import config, { initConfig, baseUrl } from './config';
|
||||
import configDev from './config-dev';
|
||||
import configProd from './config-prod';
|
||||
import configBuildTypes from './config-buildTypes';
|
||||
import config, { initConfig, runningInDocker, EnvVariables } from './config';
|
||||
import { createDb, dropDb } from './tools/dbTools';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection } from './db';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteFilePath } from './db';
|
||||
import modelFactory from './models/factory';
|
||||
import { AppContext, Config, Env } from './utils/types';
|
||||
import { AppContext, Env } from './utils/types';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import routeHandler from './middleware/routeHandler';
|
||||
import notificationHandler from './middleware/notificationHandler';
|
||||
import ownerHandler from './middleware/ownerHandler';
|
||||
|
||||
const nodeEnvFile = require('node-env-file');
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
const env: Env = argv.env as Env || Env.Prod;
|
||||
|
||||
interface Configs {
|
||||
[name: string]: Config;
|
||||
}
|
||||
|
||||
const configs: Configs = {
|
||||
dev: configDev,
|
||||
prod: configProd,
|
||||
buildTypes: configBuildTypes,
|
||||
const envVariables: Record<Env, EnvVariables> = {
|
||||
dev: {
|
||||
SQLITE_DATABASE: 'dev',
|
||||
},
|
||||
buildTypes: {
|
||||
SQLITE_DATABASE: 'buildTypes',
|
||||
},
|
||||
prod: {}, // Actually get the env variables from the environment
|
||||
};
|
||||
|
||||
let appLogger_: LoggerWrapper = null;
|
||||
@@ -52,11 +50,31 @@ app.use(ownerHandler);
|
||||
app.use(notificationHandler);
|
||||
app.use(routeHandler);
|
||||
|
||||
async function main() {
|
||||
const configObject: Config = configs[env];
|
||||
if (!configObject) throw new Error(`Invalid env: ${env}`);
|
||||
function markPasswords(o: Record<string, any>): Record<string, any> {
|
||||
const output: Record<string, any> = {};
|
||||
|
||||
initConfig(configObject);
|
||||
for (const k of Object.keys(o)) {
|
||||
if (k.toLowerCase().includes('password')) {
|
||||
output[k] = '********';
|
||||
} else {
|
||||
output[k] = o[k];
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (argv.envFile) {
|
||||
nodeEnvFile(argv.envFile);
|
||||
}
|
||||
|
||||
if (!envVariables[env]) throw new Error(`Invalid env: ${env}`);
|
||||
|
||||
initConfig({
|
||||
...envVariables[env],
|
||||
...process.env,
|
||||
});
|
||||
|
||||
await fs.mkdirp(config().logDir);
|
||||
Logger.fsDriver_ = new FsDriverNode();
|
||||
@@ -90,8 +108,11 @@ async function main() {
|
||||
await createDb(config().database);
|
||||
} else {
|
||||
appLogger().info(`Starting server (${env}) on port ${config().port} and PID ${process.pid}...`);
|
||||
appLogger().info('Public base URL:', baseUrl());
|
||||
appLogger().info('DB Config:', config().database);
|
||||
appLogger().info('Running in Docker:', runningInDocker());
|
||||
appLogger().info('Public base URL:', config().baseUrl);
|
||||
appLogger().info('Log dir:', config().logDir);
|
||||
appLogger().info('DB Config:', markPasswords(config().database));
|
||||
if (config().database.client === 'sqlite3') appLogger().info('DB file:', sqliteFilePath(config().database.name));
|
||||
|
||||
const appContext = app.context as AppContext;
|
||||
|
||||
@@ -104,13 +125,13 @@ async function main() {
|
||||
appLogger().info('Connection check:', connectionCheckLogInfo);
|
||||
appContext.env = env;
|
||||
appContext.db = connectionCheck.connection;
|
||||
appContext.models = modelFactory(appContext.db, baseUrl());
|
||||
appContext.models = modelFactory(appContext.db, config().baseUrl);
|
||||
appContext.appLogger = appLogger;
|
||||
|
||||
appLogger().info('Migrating database...');
|
||||
await migrateDb(appContext.db);
|
||||
|
||||
appLogger().info(`Call this for testing: \`curl ${baseUrl()}/api/ping\``);
|
||||
appLogger().info(`Call this for testing: \`curl ${config().baseUrl}/api/ping\``);
|
||||
|
||||
app.listen(config().port);
|
||||
}
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import { Config } from './utils/types';
|
||||
import * as pathUtils from 'path';
|
||||
|
||||
const rootDir = pathUtils.dirname(__dirname);
|
||||
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
|
||||
|
||||
const envPort = Number(process.env.JOPLIN_PORT);
|
||||
|
||||
const config: Config = {
|
||||
port: (envPort && !isNaN(envPort)) ? envPort : 22300,
|
||||
viewDir: viewDir,
|
||||
rootDir: rootDir,
|
||||
layoutDir: `${viewDir}/layouts`,
|
||||
logDir: `${rootDir}/logs`,
|
||||
database: {
|
||||
client: 'pg',
|
||||
name: 'joplin',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@@ -1,13 +0,0 @@
|
||||
import { Config } from './utils/types';
|
||||
import configBase from './config-base';
|
||||
|
||||
const config: Config = {
|
||||
...configBase,
|
||||
database: {
|
||||
name: 'buildTypes',
|
||||
client: 'sqlite3',
|
||||
asyncStackTraces: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@@ -1,22 +0,0 @@
|
||||
import { Config } from './utils/types';
|
||||
import configBase from './config-base';
|
||||
|
||||
const config: Config = {
|
||||
...configBase,
|
||||
database: {
|
||||
name: 'dev',
|
||||
client: 'sqlite3',
|
||||
asyncStackTraces: true,
|
||||
},
|
||||
// database: {
|
||||
// client: 'pg',
|
||||
// name: 'joplin',
|
||||
// user: 'joplin',
|
||||
// host: 'localhost',
|
||||
// port: 5432,
|
||||
// password: 'joplin',
|
||||
// asyncStackTraces: true,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
@@ -1,20 +0,0 @@
|
||||
import { Config } from './utils/types';
|
||||
import configBase from './config-base';
|
||||
|
||||
const rootDir = '/home/joplin/';
|
||||
|
||||
const config: Config = {
|
||||
...configBase,
|
||||
rootDir: rootDir,
|
||||
logDir: `${rootDir}/logs`,
|
||||
database: {
|
||||
client: 'pg',
|
||||
name: 'joplin',
|
||||
user: 'joplin',
|
||||
host: 'db',
|
||||
port: 5432,
|
||||
password: 'joplin',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@@ -1,13 +0,0 @@
|
||||
import { Config } from './utils/types';
|
||||
import configBase from './config-base';
|
||||
|
||||
const config: Config = {
|
||||
...configBase,
|
||||
database: {
|
||||
name: 'DYNAMIC',
|
||||
client: 'sqlite3',
|
||||
asyncStackTraces: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@@ -1,28 +1,93 @@
|
||||
import { rtrimSlashes } from '@joplin/lib/path-utils';
|
||||
import { Config } from './utils/types';
|
||||
import { Config, DatabaseConfig, DatabaseConfigClient } from './utils/types';
|
||||
import * as pathUtils from 'path';
|
||||
|
||||
let baseConfig_: Config = null;
|
||||
let baseUrl_: string = null;
|
||||
export interface EnvVariables {
|
||||
APP_BASE_URL?: string;
|
||||
APP_PORT?: string;
|
||||
DB_CLIENT?: string;
|
||||
RUNNING_IN_DOCKER?: string;
|
||||
|
||||
export function initConfig(baseConfig: Config) {
|
||||
baseConfig_ = baseConfig;
|
||||
POSTGRES_PASSWORD?: string;
|
||||
POSTGRES_DATABASE?: string;
|
||||
POSTGRES_USER?: string;
|
||||
POSTGRES_HOST?: string;
|
||||
POSTGRES_PORT?: string;
|
||||
|
||||
SQLITE_DATABASE?: string;
|
||||
}
|
||||
|
||||
let runningInDocker_: boolean = false;
|
||||
|
||||
export function runningInDocker(): boolean {
|
||||
return runningInDocker_;
|
||||
}
|
||||
|
||||
function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): string {
|
||||
if (env.POSTGRES_HOST) {
|
||||
// When running within Docker, the app localhost is different from the
|
||||
// host's localhost. To access the latter, Docker defines a special host
|
||||
// called "host.docker.internal", so here we swap the values if necessary.
|
||||
if (runningInDocker && ['localhost', '127.0.0.1'].includes(env.POSTGRES_HOST)) {
|
||||
return 'host.docker.internal';
|
||||
} else {
|
||||
return env.POSTGRES_HOST;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): DatabaseConfig {
|
||||
if (env.DB_CLIENT === 'pg') {
|
||||
return {
|
||||
client: DatabaseConfigClient.PostgreSQL,
|
||||
name: env.POSTGRES_DATABASE || 'joplin',
|
||||
user: env.POSTGRES_USER || 'joplin',
|
||||
password: env.POSTGRES_PASSWORD || 'joplin',
|
||||
port: env.POSTGRES_PORT ? Number(env.POSTGRES_PORT) : 5432,
|
||||
host: databaseHostFromEnv(runningInDocker, env) || 'localhost',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
client: DatabaseConfigClient.SQLite,
|
||||
name: env.SQLITE_DATABASE || 'prod',
|
||||
asyncStackTraces: true,
|
||||
};
|
||||
}
|
||||
|
||||
function baseUrlFromEnv(env: any, appPort: number): string {
|
||||
if (env.APP_BASE_URL) {
|
||||
return rtrimSlashes(env.APP_BASE_URL);
|
||||
} else {
|
||||
return `http://localhost:${appPort}`;
|
||||
}
|
||||
}
|
||||
|
||||
let config_: Config = null;
|
||||
|
||||
export function initConfig(env: EnvVariables) {
|
||||
runningInDocker_ = !!env.RUNNING_IN_DOCKER;
|
||||
|
||||
const rootDir = pathUtils.dirname(__dirname);
|
||||
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
|
||||
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
|
||||
|
||||
config_ = {
|
||||
rootDir: rootDir,
|
||||
viewDir: viewDir,
|
||||
layoutDir: `${viewDir}/layouts`,
|
||||
logDir: `${rootDir}/logs`,
|
||||
database: databaseConfigFromEnv(runningInDocker_, env),
|
||||
port: appPort,
|
||||
baseUrl: baseUrlFromEnv(env, appPort),
|
||||
};
|
||||
}
|
||||
|
||||
function config(): Config {
|
||||
if (!baseConfig_) throw new Error('Config has not been initialized!');
|
||||
return baseConfig_;
|
||||
}
|
||||
|
||||
export function baseUrl() {
|
||||
if (baseUrl_) return baseUrl_;
|
||||
|
||||
if (process.env.JOPLIN_BASE_URL) {
|
||||
baseUrl_ = rtrimSlashes(process.env.JOPLIN_BASE_URL);
|
||||
} else {
|
||||
baseUrl_ = `http://localhost:${config().port}`;
|
||||
}
|
||||
|
||||
return baseUrl_;
|
||||
if (!config_) throw new Error('Config has not been initialized!');
|
||||
return config_;
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
@@ -47,15 +47,15 @@ export interface ConnectionCheckResult {
|
||||
connection: DbConnection;
|
||||
}
|
||||
|
||||
export function sqliteFilePath(dbConfig: DatabaseConfig): string {
|
||||
return `${sqliteDbDir}/db-${dbConfig.name}.sqlite`;
|
||||
export function sqliteFilePath(name: string): string {
|
||||
return `${sqliteDbDir}/db-${name}.sqlite`;
|
||||
}
|
||||
|
||||
export function makeKnexConfig(dbConfig: DatabaseConfig): KnexDatabaseConfig {
|
||||
const connection: DbConfigConnection = {};
|
||||
|
||||
if (dbConfig.client === 'sqlite3') {
|
||||
connection.filename = sqliteFilePath(dbConfig);
|
||||
connection.filename = sqliteFilePath(dbConfig.name);
|
||||
} else {
|
||||
connection.database = dbConfig.name;
|
||||
connection.host = dbConfig.host;
|
||||
|
@@ -4,47 +4,80 @@ import { defaultAdminEmail, defaultAdminPassword, NotificationLevel } from '../d
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
import config from '../config';
|
||||
|
||||
const logger = Logger.create('notificationHandler');
|
||||
|
||||
async function handleChangeAdminPasswordNotification(ctx: AppContext) {
|
||||
if (!ctx.owner.is_admin) return;
|
||||
|
||||
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
|
||||
const notificationModel = ctx.models.notification({ userId: ctx.owner.id });
|
||||
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
'change_admin_password',
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
|
||||
);
|
||||
} else {
|
||||
await notificationModel.markAsRead('change_admin_password');
|
||||
}
|
||||
|
||||
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
'using_sqlite_in_prod',
|
||||
NotificationLevel.Important,
|
||||
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||
if (!ctx.owner.is_admin) return;
|
||||
|
||||
const notificationModel = ctx.models.notification({ userId: ctx.owner.id });
|
||||
|
||||
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
'using_sqlite_in_prod',
|
||||
NotificationLevel.Important,
|
||||
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
|
||||
const markdownIt = new MarkdownIt();
|
||||
|
||||
const notificationModel = ctx.models.notification({ userId: ctx.owner.id });
|
||||
const notifications = await notificationModel.allUnreadByUserId(ctx.owner.id);
|
||||
const views: NotificationView[] = [];
|
||||
for (const n of notifications) {
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
// The role of this middleware is to inspect the system and to generate
|
||||
// notifications for any issue it finds. It is only active for logged in users
|
||||
// on the website. It is inactive for API calls.
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
ctx.notifications = [];
|
||||
|
||||
try {
|
||||
if (isApiRequest(ctx)) return next();
|
||||
if (!ctx.owner) return next();
|
||||
|
||||
const user = ctx.owner;
|
||||
if (!user) return next();
|
||||
|
||||
const notificationModel = ctx.models.notification({ userId: user.id });
|
||||
|
||||
if (user.is_admin) {
|
||||
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
|
||||
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
'change_admin_password',
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
|
||||
);
|
||||
} else {
|
||||
await notificationModel.markAsRead('change_admin_password');
|
||||
}
|
||||
}
|
||||
|
||||
const markdownIt = new MarkdownIt();
|
||||
const notifications = await notificationModel.allUnreadByUserId(user.id);
|
||||
const views: NotificationView[] = [];
|
||||
for (const n of notifications) {
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
|
||||
ctx.notifications = views;
|
||||
await handleChangeAdminPasswordNotification(ctx);
|
||||
await handleSqliteInProdNotification(ctx);
|
||||
ctx.notifications = await makeNotificationViews(ctx);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ export default async function(ctx: AppContext) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject, {
|
||||
notifications: ctx.notifications || [],
|
||||
hasNotifications: !!ctx.notifications && !!ctx.notifications.length,
|
||||
owner: ctx.owner,
|
||||
});
|
||||
} else {
|
||||
|
@@ -39,6 +39,10 @@ async function findLocalFile(path: string): Promise<string> {
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.public = true;
|
||||
|
||||
// Used to serve static files, so it needs to be public because for example the
|
||||
// login page, which is public, needs access to the CSS files.
|
||||
router.get('', async (path: SubPath, ctx: Koa.Context) => {
|
||||
const localPath = await findLocalFile(path.raw);
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import { ErrorNotFound } from '../../utils/errors';
|
||||
import { File } from '../../db';
|
||||
import { createPaginationLinks, pageMaxSize, Pagination, PaginationOrder, PaginationOrderDir, requestPaginationOrder, validatePagination } from '../../models/utils/pagination';
|
||||
import { setQueryParameters } from '../../utils/urlUtils';
|
||||
import { baseUrl } from '../../config';
|
||||
import config from '../../config';
|
||||
import { formatDateTime } from '../../utils/time';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
@@ -51,7 +51,7 @@ router.get('files/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
async function fileToViewItem(file: File, fileFullPaths: Record<string, string>): Promise<any> {
|
||||
const filePath = fileFullPaths[file.id];
|
||||
|
||||
let url = `${baseUrl()}/files/${filePath}`;
|
||||
let url = `${config().baseUrl}/files/${filePath}`;
|
||||
if (!file.is_directory) {
|
||||
url += '/content';
|
||||
} else {
|
||||
@@ -88,7 +88,7 @@ router.get('files/:id', async (path: SubPath, ctx: AppContext) => {
|
||||
const view: View = defaultView('files');
|
||||
view.content.paginatedFiles = { ...paginatedFiles, items: files };
|
||||
view.content.paginationLinks = paginationLinks;
|
||||
view.content.postUrl = `${baseUrl()}/files`;
|
||||
view.content.postUrl = `${config().baseUrl}/files`;
|
||||
view.content.parentId = parent.id;
|
||||
view.cssFiles = ['index/files'];
|
||||
view.partials.push('pagination');
|
||||
|
@@ -2,7 +2,7 @@ import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { formParse } from '../../utils/requestUtils';
|
||||
import { baseUrl } from '../../config';
|
||||
import config from '../../config';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
|
||||
@@ -27,7 +27,7 @@ router.post('login', async (_path: SubPath, ctx: AppContext) => {
|
||||
|
||||
const session = await ctx.models.session().authenticate(body.fields.email, body.fields.password);
|
||||
ctx.cookies.set('sessionId', session.id);
|
||||
return redirect(ctx, `${baseUrl()}/home`);
|
||||
return redirect(ctx, `${config().baseUrl}/home`);
|
||||
} catch (error) {
|
||||
return makeView(error);
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { baseUrl } from '../../config';
|
||||
import config from '../../config';
|
||||
import { contextSessionId } from '../../utils/requestUtils';
|
||||
|
||||
const router = new Router();
|
||||
@@ -10,7 +10,7 @@ router.post('logout', async (_path: SubPath, ctx: AppContext) => {
|
||||
const sessionId = contextSessionId(ctx, false);
|
||||
ctx.cookies.set('sessionId', '');
|
||||
await ctx.models.session().logout(sessionId);
|
||||
return redirect(ctx, `${baseUrl()}/login`);
|
||||
return redirect(ctx, `${config().baseUrl}/login`);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@@ -4,7 +4,7 @@ import { AppContext, HttpMethod } from '../../utils/types';
|
||||
import { formParse } from '../../utils/requestUtils';
|
||||
import { ErrorUnprocessableEntity } from '../../utils/errors';
|
||||
import { User } from '../../db';
|
||||
import { baseUrl } from '../../config';
|
||||
import config from '../../config';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
|
||||
@@ -55,11 +55,11 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
||||
let postUrl = '';
|
||||
|
||||
if (isNew) {
|
||||
postUrl = `${baseUrl()}/users/new`;
|
||||
postUrl = `${config().baseUrl}/users/new`;
|
||||
} else if (isMe) {
|
||||
postUrl = `${baseUrl()}/users/me`;
|
||||
postUrl = `${config().baseUrl}/users/me`;
|
||||
} else {
|
||||
postUrl = `${baseUrl()}/users/${user.id}`;
|
||||
postUrl = `${config().baseUrl}/users/${user.id}`;
|
||||
}
|
||||
|
||||
const view: View = defaultView('user');
|
||||
@@ -100,7 +100,7 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
|
||||
throw new Error('Invalid form button');
|
||||
}
|
||||
|
||||
return redirect(ctx, `${baseUrl()}/users${userIsMe(path) ? '/me' : ''}`);
|
||||
return redirect(ctx, `${config().baseUrl}/users${userIsMe(path) ? '/me' : ''}`);
|
||||
} catch (error) {
|
||||
const endPoint = router.findEndPoint(HttpMethod.GET, 'users/:id');
|
||||
return endPoint(path, ctx, user, error);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as Mustache from 'mustache';
|
||||
import * as fs from 'fs-extra';
|
||||
import config, { baseUrl } from '../config';
|
||||
import config from '../config';
|
||||
|
||||
export interface RenderOptions {
|
||||
partials?: any;
|
||||
@@ -30,7 +30,7 @@ class MustacheService {
|
||||
|
||||
private get defaultLayoutOptions(): any {
|
||||
return {
|
||||
baseUrl: baseUrl(),
|
||||
baseUrl: config().baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class MustacheService {
|
||||
private resolvesFilePaths(type: string, paths: string[]): string[] {
|
||||
const output: string[] = [];
|
||||
for (const path of paths) {
|
||||
output.push(`${baseUrl()}/${type}/${path}.${type}`);
|
||||
output.push(`${config().baseUrl}/${type}/${path}.${type}`);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
|
||||
|
||||
await execCommand(cmd.join(' '));
|
||||
} else if (config.client === 'sqlite3') {
|
||||
const filePath = sqliteFilePath(config);
|
||||
const filePath = sqliteFilePath(config.name);
|
||||
|
||||
if (await fs.pathExists(filePath)) {
|
||||
if (options.dropIfExists) {
|
||||
@@ -71,6 +71,6 @@ export async function dropDb(config: DatabaseConfig, options: DropDbOptions = nu
|
||||
throw error;
|
||||
}
|
||||
} else if (config.client === 'sqlite3') {
|
||||
await fs.remove(sqliteFilePath(config));
|
||||
await fs.remove(sqliteFilePath(config.name));
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { User, Session, DbConnection, connectDb, disconnectDb, File, truncateTables } from '../../db';
|
||||
import { User, Session, DbConnection, connectDb, disconnectDb, File, truncateTables, sqliteFilePath } from '../../db';
|
||||
import { createDb } from '../../tools/dbTools';
|
||||
import modelFactory from '../../models/factory';
|
||||
import baseConfig from '../../config-tests';
|
||||
import { AppContext, Config, Env } from '../types';
|
||||
import { initConfig } from '../../config';
|
||||
import { AppContext, Env } from '../types';
|
||||
import config, { initConfig } from '../../config';
|
||||
import FileModel from '../../models/FileModel';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import FakeCookies from './koa/FakeCookies';
|
||||
@@ -34,18 +33,16 @@ export async function tempDir(): Promise<string> {
|
||||
return tempDir_;
|
||||
}
|
||||
|
||||
let createdDbName_: string = null;
|
||||
export async function beforeAllDb(unitName: string) {
|
||||
const config: Config = {
|
||||
...baseConfig,
|
||||
database: {
|
||||
...baseConfig.database,
|
||||
name: unitName,
|
||||
},
|
||||
};
|
||||
createdDbName_ = unitName;
|
||||
|
||||
initConfig(config);
|
||||
await createDb(config.database, { dropIfExists: true });
|
||||
db_ = await connectDb(config.database);
|
||||
initConfig({
|
||||
SQLITE_DATABASE: createdDbName_,
|
||||
});
|
||||
|
||||
await createDb(config().database, { dropIfExists: true });
|
||||
db_ = await connectDb(config().database);
|
||||
}
|
||||
|
||||
export async function afterAllTests() {
|
||||
@@ -58,6 +55,12 @@ export async function afterAllTests() {
|
||||
await fs.remove(tempDir_);
|
||||
tempDir_ = null;
|
||||
}
|
||||
|
||||
if (createdDbName_) {
|
||||
const filePath = sqliteFilePath(createdDbName_);
|
||||
await fs.remove(filePath);
|
||||
createdDbName_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function beforeEachDb() {
|
||||
|
@@ -25,8 +25,13 @@ export interface AppContext extends Koa.Context {
|
||||
owner: User;
|
||||
}
|
||||
|
||||
export enum DatabaseConfigClient {
|
||||
PostgreSQL = 'pg',
|
||||
SQLite = 'sqlite3',
|
||||
}
|
||||
|
||||
export interface DatabaseConfig {
|
||||
client: string;
|
||||
client: DatabaseConfigClient;
|
||||
name: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
@@ -40,8 +45,11 @@ export interface Config {
|
||||
rootDir: string;
|
||||
viewDir: string;
|
||||
layoutDir: string;
|
||||
// Not that, for now, nothing is being logged to file. Log is just printed
|
||||
// to stdout, which is then handled by Docker own log mechanism
|
||||
logDir: string;
|
||||
database: DatabaseConfig;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export enum HttpMethod {
|
||||
|
@@ -13,8 +13,8 @@
|
||||
</head>
|
||||
<body class="page-{{{pageName}}}">
|
||||
{{> navbar}}
|
||||
{{> notifications}}
|
||||
<main class="main">
|
||||
{{> notifications}}
|
||||
{{{contentHtml}}}
|
||||
</main>
|
||||
</body>
|
||||
|
@@ -1,9 +1,11 @@
|
||||
{{#global.notifications}}
|
||||
<div class="notification is-{{level}}" id="notification-{{id}}">
|
||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||
{{{messageHtml}}}
|
||||
</div>
|
||||
{{/global.notifications}}
|
||||
{{#global.hasNotifications}}
|
||||
{{#global.notifications}}
|
||||
<div class="notification is-{{level}}" id="notification-{{id}}">
|
||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||
{{{messageHtml}}}
|
||||
</div>
|
||||
{{/global.notifications}}
|
||||
{{/global.hasNotifications}}
|
||||
|
||||
<script>
|
||||
onDocumentReady(function() {
|
||||
|
@@ -155,4 +155,15 @@ utils.registerGulpTasks = function(gulp, tasks) {
|
||||
}
|
||||
};
|
||||
|
||||
utils.setPackagePrivateField = async function(filePath, value) {
|
||||
const text = await fs.readFile(filePath, 'utf8');
|
||||
const obj = JSON.parse(text);
|
||||
if (!value) {
|
||||
delete obj.private;
|
||||
} else {
|
||||
obj.private = true;
|
||||
}
|
||||
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
|
||||
};
|
||||
|
||||
module.exports = utils;
|
||||
|
40
packages/tools/lerna-add.ts
Normal file
40
packages/tools/lerna-add.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// // npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest && npx lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/plugin-repo-builder
|
||||
|
||||
// import { execCommand2, rootDir, gitPullTry } from './tool-utils.js';
|
||||
|
||||
// async function main() {
|
||||
// const argv = require('yargs').argv;
|
||||
|
||||
// console.info(process.argv);
|
||||
|
||||
// const args = [];
|
||||
// if (argv.D) args.push('-D');
|
||||
|
||||
// await execCommand2('npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest');
|
||||
|
||||
// //npx lerna add --no-bootstrap --scope @joplin/plugin-repo-builder -D jest && npx lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/plugin-repo-builder
|
||||
// // process.chdir(rootDir);
|
||||
// // const version = (await execCommand2('npm version patch')).trim();
|
||||
// // const versionShort = version.substr(1);
|
||||
// // const tagName = `server-${version}`;
|
||||
|
||||
// // process.chdir(rootDir);
|
||||
// // console.info(`Running from: ${process.cwd()}`);
|
||||
|
||||
// // await execCommand2(`docker build -t "joplin/server:${versionShort}" -f Dockerfile.server .`);
|
||||
// // await execCommand2(`docker tag "joplin/server:${versionShort}" "joplin/server:latest"`);
|
||||
// // await execCommand2(`docker push joplin/server:${versionShort}`);
|
||||
// // await execCommand2('docker push joplin/server:latest');
|
||||
|
||||
// // await execCommand2('git add -A');
|
||||
// // await execCommand2(`git commit -m 'Server release ${version}'`);
|
||||
// // await execCommand2(`git tag ${tagName}`);
|
||||
// // await execCommand2('git push');
|
||||
// // await execCommand2('git push --tags');
|
||||
// }
|
||||
|
||||
// main().catch((error) => {
|
||||
// console.error('Fatal error');
|
||||
// console.error(error);
|
||||
// process.exit(1);
|
||||
// });
|
@@ -14,7 +14,9 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Poedit 2.3.1\n"
|
||||
"X-Generator: Poedit 2.4.2\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
|
||||
#: packages/app-desktop/bridge.js:106 packages/app-desktop/bridge.js:110
|
||||
#: packages/app-desktop/bridge.js:126 packages/app-desktop/bridge.js:134
|
||||
@@ -61,16 +63,17 @@ msgstr "Batal"
|
||||
msgid ""
|
||||
"The app is now going to close. Please relaunch it to complete the process."
|
||||
msgstr ""
|
||||
"Aplikasi akan ditutup. Silakan luncurkan ulang untuk menyelesaikan prosesnya."
|
||||
|
||||
#: packages/app-desktop/plugins/GotoAnything.js:431
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Type a note title or part of its content to jump to it. Or type # followed "
|
||||
"by a tag name, or @ followed by a notebook name. Or type : to search for "
|
||||
"commands."
|
||||
msgstr ""
|
||||
"Ketik judul catatan atau bagian dari isinya untuk melompat ke sana. Atau "
|
||||
"ketik # diikuti dengan nama label, atau @ diikuti dengan nama buku catatan."
|
||||
"Ketik judul catatan atau bagian di isinya untuk temukan catatan yang dicari. "
|
||||
"Atau ketik # diikuti dengan nama label, atau @ diikuti dengan nama buku "
|
||||
"catatan. Atau ketik : untuk mencari perintah."
|
||||
|
||||
#: packages/app-desktop/plugins/GotoAnything.js:456
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:20
|
||||
@@ -116,9 +119,8 @@ msgid "New version: %s"
|
||||
msgstr "Versi baru: %s"
|
||||
|
||||
#: packages/app-desktop/checkForUpdates.js:154
|
||||
#, fuzzy
|
||||
msgid "Download"
|
||||
msgstr "Terunduh"
|
||||
msgstr "Unduh"
|
||||
|
||||
#: packages/app-desktop/checkForUpdates.js:154
|
||||
msgid "Full Release Notes"
|
||||
@@ -277,7 +279,6 @@ msgid "strong text"
|
||||
msgstr "teks yang ditekankan lebih kuat (strong text)"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:179
|
||||
#, fuzzy
|
||||
msgid "emphasised text"
|
||||
msgstr "teks yang ditekankan (emphasized text)"
|
||||
|
||||
@@ -324,7 +325,6 @@ msgid "Paste"
|
||||
msgstr "Tempel"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
|
||||
#, fuzzy
|
||||
msgid "Checkbox list"
|
||||
msgstr "Kotak centang"
|
||||
|
||||
@@ -355,7 +355,7 @@ msgstr "Masukkan Waktu dan Tanggal"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:1003
|
||||
msgid "Drop notes or files here"
|
||||
msgstr ""
|
||||
msgstr "Taruh catatan atau file di sini"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:1003
|
||||
#, fuzzy
|
||||
@@ -368,6 +368,8 @@ msgid ""
|
||||
"Please wait for all attachments to be downloaded and decrypted. You may also "
|
||||
"switch to %s to edit the note."
|
||||
msgstr ""
|
||||
"Silakan tunggu sampai semua lampiran sudah diunduh dan didekripsi. Anda juga "
|
||||
"bisa beralih ke %s untuk mengedit catatan."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js:73
|
||||
msgid "There was an error downloading this attachment:"
|
||||
@@ -388,7 +390,7 @@ msgstr "Simpan sebagai..."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.js:74
|
||||
msgid "Reveal file in folder"
|
||||
msgstr ""
|
||||
msgstr "Buka file di berkas"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.js:82
|
||||
msgid "Copy path to clipboard"
|
||||
@@ -416,24 +418,26 @@ msgid ""
|
||||
"This Rich Text editor has a number of limitations and it is recommended to "
|
||||
"be aware of them before using it."
|
||||
msgstr ""
|
||||
"Editor Rich Text ini punya beberapa keterbatasan dan Anda dianjurkan untuk "
|
||||
"tahu keterbatasannya sebelum menggunakan editor ini."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:340
|
||||
msgid "Read more about it"
|
||||
msgstr ""
|
||||
msgstr "Baca lebih lanjut"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:345
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
msgstr "Abaikan"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:381
|
||||
msgid "The following attachments are being watched for changes:"
|
||||
msgstr ""
|
||||
msgstr "Lampiran ini sedang diawasi untuk perubahan:"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:384
|
||||
msgid ""
|
||||
"The attachments will no longer be watched when you switch to a different "
|
||||
"note."
|
||||
msgstr ""
|
||||
msgstr "Lampiran tidak akan diawasi ketika Anda berganti ke catatan lain."
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/NoteEditor.js:389
|
||||
#, javascript-format
|
||||
@@ -481,34 +485,32 @@ msgid "Horizontal Rule"
|
||||
msgstr "Garis Horisontal"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:88
|
||||
#, fuzzy
|
||||
msgid "Delete line"
|
||||
msgstr "Hapus catatan?"
|
||||
msgstr "Hapus catatan"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:92
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
msgstr "Urungkan"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:96
|
||||
msgid "Redo"
|
||||
msgstr ""
|
||||
msgstr "Ulangi"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:100
|
||||
msgid "Indent less"
|
||||
msgstr ""
|
||||
msgstr "Kurangi indentasi"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:104
|
||||
msgid "Indent more"
|
||||
msgstr ""
|
||||
msgstr "Tambah indentasi"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:108
|
||||
#, fuzzy
|
||||
msgid "Toggle comment"
|
||||
msgstr "Alihkan daftar catatan"
|
||||
msgstr "Lihat/sembunyikan komentar"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:112
|
||||
msgid "Sort selected lines"
|
||||
msgstr ""
|
||||
msgstr "Sortir baris terpilih"
|
||||
|
||||
#: packages/app-desktop/gui/NoteEditor/commands/editorCommandDeclarations.js:116
|
||||
msgid "Swap line up"
|
||||
@@ -580,9 +582,9 @@ msgstr "Properti catatan"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:63
|
||||
#: packages/lib/services/KeymapService.js:176
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Error: %s"
|
||||
msgstr "Kesalahan"
|
||||
msgstr "Terjadi kesalahan: %s"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:110
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:443
|
||||
@@ -599,12 +601,11 @@ msgstr "Impor"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:126
|
||||
msgid "Command"
|
||||
msgstr ""
|
||||
msgstr "Perintah"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js:127
|
||||
#, fuzzy
|
||||
msgid "Keyboard Shortcut"
|
||||
msgstr "Mode Papan Ketik"
|
||||
msgstr "Pintasan Papan Ketik"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:14
|
||||
#: packages/app-desktop/gui/MenuBar.js:156 packages/app-desktop/app.js:338
|
||||
@@ -627,9 +628,8 @@ msgid "Website and documentation"
|
||||
msgstr "Situs web dan dokumentasi"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:24
|
||||
#, fuzzy
|
||||
msgid "Hide Joplin"
|
||||
msgstr "Tentang Joplin"
|
||||
msgstr "Sembunyikan Joplin"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:26
|
||||
#: packages/app-desktop/gui/MenuBar.js:408
|
||||
@@ -637,9 +637,8 @@ msgid "Close Window"
|
||||
msgstr "Tutup jendela"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:30
|
||||
#, fuzzy
|
||||
msgid "Preferences"
|
||||
msgstr "Preferensi..."
|
||||
msgstr "Preferensi"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:30
|
||||
#: packages/app-desktop/gui/MenuBar.js:284 packages/app-desktop/gui/Root.js:100
|
||||
@@ -647,19 +646,20 @@ msgid "Options"
|
||||
msgstr "Pilihan"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/utils/getLabel.js:37
|
||||
#, fuzzy
|
||||
msgid "Invalid"
|
||||
msgstr "Jawaban tidak valid: %s"
|
||||
msgstr "Jawaban tidak valid"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:49
|
||||
msgid "Press the shortcut"
|
||||
msgstr ""
|
||||
msgstr "Klik pintasan"
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:49
|
||||
msgid ""
|
||||
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the "
|
||||
"shortcut."
|
||||
msgstr ""
|
||||
"Klik pintasan kemudian tekan ENTER atau tekan BACKSPACE untuk menghapus "
|
||||
"pintasan."
|
||||
|
||||
#: packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js:50
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:96
|
||||
@@ -673,11 +673,13 @@ msgid ""
|
||||
"may take a few minutes to complete and the app needs to be restarted. To "
|
||||
"proceed please click on the link."
|
||||
msgstr ""
|
||||
"Target sinkronisasi harus diperbarui sebelum Joplin dapat sinkronisasi. "
|
||||
"Operasinya akan berjalan beberapa menit dan aplikasi perlu dimulai ulang. "
|
||||
"Untuk melanjutkan, silakan klik linknya."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:405
|
||||
#, fuzzy
|
||||
msgid "Restart and upgrade"
|
||||
msgstr "Kunci master yang perlu upgrade"
|
||||
msgstr "Mulai ulang dan upgrade"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:409
|
||||
msgid "Some items cannot be synchronised."
|
||||
@@ -719,6 +721,8 @@ msgstr "Info lain"
|
||||
#: packages/app-desktop/gui/MainScreen/MainScreen.js:528
|
||||
msgid "Use the arrows to move the layout items. Press \"Escape\" to exit."
|
||||
msgstr ""
|
||||
"Gunakan tanda panah untuk memindahkan item tata letak. Tekan \"Escape\" "
|
||||
"untuk keluar."
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js:18
|
||||
msgid "Statistics..."
|
||||
@@ -782,13 +786,12 @@ msgid "Toggle editor layout"
|
||||
msgstr "Alihkan tata letak editor"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/toggleEditors.js:18
|
||||
#, fuzzy
|
||||
msgid "Toggle editors"
|
||||
msgstr "Alihkan tata letak editor"
|
||||
msgstr "Alihkan editor"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js:16
|
||||
msgid "Change application layout"
|
||||
msgstr ""
|
||||
msgstr "Ubah tata letak aplikasi"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/renameTag.js:30
|
||||
msgid "Rename tag:"
|
||||
@@ -805,7 +808,7 @@ msgstr "Judul buku catatan:"
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js:19
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:180
|
||||
msgid "Spell checker"
|
||||
msgstr ""
|
||||
msgstr "Pemeriksa ejaan"
|
||||
|
||||
#: packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js:16
|
||||
msgid "Share note..."
|
||||
@@ -999,9 +1002,8 @@ msgstr "Templat"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:377
|
||||
#: packages/app-desktop/gui/MenuBar.js:423
|
||||
#, fuzzy
|
||||
msgid "Export all"
|
||||
msgstr "Ekspor"
|
||||
msgstr "Ekspor semuanya"
|
||||
|
||||
#: packages/app-desktop/gui/MenuBar.js:389
|
||||
#, javascript-format
|
||||
@@ -1236,27 +1238,23 @@ msgstr ""
|
||||
"Penting: Anda hanya perlu menjalankan ini SEKALI pada satu perangkat."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:198
|
||||
#, fuzzy
|
||||
msgid "Re-encryption"
|
||||
msgstr "Enkripsi"
|
||||
msgstr "Enkripsi ulang"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:213
|
||||
msgid "Ignore"
|
||||
msgstr ""
|
||||
msgstr "Abaikan"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:244
|
||||
#: packages/app-mobile/components/screens/encryption-config.js:215
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||
"continue?"
|
||||
msgstr ""
|
||||
"Mengaktifkan enkripsi berarti *semua* catatan dan lampiran Anda akan "
|
||||
"disinkronkan ulang dan dikirim dalam keadaan terenkripsi ke target "
|
||||
"sinkronisasi. Jangan sampai kehilangan kata sandinya karena, untuk alasan "
|
||||
"keamanan, ini akan menjadi *hanya* satu-satunya cara untuk mendekripsi data! "
|
||||
"Untuk mengaktifkan enkripsi, silakan masukkan kata sandi Anda di bawah ini."
|
||||
"Menonaktifkan enkripsi berarti *semua* catatan dan lampiran Anda akan "
|
||||
"disinkronkan ulang dan dikirim dalam keadaan tidak terenkripsi ke target "
|
||||
"sinkronisasi. Apa Anda mau lanjut?"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:246
|
||||
#: packages/app-mobile/components/screens/encryption-config.js:150
|
||||
@@ -1269,8 +1267,8 @@ msgstr ""
|
||||
"Mengaktifkan enkripsi berarti *semua* catatan dan lampiran Anda akan "
|
||||
"disinkronkan ulang dan dikirim dalam keadaan terenkripsi ke target "
|
||||
"sinkronisasi. Jangan sampai kehilangan kata sandinya karena, untuk alasan "
|
||||
"keamanan, ini akan menjadi *hanya* satu-satunya cara untuk mendekripsi data! "
|
||||
"Untuk mengaktifkan enkripsi, silakan masukkan kata sandi Anda di bawah ini."
|
||||
"keamanan, ini *hanya* satu-satunya cara untuk mendekripsi data! Untuk "
|
||||
"mengaktifkan enkripsi, silakan masukkan kata sandi Anda di bawah ini."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:275
|
||||
#: packages/app-mobile/components/screens/encryption-config.js:258
|
||||
@@ -1283,28 +1281,24 @@ msgid "Enable encryption"
|
||||
msgstr "Aktifkan enkripsi"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:290
|
||||
#, fuzzy
|
||||
msgid "Master Keys"
|
||||
msgstr "Kunci Master %s"
|
||||
msgstr "Kunci Master"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:304
|
||||
#, fuzzy
|
||||
msgid "Active"
|
||||
msgstr "Aksi"
|
||||
msgstr "Aktif"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:314
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
msgstr "Sumber"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:329
|
||||
#, fuzzy
|
||||
msgid "Password"
|
||||
msgstr "Kata sandi:"
|
||||
msgstr "Kata sandi"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:334
|
||||
#, fuzzy
|
||||
msgid "Password OK"
|
||||
msgstr "Kata sandi:"
|
||||
msgstr "Kata sandi OK"
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:343
|
||||
msgid ""
|
||||
@@ -1312,6 +1306,10 @@ msgid ""
|
||||
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
"Peringatan: Hanya satu kunci master yang akan digunakan untuk enkripsi "
|
||||
"(kunci yang ditandai \"aktif\"). Kunci yang lain mungkin digunakan untuk "
|
||||
"dekripsi, tergantung bagaimana buku catatan atau catatannya awalnya "
|
||||
"dienkripsi."
|
||||
|
||||
#: packages/app-desktop/gui/EncryptionConfigScreen.min.js:371
|
||||
#: packages/app-mobile/components/screens/encryption-config.js:248
|
||||
@@ -1422,7 +1420,7 @@ msgstr "Telusuri..."
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:456
|
||||
msgid "The application must be restarted for these changes to take effect."
|
||||
msgstr ""
|
||||
msgstr "Aplikasi harus dimulai ulang agar perubahan ini diterapkan."
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:468
|
||||
#: packages/app-desktop/gui/NoteList/NoteList.js:163
|
||||
@@ -1431,25 +1429,24 @@ msgstr "Lakukan sekarang"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:468
|
||||
msgid "Later"
|
||||
msgstr ""
|
||||
msgstr "Nanti"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ConfigScreen.js:521
|
||||
#, fuzzy
|
||||
msgid "Restart now"
|
||||
msgstr "Dapatkan sekarang:"
|
||||
msgstr "Mulai ulang sekarang"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/ButtonBar.js:27
|
||||
msgid "Apply"
|
||||
msgstr "Terapkan"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js:137
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Delete plugin \"%s\"?"
|
||||
msgstr "Hapus catatan \"%s\"?"
|
||||
msgstr "Hapus plugin \"%s\"?"
|
||||
|
||||
#: packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js:181
|
||||
msgid "Install plugin"
|
||||
msgstr ""
|
||||
msgstr "Pasang plugin"
|
||||
|
||||
#: packages/app-desktop/gui/PromptDialog.min.js:249
|
||||
msgid "Clear"
|
||||
@@ -1486,12 +1483,11 @@ msgstr "Viewer"
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.js:105
|
||||
#, javascript-format
|
||||
msgid "Read time: %s min"
|
||||
msgstr ""
|
||||
msgstr "Waktu baca: %s menit"
|
||||
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.js:108
|
||||
#, fuzzy
|
||||
msgid "Statistics"
|
||||
msgstr "Statistik..."
|
||||
msgstr "Statistik"
|
||||
|
||||
#: packages/app-desktop/gui/NoteContentPropertiesDialog.js:113
|
||||
#: packages/app-desktop/gui/ShareNoteDialog.js:175
|
||||
@@ -1619,18 +1615,16 @@ msgid_plural "Copy Shareable Links"
|
||||
msgstr[0] "Salin Tautan yang Dapat Dibagikan"
|
||||
|
||||
#: packages/app-desktop/commands/toggleExternalEditing.js:18
|
||||
#, fuzzy
|
||||
msgid "Toggle external editing"
|
||||
msgstr "Hentikan pengeditan eksternal"
|
||||
msgstr "Ubah pengeditan eksternal"
|
||||
|
||||
#: packages/app-desktop/commands/toggleExternalEditing.js:37
|
||||
msgid "Stop"
|
||||
msgstr ""
|
||||
|
||||
#: packages/app-desktop/commands/copyDevCommand.js:18
|
||||
#, fuzzy
|
||||
msgid "Copy dev mode command to clipboard"
|
||||
msgstr "Salin jalur ke papan klip"
|
||||
msgstr "Salin perintah dev mode ke papan klip"
|
||||
|
||||
#: packages/app-desktop/commands/stopExternalEditing.js:18
|
||||
msgid "Stop external editing"
|
||||
@@ -1647,9 +1641,8 @@ msgid "Error opening note in editor: %s"
|
||||
msgstr "Terjadi kesalahan saat membuka catatan di editor: %s"
|
||||
|
||||
#: packages/app-desktop/commands/openProfileDirectory.js:18
|
||||
#, fuzzy
|
||||
msgid "Open profile directory"
|
||||
msgstr "Buka direktori templat"
|
||||
msgstr "Buka direktori profil"
|
||||
|
||||
#: packages/app-desktop/app.js:336
|
||||
#, javascript-format
|
||||
@@ -1677,9 +1670,8 @@ msgid "Save alarm"
|
||||
msgstr "Simpan alarm"
|
||||
|
||||
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:28
|
||||
#, fuzzy
|
||||
msgid "Open"
|
||||
msgstr "Buka..."
|
||||
msgstr "Buka"
|
||||
|
||||
#: packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js:29
|
||||
#: packages/app-mobile/components/screens/Note.js:796
|
||||
@@ -1750,8 +1742,9 @@ msgid "Refresh"
|
||||
msgstr "Segarkan"
|
||||
|
||||
#: packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js:42
|
||||
#, fuzzy
|
||||
msgid "Sync Target Upgrade"
|
||||
msgstr ""
|
||||
msgstr "Sinkronisasi Target Peningkatan"
|
||||
|
||||
#: packages/app-mobile/components/screens/notes.js:154
|
||||
#: packages/app-cli/app/command-rmbook.js:26
|
||||
@@ -1967,11 +1960,14 @@ msgid ""
|
||||
"\n"
|
||||
"You may turn off this option at any time in the Configuration screen."
|
||||
msgstr ""
|
||||
"Untuk menghubungkan geolokasi dengan catatan, aplikasi perlu izin Anda untuk "
|
||||
"mengakses lokasi Anda.\n"
|
||||
"\n"
|
||||
"Anda dapat mematikan fungsi ini kapan saja di layar Konfigurasi."
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.js:343
|
||||
#, fuzzy
|
||||
msgid "Permission needed"
|
||||
msgstr "Izin penggunaan kamera"
|
||||
msgstr "Izin diperlukan"
|
||||
|
||||
#: packages/app-mobile/components/screens/Note.js:449
|
||||
#: packages/app-cli/app/command-rmnote.js:27
|
||||
@@ -2052,11 +2048,12 @@ msgid ""
|
||||
"The default admin password is insecure and has not been changed! [Change it "
|
||||
"now](%s)"
|
||||
msgstr ""
|
||||
"Kata sandi default admin tidak aman dan belum diubah. [Ubah sekarang](%s)"
|
||||
|
||||
#: packages/lib/onedrive-api-node-utils.js:46
|
||||
#, javascript-format
|
||||
msgid "All potential ports are in use - please report the issue at %s"
|
||||
msgstr ""
|
||||
msgstr "Semua port potensial sedang digunakan - silakan laporkan isu di %s"
|
||||
|
||||
#: packages/lib/onedrive-api-node-utils.js:86
|
||||
msgid ""
|
||||
@@ -2092,9 +2089,8 @@ msgid "Dropbox"
|
||||
msgstr "Dropbox"
|
||||
|
||||
#: packages/lib/SyncTargetJoplinServer.js:30
|
||||
#, fuzzy
|
||||
msgid "Joplin Server"
|
||||
msgstr "Situs web Joplin"
|
||||
msgstr "Server Joplin"
|
||||
|
||||
#: packages/lib/shim-init-node.js:201
|
||||
#, javascript-format
|
||||
@@ -2239,21 +2235,19 @@ msgstr ""
|
||||
|
||||
#: packages/lib/models/Setting.js:259
|
||||
msgid "Joplin Server URL"
|
||||
msgstr ""
|
||||
msgstr "URL Server Joplin"
|
||||
|
||||
#: packages/lib/models/Setting.js:273
|
||||
#, fuzzy
|
||||
msgid "Joplin Server Directory"
|
||||
msgstr "Direktori Ekspor Joplin"
|
||||
|
||||
#: packages/lib/models/Setting.js:283
|
||||
msgid "Joplin Server username"
|
||||
msgstr ""
|
||||
msgstr "Nama pengguna Server Joplin"
|
||||
|
||||
#: packages/lib/models/Setting.js:293
|
||||
#, fuzzy
|
||||
msgid "Joplin Server password"
|
||||
msgstr "Masukkan kata sandi master:"
|
||||
msgstr "Kata sandi Server Joplin"
|
||||
|
||||
#: packages/lib/models/Setting.js:305
|
||||
msgid "Attachment download behaviour"
|
||||
@@ -2386,9 +2380,8 @@ msgid "Enable typographer support"
|
||||
msgstr "Aktifkan dukungan typographer"
|
||||
|
||||
#: packages/lib/models/Setting.js:562
|
||||
#, fuzzy
|
||||
msgid "Enable Linkify"
|
||||
msgstr "Aktifkan riwayat catatan"
|
||||
msgstr "Aktifkan Linkify"
|
||||
|
||||
#: packages/lib/models/Setting.js:563
|
||||
msgid "Enable math expressions"
|
||||
@@ -2403,17 +2396,16 @@ msgid "Enable Mermaid diagrams support"
|
||||
msgstr "Aktifkan dukungan diagram Mermaid"
|
||||
|
||||
#: packages/lib/models/Setting.js:566
|
||||
#, fuzzy
|
||||
msgid "Enable audio player"
|
||||
msgstr "Aktifkan emoji markdown"
|
||||
msgstr "Aktifkan pemutar audio"
|
||||
|
||||
#: packages/lib/models/Setting.js:567
|
||||
msgid "Enable video player"
|
||||
msgstr ""
|
||||
msgstr "Aktifkan pemutar video"
|
||||
|
||||
#: packages/lib/models/Setting.js:568
|
||||
msgid "Enable PDF viewer"
|
||||
msgstr ""
|
||||
msgstr "Aktifkan penampil PDF"
|
||||
|
||||
#: packages/lib/models/Setting.js:569
|
||||
msgid "Enable ==mark== syntax"
|
||||
@@ -2490,20 +2482,18 @@ msgid "Editor font family"
|
||||
msgstr "Keluarga font pada editor"
|
||||
|
||||
#: packages/lib/models/Setting.js:641
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"This should be a *monospace* font or some elements will render incorrectly. "
|
||||
"If the font is incorrect or empty, it will default to a generic monospace "
|
||||
"font."
|
||||
msgstr ""
|
||||
"Ini harus font *monospace* atau ia tidak akan berfungsi sebagaimana "
|
||||
"mestinya. Jika font tidak benar atau kosong, font akan disetel secara "
|
||||
"otomatis ke font monospace generik."
|
||||
"Ini harus font *monospace* atau beberapa elemen tidak akan berfungsi "
|
||||
"sebagaimana mestinya. Jika font tidak benar atau kosong, font akan disetel "
|
||||
"secara otomatis ke font monospace generik."
|
||||
|
||||
#: packages/lib/models/Setting.js:662
|
||||
#, fuzzy
|
||||
msgid "Custom stylesheet for rendered Markdown"
|
||||
msgstr "Lembar gaya khusus untuk gaya keseluruhan aplikasi Joplin"
|
||||
msgstr "Lembar gaya untuk rendering Markdown"
|
||||
|
||||
#: packages/lib/models/Setting.js:678
|
||||
msgid "Custom stylesheet for Joplin-wide app styles"
|
||||
@@ -2734,9 +2724,8 @@ msgid "Web Clipper"
|
||||
msgstr "Web Clipper"
|
||||
|
||||
#: packages/lib/models/Setting.js:1389
|
||||
#, fuzzy
|
||||
msgid "Keyboard Shortcuts"
|
||||
msgstr "Mode Papan Ketik"
|
||||
msgstr "Pintasan Papan Ketik"
|
||||
|
||||
#: packages/lib/models/Setting.js:1396
|
||||
msgid ""
|
||||
@@ -2774,17 +2763,20 @@ msgid "Downloaded"
|
||||
msgstr "Terunduh"
|
||||
|
||||
#: packages/lib/models/Resource.js:382
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Attachment conflict: \"%s\""
|
||||
msgstr "Lampiran"
|
||||
msgstr "Konflik lampiran: \"%s\""
|
||||
|
||||
#: packages/lib/models/Resource.js:383
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"There was a [conflict](%s) on the attachment below.\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Terjadi kesalahan saat mengunduh lampiran ini:"
|
||||
msgstr ""
|
||||
"Terjadi konflik (%s) saat mengunduh lampiran ini:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
#: packages/lib/models/Tag.js:202
|
||||
#, javascript-format
|
||||
@@ -2978,6 +2970,7 @@ msgstr "Sedang berlangsung"
|
||||
msgid ""
|
||||
"Unknown item type downloaded - please upgrade Joplin to the latest version"
|
||||
msgstr ""
|
||||
"Tipe item tidak dikenal diunduh - silakan perbarui Joplin ke versi terbaru"
|
||||
|
||||
#: packages/lib/JoplinServerApi.js:71
|
||||
#, javascript-format
|
||||
@@ -3007,7 +3000,7 @@ msgstr "Sistem berkas"
|
||||
|
||||
#: packages/lib/commands/historyForward.js:17
|
||||
msgid "Forward"
|
||||
msgstr ""
|
||||
msgstr "Maju"
|
||||
|
||||
#: packages/lib/versionInfo.js:10
|
||||
#, javascript-format
|
||||
@@ -3037,7 +3030,7 @@ msgstr "Versi Profil: %s"
|
||||
#: packages/lib/versionInfo.js:25
|
||||
#, javascript-format
|
||||
msgid "Keychain Supported: %s"
|
||||
msgstr ""
|
||||
msgstr "Keychain Didukung: %s"
|
||||
|
||||
#: packages/lib/services/RevisionService.js:221
|
||||
msgid "Restored Notes"
|
||||
@@ -3086,14 +3079,14 @@ msgid "HTML Directory"
|
||||
msgstr "Direktori HTML"
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:127
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\""
|
||||
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\""
|
||||
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\" dan output \"%s\""
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:150
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Cannot load \"%s\" module for format \"%s\" and target \"%s\""
|
||||
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\""
|
||||
msgstr "Tidak dapat memuat modul \"%s\" untuk format \"%s\" dan target \"%s\""
|
||||
|
||||
#: packages/lib/services/interop/InteropService.js:194
|
||||
#: packages/lib/services/interop/InteropService.js:206
|
||||
@@ -3131,23 +3124,23 @@ msgstr "Silakan tentukan format impor untuk %s"
|
||||
|
||||
#: packages/lib/services/KeymapService.js:273
|
||||
msgid "command"
|
||||
msgstr ""
|
||||
msgstr "perintah"
|
||||
|
||||
#: packages/lib/services/KeymapService.js:273
|
||||
#: packages/lib/services/KeymapService.js:278
|
||||
#, javascript-format
|
||||
msgid "\"%s\" is missing the required \"%s\" property."
|
||||
msgstr ""
|
||||
msgstr "\"%s\" tidak menemukan properti \"%s\"."
|
||||
|
||||
#: packages/lib/services/KeymapService.js:278
|
||||
#: packages/lib/services/KeymapService.js:285
|
||||
msgid "accelerator"
|
||||
msgstr ""
|
||||
msgstr "akselerator"
|
||||
|
||||
#: packages/lib/services/KeymapService.js:285
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Invalid %s: %s."
|
||||
msgstr "Jawaban tidak valid: %s"
|
||||
msgstr "%s tidak valid: %s."
|
||||
|
||||
#: packages/lib/services/KeymapService.js:303
|
||||
#, javascript-format
|
||||
@@ -3155,11 +3148,13 @@ msgid ""
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to "
|
||||
"unexpected behaviour."
|
||||
msgstr ""
|
||||
"Akselerator \"%s\" digunakan untuk perintah \"%s\" dan \"%s\". Ini bisa "
|
||||
"memberikan dampak tidak terduga."
|
||||
|
||||
#: packages/lib/services/KeymapService.js:328
|
||||
#, javascript-format
|
||||
msgid "Accelerator \"%s\" is not valid."
|
||||
msgstr ""
|
||||
msgstr "Akselerator \"%s\" tidak valid."
|
||||
|
||||
#: packages/lib/services/report.js:121
|
||||
msgid "Items that cannot be synchronised"
|
||||
@@ -3223,9 +3218,8 @@ msgid "Downloaded and encrypted"
|
||||
msgstr "Terunduh dan terenkripsi"
|
||||
|
||||
#: packages/lib/services/report.js:193
|
||||
#, fuzzy
|
||||
msgid "Created locally"
|
||||
msgstr "Item lokal yang dibuat: %d."
|
||||
msgstr "Item lokal yang dibuat"
|
||||
|
||||
#: packages/lib/services/report.js:206
|
||||
msgid "Attachments that could not be downloaded"
|
||||
@@ -3280,20 +3274,19 @@ msgstr "Di %s: %s"
|
||||
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:107
|
||||
msgid "No suggestions"
|
||||
msgstr ""
|
||||
msgstr "Tidak ada saran"
|
||||
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:114
|
||||
msgid "Add to dictionary"
|
||||
msgstr ""
|
||||
msgstr "Tambahkan ke kamus"
|
||||
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:153
|
||||
msgid "Use spell checker"
|
||||
msgstr ""
|
||||
msgstr "Gunakan pemeriksa ejaan"
|
||||
|
||||
#: packages/lib/services/spellChecker/SpellCheckerService.js:173
|
||||
#, fuzzy
|
||||
msgid "Change language"
|
||||
msgstr "Bahasa"
|
||||
msgstr "Ubah bahasa"
|
||||
|
||||
#: packages/app-cli/app/command-cp.js:13
|
||||
msgid ""
|
||||
@@ -3368,9 +3361,9 @@ msgid "Do not ask for confirmation."
|
||||
msgstr "Jangan meminta konfirmasi."
|
||||
|
||||
#: packages/app-cli/app/command-import.js:27
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Output format: %s"
|
||||
msgstr "Format sumber: %s"
|
||||
msgstr "Format output: %s"
|
||||
|
||||
#: packages/app-cli/app/command-import.js:65
|
||||
msgid "Importing notes..."
|
||||
@@ -3524,7 +3517,7 @@ msgstr ""
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:35
|
||||
msgid "Upgrade the sync target to the latest version."
|
||||
msgstr ""
|
||||
msgstr "Perbarui target sinkronisasi ke versi terakhir."
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:105
|
||||
#, javascript-format
|
||||
@@ -3554,7 +3547,6 @@ msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Target sinkronisasi: %s (%s)"
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:177
|
||||
#, fuzzy
|
||||
msgid "Cannot initialise synchroniser."
|
||||
msgstr "Tidak dapat menginisiasi penyinkron."
|
||||
|
||||
@@ -3569,7 +3561,7 @@ msgstr "Mengunduh sumber daya..."
|
||||
#: packages/app-cli/app/command-sync.js:242
|
||||
#, javascript-format
|
||||
msgid "Sync target must be upgraded! Run `%s` to proceed."
|
||||
msgstr ""
|
||||
msgstr "Target sinkronisasi harus diperbarui. Jalankan `%s` untuk melanjutkan."
|
||||
|
||||
#: packages/app-cli/app/command-sync.js:260
|
||||
msgid "Cancelling... Please wait."
|
||||
@@ -3628,12 +3620,11 @@ msgstr "Catatan tidak ada: \"%s\". Buat?"
|
||||
|
||||
#: packages/app-cli/app/command-edit.js:75
|
||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr ""
|
||||
msgstr "Mulai mengedit catatan. Tutup editor untuk kembali ke prompt."
|
||||
|
||||
#: packages/app-cli/app/command-edit.js:97
|
||||
#, fuzzy
|
||||
msgid "Note has been saved."
|
||||
msgstr "Belum ada buku catatan yang ditentukan."
|
||||
msgstr "Catatan sudah disimpan."
|
||||
|
||||
#: packages/app-cli/app/app-gui.js:452
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
|
2
packages/tools/package-lock.json
generated
2
packages/tools/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/tools",
|
||||
"version": "1.0.16",
|
||||
"version": "1.7.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/tools",
|
||||
"version": "1.0.16",
|
||||
"version": "1.7.2",
|
||||
"description": "Various tools for Joplin",
|
||||
"main": "index.js",
|
||||
"author": "Laurent Cozic",
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joplin/lib": "^1.0.16",
|
||||
"@joplin/lib": "^1.7.2",
|
||||
"execa": "^4.1.0",
|
||||
"fs-extra": "^4.0.3",
|
||||
"gettext-parser": "^1.3.0",
|
||||
|
@@ -1,37 +1,40 @@
|
||||
const { execCommand, githubRelease, rootDir } = require('./tool-utils.js');
|
||||
|
||||
const appDir = `${rootDir}/packages/app-desktop`;
|
||||
|
||||
async function main() {
|
||||
const argv = require('yargs').argv;
|
||||
|
||||
process.chdir(appDir);
|
||||
|
||||
console.info(`Running from: ${process.cwd()}`);
|
||||
|
||||
const version = (await execCommand('npm version patch')).trim();
|
||||
const tagName = version;
|
||||
|
||||
console.info(`New version number: ${version}`);
|
||||
|
||||
console.info(await execCommand('git add -A'));
|
||||
console.info(await execCommand(`git commit -m "Desktop release ${version}"`));
|
||||
console.info(await execCommand(`git tag ${tagName}`));
|
||||
console.info(await execCommand('git push && git push --tags'));
|
||||
|
||||
const releaseOptions = { isDraft: true, isPreRelease: !!argv.beta };
|
||||
|
||||
console.info('Release options: ', releaseOptions);
|
||||
|
||||
const release = await githubRelease('joplin', tagName, releaseOptions);
|
||||
|
||||
console.info(`Created GitHub release: ${release.html_url}`);
|
||||
console.info('GitHub release page: https://github.com/laurent22/joplin/releases');
|
||||
console.info(`To create changelog: node packages/tools/git-changelog.js ${version}`);
|
||||
'use strict';
|
||||
const __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function(resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const tool_utils_1 = require('./tool-utils');
|
||||
const appDir = `${tool_utils_1.rootDir}/packages/app-desktop`;
|
||||
function main() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield tool_utils_1.gitPullTry(false);
|
||||
const argv = require('yargs').argv;
|
||||
process.chdir(appDir);
|
||||
console.info(`Running from: ${process.cwd()}`);
|
||||
const version = (yield tool_utils_1.execCommand2('npm version patch')).trim();
|
||||
const tagName = version;
|
||||
console.info(`New version number: ${version}`);
|
||||
console.info(yield tool_utils_1.execCommand2('git add -A'));
|
||||
console.info(yield tool_utils_1.execCommand2(`git commit -m "Desktop release ${version}"`));
|
||||
console.info(yield tool_utils_1.execCommand2(`git tag ${tagName}`));
|
||||
console.info(yield tool_utils_1.execCommand2('git push && git push --tags'));
|
||||
const releaseOptions = { isDraft: true, isPreRelease: !!argv.beta };
|
||||
console.info('Release options: ', releaseOptions);
|
||||
const release = yield tool_utils_1.githubRelease('joplin', tagName, releaseOptions);
|
||||
console.info(`Created GitHub release: ${release.html_url}`);
|
||||
console.info('GitHub release page: https://github.com/laurent22/joplin/releases');
|
||||
console.info(`To create changelog: node packages/tools/git-changelog.js ${version}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
// # sourceMappingURL=release-electron.js.map
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user