public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Max R. Carrara" <m.carrara@proxmox.com>
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	[thread overview]
Message-ID: <20260623143402.772452-10-m.carrara@proxmox.com> (raw)
In-Reply-To: <20260623143402.772452-1-m.carrara@proxmox.com>

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 <m.carrara@proxmox.com>
---
 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 <input> 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





  parent reply	other threads:[~2026-06-23 14:36 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-23 14:33 [PATCH common/manager/proxmox-widget-toolkit/storage 00/13] GUI Support for Custom Storage Plugins Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-common 01/13] json schema: add multiline string format Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-storage 02/13] api: plugins/storage: add initial routes and endpoints Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-storage 03/13] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-storage 04/13] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-storage 05/13] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-storage 06/13] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-storage 07/13] all plugins: add 'title' to properties, adapt 'description's Max R. Carrara
2026-06-23 14:33 ` [PATCH proxmox-widget-toolkit 08/13] form: introduce new 'proxmoxtextarea' field Max R. Carrara
2026-06-23 14:33 ` Max R. Carrara [this message]
2026-06-23 14:33 ` [PATCH proxmox-widget-toolkit 10/13] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-manager 11/13] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-manager 12/13] ui: storage view: display error when no editor for storage type exists Max R. Carrara
2026-06-23 14:33 ` [PATCH pve-manager 13/13] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260623143402.772452-10-m.carrara@proxmox.com \
    --to=m.carrara@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal