From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 9FB3E89E2F for ; Tue, 18 Oct 2022 16:02:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8C79529738 for ; Tue, 18 Oct 2022 16:02:40 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 18 Oct 2022 16:02:34 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 0CEEE44A89 for ; Tue, 18 Oct 2022 16:02:31 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Tue, 18 Oct 2022 16:02:23 +0200 Message-Id: <20221018140226.598710-18-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221018140226.598710-1-d.csapak@proxmox.com> References: <20221018140226.598710-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.067 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [ddel.id] Subject: [pve-devel] [PATCH manager v8 09/12] ui: add form/TagEdit.js X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 18 Oct 2022 14:02:59 -0000 this is a wrapper container for holding a list of (editable) tags intended to be used in the lxc/qemu status toolbar to add a new tag, we reuse the 'pmxTag' class, but overwrite some of its behaviour so that it properly adds tags Signed-off-by: Dominik Csapak --- www/manager6/Makefile | 1 + www/manager6/form/TagEdit.js | 316 +++++++++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 www/manager6/form/TagEdit.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 9d610f71..eb4be4c5 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -75,6 +75,7 @@ JSSRC= \ form/iScsiProviderSelector.js \ form/TagColorGrid.js \ form/Tag.js \ + form/TagEdit.js \ grid/BackupView.js \ grid/FirewallAliases.js \ grid/FirewallOptions.js \ diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js new file mode 100644 index 00000000..1d832728 --- /dev/null +++ b/www/manager6/form/TagEdit.js @@ -0,0 +1,316 @@ +Ext.define('PVE.panel.TagEditContainer', { + extend: 'Ext.container.Container', + alias: 'widget.pveTagEditContainer', + + layout: { + type: 'hbox', + align: 'stretch', + }, + + controller: { + xclass: 'Ext.app.ViewController', + + loadTags: function(tagstring = '', force = false) { + let me = this; + let view = me.getView(); + + if (me.oldTags === tagstring && !force) { + return; + } + + view.suspendLayout = true; + me.forEachTag((tag) => { + view.remove(tag); + }); + me.getViewModel().set('tagCount', 0); + let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || []; + newtags.forEach((tag) => { + me.addTag(tag); + }); + view.suspendLayout = false; + view.updateLayout(); + if (!force) { + me.oldTags = tagstring; + } + }, + + onRender: function(v) { + let me = this; + let view = me.getView(); + view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), { + getDragData: function(e) { + let source = e.getTarget('.handle'); + if (!source) { + return undefined; + } + let sourceId = source.parentNode.id; + let cmp = Ext.getCmp(sourceId); + let ddel = document.createElement('div'); + ddel.classList.add('proxmox-tags-full'); + ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.Utils.tagOverrides); + let repairXY = Ext.fly(source).getXY(); + cmp.setDisabled(true); + ddel.id = Ext.id(); + return { + ddel, + repairXY, + sourceId, + }; + }, + onMouseUp: function(target, e, id) { + let cmp = Ext.getCmp(this.dragData.sourceId); + if (cmp && !cmp.isDestroyed) { + cmp.setDisabled(false); + } + }, + getRepairXY: function() { + return this.dragData.repairXY; + }, + beforeInvalidDrop: function(target, e, id) { + let cmp = Ext.getCmp(this.dragData.sourceId); + if (cmp && !cmp.isDestroyed) { + cmp.setDisabled(false); + } + }, + }); + view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), { + getTargetFromEvent: function(e) { + return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light'); + }, + getIndicator: function() { + if (!view.indicator) { + view.indicator = Ext.create('Ext.Component', { + floating: true, + html: '', + hidden: true, + shadow: false, + }); + } + return view.indicator; + }, + onContainerOver: function() { + this.getIndicator().setVisible(false); + }, + notifyOut: function() { + this.getIndicator().setVisible(false); + }, + onNodeOver: function(target, dd, e, data) { + let indicator = this.getIndicator(); + indicator.setVisible(true); + indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]); + return this.dropAllowed; + }, + onNodeDrop: function(target, dd, e, data) { + this.getIndicator().setVisible(false); + let sourceCmp = Ext.getCmp(data.sourceId); + if (!sourceCmp) { + return; + } + sourceCmp.setDisabled(false); + let targetCmp = Ext.getCmp(target.id); + view.remove(sourceCmp, { destroy: false }); + view.insert(view.items.indexOf(targetCmp), sourceCmp); + }, + }); + }, + + forEachTag: function(func) { + let me = this; + let view = me.getView(); + view.items.each((field) => { + if (field.reference === 'addTagBtn') { + return false; + } + if (field.getXType() === 'pmxTag') { + func(field); + } + return true; + }); + }, + + toggleEdit: function(cancel) { + let me = this; + let vm = me.getViewModel(); + let editMode = !vm.get('editMode'); + vm.set('editMode', editMode); + + me.forEachTag((tag) => { + tag.setMode(editMode ? 'editable' : 'normal'); + }); + + if (!vm.get('editMode')) { + let tags = []; + if (cancel) { + me.loadTags(me.oldTags, true); + } else { + me.forEachTag((cmp) => { + if (cmp.isVisible() && cmp.tag) { + tags.push(cmp.tag); + } + }); + tags = tags.join(','); + if (me.oldTags !== tags) { + me.oldTags = tags; + me.getView().fireEvent('change', tags); + } + } + } + me.getView().updateLayout(); + }, + + addTag: function(tag) { + let me = this; + let view = me.getView(); + let vm = me.getViewModel(); + let index = view.items.indexOf(me.lookup('addTagBtn')); + view.insert(index, { + xtype: 'pmxTag', + tag, + mode: vm.get('editMode') ? 'editable' : 'normal', + listeners: { + change: (field, newTag) => { + if (newTag === '') { + view.remove(field); + vm.set('tagCount', vm.get('tagCount') - 1); + } + }, + }, + }); + + vm.set('tagCount', vm.get('tagCount') + 1); + }, + + addTagClick: function(event) { + let me = this; + if (event.target.tagName === 'SPAN') { + me.lookup('addTagBtn').tagEl().innerHTML = ''; + me.lookup('addTagBtn').updateLayout(); + } + }, + + addTagMouseDown: function(event) { + let me = this; + if (event.target.tagName === 'I') { + let tag = me.lookup('addTagBtn').tagEl().innerHTML; + if (tag !== '') { + me.addTag(tag, true); + } + } + }, + + addTagChange: function(field, tag) { + let me = this; + if (tag !== '') { + me.addTag(tag, true); + } + field.tag = ''; + }, + + cancelClick: function() { + this.toggleEdit(true); + }, + + editClick: function() { + this.toggleEdit(false); + }, + + init: function(view) { + let me = this; + if (view.tags) { + me.loadTags(view.tags); + } + }, + }, + + viewModel: { + data: { + tagCount: 0, + editMode: false, + }, + + formulas: { + hideNoTags: function(get) { + return get('editMode') || get('tagCount') !== 0; + }, + editBtnHtml: function(get) { + let cls = get('editMode') ? 'check' : 'pencil'; + let qtip = get('editMode') ? gettext('Apply Changes') : gettext('Edit Tags'); + return ``; + }, + }, + }, + + loadTags: function() { + return this.getController().loadTags(...arguments); + }, + + items: [ + { + xtype: 'box', + bind: { + hidden: '{hideNoTags}', + }, + html: gettext('No Tags'), + }, + { + xtype: 'pmxTag', + reference: 'addTagBtn', + cls: 'pve-add-tag', + mode: 'editable', + tag: '', + tpl: `${gettext('Add Tag')}`, + bind: { + hidden: '{!editMode}', + }, + hidden: true, + onMouseDown: Ext.emptyFn, // prevent default behaviour + listeners: { + click: { + element: 'el', + fn: 'addTagClick', + }, + mousedown: { + element: 'el', + fn: 'addTagMouseDown', + }, + change: 'addTagChange', + }, + }, + { + xtype: 'box', + html: ``, + cls: 'pve-tag-inline-button', + hidden: true, + bind: { + hidden: '{!editMode}', + }, + listeners: { + click: 'cancelClick', + element: 'el', + }, + }, + { + xtype: 'box', + cls: 'pve-tag-inline-button', + bind: { + html: '{editBtnHtml}', + }, + listeners: { + click: 'editClick', + element: 'el', + }, + }, + ], + + listeners: { + render: 'onRender', + }, + + destroy: function() { + let me = this; + Ext.destroy(me.dragzone); + Ext.destroy(me.dropzone); + Ext.destroy(me.indicator); + me.callParent(); + }, +}); -- 2.30.2