diff --git a/README.md b/README.md index d2db5f58..4c30e2d5 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ pins — there the chips are in the tab order, so Tab to one and press Enter or leaving the keyboard. - **Multi-token URLs** — drop values into any link, e.g. `https://tool.com/{ip}/{email}` +- **Web links only** — the resolved link must be `http` or `https`, so a template starts with `http://`, `https://` or the `{url}` token; the editor warns when it does not and offers a one-click fix - **Open in bulk** — pin several values; the tray states how many tabs it will open before you click - **Only what fits** — a tool is offered only when every token in its URL has a pinned value - **Templates & sharing** — ready templates copy their filled-in text to the clipboard, and configs export / import for your team diff --git a/package-lock.json b/package-lock.json index 9e669a2d..52da64c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "clipless", - "version": "2.2.3", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clipless", - "version": "2.2.3", + "version": "2.3.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c856dffc..709a917a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clipless", - "version": "2.2.3", + "version": "2.3.0", "description": "A Clipboard manager for busy people", "main": "./out/main/index.js", "author": "Daniel Essig", diff --git a/src/renderer/src/components/settings/tools/ToolEditor.test.tsx b/src/renderer/src/components/settings/tools/ToolEditor.test.tsx index c4c90ad4..ba1fee80 100644 --- a/src/renderer/src/components/settings/tools/ToolEditor.test.tsx +++ b/src/renderer/src/components/settings/tools/ToolEditor.test.tsx @@ -127,6 +127,51 @@ describe('ToolEditor', () => { expect(screen.getByTestId('readiness')).toHaveTextContent('no tokens'); expect(screen.getByTestId('tool-preview-caption')).toHaveTextContent('Would open 1 tab'); }); + + it('warns beneath the URL field when the template has no http or https scheme and prefixes https:// on click', async () => { + await renderTools( + + ); + const warning = screen.getByTestId('tool-url-scheme'); + expect(warning).toHaveTextContent('only http and https links can open'); + expect(screen.getByTestId('tool-preview-caption')).toHaveTextContent('Would open 1 tab'); + fireEvent.click(screen.getByTestId('tool-url-scheme-fix')); + expect(screen.getByTestId('tool-url')).toHaveValue('https://example.com/{email}'); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + fireEvent.change(screen.getByTestId('tool-url'), { target: { value: 'ftp://x/{email}' } }); + expect(screen.getByTestId('tool-url-scheme')).toBeInTheDocument(); + fireEvent.change(screen.getByTestId('tool-url'), { target: { value: 'HTTP://x/{email}' } }); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + fireEvent.change(screen.getByTestId('tool-url'), { target: { value: '' } }); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + }); + + it('does not warn when the template leads with the url token, which supplies its own scheme', async () => { + await renderTools( + + ); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tool-url-scheme-fix')).not.toBeInTheDocument(); + }); + + it('the https:// fix strips leading whitespace and leaves the caret after the prefix', async () => { + await renderTools( + + ); + fireEvent.click(screen.getByTestId('tool-url-scheme-fix')); + const url = screen.getByTestId('tool-url') as HTMLInputElement; + expect(url).toHaveValue('https://vt.example/{ip}'); + expect(url.selectionStart).toBe('https://'.length); + expect(document.activeElement).toBe(url); + }); }); describe('TemplateEditor', () => { diff --git a/src/renderer/src/components/settings/tools/ToolEditor.tsx b/src/renderer/src/components/settings/tools/ToolEditor.tsx index 4874f398..cf6ccc24 100644 --- a/src/renderer/src/components/settings/tools/ToolEditor.tsx +++ b/src/renderer/src/components/settings/tools/ToolEditor.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; -import { buildToolUrls } from '../../../../../shared/tools'; +import { buildToolUrls, needsWebScheme, withWebScheme } from '../../../../../shared/tools'; import { Readiness } from './Readiness'; import { TokenPicker, insertAtCaret } from './TokenPicker'; import { TokenText } from './TokenText'; @@ -44,6 +44,7 @@ export function ToolEditor({ initial, onSave, onCancel }: ToolEditorProps) { const count = buildToolUrls({ url }, values).length; const resolved = resolveToolUrls(url, values); const needed = groupsNeeded({ url }); + const schemeless = needsWebScheme(url); const save = async () => { setSaving(true); @@ -79,6 +80,11 @@ export function ToolEditor({ initial, onSave, onCancel }: ToolEditorProps) { setCaret(next.caret); }; + const addScheme = () => { + setUrl(withWebScheme(url)); + setCaret('https://'.length); + }; + return (