all lists on 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal