Skip to content

Commit 2a689ff

Browse files
clydinKeen Yee Liau
authored and
Keen Yee Liau
committed
feat(@angular-devkit/core): auto discover multiselect schema prompt types
If a prompt is present on a schema property and the type is an array with a set of enum values, then the prompt type is a list with multiselect capabilites. This eliminates the need to specify the longhand form for typical multiselect prompts.
1 parent a8e19c2 commit 2a689ff

File tree

4 files changed

+119
-28
lines changed

4 files changed

+119
-28
lines changed

etc/api/angular_devkit/core/src/_golden-api.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ export interface PromptDefinition {
798798
multiselect?: boolean;
799799
raw?: string | JsonObject;
800800
type: string;
801-
validator?: (value: string) => boolean | string | Promise<boolean | string>;
801+
validator?: (value: JsonValue) => boolean | string | Promise<boolean | string>;
802802
}
803803

804804
export declare type PromptProvider = (definitions: Array<PromptDefinition>) => SubscribableOrPromise<{

packages/angular_devkit/core/src/json/schema/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export interface PromptDefinition {
100100
type: string;
101101
message: string;
102102
default?: string | string[] | number | boolean | null;
103-
validator?: (value: string) => boolean | string | Promise<boolean | string>;
103+
validator?: (value: JsonValue) => boolean | string | Promise<boolean | string>;
104104

105105
items?: Array<string | { value: JsonValue, label: string }>;
106106

packages/angular_devkit/core/src/json/schema/prompt_spec.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ describe('Prompt Provider', () => {
262262
.toPromise().then(done, done.fail);
263263
});
264264

265-
it('analyzes list with multiselect option and object items', done => {
265+
it('analyzes list with true multiselect option and object items', done => {
266266
const registry = new CoreSchemaRegistry();
267267
const data: any = {};
268268

@@ -301,14 +301,53 @@ describe('Prompt Provider', () => {
301301
.toPromise().then(done, done.fail);
302302
});
303303

304+
it('analyzes list with false multiselect option and object items', done => {
305+
const registry = new CoreSchemaRegistry();
306+
const data: any = {};
307+
308+
registry.usePromptProvider(async definitions => {
309+
expect(definitions.length).toBe(1);
310+
expect(definitions[0].type).toBe('list');
311+
expect(definitions[0].multiselect).toBe(false);
312+
expect(definitions[0].items).toEqual([
313+
{ 'value': 'one', 'label': 'one' },
314+
{ 'value': 'two', 'label': 'two' },
315+
]);
316+
317+
return { [definitions[0].id]: { 'value': 'one', 'label': 'one' } };
318+
});
319+
320+
registry
321+
.compile({
322+
properties: {
323+
test: {
324+
type: 'array',
325+
'x-prompt': {
326+
'type': 'list',
327+
'multiselect': false,
328+
'items': [
329+
{ 'value': 'one', 'label': 'one' },
330+
{ 'value': 'two', 'label': 'two' },
331+
],
332+
'message': 'test-message',
333+
},
334+
},
335+
},
336+
})
337+
.pipe(
338+
mergeMap(validator => validator(data)),
339+
)
340+
.toPromise().then(done, done.fail);
341+
});
342+
304343
it('analyzes list without multiselect option and object items', done => {
305344
const registry = new CoreSchemaRegistry();
306345
const data: any = {};
307346

308347
registry.usePromptProvider(async definitions => {
309348
expect(definitions.length).toBe(1);
310349
expect(definitions[0].type).toBe('list');
311-
expect(definitions[0].multiselect).toBeUndefined();
350+
expect(definitions[0].multiselect).toBe(true);
312351
expect(definitions[0].items).toEqual([
313352
{ 'value': 'one', 'label': 'one' },
314353
{ 'value': 'two', 'label': 'two' },
@@ -346,6 +385,7 @@ describe('Prompt Provider', () => {
346385
registry.usePromptProvider(async definitions => {
347386
expect(definitions.length).toBe(1);
348387
expect(definitions[0].type).toBe('list');
388+
expect(definitions[0].multiselect).toBeFalsy();
349389
expect(definitions[0].items).toEqual([
350390
'one',
351391
'two',
@@ -377,6 +417,45 @@ describe('Prompt Provider', () => {
377417
.toPromise().then(done, done.fail);
378418
});
379419

420+
it('analyzes enums WITHOUT explicit list type and multiselect', done => {
421+
const registry = new CoreSchemaRegistry();
422+
const data: any = {};
423+
424+
registry.usePromptProvider(async definitions => {
425+
expect(definitions.length).toBe(1);
426+
expect(definitions[0].type).toBe('list');
427+
expect(definitions[0].multiselect).toBe(true);
428+
expect(definitions[0].items).toEqual([
429+
'one',
430+
'two',
431+
'three',
432+
]);
433+
434+
return {};
435+
});
436+
437+
registry
438+
.compile({
439+
properties: {
440+
test: {
441+
type: 'array',
442+
items: {
443+
enum: [
444+
'one',
445+
'two',
446+
'three',
447+
],
448+
},
449+
'x-prompt': 'test-message',
450+
},
451+
},
452+
})
453+
.pipe(
454+
mergeMap(validator => validator(data)),
455+
)
456+
.toPromise().then(done, done.fail);
457+
});
458+
380459
it('analyzes boolean properties', done => {
381460
const registry = new CoreSchemaRegistry();
382461
const data: any = {};

packages/angular_devkit/core/src/json/schema/registry.ts

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
SmartDefaultProvider,
2929
} from './interface';
3030
import { JsonSchema } from './schema';
31+
import { getTypesOfSchema } from './utility';
3132
import { visitJson, visitJsonSchema } from './visitor';
3233

3334
// This interface should be exported from ajv, but they only export the class and not the type.
@@ -539,26 +540,44 @@ export class CoreSchemaRegistry implements SchemaRegistry {
539540
items = schema.items;
540541
}
541542

543+
const propertyTypes = getTypesOfSchema(parentSchema);
542544
if (!type) {
543-
if (parentSchema.type === 'boolean') {
545+
if (propertyTypes.size === 1 && propertyTypes.has('boolean')) {
544546
type = 'confirmation';
545547
} else if (Array.isArray(parentSchema.enum)) {
546548
type = 'list';
549+
} else if (
550+
propertyTypes.size === 1 &&
551+
propertyTypes.has('array') &&
552+
parentSchema.items &&
553+
Array.isArray((parentSchema.items as JsonObject).enum)
554+
) {
555+
type = 'list';
547556
} else {
548557
type = 'input';
549558
}
550559
}
551560

552-
if (type === 'list' && !items && Array.isArray(parentSchema.enum)) {
553-
type = 'list';
554-
items = [];
555-
for (const value of parentSchema.enum) {
556-
if (typeof value == 'string') {
557-
items.push(value);
558-
} else if (typeof value == 'object') {
559-
// Invalid
560-
} else {
561-
items.push({ label: value.toString(), value });
561+
let multiselect;
562+
if (type === 'list') {
563+
multiselect =
564+
schema.multiselect === undefined
565+
? propertyTypes.size === 1 && propertyTypes.has('array')
566+
: schema.multiselect;
567+
568+
const enumValues = multiselect
569+
? parentSchema.items && (parentSchema.items as JsonObject).enum
570+
: parentSchema.enum;
571+
if (!items && Array.isArray(enumValues)) {
572+
items = [];
573+
for (const value of enumValues) {
574+
if (typeof value == 'string') {
575+
items.push(value);
576+
} else if (typeof value == 'object') {
577+
// Invalid
578+
} else {
579+
items.push({ label: value.toString(), value });
580+
}
562581
}
563582
}
564583
}
@@ -569,20 +588,13 @@ export class CoreSchemaRegistry implements SchemaRegistry {
569588
message,
570589
raw: schema,
571590
items,
572-
multiselect: type === 'list' ? schema.multiselect : false,
591+
multiselect,
573592
default: typeof parentSchema.default == 'object' ? undefined : parentSchema.default,
574-
async validator(data: string) {
575-
const result = it.self.validate(parentSchema, data);
576-
if (typeof result === 'boolean') {
577-
return result;
578-
} else {
579-
try {
580-
await result;
581-
582-
return true;
583-
} catch {
584-
return false;
585-
}
593+
async validator(data: JsonValue) {
594+
try {
595+
return await it.self.validate(parentSchema, data);
596+
} catch {
597+
return false;
586598
}
587599
},
588600
};

0 commit comments

Comments
 (0)