Skip to content

Commit 2c28f94

Browse files
committed
Merge branch 'main' of https://github.com/NotePlan/plugins
2 parents 26e56d7 + f79975a commit 2c28f94

6 files changed

Lines changed: 437 additions & 19 deletions

File tree

helpers/__tests__/general.test.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,17 @@ describe(`${FILE}`, () => {
161161
})
162162
test('should create a link in an existing floating window', () => {
163163
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'filename', '', 'useExistingSubWindow')
164-
expect(res).toEqual('noteplan://x-callback-url/openNote?filename=foo&useExistingSubWindow=yes')
164+
expect(res).toEqual('noteplan://x-callback-url/openNote?filename=foo&useExistingSubWindow=yes&subWindow=yes')
165165
})
166166
test('should create a link in split view', () => {
167167
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'filename', null, 'splitView')
168168
expect(res).toEqual('noteplan://x-callback-url/openNote?filename=foo&splitView=yes')
169169
})
170+
test('should create a link in reuse split view (reuseSplitView + splitView=yes)', () => {
171+
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'filename', '', 'reuseSplitView')
172+
expect(res).toContain('reuseSplitView=yes')
173+
expect(res).toContain('splitView=yes')
174+
})
170175
test('should ignore illegal openType', () => {
171176
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'filename', '', 'baz')
172177
expect(res).toEqual('noteplan://x-callback-url/openNote?filename=foo')
@@ -183,6 +188,31 @@ describe(`${FILE}`, () => {
183188
expect(res).toEqual('noteplan://x-callback-url/openNote?filename=foo&blockID=%5E123456')
184189
})
185190
})
191+
describe('using timeframe (calendar notes)', () => {
192+
test('should add timeframe=week for date note', () => {
193+
const res = g.createOpenOrDeleteNoteCallbackUrl('today', 'date', '', null, false, '', 'week')
194+
expect(res).toContain('noteDate=today')
195+
expect(res).toContain('&timeframe=week')
196+
})
197+
test('should add timeframe=month for date note', () => {
198+
const res = g.createOpenOrDeleteNoteCallbackUrl('today', 'date', '', null, false, '', 'month')
199+
expect(res).toContain('&timeframe=month')
200+
})
201+
test('should not add timeframe for non-date paramType', () => {
202+
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'filename', '', null, false, '', 'week')
203+
expect(res).not.toContain('timeframe=')
204+
})
205+
})
206+
describe('using highlightStart/highlightLength', () => {
207+
test('should add highlightStart and highlightLength', () => {
208+
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'title', '', null, false, '', null, 0, 9999)
209+
expect(res).toContain('&highlightStart=0&highlightLength=9999')
210+
})
211+
test('should not add highlight for deleteNote', () => {
212+
const res = g.createOpenOrDeleteNoteCallbackUrl('foo', 'title', '', null, true, '', null, 0, 10)
213+
expect(res).not.toContain('highlightStart')
214+
})
215+
})
186216
})
187217

188218
describe(section(`createRunPluginCallbackUrl`), () => {
@@ -214,6 +244,28 @@ describe(`${FILE}`, () => {
214244
const exp = 'noteplan://x-callback-url/addText?noteDate=today&mode=prepend&openNote=yes&text=bar'
215245
expect(g.createAddTextCallbackUrl('today', opts)).toEqual(exp)
216246
})
247+
test('should add subWindow=yes when openType is subWindow', () => {
248+
const opts = { text: 'bar', mode: 'append', openNote: 'yes', openType: 'subWindow' }
249+
const res = g.createAddTextCallbackUrl('today', opts)
250+
expect(res).toContain('&subWindow=yes')
251+
})
252+
test('should add splitView=yes when openType is splitView', () => {
253+
const opts = { text: 'bar', mode: 'append', openNote: 'yes', openType: 'splitView' }
254+
const res = g.createAddTextCallbackUrl('today', opts)
255+
expect(res).toContain('&splitView=yes')
256+
})
257+
test('should add reuseSplitView=yes and splitView=yes when openType is reuseSplitView', () => {
258+
const opts = { text: 'bar', mode: 'append', openNote: 'yes', openType: 'reuseSplitView' }
259+
const res = g.createAddTextCallbackUrl('today', opts)
260+
expect(res).toContain('&reuseSplitView=yes')
261+
expect(res).toContain('&splitView=yes')
262+
})
263+
test('should add useExistingSubWindow=yes and subWindow=yes when openType is useExistingSubWindow', () => {
264+
const opts = { text: 'bar', mode: 'append', openNote: 'yes', openType: 'useExistingSubWindow' }
265+
const res = g.createAddTextCallbackUrl('today', opts)
266+
expect(res).toContain('&useExistingSubWindow=yes')
267+
expect(res).toContain('&subWindow=yes')
268+
})
217269
})
218270

219271
describe(section('createPrettyOpenNoteLink()'), () => {
@@ -327,6 +379,21 @@ describe(`${FILE}`, () => {
327379
`${base}text?arg0=%7B%22excludeToday%22%3Afalse%2C%22progressHeading%22%3A%22Test%20Heading%22%2C%22progressYesNo%22%3A%22%23readbook%2C%23theology%22%7D`,
328380
)
329381
})
382+
test('should create selectTag callback with name param', () => {
383+
const result = g.createCallbackUrl('selectTag', { name: '#noteplan' })
384+
expect(result).toEqual(`${base}selectTag?name=%23noteplan`)
385+
})
386+
test('should create installPlugin callback with pluginID param', () => {
387+
const result = g.createCallbackUrl('installPlugin', { pluginID: 'dwertheimer.Favorites' })
388+
expect(result).toEqual(`${base}installPlugin?pluginID=dwertheimer.Favorites`)
389+
})
390+
test('should create toggleSidebar callback with forceCollapse and forceOpen', () => {
391+
const result = g.createCallbackUrl('toggleSidebar', { forceCollapse: 'yes', forceOpen: 'no', animated: 'no' })
392+
expect(result).toContain('toggleSidebar')
393+
expect(result).toContain('forceCollapse=yes')
394+
expect(result).toContain('forceOpen=no')
395+
expect(result).toContain('animated=no')
396+
})
330397
})
331398
/*
332399
* forceLeadingSlash()

helpers/general.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -280,17 +280,23 @@ export function returnNoteLink(noteTitle: string, heading: string | null = ''):
280280
* @param {string} openType - 'subWindow' | 'splitView' | 'reuseSplitView' | 'useExistingSubWindow' (default: null)
281281
* @param {boolean} isDeleteNote - whether this is actually a deleteNote
282282
* @param {string} blockID - the blockID if this is a line link (includes the ^) -- only works with title (not filename)
283+
* @param {string | null} timeframe - for calendar notes: 'week' | 'month' | 'quarter' | 'year' (default: null)
284+
* @param {number | null} highlightStart - character index to jump/select after opening (default: null)
285+
* @param {number | null} highlightLength - length of selection; use 0 for cursor only, high value for end (default: null)
283286
* @returns {string} the x-callback-url string
284287
* @tests available
285288
*/
286-
// createOpenOrDeleteNoteCallbackUrl('theTitle', 'title', 'heading', 'openType', 'isDeleteNote')
289+
// createOpenOrDeleteNoteCallbackUrl('theTitle', 'title', 'heading', 'openType', 'isDeleteNote', blockID, timeframe, highlightStart, highlightLength)
287290
export function createOpenOrDeleteNoteCallbackUrl(
288291
titleOrFilename: string,
289292
paramType: 'title' | 'filename' | 'date' = 'title',
290293
heading: string | null = '',
291294
openType: 'subWindow' | 'splitView' | 'reuseSplitView' | 'useExistingSubWindow' | null = null,
292295
isDeleteNote: boolean = false,
293296
blockID: string = '',
297+
timeframe: 'week' | 'month' | 'quarter' | 'year' | null = null,
298+
highlightStart: number | null = null,
299+
highlightLength: number | null = null,
294300
): string {
295301
const encodePlusParens = (s: string): string => encodeURIComponent(s).replace(/\(/g, '%28').replace(/\)/g, '%29')
296302
const isFilename = paramType === 'filename'
@@ -304,21 +310,30 @@ export function createOpenOrDeleteNoteCallbackUrl(
304310
if (openType === 'reuseSplitView') {
305311
openAs += '&splitView=yes' // special case for reuseSplitView, which needs both
306312
}
313+
if (openType === 'useExistingSubWindow') {
314+
openAs += '&subWindow=yes' // special case for useExistingSubWindow, which needs both
315+
}
316+
const timeframeStr =
317+
!isDeleteNote && paramType === 'date' && timeframe && ['week', 'month', 'quarter', 'year'].includes(timeframe) ? `&timeframe=${timeframe}` : ''
318+
const highlightStr =
319+
!isDeleteNote && highlightStart != null && Number.isInteger(highlightStart)
320+
? `&highlightStart=${highlightStart}&highlightLength=${highlightLength != null && Number.isInteger(highlightLength) ? highlightLength : 0}`
321+
: ''
307322
let retVal = ''
308323
if (isLineLink) {
309324
retVal = `${xcb}${encodedTitleOrFilename}${encodeURIComponent(blockID)}`
310325
} else {
311326
if (heading?.length) {
312327
if (isFilename) {
313-
retVal = `${xcb}${encodedTitleOrFilename}${head.length ? `&heading=${head}` : ''}${openAs}`
328+
retVal = `${xcb}${encodedTitleOrFilename}${head.length ? `&heading=${head}` : ''}${openAs}${timeframeStr}${highlightStr}`
314329
} else {
315-
retVal = `${xcb}${encodedTitleOrFilename}${head.length ? `%23${head}` : ''}${openAs}`
330+
retVal = `${xcb}${encodedTitleOrFilename}${head.length ? `%23${head}` : ''}${openAs}${timeframeStr}${highlightStr}`
316331
}
317332
} else {
318333
if (isLineLink) {
319-
retVal = `${xcb}${encodedTitleOrFilename}${head.length ? `&line=${head}` : ''}${openAs}`
334+
retVal = `${xcb}${encodedTitleOrFilename}${head.length ? `&line=${head}` : ''}${openAs}${timeframeStr}${highlightStr}`
320335
} else {
321-
retVal = `${xcb}${encodedTitleOrFilename}${openAs}`
336+
retVal = `${xcb}${encodedTitleOrFilename}${openAs}${timeframeStr}${highlightStr}`
322337
}
323338
}
324339
}
@@ -328,21 +343,31 @@ export function createOpenOrDeleteNoteCallbackUrl(
328343
/**
329344
* Create an addText callback url
330345
* @param {TNote | string} note (either a note object or a date-related string, e.g. today, yesterday, tomorrow)
331-
* @param {{ text: string, mode: string, openNote: string }} options - text to add, mode ('append', 'prepend'), and whether to open the note
346+
* @param {{ text: string, mode: string, openNote: string, openType?: string }} options - text to add, mode ('append', 'prepend'), openNote, and optional openType ('subWindow' | 'splitView' | 'reuseSplitView' | 'useExistingSubWindow')
332347
* @returns {string}
333348
* @tests available
334349
*/
335-
export function createAddTextCallbackUrl(note: TNote | string, options: { text: string, mode: string, openNote: string }): string {
350+
export function createAddTextCallbackUrl(
351+
note: TNote | string,
352+
options: { text: string, mode: string, openNote: string, openType?: 'subWindow' | 'splitView' | 'reuseSplitView' | 'useExistingSubWindow' | null },
353+
): string {
336354
const { text, mode, openNote } = options
355+
const openTypeParam = options.openType
356+
let openAs = ''
357+
if (openTypeParam && ['subWindow', 'splitView', 'reuseSplitView', 'useExistingSubWindow'].includes(openTypeParam)) {
358+
openAs = `&${openTypeParam}=yes`
359+
if (openTypeParam === 'reuseSplitView') openAs += '&splitView=yes'
360+
if (openTypeParam === 'useExistingSubWindow') openAs += '&subWindow=yes'
361+
}
337362
if (typeof note !== 'string') {
338363
// this is a note
339364
const encoded = encodeURIComponent(note.filename).replace(/\(/g, '%28').replace(/\)/g, '%29')
340365
if (note && note.filename) {
341-
return `noteplan://x-callback-url/addText?filename=${encoded}&mode=${mode}&openNote=${openNote}&text=${encodeURIComponent(text)}`
366+
return `noteplan://x-callback-url/addText?filename=${encoded}&mode=${mode}&openNote=${openNote}&text=${encodeURIComponent(text)}${openAs}`
342367
}
343368
} else {
344369
// this is a date type argument
345-
return `noteplan://x-callback-url/addText?noteDate=${note}&mode=${mode}&openNote=${openNote}&text=${encodeURIComponent(text)}`
370+
return `noteplan://x-callback-url/addText?noteDate=${note}&mode=${mode}&openNote=${openNote}&text=${encodeURIComponent(text)}${openAs}`
346371
}
347372
return ''
348373
}

np.CallbackURLs/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44

55
See Plugin [README](https://github.com/NotePlan/plugins/blob/main/np.CallbackURLs/README.md) for details on available commands and use cases.
66

7+
## [1.11.0] - 2025-01-30 @dwertheimer
8+
9+
- Fix addNote wizard: was returning addText URL; now correctly returns addNote URL
10+
- Open note wizard: add timeframe (week/month/quarter/year) for calendar notes; add optional highlightStart/highlightLength (cursor/selection after open)
11+
- Add text wizard: add openType (subWindow, splitView, reuseSplitView, useExistingSubWindow) when openNote=yes
12+
- Add note wizard: add optional highlightStart/highlightLength when openNote=yes
13+
- Add selectTag wizard: create x-callback URL to select a tag in the sidebar
14+
- Add installPlugin wizard: create x-callback URL to install a plugin by ID
15+
- Add toggleSidebar wizard: create x-callback URL to toggle/show/hide sidebar (forceCollapse, forceOpen, animated)
16+
- Helpers/general: createOpenOrDeleteNoteCallbackUrl now accepts timeframe, highlightStart, highlightLength; createAddTextCallbackUrl now accepts openType in options; useExistingSubWindow URLs now include subWindow=yes
17+
- Tests: add tests for timeframe, highlight, openType (addText), reuseSplitView, selectTag, installPlugin, toggleSidebar; add NPXCallbackWizard.test.js for selectTag, installPlugin, toggleSidebar wizard functions
18+
19+
## [1.10.1] - 2025-01-30 @dwertheimer
20+
21+
- Open note wizard: add openType options (subWindow, splitView, reuseSplitView, useExistingSubWindow) so generated openNote x-callback-urls can open in floating window, split view, reuse split view, or existing sub-window
22+
723
## [1.10.0] - 2025-09-23 @dwertheimer
824

925
- Add lineLink command
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* global describe, test, expect, jest, beforeEach */
2+
/**
3+
* Tests for np.CallbackURLs wizard functions: selectTag, installPlugin, toggleSidebar
4+
* Mocks userInput (getInput, chooseOption) to test URL output
5+
*/
6+
import { DataStore, Editor, CommandBar, NotePlan } from '@mocks/index'
7+
8+
global.DataStore = DataStore
9+
global.Editor = Editor
10+
global.CommandBar = CommandBar
11+
global.NotePlan = NotePlan
12+
13+
const mockGetInput = jest.fn()
14+
const mockChooseOption = jest.fn()
15+
16+
jest.mock('@helpers/userInput', () => ({
17+
getInput: (...args) => mockGetInput(...args),
18+
chooseOption: (...args) => mockChooseOption(...args),
19+
showMessage: jest.fn(),
20+
showMessageYesNo: jest.fn(),
21+
chooseFolder: jest.fn(),
22+
chooseNote: jest.fn(),
23+
getInputTrimmed: jest.fn(),
24+
}))
25+
26+
jest.mock('@helpers/dev', () => ({
27+
log: jest.fn(),
28+
logError: jest.fn(),
29+
logDebug: jest.fn(),
30+
JSP: (x) => x,
31+
clo: jest.fn(),
32+
timer: jest.fn(),
33+
}))
34+
35+
jest.mock('@helpers/NPParagraph', () => ({
36+
getSelectedParagraph: jest.fn(),
37+
getParagraphContainingPosition: jest.fn(),
38+
}))
39+
40+
jest.mock('../src/NPTemplateRunner', () => ({
41+
getXcallbackForTemplate: jest.fn(),
42+
}))
43+
44+
jest.mock('../src/NPOpenFolders', () => ({
45+
openFolderView: jest.fn(),
46+
}))
47+
48+
jest.mock('@helpers/NPdev', () => ({
49+
chooseRunPluginXCallbackURL: jest.fn(),
50+
}))
51+
52+
describe('np.CallbackURLs NPXCallbackWizard', () => {
53+
beforeEach(() => {
54+
jest.clearAllMocks()
55+
})
56+
57+
describe('selectTag', () => {
58+
test('should return selectTag URL with # when user enters tag without #', async () => {
59+
const { selectTag } = require('../src/NPXCallbackWizard')
60+
mockGetInput.mockResolvedValue('noteplan')
61+
const url = await selectTag()
62+
expect(url).toContain('noteplan://x-callback-url/selectTag')
63+
expect(url).toContain('name=%23noteplan')
64+
})
65+
test('should return selectTag URL with user tag when user enters #tag', async () => {
66+
const { selectTag } = require('../src/NPXCallbackWizard')
67+
mockGetInput.mockResolvedValue('#noteplan')
68+
const url = await selectTag()
69+
expect(url).toContain('selectTag')
70+
expect(url).toContain('name=%23noteplan')
71+
})
72+
test('should return empty string when user cancels', async () => {
73+
const { selectTag } = require('../src/NPXCallbackWizard')
74+
mockGetInput.mockResolvedValue(false)
75+
const url = await selectTag()
76+
expect(url).toEqual('')
77+
})
78+
})
79+
80+
describe('installPlugin', () => {
81+
test('should return installPlugin URL with pluginID', async () => {
82+
const { installPlugin } = require('../src/NPXCallbackWizard')
83+
mockGetInput.mockResolvedValue('dwertheimer.Favorites')
84+
const url = await installPlugin()
85+
expect(url).toContain('noteplan://x-callback-url/installPlugin')
86+
expect(url).toContain('pluginID=dwertheimer.Favorites')
87+
})
88+
test('should return empty string when user cancels', async () => {
89+
const { installPlugin } = require('../src/NPXCallbackWizard')
90+
mockGetInput.mockResolvedValue(false)
91+
const url = await installPlugin()
92+
expect(url).toEqual('')
93+
})
94+
test('should return empty string when user enters empty string', async () => {
95+
const { installPlugin } = require('../src/NPXCallbackWizard')
96+
mockGetInput.mockResolvedValue('')
97+
const url = await installPlugin()
98+
expect(url).toEqual('')
99+
})
100+
})
101+
102+
describe('toggleSidebar', () => {
103+
test('should return toggleSidebar URL with no params when all defaults', async () => {
104+
const { toggleSidebar } = require('../src/NPXCallbackWizard')
105+
mockChooseOption.mockResolvedValueOnce('no').mockResolvedValueOnce('no').mockResolvedValueOnce('yes')
106+
const url = await toggleSidebar()
107+
expect(url).toEqual('noteplan://x-callback-url/toggleSidebar')
108+
})
109+
test('should return toggleSidebar URL with forceCollapse=yes', async () => {
110+
const { toggleSidebar } = require('../src/NPXCallbackWizard')
111+
mockChooseOption.mockResolvedValueOnce('yes').mockResolvedValueOnce('no').mockResolvedValueOnce('yes')
112+
const url = await toggleSidebar()
113+
expect(url).toContain('toggleSidebar')
114+
expect(url).toContain('forceCollapse=yes')
115+
})
116+
test('should return toggleSidebar URL with forceOpen=yes', async () => {
117+
const { toggleSidebar } = require('../src/NPXCallbackWizard')
118+
mockChooseOption.mockResolvedValueOnce('no').mockResolvedValueOnce('yes').mockResolvedValueOnce('yes')
119+
const url = await toggleSidebar()
120+
expect(url).toContain('forceOpen=yes')
121+
})
122+
test('should return empty string when user cancels first prompt', async () => {
123+
const { toggleSidebar } = require('../src/NPXCallbackWizard')
124+
mockChooseOption.mockResolvedValue(false)
125+
const url = await toggleSidebar()
126+
expect(url).toEqual('')
127+
})
128+
})
129+
})

np.CallbackURLs/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"noteplan.minAppVersion-NOTE": "Includes folder view picker",
66
"plugin.id": "np.CallbackURLs",
77
"plugin.name": "🔗 Link Creator",
8-
"plugin.version": "1.10.0",
9-
"plugin.lastUpdateInfo": "1.10.0: Add lineLink command",
8+
"plugin.version": "1.11.0",
9+
"plugin.lastUpdateInfo": "1.11.0: selectTag, installPlugin, toggleSidebar wizards; openNote timeframe/highlight; addText/addNote openType and highlight; fix addNote URL",
1010
"plugin.description": "Interactively helps you form links/x-callback-urls (and also Template Tags with runPlugin commands) to perform actions from within NotePlan or between other applications and NotePlan.",
1111
"plugin.author": "dwertheimer",
1212
"plugin.dependencies": [],

0 commit comments

Comments
 (0)