diff --git a/.eslintignore b/.eslintignore index 7b08025c0..11b3d21e3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -267,9 +267,6 @@ packages/app-desktop/gui/MainScreen/commands/renameTag.js.map packages/app-desktop/gui/MainScreen/commands/search.d.ts packages/app-desktop/gui/MainScreen/commands/search.js packages/app-desktop/gui/MainScreen/commands/search.js.map -packages/app-desktop/gui/MainScreen/commands/selectTemplate.d.ts -packages/app-desktop/gui/MainScreen/commands/selectTemplate.js -packages/app-desktop/gui/MainScreen/commands/selectTemplate.js.map packages/app-desktop/gui/MainScreen/commands/setTags.d.ts packages/app-desktop/gui/MainScreen/commands/setTags.js packages/app-desktop/gui/MainScreen/commands/setTags.js.map diff --git a/.gitignore b/.gitignore index 2296a886b..896fd1ca8 100644 --- a/.gitignore +++ b/.gitignore @@ -252,9 +252,6 @@ packages/app-desktop/gui/MainScreen/commands/renameTag.js.map packages/app-desktop/gui/MainScreen/commands/search.d.ts packages/app-desktop/gui/MainScreen/commands/search.js packages/app-desktop/gui/MainScreen/commands/search.js.map -packages/app-desktop/gui/MainScreen/commands/selectTemplate.d.ts -packages/app-desktop/gui/MainScreen/commands/selectTemplate.js -packages/app-desktop/gui/MainScreen/commands/selectTemplate.js.map packages/app-desktop/gui/MainScreen/commands/setTags.d.ts packages/app-desktop/gui/MainScreen/commands/setTags.js packages/app-desktop/gui/MainScreen/commands/setTags.js.map diff --git a/README.md b/README.md index 8cc08e324..5c14a9034 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,6 @@ The Web Clipper is a browser extension that allows you to save web pages and scr - Supports multiple languages. - External editor support - open notes in your favorite external editor with one click in Joplin. - Extensible functionality through plugin and data APIs. -- Template support with data variables for auto creation of time & dates. - Custom CSS support for customisation of both the rendered markdown and overall user interface. - Customisable layout allows toggling, movement and sizing of various elements. - Keyboard shortcuts are editable and allow binding of most Joplin commands with export/import functionality. @@ -376,29 +375,6 @@ The whole UI can be customized by placing a custom editor style file in the prof Important: userstyle.css and userchrome.css are provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable. -# Note templates - -In the **desktop app**, templates can be used to create new notes or to insert into existing ones by adding a template file to the `templates` directory (File > Templates). For example creating the file `hours.md` in the `templates` directory with the contents: - -```markdown -Date: {{date}} -Hours: -Details: -``` - -Templates can then be inserted from the menu (File->Templates). - -The currently supported template variables are: - -| Variable | Description | Example | -| --- | --- | --- | -| `{{date}}` | Today's date formatted based on the settings format | 2019-01-01 | -| `{{time}}` | Current time formatted based on the settings format | 13:00 | -| `{{datetime}}` | Current date and time formatted based on the settings format | 01/01/19 1:00 PM | -| `{{#custom_datetime}}` | Current date and/or time formatted based on a supplied string (using [moment.js](https://momentjs.com/) formatting) | `{{#custom_datetime}}M d{{/custom_datetime}}` | -| `{{bowm}}` | Date of the beginning of the week (when week starts on Monday) based on the settings format | | -| `{{bows}}` | Date of the beginning of the week (when week starts on Sunday) based on the settings format | | - # Plugins The **desktop app** has the ability to extend beyond its standard functionality by the way of plugins. These plugins adhere to the Joplin plugin API and can be installed & configured within the application via the `Plugins` page in the Configuration screen. This menu allows the manual installation of the plugin using the single 'Joplin Plugin Archive' (*.jpl) file. Once the application is reloaded the plugins will appear within the plugins menu where they can be toggled on/off or removed entirely. diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 6fd7d163f..258614203 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -41,7 +41,6 @@ const Menu = bridge().Menu; const PluginManager = require('@joplin/lib/services/PluginManager'); import RevisionService from '@joplin/lib/services/RevisionService'; import MigrationService from '@joplin/lib/services/MigrationService'; -const TemplateUtils = require('@joplin/lib/TemplateUtils'); import { loadCustomCss, injectCustomStyles } from '@joplin/lib/CssUtils'; // import populateDatabase from '@joplin/lib/services/debug/populateDatabase'; @@ -62,7 +61,6 @@ const commands = [ require('./gui/MainScreen/commands/renameFolder'), require('./gui/MainScreen/commands/renameTag'), require('./gui/MainScreen/commands/search'), - require('./gui/MainScreen/commands/selectTemplate'), require('./gui/MainScreen/commands/setTags'), require('./gui/MainScreen/commands/showModalMessage'), require('./gui/MainScreen/commands/showNoteContentProperties'), @@ -538,6 +536,26 @@ class Application extends BaseApplication { return cssString; } + private async checkForLegacyTemplates() { + const templatesDir = `${Setting.value('profileDir')}/templates`; + if (await shim.fsDriver().exists(templatesDir)) { + try { + const files = await shim.fsDriver().readDirStats(templatesDir); + for (const file of files) { + if (file.path.endsWith('.md')) { + // There is atleast one template. + this.store().dispatch({ + type: 'CONTAINS_LEGACY_TEMPLATES', + }); + break; + } + } + } catch (error) { + reg.logger().error(`Failed to read templates directory: ${error}`); + } + } + } + private async initPluginService() { const service = PluginService.instance(); @@ -617,8 +635,6 @@ class Application extends BaseApplication { argv = await super.start(argv); - await fs.mkdirp(Setting.value('templateDir'), 0o755); - await this.applySettingsSideEffects(); if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) { @@ -715,18 +731,13 @@ class Application extends BaseApplication { css: cssString, }); - const templates = await TemplateUtils.loadTemplates(Setting.value('templateDir')); - - this.store().dispatch({ - type: 'TEMPLATE_UPDATE_ALL', - templates: templates, - }); - this.store().dispatch({ type: 'NOTE_DEVTOOLS_SET', value: Setting.value('flagOpenDevTools'), }); + await this.checkForLegacyTemplates(); + // Note: Auto-update currently doesn't work in Linux: it downloads the update // but then doesn't install it on exit. if (shim.isWindows() || shim.isMac()) { diff --git a/packages/app-desktop/gui/KeymapConfig/utils/getLabel.ts b/packages/app-desktop/gui/KeymapConfig/utils/getLabel.ts index e05b2ab17..1d0f17f03 100644 --- a/packages/app-desktop/gui/KeymapConfig/utils/getLabel.ts +++ b/packages/app-desktop/gui/KeymapConfig/utils/getLabel.ts @@ -14,8 +14,6 @@ const getLabel = (commandName: string): string => { switch (commandName) { case 'quit': return _('Quit'); - case 'insertTemplate': - return _('Insert template'); case 'zoomActualSize': return _('Actual Size'); case 'gotoAnything': diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index 4813f9fac..bd5c311b3 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -63,6 +63,7 @@ interface Props { showMissingMasterKeyMessage: boolean; showNeedUpgradingMasterKeyMessage: boolean; showShouldReencryptMessage: boolean; + showInstallTemplatesPlugin: boolean; focusedField: string; themeId: number; settingEditorCodeView: boolean; @@ -70,6 +71,7 @@ interface Props { startupPluginsLoaded: boolean; shareInvitations: ShareInvitation[]; isSafeMode: boolean; + needApiAuth: boolean; } interface ShareFolderDialogOptions { @@ -123,7 +125,6 @@ const commands = [ require('./commands/renameFolder'), require('./commands/renameTag'), require('./commands/search'), - require('./commands/selectTemplate'), require('./commands/setTags'), require('./commands/showModalMessage'), require('./commands/showNoteContentProperties'), @@ -551,6 +552,16 @@ class MainScreenComponent extends React.Component { }); }; + const onViewPluginScreen = () => { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'Config', + props: { + defaultSection: 'plugins', + }, + }); + }; + const onRestartAndUpgrade = async () => { Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_MUST_DO); await Setting.saveAll(); @@ -627,6 +638,12 @@ class MainScreenComponent extends React.Component { _('Set the password'), onViewEncryptionConfigScreen ); + } else if (this.props.showInstallTemplatesPlugin) { + msg = this.renderNotificationMessage( + 'The template feature has been moved to a plugin called "Templates".', + 'Install plugin', + onViewPluginScreen + ); } return ( @@ -638,7 +655,7 @@ class MainScreenComponent extends React.Component { messageBoxVisible(props: Props = null) { if (!props) props = this.props; - return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props); + return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin; } registerCommands() { @@ -855,7 +872,6 @@ const mapStateToProps = (state: AppState) => { selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null, pluginsLegacy: state.pluginsLegacy, plugins: state.pluginService.plugins, - templates: state.templates, customCss: state.customCss, editorNoteStatuses: state.editorNoteStatuses, hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state), @@ -865,6 +881,8 @@ const mapStateToProps = (state: AppState) => { startupPluginsLoaded: state.startupPluginsLoaded, shareInvitations: state.shareService.shareInvitations, isSafeMode: state.settings.isSafeMode, + needApiAuth: state.needApiAuth, + showInstallTemplatesPlugin: state.hasLegacyTemplates && !state.pluginService.plugins['joplin.plugin.templates'], }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/newNote.ts b/packages/app-desktop/gui/MainScreen/commands/newNote.ts index bd03c39f3..ac2c42992 100644 --- a/packages/app-desktop/gui/MainScreen/commands/newNote.ts +++ b/packages/app-desktop/gui/MainScreen/commands/newNote.ts @@ -2,7 +2,6 @@ import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '@jopl import { _ } from '@joplin/lib/locale'; import Setting from '@joplin/lib/models/Setting'; import Note from '@joplin/lib/models/Note'; -const TemplateUtils = require('@joplin/lib/TemplateUtils'); export const declaration: CommandDeclaration = { name: 'newNote', @@ -12,12 +11,10 @@ export const declaration: CommandDeclaration = { export const runtime = (): CommandRuntime => { return { - execute: async (_context: CommandContext, template: string = null, isTodo: boolean = false) => { + execute: async (_context: CommandContext, body: string = '', isTodo: boolean = false) => { const folderId = Setting.value('activeFolderId'); if (!folderId) return; - const body = template ? TemplateUtils.render(template) : ''; - const defaultValues = Note.previewFieldsWithDefaultValues({ includeTimestamps: false }); let newNote = Object.assign({}, defaultValues, { diff --git a/packages/app-desktop/gui/MainScreen/commands/newTodo.ts b/packages/app-desktop/gui/MainScreen/commands/newTodo.ts index b30ac2d70..b64ed91de 100644 --- a/packages/app-desktop/gui/MainScreen/commands/newTodo.ts +++ b/packages/app-desktop/gui/MainScreen/commands/newTodo.ts @@ -9,8 +9,8 @@ export const declaration: CommandDeclaration = { export const runtime = (): CommandRuntime => { return { - execute: async (_context: CommandContext, template: string = null) => { - return CommandService.instance().execute('newNote', template, true); + execute: async (_context: CommandContext, body: string = '') => { + return CommandService.instance().execute('newNote', body, true); }, enabledCondition: 'oneFolderSelected && !inConflictFolder', }; diff --git a/packages/app-desktop/gui/MainScreen/commands/selectTemplate.ts b/packages/app-desktop/gui/MainScreen/commands/selectTemplate.ts deleted file mode 100644 index cac70cab8..000000000 --- a/packages/app-desktop/gui/MainScreen/commands/selectTemplate.ts +++ /dev/null @@ -1,33 +0,0 @@ -import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; -import { _ } from '@joplin/lib/locale'; -const TemplateUtils = require('@joplin/lib/TemplateUtils'); - -export const declaration: CommandDeclaration = { - name: 'selectTemplate', -}; - -export const runtime = (comp: any): CommandRuntime => { - return { - execute: async (_context: CommandContext, noteType: string) => { - comp.setState({ - promptOptions: { - label: _('Template file:'), - inputType: 'dropdown', - value: comp.props.templates[0], // Need to start with some value - autocomplete: comp.props.templates, - onClose: async (answer: any) => { - if (answer) { - if (noteType === 'note' || noteType === 'todo') { - void CommandService.instance().execute('newNote', answer.value, noteType === 'todo'); - } else { - void CommandService.instance().execute('insertText', TemplateUtils.render(answer.value)); - } - } - - comp.setState({ promptOptions: null }); - }, - }, - }); - }, - }; -}; diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 205c21573..395e65787 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -25,7 +25,6 @@ const packageInfo = require('../packageInfo.js'); const { clipboard } = require('electron'); const Menu = bridge().Menu; const PluginManager = require('@joplin/lib/services/PluginManager'); -const TemplateUtils = require('@joplin/lib/TemplateUtils'); const menuUtils = new MenuUtils(CommandService.instance()); @@ -301,7 +300,6 @@ function useMenu(props: Props) { const importItems = []; const exportItems = []; - const templateItems: any[] = []; const ioService = InteropService.instance(); const ioModules = ioService.modules(); for (let i = 0; i < ioModules.length; i++) { @@ -366,39 +364,6 @@ function useMenu(props: Props) { const newSubFolderItem = menuItemDic.newSubFolder; const printItem = menuItemDic.print; - templateItems.push({ - label: _('Create note from template'), - click: () => { - void CommandService.instance().execute('selectTemplate', 'note'); - }, - }, { - label: _('Create to-do from template'), - click: () => { - void CommandService.instance().execute('selectTemplate', 'todo'); - }, - }, { - label: _('Insert template'), - accelerator: keymapService.getAccelerator('insertTemplate'), - click: () => { - void CommandService.instance().execute('selectTemplate'); - }, - }, { - label: _('Open template directory'), - click: () => { - void bridge().openItem(Setting.value('templateDir')); - }, - }, { - label: _('Refresh templates'), - click: async () => { - const templates = await TemplateUtils.loadTemplates(Setting.value('templateDir')); - - props.dispatch({ - type: 'TEMPLATE_UPDATE_ALL', - templates: templates, - }); - }, - }); - let toolsItems: any[] = []; // we need this workaround, because on macOS the menu is different @@ -493,13 +458,6 @@ function useMenu(props: Props) { { type: 'separator', visible: shim.isMac() ? false : true, - }, { - label: _('Templates'), - visible: shim.isMac() ? false : true, - submenu: templateItems, - }, { - type: 'separator', - visible: shim.isMac() ? false : true, }, { label: _('Import'), visible: shim.isMac() ? false : true, @@ -555,11 +513,6 @@ function useMenu(props: Props) { selector: 'performClose:', }, { type: 'separator', - }, { - label: _('Templates'), - submenu: templateItems, - }, { - type: 'separator', }, { label: _('Import'), submenu: importItems, diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 477f39bc1..8d3551994 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -677,7 +677,6 @@ export default class BaseApplication { Setting.setConstant('env', initArgs.env); Setting.setConstant('profileDir', profileDir); - Setting.setConstant('templateDir', `${profileDir}/templates`); Setting.setConstant('resourceDirName', resourceDirName); Setting.setConstant('resourceDir', resourceDir); Setting.setConstant('tempDir', tempDir); diff --git a/packages/lib/TemplateUtils.js b/packages/lib/TemplateUtils.js deleted file mode 100644 index d1ffd92a0..000000000 --- a/packages/lib/TemplateUtils.js +++ /dev/null @@ -1,79 +0,0 @@ -const shim = require('./shim').default; -const time = require('./time').default; -const Mustache = require('mustache'); - -const TemplateUtils = {}; - - -// Mustache escapes strings (including /) with the html code by default -// This isn't useful for markdown so it's disabled -Mustache.escape = text => { - return text; -}; - -function beginningOfWeek(index) { - // index: 0 for Sunday, 1 for Monday - const thisDate = new Date(); - const day = thisDate.getDay(), - diff = day >= index ? day - index : 6 - day; - - return new Date().setDate(thisDate.getDate() - diff); -} - -TemplateUtils.render = function(input) { - // new template variables can be added here - // If there are too many, this should be moved to a new file - // view needs to be set in this function so that the formats reflect settings - const view = { - date: time.formatMsToLocal(new Date().getTime(), time.dateFormat()), - time: time.formatMsToLocal(new Date().getTime(), time.timeFormat()), - datetime: time.formatMsToLocal(new Date().getTime()), - custom_datetime: () => { - return (text, render) => { - return render(time.formatMsToLocal(new Date().getTime(), text)); - }; - }, - bowm: time.formatMsToLocal(beginningOfWeek(1), time.dateFormat()), - bows: time.formatMsToLocal(beginningOfWeek(0), time.dateFormat()), - }; - - return Mustache.render(input, view); -}; - -TemplateUtils.loadTemplates = async function(filePath) { - const templates = []; - let files = []; - - if (await shim.fsDriver().exists(filePath)) { - try { - files = await shim.fsDriver().readDirStats(filePath); - } catch (error) { - let msg = error.message ? error.message : ''; - msg = `Could not read template names from ${filePath}\n${msg}`; - error.message = msg; - throw error; - } - - // Make sure templates are always in the same order - // sensitivity ensures that the sort will ignore case - files.sort((a, b) => { return a.path.localeCompare(b.path, undefined, { sensitivity: 'accent' }); }); - - for (const file of files) { - if (file.path.endsWith('.md')) { - try { - const fileString = await shim.fsDriver().readFile(`${filePath}/${file.path}`, 'utf-8'); - templates.push({ label: file.path, value: fileString }); - } catch (error) { - let msg = error.message ? error.message : ''; - msg = `Could not load template ${file.path}\n${msg}`; - error.message = msg; - throw error; - } - } - } - } - - return templates; -}; - -module.exports = TemplateUtils; diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 6424651e2..3b4ad3352 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -108,7 +108,6 @@ export interface Constants { resourceDirName: string; resourceDir: string; profileDir: string; - templateDir: string; tempDir: string; pluginDataDir: string; cacheDir: string; @@ -190,7 +189,6 @@ class Setting extends BaseModel { resourceDirName: '', resourceDir: '', profileDir: '', - templateDir: '', tempDir: '', pluginDataDir: '', cacheDir: '', diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index 08eb98b13..31bf84e6c 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -78,7 +78,7 @@ export interface State { hasDisabledSyncItems: boolean; hasDisabledEncryptionItems: boolean; customCss: string; - templates: any[]; + hasLegacyTemplates: boolean; collapsedFolderIds: string[]; clipperServer: StateClipperServer; decryptionWorker: StateDecryptionWorker; @@ -132,7 +132,7 @@ export const defaultState: State = { hasDisabledSyncItems: false, hasDisabledEncryptionItems: false, customCss: '', - templates: [], + hasLegacyTemplates: false, collapsedFolderIds: [], clipperServer: { startState: 'idle', @@ -1016,6 +1016,10 @@ const reducer = produce((draft: Draft = defaultState, action: any) => { } break; + case 'CONTAINS_LEGACY_TEMPLATES': + draft.hasLegacyTemplates = true; + break; + case 'SYNC_STARTED': draft.syncStarted = true; break; @@ -1120,10 +1124,6 @@ const reducer = produce((draft: Draft = defaultState, action: any) => { draft.customCss += action.css; break; - case 'TEMPLATE_UPDATE_ALL': - draft.templates = action.templates; - break; - case 'SET_NOTE_TAGS': draft.selectedNoteTags = action.items; break; diff --git a/packages/lib/services/KeymapService.ts b/packages/lib/services/KeymapService.ts index fa2bbc150..713e6425c 100644 --- a/packages/lib/services/KeymapService.ts +++ b/packages/lib/services/KeymapService.ts @@ -20,7 +20,6 @@ const defaultKeymapItems = { { accelerator: 'Cmd+Q', command: 'quit' }, { accelerator: 'Cmd+,', command: 'config' }, { accelerator: 'Cmd+W', command: 'closeWindow' }, - { accelerator: 'Option+Cmd+I', command: 'insertTemplate' }, { accelerator: 'Cmd+C', command: 'textCopy' }, { accelerator: 'Cmd+X', command: 'textCut' }, { accelerator: 'Cmd+V', command: 'textPaste' }, @@ -61,7 +60,6 @@ const defaultKeymapItems = { { accelerator: 'Ctrl+T', command: 'newTodo' }, { accelerator: 'Ctrl+S', command: 'synchronize' }, { accelerator: 'Ctrl+Q', command: 'quit' }, - { accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' }, { accelerator: 'Ctrl+C', command: 'textCopy' }, { accelerator: 'Ctrl+X', command: 'textCut' }, { accelerator: 'Ctrl+V', command: 'textPaste' },