From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH manager v9 09/12] ui: add form/TagEdit.js
Date: Mon, 14 Nov 2022 10:44:01 +0100 [thread overview]
Message-ID: <20221114094404.1241050-18-d.csapak@proxmox.com> (raw)
In-Reply-To: <20221114094404.1241050-1-d.csapak@proxmox.com>
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 and css classes so that it properly adds tags
also handles the drag/drop feature for the tags in the list
when done with editing (by clicking the checkmark), sends a 'change'
event with the new tags
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v8:
* added missing css classed
www/css/ext6-pve.css | 23 ++-
www/manager6/Makefile | 1 +
www/manager6/form/TagEdit.js | 321 +++++++++++++++++++++++++++++++++++
3 files changed, 341 insertions(+), 4 deletions(-)
create mode 100644 www/manager6/form/TagEdit.js
diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index daaffa6ec..4fc83a878 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -657,7 +657,8 @@ table.osds td:first-of-type {
padding-bottom: 0px;
}
-.pve-edit-tag > i {
+.pve-edit-tag > i,
+.pve-add-tag > i {
cursor: pointer;
font-size: 14px;
}
@@ -667,7 +668,8 @@ table.osds td:first-of-type {
cursor: grab;
}
-.pve-edit-tag > i.action {
+.pve-edit-tag > i.action,
+.pve-add-tag > i.action {
padding-left: 5px;
}
@@ -676,7 +678,9 @@ table.osds td:first-of-type {
}
.pve-edit-tag.editable span,
-.pve-edit-tag.inEdit span {
+.pve-edit-tag.inEdit span,
+.pve-add-tag.editable span,
+.pve-add-tag.inEdit span {
background-color: #ffffff;
border: 1px solid #a8a8a8;
color: #000;
@@ -685,6 +689,17 @@ table.osds td:first-of-type {
min-width: 2em;
}
-.pve-edit-tag.inEdit span {
+.pve-edit-tag.inEdit span,
+.pve-add-tag.inEdit span {
border: 1px solid #000;
}
+
+.pve-add-tag {
+ background-color: #d5d5d5 ! important;
+ color: #000000 ! important;
+}
+
+.pve-tag-inline-button {
+ cursor: pointer;
+ padding-left: 2px;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 7bcc35e8e..396abffcc 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -76,6 +76,7 @@ JSSRC= \
form/TagColorGrid.js \
form/ListField.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 000000000..ac184a917
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,321 @@
+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: '<i class="fa fa-long-arrow-up"></i>',
+ 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);
+
+ // get a current tag list for editing
+ if (editMode) {
+ PVE.Utils.updateUIOptions();
+ }
+
+ 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 `<i data-qtip="${qtip}" class="fa fa-${cls}"></i>`;
+ },
+ },
+ },
+
+ 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: `<span>${gettext('Add Tag')}</span><i class="action fa fa-plus-square"></i>`,
+ 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: `<i data-qtip="${gettext('Cancel')}" class="fa fa-times"></i>`,
+ 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
next prev parent reply other threads:[~2022-11-14 9:44 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2022-11-14 9:43 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH cluster v9 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
2022-11-14 13:15 ` Wolfgang Bumiller
2022-11-14 9:43 ` [pve-devel] [PATCH cluster v9 2/4] Cluster: add get_guest_config_properties Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH cluster v9 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH cluster v9 4/4] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
2022-11-14 13:32 ` Wolfgang Bumiller
2022-11-14 9:43 ` [pve-devel] [PATCH qemu-server v9 1/1] api: update: improve tag privilege check Dominik Csapak
2022-11-14 13:37 ` Wolfgang Bumiller
2022-11-15 8:34 ` Aaron Lauterer
2022-11-14 9:43 ` [pve-devel] [PATCH container v9 1/1] check_ct_modify_config_perm: " Dominik Csapak
2022-11-14 13:37 ` Wolfgang Bumiller
2022-11-14 9:43 ` [pve-devel] [PATCH widget-toolkit v9 1/2] add tag related helpers Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH widget-toolkit v9 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 02/12] api: add /ui-options api call Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 04/12] ui: parse and save tag infos from /ui-options Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 05/12] ui: add form/TagColorGrid Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 06/12] ui: add PVE.form.ListField Dominik Csapak
2022-11-14 9:43 ` [pve-devel] [PATCH manager v9 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
2022-11-14 9:44 ` [pve-devel] [PATCH manager v9 08/12] ui: add form/Tag Dominik Csapak
2022-11-14 9:44 ` Dominik Csapak [this message]
2022-11-14 9:44 ` [pve-devel] [PATCH manager v9 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
2022-11-14 9:44 ` [pve-devel] [PATCH manager v9 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-11-14 9:44 ` [pve-devel] [PATCH manager v9 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
2022-11-14 17:20 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v9] add tags to ui Aaron Lauterer
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=20221114094404.1241050-18-d.csapak@proxmox.com \
--to=d.csapak@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