提交 99970f4d 编写于 作者: P Pine Wu

Validation for array-of-string settings. Fix #77459

上级 4454769a
......@@ -369,6 +369,10 @@
margin-top: -1px;
z-index: 1;
}
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-item-contents.invalid-input .setting-item-validation-message {
position: static;
margin-top: 1rem;
}
.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-text .setting-item-validation-message {
width: 500px;
......
......@@ -224,8 +224,9 @@ interface ISettingComplexItemTemplate extends ISettingItemTemplate<void> {
button: Button;
}
interface ISettingListItemTemplate extends ISettingItemTemplate<void> {
interface ISettingListItemTemplate extends ISettingItemTemplate<string[] | undefined> {
listWidget: ListSettingWidget;
validationErrorMessageElement: HTMLElement;
}
interface ISettingExcludeItemTemplate extends ISettingItemTemplate<void> {
......@@ -679,6 +680,9 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
renderTemplate(container: HTMLElement): ISettingListItemTemplate {
const common = this.renderCommonTemplate(null, container, 'list');
const descriptionElement = common.containerElement.querySelector('.setting-item-description')!;
const validationErrorMessageElement = $('.setting-item-validation-message');
descriptionElement.after(validationErrorMessageElement);
const listWidget = this._instantiationService.createInstance(ListSettingWidget, common.controlElement);
listWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
......@@ -686,19 +690,40 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
const template: ISettingListItemTemplate = {
...common,
listWidget
listWidget,
validationErrorMessageElement
};
this.addSettingElementFocusHandler(template);
common.toDispose.push(listWidget.onDidChangeList(e => this.onDidChangeList(template, e)));
common.toDispose.push(
listWidget.onDidChangeList(e => {
const newList = this.computeNewList(template, e);
this.onDidChangeList(template, newList);
if (newList !== null && template.onChange) {
template.onChange(newList);
}
})
);
return template;
}
private onDidChangeList(template: ISettingListItemTemplate, e: IListChangeEvent): void {
private onDidChangeList(template: ISettingListItemTemplate, newList: string[] | undefined | null): void {
if (!template.context || newList === null) {
return;
}
this._onDidChangeSetting.fire({
key: template.context.setting.key,
value: newList,
type: template.context.valueType
});
}
private computeNewList(template: ISettingListItemTemplate, e: IListChangeEvent): string[] | undefined | null {
if (template.context) {
let newValue: any[] = [];
let newValue: string[] = [];
if (isArray(template.context.scopeValue)) {
newValue = [...template.context.scopeValue];
} else if (isArray(template.context.value)) {
......@@ -732,29 +757,30 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
template.context.defaultValue.length === newValue.length &&
template.context.defaultValue.join() === newValue.join()
) {
return this._onDidChangeSetting.fire({
key: template.context.setting.key,
value: undefined, // reset setting
type: template.context.valueType
});
return undefined;
}
this._onDidChangeSetting.fire({
key: template.context.setting.key,
value: newValue,
type: template.context.valueType
});
return newValue;
}
return undefined;
}
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingListItemTemplate): void {
super.renderSettingElement(element, index, templateData);
}
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string) => void): void {
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string[] | undefined) => void): void {
const value = getListDisplayValue(dataElement);
template.listWidget.setValue(value);
template.context = dataElement;
template.onChange = (v) => {
onChange(v);
renderArrayValidations(dataElement, template, v, false);
};
renderArrayValidations(dataElement, template, value.map(v => v.value), true);
}
}
......@@ -1237,6 +1263,29 @@ function renderValidations(dataElement: SettingsTreeSettingElement, template: IS
DOM.removeClass(template.containerElement, 'invalid-input');
}
function renderArrayValidations(
dataElement: SettingsTreeSettingElement,
template: ISettingListItemTemplate,
value: string[] | undefined,
calledOnStartup: boolean
) {
DOM.addClass(template.containerElement, 'invalid-input');
if (dataElement.setting.validator) {
const errMsg = dataElement.setting.validator(value);
if (errMsg && errMsg !== '') {
DOM.addClass(template.containerElement, 'invalid-input');
template.validationErrorMessageElement.innerText = errMsg;
const validationError = localize('validationError', "Validation Error.");
template.containerElement.setAttribute('aria-label', [dataElement.setting.key, validationError, errMsg].join(' '));
if (!calledOnStartup) { ariaAlert(validationError + ' ' + errMsg); }
return;
} else {
template.containerElement.setAttribute('aria-label', dataElement.setting.key);
DOM.removeClass(template.containerElement, 'invalid-input');
}
}
}
function cleanRenderedMarkdown(element: Node): void {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes.item(i);
......
......@@ -1028,6 +1028,67 @@ class SettingsContentBuilder {
}
export function createValidator(prop: IConfigurationPropertySchema): (value: any) => (string | null) {
// Only for array of string
if (prop.type === 'array' && prop.items && !isArray(prop.items) && prop.items.type === 'string') {
const propItems = prop.items;
if (propItems && !isArray(propItems) && propItems.type === 'string') {
const withQuotes = (s: string) => `'` + s + `'`;
return value => {
if (!value) {
return null;
}
let message = '';
const stringArrayValue = value as string[];
if (prop.minItems && stringArrayValue.length < prop.minItems) {
message += nls.localize('validations.stringArrayMinItem', 'Array must have at least {0} items', prop.minItems);
message += '\n';
}
if (prop.maxItems && stringArrayValue.length > prop.maxItems) {
message += nls.localize('validations.stringArrayMaxItem', 'Array must have less than {0} items', prop.maxItems);
message += '\n';
}
if (typeof propItems.pattern === 'string') {
const patternRegex = new RegExp(propItems.pattern);
stringArrayValue.forEach(v => {
if (!patternRegex.test(v)) {
message +=
propItems.patternErrorMessage ||
nls.localize(
'validations.stringArrayItemPattern',
'Value {0} must match regex {1}.',
withQuotes(v),
withQuotes(propItems.pattern!)
);
}
});
}
const propItemsEnum = propItems.enum;
if (propItemsEnum) {
stringArrayValue.forEach(v => {
if (propItemsEnum.indexOf(v) === -1) {
message += nls.localize(
'validations.stringArrayItemEnum',
'Value {0} is not one of {1}',
withQuotes(v),
'[' + propItemsEnum.map(withQuotes).join(', ') + ']'
);
message += '\n';
}
});
}
return message;
};
}
}
return value => {
let exclusiveMax: number | undefined;
let exclusiveMin: number | undefined;
......
......@@ -250,4 +250,82 @@ suite('Preferences Model test', () => {
withMessage.rejects(' ').withMessage('always error!');
withMessage.rejects('1').withMessage('always error!');
});
});
\ No newline at end of file
class ArrayTester {
private validator: (value: any) => string | null;
constructor(private settings: IConfigurationPropertySchema) {
this.validator = createValidator(settings)!;
}
public accepts(input: string[]) {
assert.equal(this.validator(input), '', `Expected ${JSON.stringify(this.settings)} to accept \`${JSON.stringify(input)}\`. Got ${this.validator(input)}.`);
}
public rejects(input: any[]) {
assert.notEqual(this.validator(input), '', `Expected ${JSON.stringify(this.settings)} to reject \`${JSON.stringify(input)}\`.`);
return {
withMessage:
(message: string) => {
const actual = this.validator(input);
assert.ok(actual);
assert(actual!.indexOf(message) > -1,
`Expected error of ${JSON.stringify(this.settings)} on \`${input}\` to contain ${message}. Got ${this.validator(input)}.`);
}
};
}
}
test('simple array', () => {
{
const arr = new ArrayTester({ type: 'array', items: { type: 'string' } });
arr.accepts([]);
arr.accepts(['foo']);
arr.accepts(['foo', 'bar']);
}
});
test('min-max items array', () => {
{
const arr = new ArrayTester({ type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 2 });
arr.rejects([]).withMessage('Array must have at least 1 items');
arr.accepts(['a']);
arr.accepts(['a', 'a']);
arr.rejects(['a', 'a', 'a']).withMessage('Array must have less than 2 items');
}
});
test('array of enums', () => {
{
const arr = new ArrayTester({ type: 'array', items: { type: 'string', enum: ['a', 'b'] } });
arr.accepts(['a']);
arr.accepts(['a', 'b']);
arr.rejects(['c']).withMessage(`Value 'c' is not one of`);
arr.rejects(['a', 'c']).withMessage(`Value 'c' is not one of`);
arr.rejects(['c', 'd']).withMessage(`Value 'c' is not one of`);
arr.rejects(['c', 'd']).withMessage(`Value 'd' is not one of`);
}
});
test('min-max and enum', () => {
const arr = new ArrayTester({ type: 'array', items: { type: 'string', enum: ['a', 'b'] }, minItems: 1, maxItems: 2 });
arr.rejects(['a', 'b', 'c']).withMessage('Array must have less than 2 items');
arr.rejects(['a', 'b', 'c']).withMessage(`Value 'c' is not one of`);
});
test('pattern', () => {
const arr = new ArrayTester({ type: 'array', items: { type: 'string', pattern: '^(hello)*$' } });
arr.accepts(['hello']);
arr.rejects(['a']).withMessage(`Value 'a' must match regex`);
});
test('pattern with error message', () => {
const arr = new ArrayTester({ type: 'array', items: { type: 'string', pattern: '^(hello)*$', patternErrorMessage: 'err: must be friendly' } });
arr.rejects(['a']).withMessage(`err: must be friendly`);
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册