diff --git a/app/core/models/courseModel.js b/app/core/models/courseModel.js index f353d1fc..972119ee 100644 --- a/app/core/models/courseModel.js +++ b/app/core/models/courseModel.js @@ -16,7 +16,9 @@ define(function(require) { }, isEditable: function () { - return this.get('_isShared') || this.get('createdBy') == Origin.sessionModel.get('id'); + var access = this.get('_access') || {}; + var meId = Origin.sessionModel.get('id'); + return access.public || (access.users || []).includes(meId) || this.get('createdBy') == meId; } }); diff --git a/app/modules/projects/views/projectView.js b/app/modules/projects/views/projectView.js index 5504da38..ee40c449 100644 --- a/app/modules/projects/views/projectView.js +++ b/app/modules/projects/views/projectView.js @@ -79,7 +79,8 @@ define(function(require) { deleteProjectPrompt: function(event) { event && event.preventDefault(); - var isShared = this.model.get('_isShared') || (this.model.get('_shareWithUsers') && this.model.get('_shareWithUsers').length > 0); + var access = this.model.get('_access') || {}; + var isShared = access.public || (access.users && access.users.length > 0); Origin.Notify.confirm({ type: 'warning', title: Origin.l10n.t(`${isShared ? 'app.deletesharedproject' : 'app.deleteproject'}`), diff --git a/app/modules/scaffold/index.js b/app/modules/scaffold/index.js index 82627ef3..25c449e0 100644 --- a/app/modules/scaffold/index.js +++ b/app/modules/scaffold/index.js @@ -2,6 +2,7 @@ define([ 'core/origin', 'core/helpers', './backboneFormsOverrides', + './views/scaffoldAccessView', './views/scaffoldAssetItemView', './views/scaffoldAssetView', './views/scaffoldCodeEditorView', @@ -12,7 +13,7 @@ define([ './views/scaffoldListView', './views/scaffoldTagsView', './views/scaffoldUsersView' -], function(Origin, Helpers, Overrides, ScaffoldAssetItemView, ScaffoldAssetView, ScaffoldCodeEditorView, ScaffoldColourPickerView, ScaffoldDisplayTitleView, ScaffoldFileView, ScaffoldItemsModalView, ScaffoldListView, ScaffoldTagsView, ScaffoldUsersView) { +], function(Origin, Helpers, Overrides, ScaffoldAccessView, ScaffoldAssetItemView, ScaffoldAssetView, ScaffoldCodeEditorView, ScaffoldColourPickerView, ScaffoldDisplayTitleView, ScaffoldFileView, ScaffoldItemsModalView, ScaffoldListView, ScaffoldTagsView, ScaffoldUsersView) { var Scaffold = {}; var alternativeModel; @@ -45,6 +46,15 @@ define([ if (editor) { return editor; } + var adaptType = item._adapt && item._adapt.inputType; + adaptType = typeof adaptType === 'string' ? adaptType : adaptType && adaptType.type; + + if (adaptType) { + if (Backbone.Form.editors[adaptType]) { + return adaptType; + } + console.warn('Scaffold: no editor registered for _adapt.inputType "' + adaptType + '", falling back to the primitive type editor'); + } switch (item.type) { case 'array': return 'List'; @@ -138,10 +148,12 @@ define([ '_hasPreview', '_id', '_isSelected', + '_isShared', '_latestTrackingId', '_layout', '_menu', '_parentId', + '_shareWithUsers', '_sortOrder', '_supportedLayout', '_theme', diff --git a/app/modules/scaffold/lang/en.json b/app/modules/scaffold/lang/en.json index 80bbc943..d28d892b 100644 --- a/app/modules/scaffold/lang/en.json +++ b/app/modules/scaffold/lang/en.json @@ -4,6 +4,11 @@ "properties": "Properties", "settings": "Settings", "extensions": "Extensions", - "colourPickerCancel": "Cancel selection" + "colourPickerCancel": "Cancel selection", + "access": { + "onlyme": "Only me", + "specific": "Specific people", + "anyone": "Anyone" + } } } diff --git a/app/modules/scaffold/less/access.less b/app/modules/scaffold/less/access.less new file mode 100644 index 00000000..25d9a28b --- /dev/null +++ b/app/modules/scaffold/less/access.less @@ -0,0 +1,22 @@ +.field[data-type="Access"] { + .scaffold-access-tiers { + display: flex; + flex-direction: column; + gap: 5px; + } + .scaffold-access-option { + display: flex; + align-items: center; + cursor: pointer; + + input { margin-right: 8px; } + } + .scaffold-access-people { + margin-top: 10px; + + .option { + .name { font-weight: @font-weight-bold; } + .email { margin-left: 5px; } + } + } +} diff --git a/app/modules/scaffold/views/scaffoldAccessView.js b/app/modules/scaffold/views/scaffoldAccessView.js new file mode 100644 index 00000000..755de85b --- /dev/null +++ b/app/modules/scaffold/views/scaffoldAccessView.js @@ -0,0 +1,155 @@ +define([ 'core/origin' ], function(Origin) { + var TIERS = [ 'onlyme', 'specific', 'anyone' ]; + + var ScaffoldAccessView = Backbone.Form.editors.Base.extend({ + tagName: 'div', + className: 'scaffold-access', + events: { + 'change .scaffold-access-tier': function(event) { + this.setTier(event.target.value); + this.trigger('change', this); + }, + 'focus': function() { this.trigger('focus', this); }, + 'blur': function() { this.trigger('blur', this); } + }, + + render: function() { + this.deriveFromValue(this.value); + this.$el.html(this.template()); + this.$people = this.$el.find('.scaffold-access-users'); + this.fetchUsers(this.initSelectize); + this.togglePeople(); + return this; + }, + + template: function() { + var tier = this.tier; + var radios = TIERS.map(function(name) { + return ''; + }, this).join(''); + return '
' + radios + '
' + + '
' + + '' + + '
'; + }, + + renderItem: function(item, escape) { + return Handlebars.templates.scaffoldUsersOption({ + name: item.firstName && item.lastName ? escape(item.firstName + ' ' + item.lastName) : false, + email: escape(item.email), + disabled: item.disabled + }); + }, + + fetchUsers: function(callback) { + $.get('api/users') + .done(callback.bind(this)) + .fail(error => Origin.Notify.alert({ type: 'error', text: error })); + }, + + initSelectize: function(users) { + this.$people.val(this.users.join(',')); + + var meId = Origin.sessionModel.get('id'); + + users.forEach(function(user) { + if(user._id === meId) user.disabled = true; + }); + + this.$people.selectize({ + labelField: 'email', + valueField: '_id', + options: users, + searchField: [ 'email', 'firstName', 'lastName' ], + render: { + item: this.renderItem, + option: this.renderItem + }, + onChange: () => this.trigger('change', this), + onItemRemove: function(value) { + if(value !== Origin.sessionModel.get('id')) { + return; + } + Origin.Notify.alert({ + type: 'warning', + text: Origin.l10n.t('app.stopsharingwithyourself') + }); + this.addItem(value, true); + this.close(); + } + }); + }, + + deriveFromValue: function(value) { + var access = value || {}; + this.originalAccess = _.extend({}, access); + + var isPublic = access.public; + if(isPublic === undefined) isPublic = this.model.get('_isShared') || false; + + var users = access.users; + if(users === undefined) users = this.model.get('_shareWithUsers'); + this.users = (users || []).slice(); + + this.tier = isPublic ? 'anyone' : (this.users.length ? 'specific' : 'onlyme'); + }, + + setTier: function(tier) { + this.tier = tier; + this.togglePeople(); + }, + + togglePeople: function() { + this.$el.find('.scaffold-access-people').toggle(this.tier === 'specific'); + }, + + selectedUsers: function() { + var selectize = this.$people && this.$people[0] && this.$people[0].selectize; + if(selectize) return selectize.getValue(); + return this.users; + }, + + getValue: function() { + var access = _.omit(this.originalAccess || {}, 'public', 'users'); + switch(this.tier) { + case 'anyone': + access.public = true; + break; + case 'specific': + access.public = false; + access.users = this.selectedUsers(); + break; + default: + access.public = false; + } + return access; + }, + + setValue: function(value) { + this.deriveFromValue(value); + if(!this.$people) return; + this.$el.find('.scaffold-access-tier[value="' + this.tier + '"]').prop('checked', true); + var selectize = this.$people[0] && this.$people[0].selectize; + if(selectize) selectize.setValue(this.users, true); + this.togglePeople(); + }, + + focus: function() { + if(!this.hasFocus) this.$el.find('input').first().focus(); + }, + + blur: function() { + if(this.hasFocus) this.$el.find('input').first().blur(); + } + }); + + Origin.on('origin:dataReady', function() { + Origin.scaffold.addCustomField('Access', ScaffoldAccessView); + }); + + return ScaffoldAccessView; +});