Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/docs/Advanced/onePageInputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ This feature is currently in Beta.
## FIELD UX
- FIELD inputs get inline suggestions from your vault (Dataview if available, with a manual fallback).

## Optional fields
- Fields whose tokens carry the `|optional` flag (see [Optional fields](../FormatSyntax.md#optional-fields)) show an "(optional)" badge and may be left empty. An empty optional field stores an intentional empty value — the sequential prompt will not re-ask for it.
- A field counts as optional only when **every** occurrence of that variable across the scanned formats is flagged.
- Optional dropdowns get a "Skip (leave empty)" entry; the first real option stays preselected.
- Optional date fields left blank resolve to empty. If the typed text cannot be parsed as a date, the field is handed to the regular sequential date prompt after submit instead of silently becoming empty.

## Skipping the modal
- If all required inputs already have values (e.g., prefilled by an earlier macro step), the modal will not open.
- Empty string is considered an intentional value and will not prompt again.
- Empty string is considered an intentional value and will not prompt again. This now applies to `{{VDATE}}` variables too: a script-set `""` renders empty instead of re-prompting.
- For Capture choices, a non-empty editor selection will prefill `{{VALUE}}` during preflight when selection-as-value is enabled.

Note: For date fields with a default, leaving the input blank will apply the default automatically at submit time.
Note: For **required** date fields with a default, leaving the input blank will apply the default automatically at submit time. A **required** date field left blank without a usable default is re-asked by the sequential date prompt after submit. Optional date fields left blank stay empty.

### Cancel behavior
- If you press Cancel in the one-page modal, the preflight is aborted and the choice proceeds with the standard step-by-step prompts at runtime.
Expand Down Expand Up @@ -86,6 +92,7 @@ Supported input fields:
- `options` (string[] for dropdown and suggester)
- `dateFormat` (string for date)
- `description` (string)
- `optional` (boolean — field may be left empty; shows an "(optional)" badge)
- `suggesterConfig` (object for suggester: `{ allowCustomInput?: boolean, caseSensitive?: boolean, multiSelect?: boolean }`)

**Field Type Details:**
Expand Down
37 changes: 35 additions & 2 deletions docs/docs/FormatSyntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Same as above, but with a default value. If you leave the prompt empty, the defa

Example: `{{VDATE:due,YYYY-MM-DD|next monday}}`.

**Note:** If your date format contains pipe characters (`|`), escape them as `\|` or wrap them in square brackets, such as `[|]`, so QuickAdd does not treat them as the default value separator.
You can combine a default with the `optional` flag in any order: `{{VDATE:due,YYYY-MM-DD|tomorrow|optional}}` and `{{VDATE:due,YYYY-MM-DD|optional|tomorrow}}` are equivalent. See [Optional fields](#optional-fields).

**Note:** Pipe characters (`|`) cannot be used inside VDATE date formats — everything after the first pipe is treated as the default value (and flags). Use a different literal, e.g. wrap text in square brackets: `{{VDATE:due,[Due ]YYYY-MM-DD}}`.

## `{{VALUE}}` / `{{NAME}}` {#value}

Expand Down Expand Up @@ -72,7 +74,7 @@ Example: `priority: {{VALUE:🔽,🔼,⏫|text:Low,Normal,High}}`.

## `{{VALUE:<variable name>|<default>}}` {#value-default}

Same as above, but with a default value. For single-value prompts (e.g., `{{VALUE:name|Anonymous}}`), the default is pre-populated in the input field - press Enter to accept or clear/edit it. For multi-value suggesters without `|custom`, you must select one of the provided options (no default applies). If you combine keyed options like `|label:`, `|default:`, `|type:`, or `|case:`, shorthand defaults like `|Anonymous` are ignored; use `|default:Anonymous` instead.
Same as above, but with a default value. For single-value prompts (e.g., `{{VALUE:name|Anonymous}}`), the default is pre-populated in the input field - press Enter to accept or clear/edit it. For multi-value suggesters without `|custom`, you must select one of the provided options (no default applies). If you combine keyed options like `|label:`, `|default:`, `|type:`, or `|case:`, shorthand defaults like `|Anonymous` are ignored; use `|default:Anonymous` instead. The bare `|optional` flag is the exception: `{{VALUE:name|Anonymous|optional}}` keeps the shorthand default. Because `optional` is now a reserved flag word, a literal default of "optional" needs the keyed form: `|default:optional`.

Example: `status: {{VALUE:status|Draft}}`.

Expand Down Expand Up @@ -103,6 +105,37 @@ Example: `{{DATE:YYYY-MM-DD}}-{{VALUE:title|case:slug}}.md`.

Allows you to type custom values in addition to selecting from the provided options. Example: `{{VALUE:Red,Green,Blue|custom}}` will suggest Red, Green, and Blue, but also allows you to type any other value like "Purple". This is useful when you have common options but want flexibility for edge cases. **Note:** You cannot combine `|custom` with a shorthand default value - use `|default:` if you need both.

## Optional fields: `|optional` {#optional-fields}

Marks a prompt as optional, so it can be skipped and resolve to nothing. Works on `{{VALUE}}`/`{{NAME}}`, `{{VALUE:<variable>}}`, option lists, and `{{VDATE:...}}`.

```markdown
{{VALUE:reminder|optional}}
{{VDATE:due,YYYY-MM-DD|optional}}
{{VALUE:low,medium,high|optional}}
```

What `optional` changes:

- **Prompts gain a Skip button** (and a hint line). Skipping — or submitting an empty input — accepts "empty" as the answer: the placeholder resolves to nothing, and you are not re-prompted for the same variable later in the run.
- **Empty beats the default.** For optional tokens with a default, the default is pre-filled in the input box; clearing it and submitting yields empty. (Required tokens keep today's behavior: an empty submission falls back to the default.)
- **Optional dates accept blank input** instead of failing the whole choice. A typo like "tomorow" still errors — only a blank input means "leave empty".
- **Option lists** show a skip instruction in the suggester footer (Ctrl/Cmd+Shift+Enter) instead of forcing a pick.
- **In the One-Page Input modal**, optional fields show an "(optional)" badge and may be left empty; optional dropdowns get a "Skip (leave empty)" entry.
- **Esc still cancels the whole choice** — skipping is an answer, cancelling is not.

The keyed form `|optional:false` turns the flag off explicitly (useful when a shared snippet adds it). The flag can sit next to a shorthand default: `{{VALUE:reminder|call mom|optional}}`. Because `optional` is a reserved flag word, a literal default of "optional" needs the keyed form: `{{VALUE:x|default:optional}}`.

**Tip — make decoration disappear with the date:** put literal text inside the moment format using square brackets. With

```markdown
- [ ] {{VALUE}} {{VDATE:due,[📅 ]YYYY-MM-DD|optional}}
```

an answered date renders `📅 2026-06-14`, and a skipped date renders nothing at all — the emoji vanishes with it. The same works for prefixes like `[Due: ]YYYY-MM-DD`.

**Scripting note:** setting a variable to the empty string (`params.variables.myVar = ""`) now counts as "answered, empty" for **all** token types, including `{{VDATE}}` — it renders empty instead of re-prompting. To force a prompt, leave the variable unset (or `delete` it / set it to `undefined`). The old workaround of assigning a single space (`" "`) still works but is no longer needed.

## `{{LINKCURRENT}}` {#linkcurrent}

A link to the file from which the template or capture was triggered (`[[link]]` format). When the append-link setting is set to **Enabled (skip if no active file)**, this token resolves to an empty string instead of throwing an error if no note is focused.
Expand Down
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const VARIABLE_LABEL_SYNTAX =
"{{value:<variable name>|label:<helper text>}}";
export const VARIABLE_TEXT_SYNTAX =
"{{value:<items>|text:<display items>}}";
export const VARIABLE_OPTIONAL_SYNTAX =
"{{value:<variable name>|optional}}";
export const VDATE_OPTIONAL_SYNTAX =
"{{vdate:<variable name>, <date format>|optional}}";
export const VALUE_CASE_SYNTAX = "{{value|case:kebab}}";
export const VARIABLE_CASE_SYNTAX = "{{value:<variable name>|case:kebab}}";
export const FIELD_VAR_SYNTAX = "{{field:<field name>}}";
Expand All @@ -28,6 +32,7 @@ export const FORMAT_SYNTAX: string[] = [
"{{date:<dateformat>}}",
"{{vdate:<variable name>, <date format>}}",
"{{vdate:<variable name>, <date format>|<default value>}}",
VDATE_OPTIONAL_SYNTAX,
GLOBAL_VAR_SYNTAX,
VALUE_SYNTAX,
NAME_SYNTAX,
Expand All @@ -38,6 +43,7 @@ export const FORMAT_SYNTAX: string[] = [
VARIABLE_DEFAULT_OPTION_SYNTAX,
VARIABLE_LABEL_SYNTAX,
VARIABLE_TEXT_SYNTAX,
VARIABLE_OPTIONAL_SYNTAX,
FIELD_VAR_SYNTAX,
"{{field:<fieldname>|folder:<path>}}",
"{{field:<fieldname>|tag:<tagname>}}",
Expand Down Expand Up @@ -72,6 +78,8 @@ export const FILE_NAME_FORMAT_SYNTAX: string[] = [
FIELD_VAR_SYNTAX,
RANDOM_SYNTAX,
];
// Note: |optional is deliberately absent from FILE_NAME_FORMAT_SYNTAX — an
// all-optional file name that resolves empty is rejected at creation time.

export const TEMPLATE_FORMAT_SYNTAX: string[] = [TITLE_SYNTAX];

Expand Down
10 changes: 10 additions & 0 deletions src/engine/TemplateEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,11 @@ export abstract class TemplateEngine extends QuickAddEngine {
format,
promptHeader
);
if (!formattedName.trim()) {
throw new Error(
"File name is empty after formatting. Provide a value or remove |optional from tokens used in the file name format."
);
}
return this.normalizeMarkdownFilePath(folderPath, formattedName);
}

Expand Down Expand Up @@ -514,6 +519,11 @@ export abstract class TemplateEngine extends QuickAddEngine {
.replace(MARKDOWN_FILE_EXTENSION_REGEX, "")
.replace(CANVAS_FILE_EXTENSION_REGEX, "")
.replace(BASE_FILE_EXTENSION_REGEX, "");
if (!formattedFileName.trim()) {
throw new Error(
"File name is empty after formatting. Provide a value or remove |optional from tokens used in the file name format."
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return `${actualFolderPath}${formattedFileName}${extension}`;
}

Expand Down
5 changes: 5 additions & 0 deletions src/engine/TemplateInsertEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ export class TemplateInsertEngine extends TemplateEngine {
);
}

// An all-optional file name format can resolve empty; there is no
// meaningful move offer in that case (and normalizeTemplateFilePath
// rejects empty names).
if (!fileName.trim()) return null;

return this.normalizeTemplateFilePath(
treatAsVaultRelativePath ? "" : folderPath,
fileName,
Expand Down
11 changes: 11 additions & 0 deletions src/formatters/completeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ export class CompleteFormatter extends Formatter {
);
const defaultValue = this.valuePromptContext?.defaultValue;
const description = this.valuePromptContext?.description;
const promptOptions = this.valuePromptContext?.optional
? { optional: true }
: undefined;
if (linkSourcePath) {
this.value = await promptFactory.PromptWithContext(
this.app,
Expand All @@ -184,6 +187,7 @@ export class CompleteFormatter extends Formatter {
defaultValue,
linkSourcePath,
description,
promptOptions,
);
} else {
this.value = await promptFactory.Prompt(
Expand All @@ -192,6 +196,7 @@ export class CompleteFormatter extends Formatter {
undefined,
defaultValue,
description,
promptOptions,
);
}
} catch (error) {
Expand All @@ -218,6 +223,7 @@ export class CompleteFormatter extends Formatter {
"Enter a date (e.g., 'tomorrow', 'next friday', '2025-12-25')",
context.defaultValue,
context.dateFormat ?? "YYYY-MM-DD",
context.optional ? { optional: true } : undefined,
);
}

Expand All @@ -229,6 +235,7 @@ export class CompleteFormatter extends Formatter {
(context?.defaultValue ? context.defaultValue : undefined),
context?.defaultValue,
context?.description,
context?.optional ? { optional: true } : undefined,
);
} catch (error) {
if (isCancellationError(error)) {
Expand Down Expand Up @@ -256,6 +263,7 @@ export class CompleteFormatter extends Formatter {
placeholder?: string;
variableKey?: string;
displayValues?: string[];
optional?: boolean;
},
) {
try {
Expand All @@ -269,6 +277,7 @@ export class CompleteFormatter extends Formatter {
...(context?.placeholder
? { placeholder: context.placeholder }
: {}),
...(context?.optional ? { skippable: true } : {}),
},
);
}
Expand All @@ -277,6 +286,8 @@ export class CompleteFormatter extends Formatter {
displayValues,
suggestedValues,
context?.placeholder,
undefined,
context?.optional ? { skippable: true } : undefined,
);
} catch (error) {
if (isCancellationError(error)) {
Expand Down
13 changes: 9 additions & 4 deletions src/formatters/formatDisplayFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DateFormatPreviewGenerator
} from "./helpers/previewHelpers";
import { getValueVariableBaseName } from "../utils/valueSyntax";
import { parseVDateOptions } from "../utils/vdateSyntax";

export class FormatDisplayFormatter extends Formatter {
constructor(
Expand Down Expand Up @@ -156,11 +157,12 @@ export class FormatDisplayFormatter extends Formatter {
let output: string = input;

// For preview, show helpful format examples instead of failing
output = output.replace(new RegExp(DATE_VARIABLE_REGEX.source, 'gi'), (match, variableName, dateFormat, defaultValue) => {
output = output.replace(new RegExp(DATE_VARIABLE_REGEX.source, 'gi'), (match, variableName, dateFormat, rawOptions) => {
const cleanVariableName = variableName?.trim();
const cleanDateFormat = dateFormat?.trim();
const cleanDefaultValue = defaultValue?.trim();

const { defaultValue: cleanDefaultValue, optional } =
parseVDateOptions(rawOptions);

if (!cleanVariableName || !cleanDateFormat) {
return match; // Return original if incomplete
}
Expand All @@ -181,7 +183,10 @@ export class FormatDisplayFormatter extends Formatter {
if (cleanDefaultValue) {
formattedExample += ` (default: ${cleanDefaultValue})`;
}

if (optional) {
formattedExample += ` (optional)`;
}

return formattedExample;
});

Expand Down
3 changes: 3 additions & 0 deletions src/formatters/formatSyntax.docs-examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const CURRENT_AND_2120_EXAMPLES = [
"## Summary\n{{VALUE:summary|type:multiline|label:Summary}}",
"{{DATE:YYYY-MM-DD}}-{{VALUE:title|case:slug}}.md",
"{{VALUE:Red,Green,Blue|custom}}",
"{{VALUE:reminder|optional}}",
"{{VALUE:reminder|call mom|optional}}",
"- [ ] {{VALUE|label:Task}} {{VDATE:followup,[📅 ]YYYY-MM-DD|optional}}",
"Source: {{LINKCURRENT}}",
"Notes from {{FILENAMECURRENT}}",
"{{MACRO:Generate summary}}",
Expand Down
Loading