Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,10 @@ fn._createJSTree = function () {
var args = Array.prototype.slice.call(arguments),
node = this.parent.redraw_node.apply(this.inst, args);
obj = this.inst.get_node(obj);
// add any extra icons if present
if (node && obj.data?.extraIcons) {
$(node).find('a > i').first().after(Object.values(obj.data.extraIcons));
}
// decorate node with error indicator if present
if (node && obj.data && obj.data.errors) {
$(node).find('a > i').first().after(obj.data.errors);
Expand Down Expand Up @@ -1529,6 +1533,7 @@ fn._populateTree = function (selectedHashtag) {
if (!changed && mug.hasErrors()) {
_this.setTreeValidationIcon(mug);
}
_this.setTreeExtraIcons(mug);
_this.setTreeActions(mug);
}
});
Expand Down Expand Up @@ -1961,6 +1966,8 @@ fn.setTreeValidationIcon = function (mug) {
}
};

fn.setTreeExtraIcons = function (mug) {};

fn._resetMessages = function (errors) {
var error, messages_div = this.$f.find('.fd-messages');

Expand Down
144 changes: 127 additions & 17 deletions src/lock.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ const LOCKED_CHILDREN_MSG_KEY = "mug-has-locked-children";
const SELECT_CLASSES = ["Select", "MSelect", "Choice"];

$.vellum.plugin("lock", {}, {
init: function () {
const data = this.data.lock;
data.locks = {};
},
loadXML: function () {
this.__callOld();

// we don't know whether there are any locks until XML is loaded
// tools menu items are already defined at this point, so target what we want and remove them here
if (_.some(this.data.lock.locks) && !this.opts().features.edit_locked_questions) {
const $menu = this.$f.find('.fd-tools-menu').parent();
$menu.find('a:contains("' + gettext("Edit Source XML") + '")').parent().remove();
$menu.find('a:contains("' + gettext("Edit Bulk Translations") + '")').parent().remove();
}

},
parseBindElement: function (form, el, path) {
this.__callOld();
const locked = el.xmlAttr(LOCKED_BIND_ATTR);
Expand All @@ -38,13 +54,30 @@ $.vellum.plugin("lock", {}, {
},
handleMugParseFinish: function (mug) {
this.__callOld();
this._setLockedFromParent(mug);
setControlOnlyChildrenLocked(mug);
},
setTreeActions: function (mug) {
if (mug.p.locked) {
mug.options.canAddChoices = false;
if (!mug.options.hasOwnProperty('_originalCanAddChoices')) {
mug.options._originalCanAddChoices = mug.options.canAddChoices;
}
mug.options.canAddChoices = mug.p.locked ? false : mug.options._originalCanAddChoices;
this.__callOld();
},
setTreeExtraIcons: function (mug) {
this.__callOld();
const node = mug.ufid && this.jstree('get_node', mug.ufid);
if (!node) { return; }

let icon = null;
if (mug.p.locked) {
icon = treeLockIcon(mug);
}

if (node.data.extraIcons?.lock !== icon) {
node.data.extraIcons = node.data.extraIcons || {};
node.data.extraIcons.lock = icon;
this.jstree('redraw_node', node);
}
},
getMainProperties: function () {
const properties = this.__callOld();
Expand All @@ -65,16 +98,40 @@ $.vellum.plugin("lock", {}, {
help: gettext("A locked question cannot be edited, moved, or deleted from the form."),
helpURL: "https://www.example.com", // placeholder for public documentation
serialize: () => {},
deserialize: () => {},
deserialize: (data, key, mug, context) => {
if (mug.p.rawBindAttributes && mug.p.rawBindAttributes[LOCKED_BIND_ATTR]) {
delete mug.p.rawBindAttributes[LOCKED_BIND_ATTR];
}
},
setter: function (mug, attr, value) {
if (value === true) {
mug.p.rawBindAttributes = mug.p.rawBindAttributes || {};
mug.p.rawBindAttributes[LOCKED_BIND_ATTR] = 'all';
_this.data.lock.locks[mug.ufid] = 'all';
} else {
delete mug.p.rawBindAttributes[LOCKED_BIND_ATTR];
delete _this.data.lock.locks[mug.ufid];
}
mug.p.set(attr, value);
mug.form.getChildren(mug).forEach(child => _this._setLockedFromParent(child));
_this.setTreeExtraIcons(mug);

if (_.contains(["Select", "MSelect"], mug.__className)) {
setControlOnlyChildrenLocked(mug);
}

if (!mug.form.isLoadingXForm) {
if (mug.__className === "Group") {
cascadeLockToChildren(mug, value);
} else if (_.contains(["Select", "MSelect"], mug.__className)) {
_this.setTreeActions(mug);
}
if (_this.getCurrentlySelectedMug() === mug) {
_this.displayMugProperties(mug);
}
if (mug.parentMug) {
updateParentTreeIcons(mug.parentMug);
}
}
},
};
return spec;
Expand Down Expand Up @@ -102,16 +159,6 @@ $.vellum.plugin("lock", {}, {
}
return this.__callOld();
},
_hasLockedChildren: function (mug) {
return mug.form.getChildren(mug).some(child =>
child.p.locked || this._hasLockedChildren(child)
);
},
_setLockedFromParent: function (mug) {
if (mug.parentMug && mug.options.isControlOnly) {
mug.p.set('locked', mug.parentMug.p.locked);
}
},
isPropertyLocked: function (mug, propertyPath) {
const locked = this.__callOld();
if (locked || mug.p.locked) {
Expand All @@ -132,12 +179,75 @@ $.vellum.plugin("lock", {}, {
return false;
},
isMugPathMoveable: function (mug) {
return this.__callOld() && !mug.p.locked && !this._hasLockedChildren(mug);
return this.__callOld() && !mug.p.locked && !hasLockedChildren(mug);
},
isMugRemoveable: function (mug) {
return this.__callOld() && !mug.p.locked && !this._hasLockedChildren(mug);
return this.__callOld() && !mug.p.locked && !hasLockedChildren(mug);
},
isMugTypeChangeable: function (mug) {
return this.__callOld() && !mug.p.locked;
},
});

function hasLockedChildren(mug) {
return mug.form.getChildren(mug).some(child =>
child.p.locked || hasLockedChildren(child)
);
}

function hasUnlockedChildren(mug) {
return mug.form.getChildren(mug).some(child =>
!child.p.locked || hasUnlockedChildren(child)
);
}

function setControlOnlyChildrenLocked(mug) {
mug.form.getChildren(mug).forEach(child => {
if (child.options.isControlOnly) {
child.p.set('locked', mug.p.locked);
}
});
}

function cascadeLockToChildren(mug, value) {
mug.form.getChildren(mug).forEach(child => {
if (child.p.locked !== value) {
child.p.locked = value;
} else if (child.__className === "Group") {
cascadeLockToChildren(child, value);
}
});
}

function updateParentTreeIcons(parent) {
while (parent) {
if (parent.p.locked && parent.__className === "Group") {
parent.form.vellum.setTreeExtraIcons(parent);
}
parent = parent.parentMug;
}
}

function treeLockIcon(mug) {
if (mug.options.isControlOnly) {
return;
}

let iconClass = "fa-lock";
let tooltipText = gettext("Only a user with permission can edit this question.");
if (mug.__className === "Group") {
if (hasUnlockedChildren(mug)) {
iconClass = "fa-unlock";
tooltipText = gettext(
"Only a user with permission can edit this group. " +
"Some questions are unlocked."
);
} else {
tooltipText = gettext(
"Only a user with permission can edit this group. " +
"All questions are locked."
);
}
}
return `<i class="fa ${iconClass}" title="${tooltipText}"></i>&nbsp;`;
}
Loading
Loading