From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 870F31FF146 for ; Tue, 23 Jun 2026 16:36:11 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 20172482D; Tue, 23 Jun 2026 16:35:13 +0200 (CEST) From: "Max R. Carrara" To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-widget-toolkit 09/13] utils: introduce helper function getFieldDefFromPropertySchema Date: Tue, 23 Jun 2026 16:33:26 +0200 Message-ID: <20260623143402.772452-10-m.carrara@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260623143402.772452-1-m.carrara@proxmox.com> References: <20260623143402.772452-1-m.carrara@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1782225249994 X-SPAM-LEVEL: Spam detection results: 0 AWL -1.221 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy ENA_SUBJ_ODD_CASE 2.6 Subject has odd case KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 2UDPYVOGTQNW2AJTCC5EU7FJRYQ4CAY5 X-Message-ID-Hash: 2UDPYVOGTQNW2AJTCC5EU7FJRYQ4CAY5 X-MailFrom: m.carrara@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This helper takes the JSON schema of a single property and transforms it into an object representing an Ext JS field. Currently, four types for properties are supported: `string`, `integer`, `number` and `boolean`. Many JSON schema validation keywords are also supported and converted to Ext JS analogues. Types --> Ext JS field types: - boolean --> proxmoxcheckbox - integer --> proxmoxintegerfield - number --> numberfield - string --> proxmoxtextfield Keywords --> Ext JS properties / behavior: - minLength --> minLength - maxLength --> maxLength - minimum --> minValue - maximum --> maxValue - title --> fieldLabel (using the field's name if no title) - optional --> allowBlank - description --> shown on hover - enum --> use proxmoxKVComboBox as field instead - default --> set as `value` of the field and use as `emptyText` Furthermore, add support for the `multiline` JSON schema `format`. This means that any property with the `multiline` format becomes a `proxmoxtextarea` field. Additionally, the helper also takes "extra attributes" and a "context" which allows for more fine-grained control over how the ExtJS field is actually constructed and also makes it possible to express different UI-specific behaviors that cannot be modeled via JSONSchema. The only variable in the context is `isCreate`, which is used to hint whether the field is used when creating a record or not. The following extra attributes are added: - sensitive Marks a field as "sensitive", which is useful for password fields for example. This causes most fields to become `proxmoxtextfield`s instead, with their inputs obfuscated. Fields with the `multiline` format are kept as `proxmoxtextarea` to allow copy-pasting private keys into the field or similar. - readonly Causes a field to become a `displayfield` instead. Boolean fields are rendered as "Yes / No" (locale-aware). Signed-off-by: Max R. Carrara --- src/Utils.js | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/Utils.js b/src/Utils.js index 14a90bf..f41080f 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -1600,6 +1600,156 @@ Ext.define('Proxmox.Utils', { } return degrees; }, + + /** + * Takes a JSONSchema of a single property and transforms it into an + * object representing an ExtJS field. + * + * @param {string} fieldName - The value for the `name` property of the resulting object. + * @param {Object} propSchema - The JSON schema of a property. + * @param {Object} extraAttributes - Additional attributes describing the + * field which do not exist as keywords in JSON schema. + * Note that none of these attributes represent any specific ExtJS property. + * @param {Boolean} extraAttributes.readonly - Whether the field shall be read-only. + * "Read-only" here means that the field's `xtype` will be set to + * `'displayfield'`, regardless of what data type the field will + * display. Additionally, checkboxes (booleans) will be displayed as + * "Yes" or "No" depending on their value (locale-aware). + * @param {Boolean} extraAttributes.sensitive - Whether the field + * contains sensitive data, such as a password or similar. + * @param {Object} context - The context within which the field is being used. + * @param {Boolean} context.isCreate - Whether the field is used when creating a record. + */ + getFieldDefFromPropertySchema: function (fieldName, propSchema, extraAttributes, context) { + const fieldPropsByType = { + string: { + xtype: 'proxmoxtextfield', + minLength: propSchema.minLength, + maxLength: propSchema.maxLength, + }, + integer: { + xtype: 'proxmoxintegerfield', + minValue: propSchema.minimum, + maxValue: propSchema.maximum, + }, + number: { + xtype: 'numberfield', + minValue: propSchema.minimum, + maxValue: propSchema.maximum, + }, + boolean: { + xtype: 'proxmoxcheckbox', + uncheckedValue: 0, + }, + }; + + const commonFieldProps = { + name: fieldName, + fieldLabel: Ext.htmlEncode(fieldName), + emptyText: propSchema.default || '', + allowBlank: propSchema.optional, + }; + + const fieldPropsByFormat = { + multiline: { + xtype: 'proxmoxtextarea', + }, + }; + + const anyToBool = (value) => { + if (typeof value === 'string' || value instanceof String) { + value = value.trim().toLowerCase(); + return ['y', 'yes', 'true'].includes(value); + } + + return Boolean(value); + }; + + let fieldProps = fieldPropsByType[propSchema.type]; + if (fieldProps === undefined) { + console.warn(`Unhandled property type '${propSchema.type}'`); + fieldProps = fieldPropsByType.string; + } + + let formatFieldProps = fieldPropsByFormat[propSchema.format] ?? {}; + + let field = { + ...fieldProps, + ...commonFieldProps, + ...formatFieldProps, + }; + + if (propSchema.title) { + field.fieldLabel = Ext.htmlEncode(propSchema.title); + } + + if (propSchema.description) { + field.autoEl = { + tag: 'div', + 'data-qtip': Ext.htmlEncode(Ext.htmlEncode(propSchema.description)), + }; + } + + if (propSchema.enum) { + const comboBoxProps = { + xtype: 'proxmoxKVComboBox', + comboItems: propSchema.enum.map((item) => [item, Ext.htmlEncode(item)]), + deleteEmpty: !context.isCreate, + editable: true, + }; + + Object.assign(field, comboBoxProps); + } + + if (context.isCreate && propSchema.default !== undefined) { + if (propSchema.type === 'boolean') { + field.checked = anyToBool(propSchema.default); + } + + if (field.xtype === 'proxmoxKVComboBox') { + let defaultText = + Proxmox.Utils.defaultText + ' (' + Ext.htmlEncode(propSchema.default) + ')'; + + field.comboItems.unshift(['__default__', defaultText]); + field.value = '__default__'; + } else { + field.value = propSchema.default; + } + } + + if (extraAttributes.sensitive) { + // textarea fields for multiline strings stay as-is, because + // Ext JS will turn them into elements otherwise, which + // in turn causes newlines to be replaced with spaces. + // 'inputType' unfortunately has no effect for textarea fields. + if (field.xtype !== 'proxmoxtextarea') { + field.xtype = 'proxmoxtextfield'; + field.inputType = 'password'; + } + + if (context.isCreate) { + field.value = ''; + field.emptyText = Proxmox.Utils.NoneText; + } else { + field.emptyText = gettext('Unchanged'); + } + } + + if (extraAttributes.readonly) { + field.xtype = 'displayfield'; + + if (propSchema.type === 'boolean') { + field.renderer = Proxmox.Utils.format_boolean; + } + + if (extraAttributes.sensitive) { + field.value = '*********'; + field.emptyText = ''; + } + } + + return field; + }, }, singleton: true, -- 2.47.3