Skip to content

Commit c70f849

Browse files
committed
feat: replace integration confirm prompts with multiselect
Single 'Integrations to enable' multiselect (Jira / Notion / Slack) replaces three separate yes/no prompts. Credentials are only asked for selected providers. Jira and Notion are grouped with a 'tickets provider' hint so users know only one can be active per project.
1 parent 65b638a commit c70f849

2 files changed

Lines changed: 44 additions & 28 deletions

File tree

src/commands/config.ts

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Command } from 'commander';
22
import { configManager } from '../config/manager';
33
import type { GlobalConfig } from '../config/schemas';
44
import { theme, symbols } from '../ui/theme';
5-
import { intro, outro, text, password, confirm, select } from '../ui/prompts';
5+
import { intro, outro, text, password, select, multiselect } from '../ui/prompts';
66
import { requireTrackedRepo } from '../utils/detect';
77

88
async function runConfigWizard(existing: GlobalConfig | undefined): Promise<GlobalConfig> {
@@ -63,13 +63,24 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise<Glob
6363
initialValue: existing?.autoUpdateTicketStatus ?? 'ask',
6464
});
6565

66-
const enableJira = await confirm({
67-
message: 'Enable Jira integration?',
68-
initialValue: existing?.integrations.jira?.enabled ?? false,
66+
type Integration = 'jira' | 'notion' | 'slack';
67+
const currentIntegrations: Integration[] = [];
68+
if (existing?.integrations.jira?.enabled) currentIntegrations.push('jira');
69+
if (existing?.integrations.notion?.enabled) currentIntegrations.push('notion');
70+
if (existing?.integrations.slack?.enabled) currentIntegrations.push('slack');
71+
72+
const enabledIntegrations = await multiselect<Integration>({
73+
message: 'Integrations to enable (space to toggle)',
74+
options: [
75+
{ value: 'jira', label: 'Jira', hint: 'tickets provider' },
76+
{ value: 'notion', label: 'Notion', hint: 'tickets provider' },
77+
{ value: 'slack', label: 'Slack', hint: 'messaging / standup' },
78+
],
79+
initialValues: currentIntegrations,
6980
});
7081

7182
let jiraConfig: GlobalConfig['integrations']['jira'] = undefined;
72-
if (enableJira) {
83+
if (enabledIntegrations.includes('jira')) {
7384
const baseUrl = await text({
7485
message: 'Jira base URL (e.g. https://yourorg.atlassian.net)',
7586
initialValue: existing?.integrations.jira?.baseUrl,
@@ -91,13 +102,21 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise<Glob
91102
jiraConfig = { enabled: true, baseUrl, userEmail, apiToken };
92103
}
93104

94-
const enableSlack = await confirm({
95-
message: 'Enable Slack integration?',
96-
initialValue: existing?.integrations.slack?.enabled ?? false,
97-
});
105+
let notionConfig: GlobalConfig['integrations']['notion'] = undefined;
106+
if (enabledIntegrations.includes('notion')) {
107+
const existingNotionToken = existing?.integrations.notion?.apiToken;
108+
const notionApiTokenRaw = await password({
109+
message: existingNotionToken
110+
? 'Notion integration token (secret_...) — leave blank to keep existing'
111+
: 'Notion integration token (secret_...)',
112+
validate: (v) => (!v?.trim() && !existingNotionToken ? 'Required' : undefined),
113+
});
114+
const apiToken = notionApiTokenRaw.trim() || existingNotionToken!;
115+
notionConfig = { enabled: true, apiToken };
116+
}
98117

99118
let slackConfig: GlobalConfig['integrations']['slack'] = undefined;
100-
if (enableSlack) {
119+
if (enabledIntegrations.includes('slack')) {
101120
const existingSlackToken = existing?.integrations.slack?.apiToken;
102121
const slackApiTokenRaw = await password({
103122
message: existingSlackToken
@@ -116,24 +135,6 @@ async function runConfigWizard(existing: GlobalConfig | undefined): Promise<Glob
116135
slackConfig = { enabled: true, apiToken, standupChannel: standupChannel.trim() || undefined };
117136
}
118137

119-
const enableNotion = await confirm({
120-
message: 'Enable Notion integration?',
121-
initialValue: existing?.integrations.notion?.enabled ?? false,
122-
});
123-
124-
let notionConfig: GlobalConfig['integrations']['notion'] = undefined;
125-
if (enableNotion) {
126-
const existingNotionToken = existing?.integrations.notion?.apiToken;
127-
const notionApiTokenRaw = await password({
128-
message: existingNotionToken
129-
? 'Notion integration token (secret_...) — leave blank to keep existing'
130-
: 'Notion integration token (secret_...)',
131-
validate: (v) => (!v?.trim() && !existingNotionToken ? 'Required' : undefined),
132-
});
133-
const apiToken = notionApiTokenRaw.trim() || existingNotionToken!;
134-
notionConfig = { enabled: true, apiToken };
135-
}
136-
137138
return {
138139
version: 1,
139140
githubUsername,

src/ui/prompts.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ export async function select<T extends string>(opts: {
6262
return result as T;
6363
}
6464

65+
export async function multiselect<T extends string>(opts: {
66+
message: string;
67+
options: { value: T; label: string; hint?: string }[];
68+
initialValues?: T[];
69+
required?: boolean;
70+
}): Promise<T[]> {
71+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
72+
const result = await clack.multiselect({ required: false, ...opts } as any);
73+
if (clack.isCancel(result)) {
74+
clack.cancel('Operation cancelled.');
75+
process.exit(0);
76+
}
77+
return result as T[];
78+
}
79+
6580
export async function editor(opts: { message: string; initialValue?: string }): Promise<string> {
6681
const tmpFile = join(tmpdir(), `morg-${Date.now()}.md`);
6782
writeFileSync(tmpFile, opts.initialValue ?? '', 'utf-8');

0 commit comments

Comments
 (0)