ui: add form for editing tags

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 'pveTag' 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>
This commit is contained in:
Dominik Csapak 2022-11-16 16:48:11 +01:00 committed by Thomas Lamprecht
parent b6f9fc78c0
commit 1b42bb8657
3 changed files with 345 additions and 4 deletions

View File

@ -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;
}

View File

@ -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 \

View File

@ -0,0 +1,325 @@
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() === 'pveTag') {
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: 'pveTag',
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);
}
me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
me.loadTags(me.oldTags, true); // refresh tag colors
});
},
},
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: 'pveTag',
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();
},
});