mirror of https://github.com/laurent22/joplin.git
Merge branch 'dev' into release-2.0
commit
4098c01e7c
|
@ -140,6 +140,9 @@ packages/app-desktop/commands/openProfileDirectory.js.map
|
|||
packages/app-desktop/commands/replaceMisspelling.d.ts
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js.map
|
||||
packages/app-desktop/commands/restoreNoteRevision.d.ts
|
||||
packages/app-desktop/commands/restoreNoteRevision.js
|
||||
packages/app-desktop/commands/restoreNoteRevision.js.map
|
||||
packages/app-desktop/commands/startExternalEditing.d.ts
|
||||
packages/app-desktop/commands/startExternalEditing.js
|
||||
packages/app-desktop/commands/startExternalEditing.js.map
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# Setup environment variables
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
ROOT_DIR="$SCRIPT_DIR/../.."
|
||||
|
||||
IS_PULL_REQUEST=0
|
||||
IS_DEV_BRANCH=0
|
||||
IS_LINUX=0
|
||||
IS_MACOS=0
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
|
||||
IS_PULL_REQUEST=1
|
||||
fi
|
||||
|
||||
if [ "$GITHUB_REF" == "refs/heads/dev" ]; then
|
||||
IS_DEV_BRANCH=1
|
||||
fi
|
||||
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
IS_LINUX=1
|
||||
IS_MACOS=0
|
||||
else
|
||||
IS_LINUX=0
|
||||
IS_MACOS=1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Print environment
|
||||
# =============================================================================
|
||||
|
||||
echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW"
|
||||
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
||||
echo "GITHUB_REF=$GITHUB_REF"
|
||||
echo "RUNNER_OS=$RUNNER_OS"
|
||||
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
|
||||
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
|
||||
echo "IS_DEV_BRANCH=$IS_DEV_BRANCH"
|
||||
echo "IS_LINUX=$IS_LINUX"
|
||||
echo "IS_MACOS=$IS_MACOS"
|
||||
|
||||
echo "Node $( node -v )"
|
||||
echo "Npm $( npm -v )"
|
||||
|
||||
# =============================================================================
|
||||
# Install packages
|
||||
# =============================================================================
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
npm install
|
||||
|
||||
# =============================================================================
|
||||
# Run test units. Only do it for pull requests and dev branch because we don't
|
||||
# want it to randomly fail when trying to create a desktop release.
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
|
||||
npm run test-ci
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Run linter for pull requests only. We also don't want this to make the desktop
|
||||
# release randomly fail.
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" != "1" ]; then
|
||||
npm run linter-ci ./
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Validate translations - this is needed as some users manually edit .po files
|
||||
# (and often make mistakes) instead of using a proper tool like poedit. Doing it
|
||||
# for Linux only is sufficient.
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ]; then
|
||||
if [ "$IS_LINUX" == "1" ]; then
|
||||
node packages/tools/validate-translation.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Find out if we should run the build or not. Electron-builder gets stuck when
|
||||
# building PRs so we disable it in this case. The Linux build should provide
|
||||
# enough info if the app builds or not.
|
||||
# https://github.com/electron-userland/electron-builder/issues/4263
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ]; then
|
||||
if [ "$IS_MACOS" == "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Prepare the Electron app and build it
|
||||
#
|
||||
# If the current tag is a desktop release tag (starts with "v", such as
|
||||
# "v1.4.7"), we build and publish to github
|
||||
#
|
||||
# Otherwise we only build but don't publish to GitHub. It helps finding
|
||||
# out any issue in pull requests and dev branch.
|
||||
# =============================================================================
|
||||
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
USE_HARD_LINKS=false npm run dist
|
||||
else
|
||||
USE_HARD_LINKS=false npm run dist -- --publish=never
|
||||
fi
|
|
@ -0,0 +1,37 @@
|
|||
name: Joplin Continuous Integration
|
||||
on: [push]
|
||||
jobs:
|
||||
Main:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
steps:
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't
|
||||
# exist) since otherwise it will make the whole build fails, even though
|
||||
# it might work without update. libsecret-1-dev is required for keytar -
|
||||
# https://github.com/atom/node-keytar
|
||||
- name: Install Linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y gettext
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: olegtarasov/get-tag@v2.1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '12'
|
||||
|
||||
- name: Run script...
|
||||
env:
|
||||
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh"
|
|
@ -126,6 +126,9 @@ packages/app-desktop/commands/openProfileDirectory.js.map
|
|||
packages/app-desktop/commands/replaceMisspelling.d.ts
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js.map
|
||||
packages/app-desktop/commands/restoreNoteRevision.d.ts
|
||||
packages/app-desktop/commands/restoreNoteRevision.js
|
||||
packages/app-desktop/commands/restoreNoteRevision.js.map
|
||||
packages/app-desktop/commands/startExternalEditing.d.ts
|
||||
packages/app-desktop/commands/startExternalEditing.js
|
||||
packages/app-desktop/commands/startExternalEditing.js.map
|
||||
|
|
138
.travis.yml
138
.travis.yml
|
@ -1,138 +0,0 @@
|
|||
# Only build tags (Doesn't work - doesn't build anything)
|
||||
if: tag IS present OR type = pull_request OR branch = dev
|
||||
|
||||
rvm: 2.3.3
|
||||
|
||||
# It's important to only build production branches otherwise Electron Builder
|
||||
# might take assets from dev branches and overwrite those of production.
|
||||
# https://docs.travis-ci.com/user/customizing-the-build/#Building-Specific-Branches
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- dev
|
||||
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode12
|
||||
language: node_js
|
||||
node_js: "12"
|
||||
cache:
|
||||
npm: false
|
||||
# Cache was disabled because when changing from node_js 10 to node_js 12
|
||||
# it was still using build files from Node 10 when building SQLite which
|
||||
# was making it fail. Might be ok to re-enable later on, although it doesn't
|
||||
# make build that much faster.
|
||||
#
|
||||
# env:
|
||||
# - ELECTRON_CACHE=$HOME/.cache/electron
|
||||
# - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: node_js
|
||||
node_js: "12"
|
||||
cache:
|
||||
npm: false
|
||||
# env:
|
||||
# - ELECTRON_CACHE=$HOME/.cache/electron
|
||||
# - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
|
||||
# cache:
|
||||
# directories:
|
||||
# - node_modules
|
||||
# - $HOME/.cache/electron
|
||||
# - $HOME/.cache/electron-builder
|
||||
|
||||
before_install:
|
||||
# HOMEBREW_NO_AUTO_UPDATE needed so that Homebrew doesn't upgrade to the next
|
||||
# version, which requires Ruby 2.3, which is not available on the Travis VM.
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't exist) since
|
||||
# otherwise it will make the whole build fails, even though all we need is yarn.
|
||||
|
||||
# libsecret-1-dev is required for keytar - https://github.com/atom/node-keytar
|
||||
- |
|
||||
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install yarn
|
||||
else
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y yarn
|
||||
sudo apt-get install -y gettext
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
# Prints some env variables
|
||||
echo "TRAVIS_OS_NAME=$TRAVIS_OS_NAME"
|
||||
echo "TRAVIS_BRANCH=$TRAVIS_BRANCH"
|
||||
echo "TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST"
|
||||
echo "TRAVIS_TAG=$TRAVIS_TAG"
|
||||
|
||||
# Install tools
|
||||
npm install
|
||||
|
||||
# Run test units.
|
||||
# Only do it for pull requests because Travis randomly fails to run them
|
||||
# and that would break the desktop release.
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ] || [ "$TRAVIS_BRANCH" = "dev" ]; then
|
||||
npm run test-ci
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run linter for pull requests only - this is so that
|
||||
# bypassing eslint is allowed for urgent fixes.
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
npm run linter-ci ./
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate translations - this is needed as some users manually
|
||||
# edit .po files (and often make mistakes) instead of using a proper
|
||||
# tool like poedit. Doing it for Linux only is sufficient.
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
if [ "$TRAVIS_OS_NAME" != "osx" ]; then
|
||||
node packages/tools/validate-translation.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Find out if we should run the build or not. Electron-builder gets stuck when
|
||||
# building PRs so we disable it in this case. The Linux build should provide
|
||||
# enough info if the app builds or not.
|
||||
# https://github.com/electron-userland/electron-builder/issues/4263
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prepare the Electron app and build it
|
||||
#
|
||||
# If the current tag is a desktop release tag (starts with "v", such as
|
||||
# "v1.4.7"), we build and publish to github
|
||||
#
|
||||
# Otherwise we only build but don't publish to GitHub. It helps finding
|
||||
# out any issue in pull requests and dev branch.
|
||||
|
||||
cd packages/app-desktop
|
||||
|
||||
if [[ $TRAVIS_TAG = v* ]]; then
|
||||
USE_HARD_LINKS=false npm run dist
|
||||
else
|
||||
USE_HARD_LINKS=false npm run dist -- --publish=never
|
||||
fi
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
$katexcode$
|
||||
Hello World:$katexcode$
|
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,5 @@
|
|||
Hello World :
|
||||
|
||||
$$
|
||||
katexcode
|
||||
\sqrt{3x}
|
||||
$$
|
|
@ -96,6 +96,7 @@ const globalCommands = [
|
|||
require('./commands/stopExternalEditing'),
|
||||
require('./commands/toggleExternalEditing'),
|
||||
require('./commands/toggleSafeMode'),
|
||||
require('./commands/restoreNoteRevision'),
|
||||
require('@joplin/lib/commands/historyBackward'),
|
||||
require('@joplin/lib/commands/historyForward'),
|
||||
require('@joplin/lib/commands/synchronize'),
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'restoreNoteRevision',
|
||||
label: 'Restore a note from history',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext, noteId: string, reverseRevIndex: number = 0) => {
|
||||
try {
|
||||
const note = await RevisionService.instance().restoreNoteById(noteId, reverseRevIndex);
|
||||
alert(RevisionService.instance().restoreSuccessMessage(note));
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -13,7 +13,7 @@ const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
|
|||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
const time = require('@joplin/lib/time').default;
|
||||
const ReactTooltip = require('react-tooltip');
|
||||
const { urlDecode, substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||
const { urlDecode } = require('@joplin/lib/string-utils');
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
const markupLanguageUtils = require('../utils/markupLanguageUtils').default;
|
||||
|
||||
|
@ -75,7 +75,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
|||
this.setState({ restoring: true });
|
||||
await RevisionService.instance().importRevisionNote(this.state.note);
|
||||
this.setState({ restoring: false });
|
||||
alert(_('The note "%s" has been successfully restored to the notebook "%s".', substrWithEllipsis(this.state.note.title, 0, 32), RevisionService.instance().restoreFolderTitle()));
|
||||
alert(RevisionService.instance().restoreSuccessMessage(this.state.note));
|
||||
}
|
||||
|
||||
backButton_click() {
|
||||
|
|
|
@ -147,7 +147,10 @@ function StatusScreen(props: Props) {
|
|||
}
|
||||
|
||||
if (section.canRetryAll) {
|
||||
itemsHtml.push(renderSectionRetryAllHtml(section.title, section.retryAllHandler));
|
||||
itemsHtml.push(renderSectionRetryAllHtml(section.title, async () => {
|
||||
await section.retryAllHandler();
|
||||
void resfreshScreen();
|
||||
}));
|
||||
}
|
||||
|
||||
return <div key={key}>{itemsHtml}</div>;
|
||||
|
|
|
@ -47,6 +47,12 @@ interface State {
|
|||
listType: number;
|
||||
showHelp: boolean;
|
||||
resultsInBody: boolean;
|
||||
commandArgs: string[];
|
||||
}
|
||||
|
||||
interface CommandQuery {
|
||||
name: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
class GotoAnything {
|
||||
|
@ -87,6 +93,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
listType: BaseModel.TYPE_NOTE,
|
||||
showHelp: false,
|
||||
resultsInBody: false,
|
||||
commandArgs: [],
|
||||
};
|
||||
|
||||
this.styles_ = {};
|
||||
|
@ -250,6 +257,15 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
return this.markupToHtml_;
|
||||
}
|
||||
|
||||
private parseCommandQuery(query: string): CommandQuery {
|
||||
const fullQuery = query;
|
||||
const splitted = fullQuery.split(/\s+/);
|
||||
return {
|
||||
name: splitted.length ? splitted[0] : '',
|
||||
args: splitted.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
async updateList() {
|
||||
let resultsInBody = false;
|
||||
|
||||
|
@ -260,13 +276,16 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
let listType = null;
|
||||
let searchQuery = '';
|
||||
let keywords = null;
|
||||
let commandArgs: string[] = [];
|
||||
|
||||
if (this.state.query.indexOf(':') === 0) { // COMMANDS
|
||||
const query = this.state.query.substr(1);
|
||||
listType = BaseModel.TYPE_COMMAND;
|
||||
keywords = [query];
|
||||
const commandQuery = this.parseCommandQuery(this.state.query.substr(1));
|
||||
|
||||
const commandResults = CommandService.instance().searchCommands(query, true);
|
||||
listType = BaseModel.TYPE_COMMAND;
|
||||
keywords = [commandQuery.name];
|
||||
commandArgs = commandQuery.args;
|
||||
|
||||
const commandResults = CommandService.instance().searchCommands(commandQuery.name, true);
|
||||
|
||||
results = commandResults.map((result: CommandSearchResult) => {
|
||||
return {
|
||||
|
@ -367,6 +386,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
keywords: keywords ? keywords : await this.keywords(searchQuery),
|
||||
selectedItemId: results.length === 0 ? null : results[0].id,
|
||||
resultsInBody: resultsInBody,
|
||||
commandArgs: commandArgs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -379,7 +399,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
});
|
||||
|
||||
if (item.type === BaseModel.TYPE_COMMAND) {
|
||||
void CommandService.instance().execute(item.id);
|
||||
void CommandService.instance().execute(item.id, ...item.commandArgs);
|
||||
void focusEditorIfEditorCommand(item.id, CommandService.instance());
|
||||
return;
|
||||
}
|
||||
|
@ -426,6 +446,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
id: itemId,
|
||||
parent_id: parentId,
|
||||
type: itemType,
|
||||
commandArgs: this.state.commandArgs,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -466,7 +487,7 @@ class Dialog extends React.PureComponent<Props, State> {
|
|||
selectedItem() {
|
||||
const index = this.selectedItemIndex();
|
||||
if (index < 0) return null;
|
||||
return this.state.results[index];
|
||||
return { ...this.state.results[index], commandArgs: this.state.commandArgs };
|
||||
}
|
||||
|
||||
input_onKeyDown(event: any) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.facebook.soloader.SoLoader;
|
|||
|
||||
import net.cozic.joplin.share.SharePackage;
|
||||
import net.cozic.joplin.ssl.SslPackage;
|
||||
import net.cozic.joplin.textinput.TextInputPackage;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
@ -44,6 +45,7 @@ public class MainApplication extends Application implements ReactApplication {
|
|||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
packages.add(new SharePackage());
|
||||
packages.add(new SslPackage());
|
||||
packages.add(new TextInputPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package net.cozic.joplin.textinput;
|
||||
|
||||
import android.text.Selection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
import com.facebook.react.views.textinput.ReactTextInputManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class provides a workaround for <a href="https://github.com/facebook/react-native/issues/29911">
|
||||
* https://github.com/facebook/react-native/issues/29911</a>
|
||||
*
|
||||
* The reason the editor is scrolled seems to be due to this block in
|
||||
* <pre>android.widget.Editor#onFocusChanged:</pre>
|
||||
*
|
||||
* <pre>
|
||||
* // The DecorView does not have focus when the 'Done' ExtractEditText button is
|
||||
* // pressed. Since it is the ViewAncestor's mView, it requests focus before
|
||||
* // ExtractEditText clears focus, which gives focus to the ExtractEditText.
|
||||
* // This special case ensure that we keep current selection in that case.
|
||||
* // It would be better to know why the DecorView does not have focus at that time.
|
||||
* if (((mTextView.isInExtractedMode()) || mSelectionMoved)
|
||||
* && selStart >= 0 && selEnd >= 0) {
|
||||
* Selection.setSelection((Spannable)mTextView.getText(),selStart,selEnd);
|
||||
* }
|
||||
* </pre>
|
||||
* When using native Android TextView mSelectionMoved is false so this block is skipped,
|
||||
* with RN however it's true and this is where the scrolling comes from.
|
||||
*
|
||||
* The below workaround resets the selection before a focus event is passed on to the native component.
|
||||
* This way when the above condition is reached <pre>selStart == selEnd == -1</pre> and no scrolling
|
||||
* happens.
|
||||
*/
|
||||
public class TextInputPackage implements com.facebook.react.ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.singletonList(new ReactTextInputManager() {
|
||||
@Override
|
||||
public void receiveCommand(ReactEditText reactEditText, String commandId, @Nullable ReadableArray args) {
|
||||
if ("focus".equals(commandId) || "focusTextInput".equals(commandId)) {
|
||||
Selection.removeSelection(reactEditText.getText());
|
||||
}
|
||||
super.receiveCommand(reactEditText, commandId, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -766,9 +766,10 @@ export default class BaseApplication {
|
|||
}
|
||||
|
||||
if (Setting.value('env') === Env.Dev) {
|
||||
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
|
||||
Setting.setValue('sync.10.path', 'http://api-joplincloud.local:22300');
|
||||
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
|
||||
Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
|
||||
// Setting.setValue('sync.10.path', 'http://api-joplincloud.local:22300');
|
||||
// Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
|
||||
}
|
||||
|
||||
// For now always disable fuzzy search due to performance issues:
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class JoplinServerApi {
|
|||
this.options_ = options;
|
||||
|
||||
if (options.env === Env.Dev) {
|
||||
this.debugRequests_ = true;
|
||||
// this.debugRequests_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,13 +175,16 @@ export default class JoplinServerApi {
|
|||
logger.debug('Response', Date.now() - startTime, options.responseFormat, responseText);
|
||||
}
|
||||
|
||||
const shortResponseText = () => {
|
||||
return (`${responseText}`).substr(0, 1024);
|
||||
};
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = (message: string, code: number = 0) => {
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
// return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
|
||||
return new JoplinError(message, code, `${method} ${path}: ${message} (${code}): ${shortResponseText}`);
|
||||
return new JoplinError(message, code, `${method} ${path}: ${message} (${code}): ${shortResponseText()}`);
|
||||
};
|
||||
|
||||
let responseJson_: any = null;
|
||||
|
@ -207,7 +210,21 @@ export default class JoplinServerApi {
|
|||
throw newError(`${json.error}`, json.code ? json.code : response.status);
|
||||
}
|
||||
|
||||
throw newError('Unknown error', response.status);
|
||||
// "Unknown error" means it probably wasn't generated by the
|
||||
// application but for example by the Nginx or Apache reverse
|
||||
// proxy. So in that case we attach the response content to the
|
||||
// error message so that it shows up in logs. It might be for
|
||||
// example an error returned by the Nginx or Apache reverse
|
||||
// proxy. For example:
|
||||
//
|
||||
// <html>
|
||||
// <head><title>413 Request Entity Too Large</title></head>
|
||||
// <body>
|
||||
// <center><h1>413 Request Entity Too Large</h1></center>
|
||||
// <hr><center>nginx/1.18.0 (Ubuntu)</center>
|
||||
// </body>
|
||||
// </html>
|
||||
throw newError(`Unknown error: ${shortResponseText()}`, response.status);
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
|
|
@ -41,11 +41,11 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
|||
public static async checkConfig(options: FileApiOptions) {
|
||||
return SyncTargetJoplinServer.checkConfig({
|
||||
...options,
|
||||
});
|
||||
}, SyncTargetJoplinCloud.id());
|
||||
}
|
||||
|
||||
protected async initFileApi() {
|
||||
return initFileApi(this.logger(), {
|
||||
return initFileApi(SyncTargetJoplinCloud.id(), this.logger(), {
|
||||
path: () => Setting.value('sync.10.path'),
|
||||
userContentPath: () => Setting.value('sync.10.userContentPath'),
|
||||
username: () => Setting.value('sync.10.username'),
|
||||
|
|
|
@ -31,8 +31,8 @@ export async function newFileApi(id: number, options: FileApiOptions) {
|
|||
return fileApi;
|
||||
}
|
||||
|
||||
export async function initFileApi(logger: Logger, options: FileApiOptions) {
|
||||
const fileApi = await newFileApi(SyncTargetJoplinServer.id(), options);
|
||||
export async function initFileApi(syncTargetId: number, logger: Logger, options: FileApiOptions) {
|
||||
const fileApi = await newFileApi(syncTargetId, options);
|
||||
fileApi.setLogger(logger);
|
||||
return fileApi;
|
||||
}
|
||||
|
@ -63,14 +63,16 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
|
|||
return super.fileApi();
|
||||
}
|
||||
|
||||
public static async checkConfig(options: FileApiOptions) {
|
||||
public static async checkConfig(options: FileApiOptions, syncTargetId: number = null) {
|
||||
const output = {
|
||||
ok: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
syncTargetId = syncTargetId === null ? SyncTargetJoplinServer.id() : syncTargetId;
|
||||
|
||||
try {
|
||||
const fileApi = await newFileApi(SyncTargetJoplinServer.id(), options);
|
||||
const fileApi = await newFileApi(syncTargetId, options);
|
||||
fileApi.requestRepeatCount_ = 0;
|
||||
|
||||
await fileApi.put('testing.txt', 'testing');
|
||||
|
@ -87,7 +89,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
|
|||
}
|
||||
|
||||
protected async initFileApi() {
|
||||
return initFileApi(this.logger(), {
|
||||
return initFileApi(SyncTargetJoplinServer.id(), this.logger(), {
|
||||
path: () => Setting.value('sync.9.path'),
|
||||
userContentPath: () => Setting.value('sync.9.userContentPath'),
|
||||
username: () => Setting.value('sync.9.username'),
|
||||
|
|
|
@ -284,8 +284,20 @@ describe('models_Note', function() {
|
|||
expect(externalToInternal).toBe(input);
|
||||
}
|
||||
|
||||
const result = await Note.replaceResourceExternalToInternalLinks(`[](joplin://${note1.id})`);
|
||||
expect(result).toBe(`[](:/${note1.id})`);
|
||||
{
|
||||
const result = await Note.replaceResourceExternalToInternalLinks(`[](joplin://${note1.id})`);
|
||||
expect(result).toBe(`[](:/${note1.id})`);
|
||||
}
|
||||
|
||||
{
|
||||
// This is a regular file path that contains the resourceDirName
|
||||
// inside but it shouldn't be changed.
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/5034
|
||||
const noChangeInput = `[docs](file:///c:/foo/${resourceDirName}/docs)`;
|
||||
const result = await Note.replaceResourceExternalToInternalLinks(noChangeInput, { useAbsolutePaths: false });
|
||||
expect(result).toBe(noChangeInput);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should perform natural sorting', (async () => {
|
||||
|
|
|
@ -208,9 +208,9 @@ export default class Note extends BaseItem {
|
|||
for (const basePath of pathsToTry) {
|
||||
const reStrings = [
|
||||
// Handles file://path/to/abcdefg.jpg?t=12345678
|
||||
`${pregQuote(`${basePath}/`)}[a-zA-Z0-9.]+\\?t=[0-9]+`,
|
||||
`${pregQuote(`${basePath}/`)}[a-zA-Z0-9]{32}\\.[a-zA-Z0-9]+\\?t=[0-9]+`,
|
||||
// Handles file://path/to/abcdefg.jpg
|
||||
`${pregQuote(`${basePath}/`)}[a-zA-Z0-9.]+`,
|
||||
`${pregQuote(`${basePath}/`)}[a-zA-Z0-9]{32}\\.[a-zA-Z0-9]+`,
|
||||
];
|
||||
for (const reString of reStrings) {
|
||||
const re = new RegExp(reString, 'gi');
|
||||
|
|
|
@ -140,6 +140,28 @@ export default class ReportService {
|
|||
return output;
|
||||
}
|
||||
|
||||
private addRetryAllHandler(section: ReportSection): ReportSection {
|
||||
const retryHandlers: Function[] = [];
|
||||
|
||||
for (let i = 0; i < section.body.length; i++) {
|
||||
const item: RerportItemOrString = section.body[i];
|
||||
if (typeof item !== 'string' && item.canRetry) {
|
||||
retryHandlers.push(item.retryHandler);
|
||||
}
|
||||
}
|
||||
|
||||
if (retryHandlers.length) {
|
||||
section.canRetryAll = true;
|
||||
section.retryAllHandler = async () => {
|
||||
for (const retryHandler of retryHandlers) {
|
||||
await retryHandler();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
async status(syncTarget: number): Promise<ReportSection[]> {
|
||||
const r = await this.syncStatus(syncTarget);
|
||||
const sections: ReportSection[] = [];
|
||||
|
@ -175,6 +197,8 @@ export default class ReportService {
|
|||
|
||||
section.body.push({ type: ReportItemType.CloseList });
|
||||
|
||||
section = this.addRetryAllHandler(section);
|
||||
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
|
@ -200,23 +224,7 @@ export default class ReportService {
|
|||
});
|
||||
}
|
||||
|
||||
const retryHandlers: Function[] = [];
|
||||
|
||||
for (let i = 0; i < section.body.length; i++) {
|
||||
const item: RerportItemOrString = section.body[i];
|
||||
if (typeof item !== 'string' && item.canRetry) {
|
||||
retryHandlers.push(item.retryHandler);
|
||||
}
|
||||
}
|
||||
|
||||
if (retryHandlers.length > 1) {
|
||||
section.canRetryAll = true;
|
||||
section.retryAllHandler = async () => {
|
||||
for (const retryHandler of retryHandlers) {
|
||||
await retryHandler();
|
||||
}
|
||||
};
|
||||
}
|
||||
section = this.addRetryAllHandler(section);
|
||||
|
||||
sections.push(section);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import shim from '../shim';
|
|||
import BaseService from './BaseService';
|
||||
import { _ } from '../locale';
|
||||
import { ItemChangeEntity, NoteEntity, RevisionEntity } from './database/types';
|
||||
const { substrWithEllipsis } = require('../string-utils');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { wrapError } = require('../errorUtils');
|
||||
|
||||
|
@ -230,7 +231,23 @@ export default class RevisionService extends BaseService {
|
|||
return folder;
|
||||
}
|
||||
|
||||
async importRevisionNote(note: NoteEntity) {
|
||||
// reverseRevIndex = 0 means restoring the latest version. reverseRevIndex =
|
||||
// 1 means the version before that, etc.
|
||||
public async restoreNoteById(noteId: string, reverseRevIndex: number): Promise<NoteEntity> {
|
||||
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
|
||||
if (!revisions.length) throw new Error(`No revision for note "${noteId}"`);
|
||||
|
||||
const revIndex = revisions.length - 1 - reverseRevIndex;
|
||||
|
||||
const note = await this.revisionNote(revisions, revIndex);
|
||||
return this.importRevisionNote(note);
|
||||
}
|
||||
|
||||
public restoreSuccessMessage(note: NoteEntity): string {
|
||||
return _('The note "%s" has been successfully restored to the notebook "%s".', substrWithEllipsis(note.title, 0, 32), this.restoreFolderTitle());
|
||||
}
|
||||
|
||||
async importRevisionNote(note: NoteEntity): Promise<NoteEntity> {
|
||||
const toImport = Object.assign({}, note);
|
||||
delete toImport.id;
|
||||
delete toImport.updated_time;
|
||||
|
@ -242,7 +259,7 @@ export default class RevisionService extends BaseService {
|
|||
|
||||
toImport.parent_id = folder.id;
|
||||
|
||||
await Note.save(toImport);
|
||||
return Note.save(toImport);
|
||||
}
|
||||
|
||||
async maintenance() {
|
||||
|
|
|
@ -141,7 +141,7 @@ export default async function(args: Args) {
|
|||
await doSaveStats();
|
||||
} else {
|
||||
const fileInfo = await stat(statFilePath);
|
||||
if (Date.now() - fileInfo.mtime.getTime() >= 24 * 60 * 60 * 1000) {
|
||||
if (Date.now() - fileInfo.mtime.getTime() >= 7 * 24 * 60 * 60 * 1000) {
|
||||
await doSaveStats();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ export interface EnvVariables {
|
|||
|
||||
SIGNUP_ENABLED?: string;
|
||||
TERMS_ENABLED?: string;
|
||||
|
||||
ERROR_STACK_TRACES?: string;
|
||||
}
|
||||
|
||||
let runningInDocker_: boolean = false;
|
||||
|
@ -145,6 +147,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
|||
stripe: stripeConfigFromEnv(env),
|
||||
port: appPort,
|
||||
baseUrl,
|
||||
showErrorStackTraces: (env.ERROR_STACK_TRACES === undefined && envType === Env.Dev) || env.ERROR_STACK_TRACES === '1',
|
||||
apiBaseUrl: env.API_BASE_URL ? env.API_BASE_URL : baseUrl,
|
||||
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
|
||||
signupEnabled: env.SIGNUP_ENABLED === '1',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { routeResponseFormat, Response, RouteResponseFormat, execRequest } from '../utils/routeUtils';
|
||||
import { AppContext, Env } from '../utils/types';
|
||||
import { isView, View } from '../services/MustacheService';
|
||||
import config from '../config';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
const requestStartTime = Date.now();
|
||||
|
@ -11,8 +12,9 @@ export default async function(ctx: AppContext) {
|
|||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await ctx.services.mustache.renderView(responseObject, {
|
||||
const view = responseObject as View;
|
||||
ctx.response.status = view?.content?.error ? view?.content?.error?.httpCode || 500 : 200;
|
||||
ctx.response.body = await ctx.services.mustache.renderView(view, {
|
||||
notifications: ctx.notifications || [],
|
||||
hasNotifications: !!ctx.notifications && !!ctx.notifications.length,
|
||||
owner: ctx.owner,
|
||||
|
@ -44,7 +46,7 @@ export default async function(ctx: AppContext) {
|
|||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: ctx.env === Env.Dev ? error.stack : '',
|
||||
stack: config().showErrorStackTraces ? error.stack : '',
|
||||
owner: ctx.owner,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ function makeView(error: any = null): View {
|
|||
error,
|
||||
signupUrl: config().signupEnabled ? makeUrl(UrlType.Signup) : '',
|
||||
};
|
||||
view.navbar = false;
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import config from '../../config';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { AccountType } from '../../models/UserModel';
|
||||
import { MB } from '../../utils/bytes';
|
||||
import { execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils';
|
||||
import { FormUser } from './signup';
|
||||
|
||||
describe('index_signup', function() {
|
||||
|
||||
|
@ -19,12 +21,22 @@ describe('index_signup', function() {
|
|||
});
|
||||
|
||||
test('should create a new account', async function() {
|
||||
const context = await execRequestC('', 'POST', 'signup', {
|
||||
const formUser: FormUser = {
|
||||
full_name: 'Toto',
|
||||
email: 'toto@example.com',
|
||||
password: 'testing',
|
||||
password2: 'testing',
|
||||
});
|
||||
};
|
||||
|
||||
// First confirm that it doesn't work if sign up is disabled
|
||||
{
|
||||
config().signupEnabled = false;
|
||||
await execRequestC('', 'POST', 'signup', formUser);
|
||||
expect(await models().user().loadByEmail('toto@example.com')).toBeFalsy();
|
||||
}
|
||||
|
||||
config().signupEnabled = true;
|
||||
const context = await execRequestC('', 'POST', 'signup', formUser);
|
||||
|
||||
// Check that the user has been created
|
||||
const user = await models().user().loadByEmail('toto@example.com');
|
||||
|
|
|
@ -18,11 +18,10 @@ function makeView(error: Error = null): View {
|
|||
postUrl: makeUrl(UrlType.Signup),
|
||||
loginUrl: makeUrl(UrlType.Login),
|
||||
};
|
||||
view.navbar = false;
|
||||
return view;
|
||||
}
|
||||
|
||||
interface FormUser {
|
||||
export interface FormUser {
|
||||
full_name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ErrorForbidden } from '../../utils/errors';
|
|||
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils';
|
||||
|
||||
export async function postUser(sessionId: string, email: string, password: string): Promise<User> {
|
||||
export async function postUser(sessionId: string, email: string, password: string, props: any = null): Promise<User> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
|
@ -16,6 +16,7 @@ export async function postUser(sessionId: string, email: string, password: strin
|
|||
password: password,
|
||||
password2: password,
|
||||
post_button: true,
|
||||
...props,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -74,13 +75,16 @@ describe('index_users', function() {
|
|||
test('should create a new user', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
await postUser(session.id, 'test@example.com', '123456');
|
||||
await postUser(session.id, 'test@example.com', '123456', {
|
||||
max_item_size: '',
|
||||
});
|
||||
const newUser = await models().user().loadByEmail('test@example.com');
|
||||
|
||||
expect(!!newUser).toBe(true);
|
||||
expect(!!newUser.id).toBe(true);
|
||||
expect(!!newUser.is_admin).toBe(false);
|
||||
expect(!!newUser.email).toBe(true);
|
||||
expect(newUser.max_item_size).toBe(0);
|
||||
|
||||
const userModel = models().user();
|
||||
const userFromModel: User = await userModel.load(newUser.id);
|
||||
|
@ -108,7 +112,11 @@ describe('index_users', function() {
|
|||
const beforeUserCount = (await userModel.all()).length;
|
||||
expect(beforeUserCount).toBe(2);
|
||||
|
||||
await postUser(session.id, 'test@example.com', '123456');
|
||||
try {
|
||||
await postUser(session.id, 'test@example.com', '123456');
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
const afterUserCount = (await userModel.all()).length;
|
||||
expect(beforeUserCount).toBe(afterUserCount);
|
||||
|
|
|
@ -34,7 +34,7 @@ function makeUser(isNew: boolean, fields: any): User {
|
|||
if ('email' in fields) user.email = fields.email;
|
||||
if ('full_name' in fields) user.full_name = fields.full_name;
|
||||
if ('is_admin' in fields) user.is_admin = fields.is_admin;
|
||||
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size;
|
||||
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size || 0;
|
||||
user.can_share = fields.can_share ? 1 : 0;
|
||||
|
||||
const password = checkPassword(fields, false);
|
||||
|
|
|
@ -32,6 +32,7 @@ interface GlobalParams {
|
|||
appName?: string;
|
||||
termsUrl?: string;
|
||||
privacyUrl?: string;
|
||||
showErrorStackTraces?: boolean;
|
||||
}
|
||||
|
||||
export function isView(o: any): boolean {
|
||||
|
@ -85,6 +86,7 @@ export default class MustacheService {
|
|||
appName: config().appName,
|
||||
termsUrl: config().termsEnabled ? makeUrl(UrlType.Terms) : '',
|
||||
privacyUrl: config().termsEnabled ? makeUrl(UrlType.Privacy) : '',
|
||||
showErrorStackTraces: config().showErrorStackTraces,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -194,6 +194,7 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
|
|||
appContext.req = req;
|
||||
appContext.query = req.query;
|
||||
appContext.method = req.method;
|
||||
appContext.redirect = () => {};
|
||||
|
||||
if (options.sessionId) {
|
||||
appContext.cookies.set('sessionId', options.sessionId);
|
||||
|
|
|
@ -80,6 +80,7 @@ export interface Config {
|
|||
userContentBaseUrl: string;
|
||||
signupEnabled: boolean;
|
||||
termsEnabled: boolean;
|
||||
showErrorStackTraces: boolean;
|
||||
database: DatabaseConfig;
|
||||
mailer: MailerConfig;
|
||||
stripe: StripeConfig;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<section class="section login-box">
|
||||
<h1 class="title">Login to {{global.appName}}</h1>
|
||||
<p class="subtitle">Please input your details to login to {{global.appName}}</p>
|
||||
|
||||
<div class="container block">
|
||||
{{> errorBanner}}
|
||||
<form action="{{{global.baseUrl}}}/login" method="POST">
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{{> errorBanner}}
|
||||
<section class="section login-box">
|
||||
<h1 class="title">Sign up for {{global.appName}}</h1>
|
||||
<p class="subtitle">Please input your details to sign up for {{global.appName}}</p>
|
||||
|
||||
<div class="container block">
|
||||
<form action="{{{postUrl}}}" method="POST">
|
||||
<div class="field">
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{{#error}}
|
||||
<div class="notification is-danger">
|
||||
<strong>{{message}}</strong>
|
||||
{{#stack}}
|
||||
<pre>{{.}}</pre>
|
||||
{{/stack}}
|
||||
{{#global.showErrorStackTraces}}
|
||||
{{#stack}}
|
||||
<pre>{{.}}</pre>
|
||||
{{/stack}}
|
||||
{{/global.showErrorStackTraces}}
|
||||
</div>
|
||||
{{/error}}
|
|
@ -6,25 +6,36 @@
|
|||
<img class="logo" src="{{{global.baseUrl}}}/images/Logo.png"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/home">Home</a>
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users">Users</a>
|
||||
{{/global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/items">Items</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">Log</a>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">{{global.owner.email}}</div>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users/me">Profile</a>
|
||||
<div class="navbar-item">
|
||||
<form method="post" action="{{{global.baseUrl}}}/logout">
|
||||
<button class="button is-primary">Logout</button>
|
||||
</form>
|
||||
|
||||
{{#global.owner}}
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/home">Home</a>
|
||||
{{#global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users">Users</a>
|
||||
{{/global.owner.is_admin}}
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/items">Items</a>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/changes">Log</a>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">{{global.owner.email}}</div>
|
||||
<a class="navbar-item" href="{{{global.baseUrl}}}/users/me">Profile</a>
|
||||
<div class="navbar-item">
|
||||
<form method="post" action="{{{global.baseUrl}}}/logout">
|
||||
<button class="button is-primary">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/global.owner}}
|
||||
|
||||
{{^global.owner}}
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<span class="navbar-item">{{global.appName}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/global.owner}}
|
||||
</div>
|
||||
</nav>
|
||||
{{/navbar}}
|
|
@ -608,6 +608,7 @@ function joplinEditableBlockInfo(node) {
|
|||
if (!node.classList.contains('joplin-editable')) return null;
|
||||
|
||||
let sourceNode = null;
|
||||
let isInline = false;
|
||||
for (const childNode of node.childNodes) {
|
||||
if (childNode.classList.contains('joplin-source')) {
|
||||
sourceNode = childNode;
|
||||
|
@ -616,11 +617,13 @@ function joplinEditableBlockInfo(node) {
|
|||
}
|
||||
|
||||
if (!sourceNode) return null;
|
||||
if (!node.isBlock) isInline = true;
|
||||
|
||||
return {
|
||||
openCharacters: sourceNode.getAttribute('data-joplin-source-open'),
|
||||
closeCharacters: sourceNode.getAttribute('data-joplin-source-close'),
|
||||
content: sourceNode.textContent,
|
||||
isInline
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -637,7 +640,8 @@ rules.joplinSourceBlock = {
|
|||
const info = joplinEditableBlockInfo(node);
|
||||
if (!info) return;
|
||||
|
||||
return '\n\n' + info.openCharacters + info.content + info.closeCharacters + '\n\n';
|
||||
const surroundingCharacter = info.isInline? '' : '\n\n';
|
||||
return surroundingCharacter + info.openCharacters + info.content + info.closeCharacters + surroundingCharacter;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue