Created
November 6, 2015 06:35
-
-
Save gregwiechec/4cddf6b3952776bbefec to your computer and use it in GitHub Desktop.
Show additional properties on content creation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define("epi-cms/contentediting/viewmodel/CreateContentViewModel", [ | |
// dojo | |
"dojo/_base/array", | |
"dojo/_base/declare", | |
"dojo/_base/lang", | |
"dojo/_base/json", | |
"dojo/Stateful", | |
"dojo/string", | |
"dojo/when", | |
"dojo/Evented", | |
// epi | |
"epi/dependency", | |
"epi/shell/TypeDescriptorManager", | |
"epi-cms/core/ContentReference", | |
"epi-cms/contentediting/ContentEditingValidator", | |
// resources | |
"epi/i18n!epi/shell/ui/nls/episerver.shared.messages", | |
"epi/i18n!epi/cms/nls/episerver.cms.components.requiredproperties" | |
], | |
function ( | |
// dojo | |
array, | |
declare, | |
lang, | |
json, | |
Stateful, | |
string, | |
when, | |
Evented, | |
// epi | |
dependency, | |
TypeDescriptorManager, | |
ContentReference, | |
ContentEditingValidator, | |
// resources | |
sharedMessages, | |
reqPropsResources | |
) { | |
return declare([Stateful, Evented], { | |
// summary: | |
// View model of epi-cms/contentediting/CreateContent component. | |
// tags: | |
// internal | |
// ======================================================================= | |
// Private stuffs | |
// ======================================================================= | |
// _topLevelContainerType: String | |
// The default top level container type used in properties form. | |
_topLevelContainerType: "epi/shell/layout/SimpleContainer", | |
// _groupContainerType: String | |
// The default group container type used in properties form. | |
_groupContainerType: "epi-cms/layout/CreateContentGroupContainer", | |
// ======================================================================= | |
// Dependencies | |
// ======================================================================= | |
// contentDataStore: epi/shell/RestStore | |
// The content data store instance. | |
contentDataStore: null, | |
// metadataManager: epi/shell/MetadataManager | |
// The metadata manager instance. | |
metadataManager: null, | |
// typeIdentifierManager: epi/shell/TypeIdentifierManager | |
// The type identifier manager instance. | |
typeIdentifierManager: null, | |
// validator: epi-cms/contentediting/ContentEditingValidator | |
// The content editing validator instance. | |
validator: null, | |
// ======================================================================= | |
// Data model properties | |
// ======================================================================= | |
// parent: Content | |
// The parent content on which the new content is created. | |
parent: null, | |
// contentTypeId: Number | |
// The content type id of which the new content is created. | |
contentTypeId: null, | |
// requestedType: String | |
// The type indentifier of created content. This is used for filtering available content types. | |
requestedType: null, | |
// createAsLocalAsset: Boolean | |
// Indicates that the content is created as local asset which means it will be attached to the parent content's asset folder. | |
createAsLocalAsset: null, | |
// autoPublish: Boolean | |
// Indicates if the content should be published automatically when created if the user has publish rights. | |
autoPublish: false, | |
// addToDestination: Delegate | |
// A delagate object which contains a save method. The method will be executed after the content is successfully created. | |
addToDestination: null, | |
// ======================================================================= | |
// View model properties | |
// ======================================================================= | |
// wizardStep: Number | |
// Keeps track the current step of the creation wizard. 0 means selecting content type while 1 means collecting properties value. | |
wizardStep: 0, | |
// startWizardStep: [protected] Number | |
// Which step to start with | |
startWizardStep: 0, | |
// contentName: String | |
// Name of the content being created. This is initialized from the resource bundle and bound to the name text box in the widget. | |
contentName: null, | |
// ignoreDefaultNameWarning: Boolean | |
// Indicates that name checking should be ignored. | |
ignoreDefaultNameWarning: null, | |
// properties: Collection | |
// The properties collection used to create new content. | |
properties: null, | |
// headingText: String | |
// Heading text which is displayed on the top of the toolbar. This is set according to the current request type. | |
headingText: null, | |
// contentNameHelpText: String | |
// Help text for the content name input | |
contentNameHelpText: "", | |
// createAsLocalAssetHelpText: Boolean | |
// Helper text that displayed in the sub header | |
createAsLocalAssetHelpText: null, | |
// namePanelIsVisible: Boolean | |
// Indicate that the name panel is visible. If the content is created as local asset, name panel should not be visible | |
namePanelIsVisible: null, | |
// headingPanelIsVisible: Boolean | |
// Indicates that the heading panel is visible. Similar to the name panel, heading panel is not visible when the content is created as local asset. | |
// In that case, a more detail heading is displayed inside the content type selection list or properties form. | |
headingPanelIsVisible: null, | |
// seamlessTopPanel: Boolean | |
// Indicates that the top panel should show seamlessly. | |
seamlessTopPanel: null, | |
// saveButtonIsVisible: Boolean | |
// Indicate that the save button should be visible, normally in the last step. | |
saveButtonIsVisible: null, | |
// saveButtonDisabled: Boolean | |
// Indicate that the save button should be disabled, when saving is on going. | |
saveButtonDisabled: null, | |
// showAllProperties: Boolean | |
// Indicates that it should show all properties for user to enter initial value, normally when content is created from content area. | |
showAllProperties: null, | |
// showCurrentNodeOnBreadcrumb: Boolean | |
// Indicates that the breadcrumb should show current content node. | |
showCurrentNodeOnBreadcrumb: null, | |
// actualParentLink: String | |
// Link of the parent beneath which the content is created. It could be the given parent or the given parent's local asset folder. | |
actualParentLink: null, | |
// metadata: Object | |
// The metadata object of the content being created. | |
metadata: null, | |
// ======================================================================= | |
// Public methods | |
// ======================================================================= | |
postscript: function () { | |
// summary: | |
// Initializes internal objects and state after constructed. | |
// description: | |
// Obtains content data store and metadata manager instances from dependency manager. | |
// tags: | |
// protected | |
this.inherited(arguments); | |
if (!this.contentDataStore) { | |
var registry = dependency.resolve("epi.storeregistry"); | |
this.contentDataStore = registry.get("epi.cms.contentdata"); | |
} | |
this.metadataManager = this.metadataManager || dependency.resolve("epi.shell.MetadataManager"); | |
// Could the dependency handle non-singleton objects? | |
this.validator = this.validator || new ContentEditingValidator({ contextTypeName: "epi.cms.contentdata" }); | |
this.set("propertyFilter", lang.hitch(this, this._propertyFilter)); | |
}, | |
update: function (settings) { | |
// summary: | |
// Update the component with new settings. | |
// description: | |
// Taking settings from the ones who request the Create content component. | |
// The supported settings are: allowedTypes, restrictedTypes, createAsLocalAsset, addToDestination, contentTypeId. They will be mixed to the current instance. | |
// settings: Object | |
// The settings object. | |
// tags: | |
// protected | |
// Reset view properties | |
this.set("ignoreDefaultNameWarning", false); | |
this.set("properties", null); | |
this.set("saveButtonIsVisible", false); | |
this.set("wizardStep", this.startWizardStep); | |
this.set("isContentTypeSelected", false); | |
this.set("namePanelIsVisible", true); | |
this.set("headingPanelIsVisible", true); | |
this.set("showCurrentNodeOnBreadcrumb", true); | |
this.set("seamlessTopPanel", true); | |
// Copy data properties from topic sender | |
if (settings) { | |
array.forEach(["allowedTypes", "restrictedTypes", "requestedType", "parent", "createAsLocalAsset", "autoPublish", "addToDestination", "contentTypeId"], function (property) { | |
this.set(property, settings[property]); | |
}, this); | |
} | |
// Setup the validator | |
if (this.parent) { | |
var notificationContextId = this.parent.contentLink + "_new_content"; // A pseudo context id for the content that not yet created. | |
this.validator.clearErrorsBySource(); | |
this.validator.setContextId(notificationContextId); | |
this.set("notificationContext", { contextTypeName: "epi.cms.contentdata", contextId: notificationContextId }); | |
} | |
}, | |
save: function () { | |
// summary: | |
// Save the content and finish the wizard. | |
// description: | |
// save() will validate the content name if ignoreDefaultNameWarning is not set. By then "invalidContentName" might be emitted. | |
// If data validation is fine, the new content object will be put on the content data store. | |
// On success, "saveSuccess" event is emitted, otherwise "saveError" is emitted. | |
// tags: | |
// public | |
this.set("saveButtonDisabled", true); | |
// Validate content name | |
if (!this.ignoreDefaultNameWarning && (!this.contentName || this.contentName === "" || this.contentName === this.defaultName)) { | |
var contentName = this.contentName; | |
if (this.metadata && this.metadata.additionalValues && this.metadata.additionalValues.modelTypeIdentifier) { | |
contentName = TypeDescriptorManager.getResourceValue(this.metadata.additionalValues.modelTypeIdentifier, "newitemdefaultname"); | |
} | |
this._emitSaveEvent("invalidContentName", contentName); | |
return; | |
} | |
// Ask the validator to do properties validation and/or other global validation | |
this.validator.clearErrorsBySource(this.validator.validationSource.server); | |
when(this.validator.validate(), lang.hitch(this, function (hasErrors) { | |
if (hasErrors) { | |
this._emitSaveEvent("validationError"); | |
return; | |
} | |
// Build the content object | |
var content = this.buildContentObject(); | |
// Put the new content into content data store | |
this.contentDataStore.put(content).then(lang.hitch(this, this._saveSuccessHandler), lang.hitch(this, this._saveErrorHandler)); | |
}), lang.hitch(this, function () { | |
this._emitSaveEvent("validationError"); | |
})); | |
}, | |
_emitSaveEvent: function (eventName, params) { | |
this.set("saveButtonDisabled", false); | |
this.emit(eventName, params); | |
}, | |
cancel: function () { | |
// summary: | |
// Cancel operation and finish the wizard | |
// tags: | |
// Public | |
this.addToDestination && (typeof this.addToDestination.cancel === "function") && this.addToDestination.cancel(); | |
}, | |
_propertyFilter: function (ownerMetadata, propertyMetadata) { | |
// summary: | |
// Filter out meta properties which are not supposed to have value on creation. | |
// tags: | |
// private | |
var isNotInSpecialGroup = propertyMetadata.originalGroupName !== "EPiServerCMS_SettingsPanel" && propertyMetadata.originalGroupName !== "Advanced"; | |
var required = this._isRequiredProperty(ownerMetadata, propertyMetadata); | |
if (this.showAllProperties) { | |
// show properties which do not belong to a special groups at all OR belong to special groups but required | |
return isNotInSpecialGroup || required; | |
} else { | |
// ======================================================================= | |
// | |
// Original file changes | |
// | |
// ======================================================================= | |
if (!required) { | |
var displayOnContentCreate = this._hasDisplayOnContentCreate(propertyMetadata); | |
return displayOnContentCreate; | |
} | |
// ======================================================================= | |
// only show properties from "required" group | |
return required; | |
} | |
}, | |
// ======================================================================= | |
// | |
// Original file changes | |
// | |
// ======================================================================= | |
_hasDisplayOnContentCreate: function (property) { | |
return property.settings && property.settings.displayOnContentCreate; | |
}, | |
// ======================================================================= | |
_saveSuccessHandler: function (contentLink) { | |
// summary: | |
// Save success handler | |
// contentLink: String | |
// The newly created content's link | |
// tags: | |
// private | |
var ref = new ContentReference(contentLink), | |
versionAgnosticRef = ref.createVersionUnspecificReference(), | |
changeToNewContext = lang.hitch(this, function (/*String*/targetLink) { | |
this._emitSaveEvent("saveSuccess", { | |
newContentLink: targetLink, | |
changeContext: true | |
}); | |
}); | |
when(this.contentDataStore.refresh(contentLink), lang.hitch(this, function (newContent) { | |
if (this.addToDestination) { | |
this.addToDestination.save({ | |
contentLink: versionAgnosticRef.toString(), | |
name: newContent.name, | |
typeIdentifier: this.requestedType | |
}); | |
// Keep the current context | |
this._emitSaveEvent("saveSuccess", { | |
changeContext: false | |
}); | |
} else { | |
// Change to new context | |
changeToNewContext(versionAgnosticRef.toString()); | |
} | |
})); | |
}, | |
_saveErrorHandler: function (err) { | |
// summary: | |
// Save error handler | |
// err: Object | |
// The error object. | |
// tags: | |
// private | |
// err is actually the xhr error, as for now the rest store doesn't pre handle errors. | |
if (err && err.responseText) { | |
// received a list of problems | |
// NOTE: Do not copy this code since it's not a good practice to handle error. We are finding a pattern to handle server errors in a nicer manner, perhaps inside the rest store. | |
var validationErrors = json.fromJson(err.responseText); | |
array.forEach(validationErrors, function (item) { | |
if (item.propertyName) { | |
this.validator.setPropertyErrors(item.propertyName, [{ | |
severity: item.severity, | |
errorMessage: item.errorMessage | |
}], this.validator.validationSource.server); | |
} else { | |
this.validator.setGlobalErrors([{ | |
severity: item.severity, | |
errorMessage: item.errorMessage | |
}], this.validator.validationSource.server); | |
} | |
}, this); | |
} else if (err) { | |
// general error | |
this.validator.setGlobalErrors([{ | |
severity: this.validator.severity.error, | |
errorMessage: err.message | |
}], this.validator.validationSource.server); | |
} | |
this._emitSaveEvent("saveError", err); | |
}, | |
buildContentObject: function () { | |
// summary: | |
// Build up the content object to create from model properties. | |
// tags: | |
// protected | |
return { | |
name: string.trim(this.contentName + ""), | |
parentLink: this._getParentLink(), | |
contentTypeId: this.contentTypeId, | |
properties: this.properties, | |
createAsLocalAsset: this.createAsLocalAsset, | |
autoPublish: this.autoPublish | |
}; | |
}, | |
addInvalidProperty: function (propertyName, errorMessage) { | |
// summary: | |
// State that a property has become invalid after client validation. | |
// propertyName: String | |
// The property name. | |
// errorMessage: String | |
// The error message. | |
// tags: | |
// public | |
this.validator.setPropertyErrors(propertyName, [{ | |
severity: this.validator.severity.error, | |
errorMessage: errorMessage | |
}], this.validator.validationSource.client); | |
}, | |
removeInvalidProperty: function (propertyName) { | |
// summary: | |
// State that a property is no more invalid after client validation. | |
// propertyName: String | |
// The property name. | |
// tags: | |
// public | |
this.validator.clearPropertyErrors(propertyName); | |
}, | |
// ======================================================================= | |
// Property setters | |
// ======================================================================= | |
_contentTypeIdSetter: function (contentTypeId) { | |
// summary: | |
// Set content type id. | |
// description: | |
// After the value is set, metadata is also updated. If it is neccessary to enter properties, the wizard is stepped forward, otherwise save is executed. | |
// contentTypeId: Number | |
// The value. | |
// tags: | |
// private | |
this.contentTypeId = contentTypeId; | |
if (contentTypeId && this.parent) { | |
this.set("isContentTypeSelected", true); | |
this.validator.clearErrorsBySource(this.validator.validationSource.server); | |
when(this._getMetadata(this.parent.contentLink, contentTypeId), lang.hitch(this, function (metadata) { | |
metadata = this._regroupProperties(metadata); | |
this.set("metadata", metadata); | |
if (this.autoPublish || this._hasRequiredProperties(metadata)) { | |
this.set("wizardStep", 1); | |
this.set("saveButtonIsVisible", true); | |
} else { | |
this.save(); | |
} | |
}), lang.hitch(this, function () { | |
this.set("isContentTypeSelected", false); | |
this.validator.setGlobalErrors([{ | |
severity: this.validator.severity.error, | |
errorMessage: sharedMessages.unexpectederror | |
}], this.validator.validationSource.server); | |
})); | |
} | |
}, | |
_createAsLocalAssetSetter: function (createAsLocalAsset) { | |
// summary: | |
// Set createAsLocalAsset option. | |
// createAsLocalAsset: Boolean | |
// The value. | |
// tags: | |
// private | |
this.createAsLocalAsset = createAsLocalAsset; | |
if (this.parent) { | |
this.set("actualParentLink", this._getParentLink()); | |
} | |
}, | |
_autoPublishSetter: function (autoPublish) { | |
// summary: | |
// Sets autoPublish option. | |
// autoPublish: Boolean | |
// The value. | |
// tags: | |
// private | |
this.set("showAllProperties", autoPublish); | |
this.autoPublish = autoPublish; | |
}, | |
_parentSetter: function (parent) { | |
this.parent = parent; | |
var typeIdentifier = this.parent.typeIdentifier, | |
requestedTypeName = TypeDescriptorManager.getResourceValue(this.requestedType, "name"), | |
parentTypeName = TypeDescriptorManager.getResourceValue(typeIdentifier, "name"), | |
createAsLocalAssetHelpText = TypeDescriptorManager.getResourceValue(typeIdentifier, "createaslocalassethelptext"); | |
if (requestedTypeName && parentTypeName) { | |
createAsLocalAssetHelpText = lang.replace(createAsLocalAssetHelpText, [requestedTypeName.toLowerCase(), parentTypeName.toLowerCase()]); | |
} | |
this.set("createAsLocalAssetHelpText", createAsLocalAssetHelpText); | |
}, | |
_requestedTypeSetter: function (requestedType) { | |
// summary: | |
// Set requested type. | |
// description: | |
// After the value is set, heading text and content name are also updated. | |
// requestedType: String | |
// The value. | |
// tags: | |
// private | |
var headingText = TypeDescriptorManager.getResourceValue(requestedType, "create"), | |
defaultName = TypeDescriptorManager.getResourceValue(requestedType, "newitemdefaultname"); | |
this.defaultName = defaultName; | |
this.set("headingText", headingText); | |
this.set("contentName", defaultName); | |
this.requestedType = requestedType; | |
}, | |
// ======================================================================= | |
// Private methods | |
// ======================================================================= | |
_getParentLink: function() { | |
// summary: | |
// Gets the link to the parent where the content should be created under. | |
// If the parent is a Content Asset folder the link to the owner content will be returned. | |
// tags: | |
// private | |
if(!this.parent) { | |
return null; | |
} | |
if(this.createAsLocalAsset) { | |
return this.parent.ownerContentLink ? this.parent.ownerContentLink : this.parent.contentLink; | |
} | |
return this.parent.contentLink; | |
}, | |
_hasRequiredProperties: function (metadata) { | |
// summary: | |
// Checks if the metadata object contains any required property. | |
// metadata: Object | |
// The metadata object. | |
// tags: | |
// protected internal | |
return array.some(metadata.properties, function (property) { | |
// ======================================================================= | |
// | |
// Original file changes | |
// | |
// ======================================================================= | |
return this._isRequiredProperty(metadata, property) || this._hasDisplayOnContentCreate(property); | |
// ======================================================================= | |
}, this); | |
}, | |
_isRequired: function (property) { | |
// summary: | |
// Checks if the given property is required. | |
// property: Object | |
// The metadata property. | |
// tags: | |
// protected, extension | |
return property.settings && property.settings.required; | |
}, | |
_isRequiredProperty: function (metadata, property) { | |
// summary: | |
// Checks if the given property is required. | |
// description: | |
// A property is considered to be required if is required or it contains required sub properties. | |
// It should also have no value set, is visible, and is not the icontent_name property which is entered outside the properties form. | |
// metadata: Object | |
// The metadata object. | |
// property: Object | |
// The metadata property. | |
// tags: | |
// private | |
var isNotSet = !property.additionalValues.hasValue; | |
var isShownForEdit = property.showForEdit && array.every(metadata.groups, function (group) { | |
// Every group must be either not the property's original group or have displayui true | |
return (group.name !== property.groupName && group.name !== property.originalGroupName) || group.displayUI; | |
}); | |
var hasRequiredChildProperties = array.some(property.properties, function (childProperty) { | |
return this._isRequiredProperty(property, childProperty); | |
}, this); | |
return (this._isRequired(property) || hasRequiredChildProperties) && isNotSet && isShownForEdit && (property.name !== "icontent_name"); | |
}, | |
_regroupProperties: function (metadata) { | |
// summary: | |
// Regroup properties into required and additional property groups. | |
// metadata: Object | |
// The metadata object. | |
// tags: | |
// private | |
metadata = lang.clone(metadata); | |
metadata.layoutType = this._topLevelContainerType; | |
array.forEach(metadata.properties, function (prop) { | |
var isMandatory = this._isRequiredProperty(metadata, prop); | |
prop.originalGroupName = prop.groupName; | |
prop.groupName = isMandatory ? "required" : "additional"; | |
}, this); | |
metadata.groups = [ | |
{ | |
name: "required", | |
displayUI: true, | |
title: reqPropsResources.groups.required, | |
uiType: this._groupContainerType | |
}, | |
{ | |
name: "additional", | |
displayUI: true, | |
title: reqPropsResources.groups.additional, | |
uiType: this._groupContainerType | |
} | |
]; | |
return metadata; | |
}, | |
_getMetadata: function (parentLink, contentTypeId) { | |
// summary: | |
// Get the metadata for the newly created content. | |
// parentLink: String | |
// The parent content link. | |
// contentTypeId: Number | |
// The content type id. | |
// tags: | |
// private | |
return this.metadataManager.getMetadataForType('EPiServer.Core.ContentData', { | |
parentLink: parentLink, | |
contentTypeId: contentTypeId | |
}); | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using EPiServer.Core; | |
using EPiServer.Framework; | |
using EPiServer.Framework.Initialization; | |
using EPiServer.Shell.ObjectEditing; | |
namespace DisplayOnContentCreate.Business | |
{ | |
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] | |
public class DisplayOnContentCreateAttribute : Attribute | |
{ | |
} | |
public class ContentCreateMetadataExtender : IMetadataExtender | |
{ | |
public void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes) | |
{ | |
foreach (ExtendedMetadata property in metadata.Properties) | |
{ | |
property.EditorConfiguration["displayOnContentCreate"] = | |
property.Attributes.OfType<DisplayOnContentCreateAttribute>().Any(); | |
} | |
} | |
} | |
[InitializableModule] | |
[ModuleDependency(typeof (EPiServer.Cms.Shell.InitializableModule))] | |
public class SiteMetadataExtenderInitialization : IInitializableModule | |
{ | |
public void Initialize(InitializationEngine context) | |
{ | |
if (context.HostType == HostType.WebApplication) | |
{ | |
var registry = context.Locate.Advanced.GetInstance<MetadataHandlerRegistry>(); | |
registry.RegisterMetadataHandler(typeof (ContentData), new ContentCreateMetadataExtender()); | |
} | |
} | |
public void Preload(string[] parameters) | |
{ | |
} | |
public void Uninitialize(InitializationEngine context) | |
{ | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<module> | |
<clientResources> | |
<!-- Inject our custom Display Option selector --> | |
<add name="epi-cms.widgets.base" | |
path="~/modules/createContent/CreateContentViewModel.js" | |
resourceType="Script" | |
/> | |
</clientResources> | |
</module> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment