feat: add question type presentation controls
This commit is contained in:
+45
-1
@@ -13,6 +13,7 @@ import {
|
||||
submitEditorDraft,
|
||||
syncStateToSelection,
|
||||
} from "../state/editor.ts";
|
||||
import { cycleCurrentQuestionType } from "../state/question-type.ts";
|
||||
import { toAskResult } from "../state/result.ts";
|
||||
import {
|
||||
getCurrentOption,
|
||||
@@ -58,6 +59,7 @@ type Keybindings = CustomCallbackArgs[2];
|
||||
type Done = (result: AskResult) => void;
|
||||
interface AskFlowOptions {
|
||||
allowFreeform?: boolean;
|
||||
presentSingleAsMulti?: boolean;
|
||||
}
|
||||
|
||||
type AskFlowParams = AskParams &
|
||||
@@ -73,6 +75,7 @@ interface AskFlowController {
|
||||
dismissNotice?: string;
|
||||
done: Done;
|
||||
editor: Editor;
|
||||
pendingQuestionTypeChangeQuestionId?: string;
|
||||
pendingReviewShortcutActionIndex?: number;
|
||||
settingsOpen: boolean;
|
||||
state: AskState;
|
||||
@@ -89,13 +92,18 @@ export async function runAskFlow(
|
||||
): Promise<AskResult> {
|
||||
const store = getAskConfigStore();
|
||||
const config = await store.getConfig();
|
||||
const flowOptions = {
|
||||
...options,
|
||||
presentSingleAsMulti:
|
||||
options.presentSingleAsMulti ?? config.behaviour.presentSingleAsMulti,
|
||||
};
|
||||
return ctx.ui.custom<AskResult>((...args) =>
|
||||
createAskFlowController(args, {
|
||||
...params,
|
||||
config,
|
||||
cwd: ctx.cwd,
|
||||
ctx,
|
||||
flowOptions: options,
|
||||
flowOptions,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -118,6 +126,7 @@ function createAskFlowController(
|
||||
settingsOpen: false,
|
||||
state: createInitialState(params, params.flowOptions),
|
||||
suppressAutoInputForSelection: false,
|
||||
pendingQuestionTypeChangeQuestionId: undefined,
|
||||
pendingReviewShortcutActionIndex: undefined,
|
||||
theme,
|
||||
tui,
|
||||
@@ -238,30 +247,41 @@ function handleNavigationCommand(
|
||||
switch (command.kind) {
|
||||
case "moveTab":
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
commitState(controller, moveTab(controller.state, command.delta));
|
||||
return;
|
||||
case "moveOption":
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
commitState(controller, moveOption(controller.state, command.delta));
|
||||
return;
|
||||
case "toggleMulti":
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
handleToggleCurrentOption(controller);
|
||||
return;
|
||||
case "changeQuestionType":
|
||||
clearReviewShortcutPending(controller);
|
||||
handleChangeQuestionType(controller);
|
||||
return;
|
||||
case "openQuestionNote":
|
||||
clearQuestionTypeChangePending(controller);
|
||||
openQuestionNote(controller);
|
||||
return;
|
||||
case "openOptionNote":
|
||||
clearQuestionTypeChangePending(controller);
|
||||
openOptionNote(controller);
|
||||
return;
|
||||
case "confirm":
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
commitState(controller, confirmCurrentSelection(controller.state), {
|
||||
finish: true,
|
||||
});
|
||||
return;
|
||||
case "cancel":
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
handleExitFlow(controller, cancelFlow(controller.state));
|
||||
return;
|
||||
case "numberShortcut":
|
||||
@@ -269,6 +289,7 @@ function handleNavigationCommand(
|
||||
return;
|
||||
}
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
commitState(
|
||||
controller,
|
||||
applyNumberShortcut(controller.state, command.digit)
|
||||
@@ -276,6 +297,7 @@ function handleNavigationCommand(
|
||||
return;
|
||||
case "dismiss":
|
||||
clearReviewShortcutPending(controller);
|
||||
clearQuestionTypeChangePending(controller);
|
||||
handleExitFlow(controller, dismissFlow(controller.state));
|
||||
return;
|
||||
case "showSettings":
|
||||
@@ -301,6 +323,24 @@ function handleToggleCurrentOption(controller: AskFlowController) {
|
||||
commitState(controller, toggleCurrentMultiOption(controller.state));
|
||||
}
|
||||
|
||||
function handleChangeQuestionType(controller: AskFlowController) {
|
||||
const question = getCurrentQuestion(controller.state);
|
||||
if (!question || isSubmitTab(controller.state)) {
|
||||
return;
|
||||
}
|
||||
const confirmed =
|
||||
controller.pendingQuestionTypeChangeQuestionId === question.id;
|
||||
const result = cycleCurrentQuestionType(controller.state, { confirmed });
|
||||
controller.dismissNotice = result.notice;
|
||||
if (result.needsConfirmation) {
|
||||
controller.pendingQuestionTypeChangeQuestionId = question.id;
|
||||
refresh(controller);
|
||||
return;
|
||||
}
|
||||
clearQuestionTypeChangePending(controller);
|
||||
commitState(controller, result.state, { finish: true });
|
||||
}
|
||||
|
||||
function openQuestionNote(controller: AskFlowController) {
|
||||
const question = getCurrentQuestion(controller.state);
|
||||
if (!question || isSubmitTab(controller.state)) {
|
||||
@@ -415,6 +455,10 @@ function clearReviewShortcutPending(controller: AskFlowController) {
|
||||
controller.pendingReviewShortcutActionIndex = undefined;
|
||||
}
|
||||
|
||||
function clearQuestionTypeChangePending(controller: AskFlowController) {
|
||||
controller.pendingQuestionTypeChangeQuestionId = undefined;
|
||||
}
|
||||
|
||||
function getActiveReviewShortcutHint(
|
||||
controller: AskFlowController
|
||||
): string | undefined {
|
||||
|
||||
@@ -17,6 +17,7 @@ export type AskInputCommand =
|
||||
| { kind: "openOptionNote" }
|
||||
| { kind: "confirm" }
|
||||
| { kind: "cancel" }
|
||||
| { kind: "changeQuestionType" }
|
||||
| { kind: "dismiss" }
|
||||
| { kind: "showSettings" }
|
||||
| { kind: "numberShortcut"; digit: number }
|
||||
@@ -136,6 +137,9 @@ function getNavigationInputCommand(
|
||||
if (matchesBinding(data, bindings.toggle)) {
|
||||
return { kind: "toggleMulti" };
|
||||
}
|
||||
if (matchesBinding(data, bindings.changeQuestionType)) {
|
||||
return { kind: "changeQuestionType" };
|
||||
}
|
||||
if (matchesBinding(data, bindings.confirm)) {
|
||||
return { kind: "confirm" };
|
||||
}
|
||||
|
||||
+46
-6
@@ -34,11 +34,33 @@ const DESCRIPTION_LINE_COUNT = 3;
|
||||
const COMPACT_WIDTH = 40;
|
||||
const RESET_CONFIRMATION_MS = 2000;
|
||||
|
||||
type SettingSection = "Actions" | "Defaults for future asks" | "Live settings";
|
||||
type ToggleSettingKey = keyof AskConfig["behaviour"] | "notifications.enabled";
|
||||
|
||||
interface BaseSetting {
|
||||
description: string;
|
||||
label: string;
|
||||
section: SettingSection;
|
||||
}
|
||||
|
||||
interface ActionSetting extends BaseSetting {
|
||||
key: "resetConfig";
|
||||
type: "action";
|
||||
}
|
||||
|
||||
interface ToggleSetting extends BaseSetting {
|
||||
key: ToggleSettingKey;
|
||||
type: "toggle";
|
||||
}
|
||||
|
||||
type Setting = ActionSetting | ToggleSetting;
|
||||
|
||||
const SETTINGS = [
|
||||
{
|
||||
description:
|
||||
"Auto-submit completed ask flows when no question or option notes were added.",
|
||||
key: "autoSubmitWhenAnsweredWithoutNotes",
|
||||
section: "Live settings",
|
||||
label: "Auto-submit when answered without notes",
|
||||
type: "toggle",
|
||||
},
|
||||
@@ -46,6 +68,7 @@ const SETTINGS = [
|
||||
description:
|
||||
"Require a second cancel or dismiss action before discarding answered or drafted ask content.",
|
||||
key: "confirmDismissWhenDirty",
|
||||
section: "Live settings",
|
||||
label: "Confirm dismiss when dirty",
|
||||
type: "toggle",
|
||||
},
|
||||
@@ -53,6 +76,7 @@ const SETTINGS = [
|
||||
description:
|
||||
"Require pressing 1, 2, or 3 twice on the review tab before triggering Submit, Elaborate, or Cancel.",
|
||||
key: "doublePressReviewShortcuts",
|
||||
section: "Live settings",
|
||||
label: "Double-press review shortcuts",
|
||||
type: "toggle",
|
||||
},
|
||||
@@ -60,26 +84,36 @@ const SETTINGS = [
|
||||
description:
|
||||
"Emit one external notification when the ask flow opens and waits for input.",
|
||||
key: "notifications.enabled",
|
||||
section: "Live settings",
|
||||
label: "Notifications",
|
||||
type: "toggle",
|
||||
},
|
||||
{
|
||||
description: "Show footer keymap hints at the bottom of the ask flow.",
|
||||
key: "showFooterHints",
|
||||
section: "Live settings",
|
||||
label: "Show footer hints",
|
||||
type: "toggle",
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Default new and replayed ask flows to render single-select questions as multi-select. This is not hot-applied to the current flow; use the type hotkey there.",
|
||||
key: "presentSingleAsMulti",
|
||||
section: "Defaults for future asks",
|
||||
label: "Present single-select as multi-select",
|
||||
type: "toggle",
|
||||
},
|
||||
{
|
||||
description:
|
||||
"Reset behaviour, keymaps, notifications, and extraction settings to defaults. Press twice quickly to confirm.",
|
||||
key: "resetConfig",
|
||||
section: "Actions",
|
||||
label: "Reset config to defaults",
|
||||
type: "action",
|
||||
},
|
||||
] as const;
|
||||
] as const satisfies readonly Setting[];
|
||||
|
||||
type Setting = (typeof SETTINGS)[number];
|
||||
type SettingKey = Extract<Setting, { type: "toggle" }>["key"];
|
||||
type SettingKey = ToggleSettingKey;
|
||||
|
||||
export class AskSettingsList {
|
||||
private closed = false;
|
||||
@@ -152,10 +186,16 @@ export class AskSettingsList {
|
||||
);
|
||||
|
||||
lines.push(this.line("", innerWidth));
|
||||
let previousSection: string | undefined;
|
||||
for (const [index, setting] of SETTINGS.entries()) {
|
||||
if (setting.type === "action") {
|
||||
lines.push(this.line("", innerWidth));
|
||||
lines.push(this.line("", innerWidth));
|
||||
if (setting.section !== previousSection) {
|
||||
if (previousSection) {
|
||||
lines.push(this.line("", innerWidth));
|
||||
}
|
||||
lines.push(
|
||||
this.line(this.theme.fg("accent", ` ${setting.section}`), innerWidth)
|
||||
);
|
||||
previousSection = setting.section;
|
||||
}
|
||||
for (const settingLine of this.renderSetting(
|
||||
setting,
|
||||
|
||||
Reference in New Issue
Block a user