Merge remote-tracking branch 'origin/v11/dev' into v12/dev

This commit is contained in:
Bjarke Berg
2023-04-25 15:43:22 +02:00
30 changed files with 641 additions and 537 deletions

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Configuration.Models;
public class RichTextEditorSettings public class RichTextEditorSettings
{ {
internal const string StaticValidElements = internal const string StaticValidElements =
"+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,video[*],audio[*],picture[*],source[*],canvas[*]"; "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*]";
internal const string StaticInvalidElements = "font"; internal const string StaticInvalidElements = "font";

View File

@@ -15,4 +15,43 @@
<key alias="translation">Traduceri</key> <key alias="translation">Traduceri</key>
<key alias="users">Utilizatori</key> <key alias="users">Utilizatori</key>
</area> </area>
<area alias="treeHeaders">
<key alias="content">Conţinut</key>
<key alias="contentBlueprints">Șabloane de conținut</key>
<key alias="media">Media</key>
<key alias="cacheBrowser">Cache Browser</key>
<key alias="contentRecycleBin">Cos de gunoi</key>
<key alias="createdPackages">Pachete create</key>
<key alias="dataTypes">Tipuri de date</key>
<key alias="dictionary">Dicţionar</key>
<key alias="installedPackages">Pachete instalate</key>
<key alias="installSkin">Instalare skin</key>
<key alias="installStarterKit">Instalați trusa de pornire</key>
<key alias="languages">Limbi</key>
<key alias="localPackage">Instalați pachetul local</key>
<key alias="macros">Macros</key>
<key alias="mediaTypes">Tipuri media</key>
<key alias="member">Membrii</key>
<key alias="memberGroups">Grupuri de membri</key>
<key alias="memberRoles">Rolurile membrilor</key>
<key alias="memberTypes">Tipuri de membri</key>
<key alias="documentTypes">Tipuri de documente</key>
<key alias="relationTypes">Tipuri de relații</key>
<key alias="packager">Pachete</key>
<key alias="packages">Pachete</key>
<key alias="partialViews">Vizualizări parțiale</key>
<key alias="partialViewMacros">Vizualizare parțială fișiere macro</key>
<key alias="repositories">Instalați din depozit</key>
<key alias="runway">Instalați pista</key>
<key alias="runwayModules">Module piste</key>
<key alias="scripting">Fișiere de scriptare</key>
<key alias="scripts">Scripturi</key>
<key alias="stylesheets">Stylesheets</key>
<key alias="templates">Șabloane</key>
<key alias="logViewer">Vizualizator de jurnal</key>
<key alias="users">Utilizatori</key>
<key alias="settingsGroup">Setări</key>
<key alias="templatingGroup">Modelare</key>
<key alias="thirdPartyGroup">Terț</key>
</area>
</language> </language>

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco. // Copyright (c) Umbraco.
// See LICENSE for more details. // See LICENSE for more details.
using System.Reflection; using System.Reflection;
@@ -125,6 +125,7 @@ public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
PropertyType = property.PropertyType, PropertyType = property.PropertyType,
Description = attribute.Description, Description = attribute.Description,
HideLabel = attribute.HideLabel, HideLabel = attribute.HideLabel,
SortOrder = attribute.SortOrder,
View = attributeView, View = attributeView,
}; };
@@ -150,6 +151,8 @@ public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
field.PropertyName = property.Name; field.PropertyName = property.Name;
field.PropertyType = property.PropertyType; field.PropertyType = property.PropertyType;
field.SortOrder = attribute.SortOrder;
if (!string.IsNullOrWhiteSpace(attribute.Key)) if (!string.IsNullOrWhiteSpace(attribute.Key))
{ {
field.Key = attribute.Key; field.Key = attribute.Key;
@@ -182,6 +185,6 @@ public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
} }
} }
return fields; return fields.OrderBy(x => x.SortOrder).ToList();
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Extensions; using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.PropertyEditors;
@@ -47,6 +48,7 @@ public class ConfigurationField
HideLabel = attribute.HideLabel; HideLabel = attribute.HideLabel;
Key = attribute.Key; Key = attribute.Key;
View = attribute.View; View = attribute.View;
SortOrder = attribute.SortOrder;
} }
/// <summary> /// <summary>
@@ -77,6 +79,12 @@ public class ConfigurationField
[DataMember(Name = "description")] [DataMember(Name = "description")]
public string? Description { get; set; } public string? Description { get; set; }
/// <summary>
/// Gets or sets the sort order of the field.
/// </summary>
[DataMember(Name = "sortOrder")]
public int SortOrder { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to hide the label of the field. /// Gets or sets a value indicating whether to hide the label of the field.
/// </summary> /// </summary>

View File

@@ -124,6 +124,11 @@ public class ConfigurationFieldAttribute : Attribute
/// </summary> /// </summary>
public string? View { get; } public string? View { get; }
/// <summary>
/// Gets or sets the sort order to use to render the field editor.
/// </summary>
public int SortOrder { get; set; }
/// <summary> /// <summary>
/// Gets or sets the description of the field. /// Gets or sets the description of the field.
/// </summary> /// </summary>

View File

@@ -1146,6 +1146,8 @@ public class ContentService : RepositoryService, IContentService
var allLangs = _languageRepository.GetMany().ToList(); var allLangs = _languageRepository.GetMany().ToList();
// Change state to publishing
content.PublishedState = PublishedState.Publishing;
var savingNotification = new ContentSavingNotification(content, evtMsgs); var savingNotification = new ContentSavingNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification)) if (scope.Notifications.PublishCancelable(savingNotification))
{ {

View File

@@ -131,7 +131,7 @@ internal class TagRepository : EntityRepositoryBase<int, ITag>, ITagRepository
var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId)
SELECT tagSet.tag, tagSet.{group}, tagSet.languageId SELECT tagSet.tag, tagSet.{group}, tagSet.languageId
FROM {tagSetSql} FROM {tagSetSql}
LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) LEFT OUTER JOIN cmsTags ON (tagSet.tag LIKE cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
WHERE cmsTags.id IS NULL"; WHERE cmsTags.id IS NULL";
Database.Execute(sql1); Database.Execute(sql1);
@@ -321,7 +321,7 @@ WHERE r.tagId IS NULL";
Sql<ISqlContext> sql = GetTaggedEntitiesSql(objectType, culture); Sql<ISqlContext> sql = GetTaggedEntitiesSql(objectType, culture);
sql = sql sql = sql
.Where<TagDto>(dto => dto.Text == tag); .WhereLike<TagDto>(dto => dto.Text, tag);
if (group.IsNullOrWhiteSpace() == false) if (group.IsNullOrWhiteSpace() == false)
{ {

View File

@@ -941,15 +941,13 @@
const openPreviewWindow = () => { const openPreviewWindow = () => {
// Chromes popup blocker will kick in if a window is opened // Chromes popup blocker will kick in if a window is opened
// without the initial scoped request. This trick will fix that. // without the initial scoped request. This trick will fix that.
//
const previewWindow = $window.open('preview/?init=true', 'umbpreview'); const previewWindow = $window.open(`preview/?id=${content.id}${$scope.culture ? `&culture=${$scope.culture}` : ''}`, 'umbpreview');
previewWindow.addEventListener('load', () => {
previewWindow.location.href = previewWindow.document.URL;
});
// Build the correct path so both /#/ and #/ work.
let query = 'id=' + content.id;
if ($scope.culture) {
query += "#?culture=" + $scope.culture;
}
previewWindow.location.href = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?' + query;
} }
//The user cannot save if they don't have access to do that, in which case we just want to preview //The user cannot save if they don't have access to do that, in which case we just want to preview

View File

@@ -2,324 +2,329 @@
// http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php
function keyboardService($window, $timeout) { function keyboardService($window, $timeout) {
var keyboardManagerService = {};
var defaultOpt = {
'type': 'keydown',
'propagate': false,
'inputDisabled': false,
'target': $window.document,
'keyCode': false
};
// Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken var keyboardManagerService = {};
var shift_nums = {
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
";": ":",
"'": "\"",
",": "<",
".": ">",
"/": "?",
"\\": "|"
};
// Special Keys - and their codes var defaultOpt = {
var special_keys = { 'type': 'keydown',
'esc': 27, 'propagate': false,
'escape': 27, 'inputDisabled': false,
'tab': 9, 'target': $window.document,
'space': 32, 'keyCode': false
'return': 13, };
'enter': 13,
'backspace': 8,
'scrolllock': 145, // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
'scroll_lock': 145, var shift_nums = {
'scroll': 145, "`": "~",
'capslock': 20, "1": "!",
'caps_lock': 20, "2": "@",
'caps': 20, "3": "#",
'numlock': 144, "4": "$",
'num_lock': 144, "5": "%",
'num': 144, "6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
";": ":",
"'": "\"",
",": "<",
".": ">",
"/": "?",
"\\": "|"
};
'pause': 19, // Special Keys - and their codes
'break': 19, var special_keys = {
'esc': 27,
'escape': 27,
'tab': 9,
'space': 32,
'return': 13,
'enter': 13,
'backspace': 8,
'insert': 45, 'scrolllock': 145,
'home': 36, 'scroll_lock': 145,
'delete': 46, 'scroll': 145,
'end': 35, 'capslock': 20,
'caps_lock': 20,
'caps': 20,
'numlock': 144,
'num_lock': 144,
'num': 144,
'pageup': 33, 'pause': 19,
'page_up': 33, 'break': 19,
'pu': 33,
'pagedown': 34, 'insert': 45,
'page_down': 34, 'home': 36,
'pd': 34, 'delete': 46,
'end': 35,
'left': 37, 'pageup': 33,
'up': 38, 'page_up': 33,
'right': 39, 'pu': 33,
'down': 40,
'f1': 112, 'pagedown': 34,
'f2': 113, 'page_down': 34,
'f3': 114, 'pd': 34,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123
};
var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; 'left': 37,
'up': 38,
'right': 39,
'down': 40,
// The event handler for bound element events 'f1': 112,
function eventHandler(e) { 'f2': 113,
e = e || $window.event; 'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123
};
var code, k; var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
// Find out which key is pressed // The event handler for bound element events
if (e.keyCode) function eventHandler(e) {
{ e = e || $window.event;
code = e.keyCode;
}
else if (e.which) {
code = e.which;
}
var character = String.fromCharCode(code).toLowerCase(); var code, k;
if (code === 188){character = ",";} // If the user presses , when the type is onkeydown // Find out which key is pressed
if (code === 190){character = ".";} // If the user presses , when the type is onkeydown if (e.keyCode) {
code = e.keyCode;
var propagate = true; } else if (e.which) {
code = e.which;
//Now we need to determine which shortcut this event is for, we'll do this by iterating over each
//registered shortcut to find the match. We use Find here so that the loop exits as soon
//as we've found the one we're looking for
_.find(_.keys(keyboardManagerService.keyboardEvent), function(key) {
var shortcutLabel = key;
var shortcutVal = keyboardManagerService.keyboardEvent[key];
// Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
var kp = 0;
// Some modifiers key
var modifiers = {
shift: {
wanted: false,
pressed: e.shiftKey ? true : false
},
ctrl: {
wanted: false,
pressed: e.ctrlKey ? true : false
},
alt: {
wanted: false,
pressed: e.altKey ? true : false
},
meta: { //Meta is Mac specific
wanted: false,
pressed: e.metaKey ? true : false
}
};
var keys = shortcutLabel.split("+");
var opt = shortcutVal.opt;
var callback = shortcutVal.callback;
// Foreach keys in label (split on +)
var l = keys.length;
for (var i = 0; i < l; i++) {
var k = keys[i];
switch (k) {
case 'ctrl':
case 'control':
kp++;
modifiers.ctrl.wanted = true;
break;
case 'shift':
case 'alt':
case 'meta':
kp++;
modifiers[k].wanted = true;
break;
}
if (k.length > 1) { // If it is a special key
if (special_keys[k] === code) {
kp++;
}
}
else if (opt['keyCode']) { // If a specific key is set into the config
if (opt['keyCode'] === code) {
kp++;
}
}
else { // The special keys did not match
if (character === k) {
kp++;
}
else {
if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase
character = shift_nums[character];
if (character === k) {
kp++;
}
}
}
}
} //for end
if (kp === keys.length &&
modifiers.ctrl.pressed === modifiers.ctrl.wanted &&
modifiers.shift.pressed === modifiers.shift.wanted &&
modifiers.alt.pressed === modifiers.alt.wanted &&
modifiers.meta.pressed === modifiers.meta.wanted) {
//found the right callback!
// Disable event handler when focus input and textarea
if (opt['inputDisabled']) {
var elt;
if (e.target) {
elt = e.target;
} else if (e.srcElement) {
elt = e.srcElement;
}
if (elt.nodeType === 3) { elt = elt.parentNode; }
if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA' || elt.hasAttribute('disable-hotkeys')) {
//This exits the Find loop
return true;
}
}
$timeout(function () {
callback(e);
}, 1);
if (!opt['propagate']) { // Stop the event
propagate = false;
}
//This exits the Find loop
return true;
}
//we haven't found one so continue looking
return false;
});
// Stop the event if required
if (!propagate) {
// e.cancelBubble is supported by IE - this will kill the bubbling process.
e.cancelBubble = true;
e.returnValue = false;
// e.stopPropagation works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return false;
}
} }
// Store all keyboard combination shortcuts var character = String.fromCharCode(code).toLowerCase();
keyboardManagerService.keyboardEvent = {};
// Add a new keyboard combination shortcut if (code === 188) {
keyboardManagerService.bind = function (label, callback, opt) { character = ",";
} // If the user presses , when the type is onkeydown
if (code === 190) {
character = ".";
} // If the user presses , when the type is onkeydown
//replace ctrl key with meta key var propagate = true;
if(isMac && label !== "ctrl+space"){
label = label.replace("ctrl","meta"); //Now we need to determine which shortcut this event is for, we'll do this by iterating over each
//registered shortcut to find the match. We use Find here so that the loop exits as soon
//as we've found the one we're looking for
_.find(_.keys(keyboardManagerService.keyboardEvent), function (key) {
var shortcutLabel = key;
var shortcutVal = keyboardManagerService.keyboardEvent[key];
// Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
var kp = 0;
// Some modifiers key
var modifiers = {
shift: {
wanted: false,
pressed: e.shiftKey ? true : false
},
ctrl: {
wanted: false,
pressed: e.ctrlKey ? true : false
},
alt: {
wanted: false,
pressed: e.altKey ? true : false
},
meta: { //Meta is Mac specific
wanted: false,
pressed: e.metaKey ? true : false
}
};
var keys = shortcutLabel.split("+");
var opt = shortcutVal.opt;
var callback = shortcutVal.callback;
// Foreach keys in label (split on +)
var l = keys.length;
for (var i = 0; i < l; i++) {
var k = keys[i];
switch (k) {
case 'ctrl':
case 'control':
kp++;
modifiers.ctrl.wanted = true;
break;
case 'shift':
case 'alt':
case 'meta':
kp++;
modifiers[k].wanted = true;
break;
} }
var elt; if (k.length > 1) { // If it is a special key
// Initialize opt object if (special_keys[k] === code) {
opt = Utilities.extend({}, defaultOpt, opt); kp++;
label = label.toLowerCase(); }
elt = opt.target; } else if (opt['keyCode']) { // If a specific key is set into the config
if(typeof opt.target === 'string'){ if (opt['keyCode'] === code) {
elt = document.getElementById(opt.target); kp++;
} }
} else { // The special keys did not match
//Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding if (character === k) {
// and raising events for now reason. So here we'll check if the event is already registered for the element kp++;
var boundValues = _.values(keyboardManagerService.keyboardEvent); } else {
var found = _.find(boundValues, function (i) { if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase
return i.target === elt && i.event === opt['type']; character = shift_nums[character];
}); if (character === k) {
kp++;
// Store shortcut }
keyboardManagerService.keyboardEvent[label] = {
'callback': callback,
'target': elt,
'opt': opt
};
if (!found) {
//Attach the function with the event
if (elt.addEventListener) {
elt.addEventListener(opt['type'], eventHandler, false);
} else if (elt.attachEvent) {
elt.attachEvent('on' + opt['type'], eventHandler);
} else {
elt['on' + opt['type']] = eventHandler;
} }
}
} }
};
// Remove the shortcut - just specify the shortcut and I will remove the binding
keyboardManagerService.unbind = function (label) {
label = label.toLowerCase();
var binding = keyboardManagerService.keyboardEvent[label];
delete(keyboardManagerService.keyboardEvent[label]);
if(!binding){return;} } //for end
var type = binding['event'], if (kp === keys.length &&
elt = binding['target'], modifiers.ctrl.pressed === modifiers.ctrl.wanted &&
callback = binding['callback']; modifiers.shift.pressed === modifiers.shift.wanted &&
modifiers.alt.pressed === modifiers.alt.wanted &&
modifiers.meta.pressed === modifiers.meta.wanted) {
if(elt.detachEvent){ //found the right callback!
elt.detachEvent('on' + type, callback);
}else if(elt.removeEventListener){ // Disable event handler when focus input and textarea
elt.removeEventListener(type, callback, false); if (opt['inputDisabled']) {
}else{ var elt;
elt['on'+type] = false; if (e.composedPath()) {
elt = e.composedPath()[0];
} else if (e.target) {
elt = e.target;
} else if (e.srcElement) {
elt = e.srcElement;
}
if (elt.nodeType === 3) {
elt = elt.parentNode;
}
if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA' || elt.hasAttribute('disable-hotkeys')) {
//This exits the Find loop
return true;
}
} }
};
//
return keyboardManagerService; $timeout(function () {
callback(e);
}, 1);
if (!opt['propagate']) { // Stop the event
propagate = false;
}
//This exits the Find loop
return true;
}
//we haven't found one so continue looking
return false;
});
// Stop the event if required
if (!propagate) {
// e.cancelBubble is supported by IE - this will kill the bubbling process.
e.cancelBubble = true;
e.returnValue = false;
// e.stopPropagation works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return false;
}
}
// Store all keyboard combination shortcuts
keyboardManagerService.keyboardEvent = {};
// Add a new keyboard combination shortcut
keyboardManagerService.bind = function (label, callback, opt) {
//replace ctrl key with meta key
if (isMac && label !== "ctrl+space") {
label = label.replace("ctrl", "meta");
}
var elt;
// Initialize opt object
opt = Utilities.extend({}, defaultOpt, opt);
label = label.toLowerCase();
elt = opt.target;
if (typeof opt.target === 'string') {
elt = document.getElementById(opt.target);
}
//Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding
// and raising events for now reason. So here we'll check if the event is already registered for the element
var boundValues = _.values(keyboardManagerService.keyboardEvent);
var found = _.find(boundValues, function (i) {
return i.target === elt && i.event === opt['type'];
});
// Store shortcut
keyboardManagerService.keyboardEvent[label] = {
'callback': callback,
'target': elt,
'opt': opt
};
if (!found) {
//Attach the function with the event
if (elt.addEventListener) {
elt.addEventListener(opt['type'], eventHandler, false);
} else if (elt.attachEvent) {
elt.attachEvent('on' + opt['type'], eventHandler);
} else {
elt['on' + opt['type']] = eventHandler;
}
}
};
// Remove the shortcut - just specify the shortcut and I will remove the binding
keyboardManagerService.unbind = function (label) {
label = label.toLowerCase();
var binding = keyboardManagerService.keyboardEvent[label];
delete (keyboardManagerService.keyboardEvent[label]);
if (!binding) {
return;
}
var type = binding['event'],
elt = binding['target'],
callback = binding['callback'];
if (elt.detachEvent) {
elt.detachEvent('on' + type, callback);
} else if (elt.removeEventListener) {
elt.removeEventListener(type, callback, false);
} else {
elt['on' + type] = false;
}
};
//
return keyboardManagerService;
} }
angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]);

View File

@@ -165,7 +165,7 @@ h6.-black {
} }
} }
umb-property:last-of-type .umb-control-group { umb-property:last-of-type > .umb-property > ng-form > .umb-control-group {
&::after { &::after {
margin-top: 0px; margin-top: 0px;
height: 0; height: 0;

View File

@@ -118,6 +118,11 @@
line-height: 36px; line-height: 36px;
} }
.login-overlay .btn-social > .umb-icon {
padding: 2px;
box-sizing: border-box;
}
.login-overlay .text-error, .login-overlay .text-error,
.login-overlay .text-info .login-overlay .text-info
{ {

View File

@@ -128,6 +128,13 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
eventsService.emit("dialogs.linkPicker.select", args); eventsService.emit("dialogs.linkPicker.select", args);
if ($scope.currentNode) { if ($scope.currentNode) {
if ($scope.currentNode.id == args.node.id && $scope.currentNode.selected) {
$scope.model.target = {};
$scope.currentNode.selected = false;
return;
}
//un-select if there's a current one selected //un-select if there's a current one selected
$scope.currentNode.selected = false; $scope.currentNode.selected = false;
} }

View File

@@ -18,10 +18,10 @@
<umb-property-editor <umb-property-editor
model="property" model="property"
node="contentNodeModel" node="contentNodeModel"
preview="(propertyEditorDisabled(property) && allowUpdate) || (!allowUpdate && !property.supportsReadOnly)" preview="((property.readonly || !allowUpdate) && !property.supportsReadOnly) || (propertyEditorDisabled(property) && allowUpdate)"
allow-unlock="allowUpdate && allowEditInvariantFromNonDefault" allow-unlock="!property.readonly && allowUpdate && allowEditInvariantFromNonDefault"
on-unlock="unlockInvariantValue(property)" on-unlock="unlockInvariantValue(property)"
ng-attr-readonly="{{ !allowUpdate || undefined}}"> ng-attr-readonly="{{property.readonly || !allowUpdate || undefined}}">
</umb-property-editor> </umb-property-editor>
</umb-property> </umb-property>
@@ -52,10 +52,10 @@
<umb-property-editor <umb-property-editor
model="property" model="property"
node="contentNodeModel" node="contentNodeModel"
preview="(propertyEditorDisabled(property) && allowUpdate) || (!allowUpdate && !property.supportsReadOnly)" preview="((property.readonly || !allowUpdate) && !property.supportsReadOnly) || (propertyEditorDisabled(property) && allowUpdate)"
allow-unlock="allowUpdate && allowEditInvariantFromNonDefault" allow-unlock="!property.readonly && allowUpdate && allowEditInvariantFromNonDefault"
on-unlock="unlockInvariantValue(property)" on-unlock="unlockInvariantValue(property)"
ng-attr-readonly="{{!allowUpdate || undefined}}"> ng-attr-readonly="{{property.readonly || !allowUpdate || undefined}}">
</umb-property-editor> </umb-property-editor>
</umb-property> </umb-property>

View File

@@ -5,7 +5,7 @@
ng-model="model.value" ng-model="model.value"
val-server="value" val-server="value"
min="0" min="0"
max="500" max="512"
fix-number /> fix-number />
<span ng-messages="prevalueTextLimitedForm.textLimitedField.$error" show-validation-on-submit > <span ng-messages="prevalueTextLimitedForm.textLimitedField.$error" show-validation-on-submit >
@@ -13,5 +13,5 @@
</span> </span>
</ng-form> </ng-form>
</div> </div>

View File

@@ -51,7 +51,7 @@
` `
${ model.stylesheet ? ` ${ model.stylesheet ? `
<style> <style>
@import "${model.stylesheet}" @import "${model.stylesheet}?umb__rnd=${Umbraco.Sys.ServerVariables.application.cacheBuster}"
</style>` </style>`
: '' : ''
} }

View File

@@ -35,8 +35,8 @@
shadowRoot.innerHTML = shadowRoot.innerHTML =
` `
<style> <style>
{{vm.stylesheet ? "@import '"+vm.stylesheet+"';" : ""}} {{vm.stylesheet ? "@import '" + vm.stylesheet + "?umb__rnd=${Umbraco.Sys.ServerVariables.application.cacheBuster}';" : ""}}
@import 'assets/css/blockgridui.css'; @import 'assets/css/blockgridui.css?umb__rnd=${Umbraco.Sys.ServerVariables.application.cacheBuster}';
:host { :host {
--umb-block-grid--grid-columns: ${vm.gridColumns}; --umb-block-grid--grid-columns: ${vm.gridColumns};
} }

View File

@@ -6,6 +6,10 @@
border-radius: @baseBorderRadius; border-radius: @baseBorderRadius;
transition: border-color 120ms, background-color 120ms; transition: border-color 120ms, background-color 120ms;
.umb-box {
margin-bottom: 0;
}
.umb-block-list__block:not(.--active) &:hover { .umb-block-list__block:not(.--active) &:hover {
border-color: @gray-8; border-color: @gray-8;
} }

View File

@@ -25,7 +25,7 @@
ng-click="openCurrentPicker()" ng-click="openCurrentPicker()"
id="{{model.alias}}" id="{{model.alias}}"
aria-label="{{model.label}}: {{labels.general_add}}" aria-label="{{model.label}}: {{labels.general_add}}"
ng-disabled="readonly"> ng-disabled="!allowAdd">
<localize key="general_add">Add</localize> <localize key="general_add">Add</localize>
<span class="sr-only">...</span> <span class="sr-only">...</span>
</button> </button>

View File

@@ -17,7 +17,7 @@
"xhr2": "^0.2.1" "xhr2": "^0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.31", "@playwright/test": "^1.32",
"del": "^6.0.0", "del": "^6.0.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"prompt": "^1.2.0", "prompt": "^1.2.0",
@@ -86,13 +86,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.31.1", "version": "1.32.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz",
"integrity": "sha512-IsytVZ+0QLDh1Hj83XatGp/GsI1CDJWbyDaBGbainsh0p2zC7F4toUocqowmjS6sQff2NGT3D9WbDj/3K2CJiA==", "integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"playwright-core": "1.31.1" "playwright-core": "1.32.3"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -742,9 +742,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.31.1", "version": "1.32.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz",
"integrity": "sha512-JTyX4kV3/LXsvpHkLzL2I36aCdml4zeE35x+G5aPc4bkLsiRiQshU5lWeVpHFAuC8xAcbI6FDcw/8z3q2xtJSQ==", "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==",
"dev": true, "dev": true,
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -1035,14 +1035,14 @@
} }
}, },
"@playwright/test": { "@playwright/test": {
"version": "1.31.1", "version": "1.32.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz",
"integrity": "sha512-IsytVZ+0QLDh1Hj83XatGp/GsI1CDJWbyDaBGbainsh0p2zC7F4toUocqowmjS6sQff2NGT3D9WbDj/3K2CJiA==", "integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
"fsevents": "2.3.2", "fsevents": "2.3.2",
"playwright-core": "1.31.1" "playwright-core": "1.32.3"
} }
}, },
"@sideway/address": { "@sideway/address": {
@@ -1528,9 +1528,9 @@
"dev": true "dev": true
}, },
"playwright-core": { "playwright-core": {
"version": "1.31.1", "version": "1.32.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz",
"integrity": "sha512-JTyX4kV3/LXsvpHkLzL2I36aCdml4zeE35x+G5aPc4bkLsiRiQshU5lWeVpHFAuC8xAcbI6FDcw/8z3q2xtJSQ==", "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==",
"dev": true "dev": true
}, },
"prompt": { "prompt": {

View File

@@ -10,7 +10,7 @@
"createTest": "node createTest.js" "createTest": "node createTest.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.31", "@playwright/test": "^1.32",
"typescript": "^4.8.3", "typescript": "^4.8.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"del": "^6.0.0", "del": "^6.0.0",

View File

@@ -35,7 +35,6 @@ test.describe('BlockGridEditorInDocument', () => {
} }
async function createDefaultDocumentTypeWithBlockGridEditor(umbracoApi, BlockGridEditor?) { async function createDefaultDocumentTypeWithBlockGridEditor(umbracoApi, BlockGridEditor?) {
const rootDocType = new DocumentTypeBuilder() const rootDocType = new DocumentTypeBuilder()
.withName(documentName) .withName(documentName)
.withAllowAsRoot(true) .withAllowAsRoot(true)
@@ -269,4 +268,4 @@ test.describe('BlockGridEditorInDocument', () => {
// Clean // Clean
await umbracoApi.documentTypes.ensureNameNotExists(elementName); await umbracoApi.documentTypes.ensureNameNotExists(elementName);
}); });
}); });

View File

@@ -4,7 +4,6 @@ import {ContentBuilder, DocumentTypeBuilder, PartialViewBuilder} from "@umbraco/
import {BlockListDataTypeBuilder} from "@umbraco/json-models-builders/dist/lib/builders/dataTypes"; import {BlockListDataTypeBuilder} from "@umbraco/json-models-builders/dist/lib/builders/dataTypes";
test.describe('BlockListEditorContent', () => { test.describe('BlockListEditorContent', () => {
const documentName = 'DocumentTestName'; const documentName = 'DocumentTestName';
const blockListName = 'BlockListTest'; const blockListName = 'BlockListTest';
const elementName = 'TestElement'; const elementName = 'TestElement';
@@ -126,6 +125,9 @@ test.describe('BlockListEditorContent', () => {
// Checks if the content was created // Checks if the content was created
await expect(page.locator('.umb-block-list__block--view')).toHaveCount(1); await expect(page.locator('.umb-block-list__block--view')).toHaveCount(1);
await expect(page.locator('.umb-block-list__block--view').nth(0)).toHaveText(elementName); await expect(page.locator('.umb-block-list__block--view').nth(0)).toHaveText(elementName);
// Checks if the content contains the correct value
await page.locator('.umb-block-list__block--view').nth(0).click();
await expect(page.locator('[id="sub-view-0"]').locator('[id="title"]')).toHaveValue('Testing...');
}); });
test('can update content with a block list editor', async ({page, umbracoApi, umbracoUi}) => { test('can update content with a block list editor', async ({page, umbracoApi, umbracoUi}) => {
@@ -385,7 +387,7 @@ test.describe('BlockListEditorContent', () => {
test('can see rendered content with a block list editor', async ({page, umbracoApi, umbracoUi}) => { test('can see rendered content with a block list editor', async ({page, umbracoApi, umbracoUi}) => {
await umbracoApi.templates.ensureNameNotExists(documentName); await umbracoApi.templates.ensureNameNotExists(documentName);
await umbracoApi.partialViews.ensureNameNotExists(elementName + '.cshtml'); await umbracoApi.partialViews.ensureNameNotExists('blocklist/Components',elementAlias + '.cshtml');
const element = new DocumentTypeBuilder() const element = new DocumentTypeBuilder()
.withName(elementName) .withName(elementName)
@@ -480,6 +482,6 @@ test.describe('BlockListEditorContent', () => {
// Clean // Clean
await umbracoApi.templates.ensureNameNotExists(documentName); await umbracoApi.templates.ensureNameNotExists(documentName);
await umbracoApi.partialViews.ensureNameNotExists(elementAlias + '.cshtml'); await umbracoApi.partialViews.ensureNameNotExists('blocklist/Components',elementAlias + '.cshtml');
}); });
}); });

View File

@@ -28,7 +28,7 @@ test.describe('Languages', () => {
const doesExistDA = await umbracoApi.languages.exists(culture); const doesExistDA = await umbracoApi.languages.exists(culture);
await expect(doesExistDA).toBe(true); await expect(doesExistDA).toBe(true);
// Cleanup // Cleanup
await umbracoApi.languages.ensureCultureNotExists(culture); await umbracoApi.languages.ensureCultureNotExists(culture);
}); });
@@ -70,7 +70,7 @@ test.describe('Languages', () => {
await expect(page.getByRole('button', {name: language2})).not.toBeVisible(); await expect(page.getByRole('button', {name: language2})).not.toBeVisible();
doesExistEN = await umbracoApi.languages.exists(language2); doesExistEN = await umbracoApi.languages.exists(language2);
await expect(doesExistEN).toBe(false); await expect(doesExistEN).toBe(false);
// Cleanup // Cleanup
await umbracoApi.languages.ensureCultureNotExists(language1); await umbracoApi.languages.ensureCultureNotExists(language1);
}); });

View File

@@ -9,7 +9,7 @@ test.describe('media File Types', () => {
await umbracoUi.goToSection(ConstantHelper.sections.media); await umbracoUi.goToSection(ConstantHelper.sections.media);
await umbracoApi.media.deleteAllMedia(); await umbracoApi.media.deleteAllMedia();
}); });
test.describe('create each File Types', () => { test.describe('create each File Types', () => {
test('create Article', async ({page, umbracoApi, umbracoUi}) => { test('create Article', async ({page, umbracoApi, umbracoUi}) => {
const articleName = "Article"; const articleName = "Article";
@@ -53,7 +53,7 @@ test.describe('media File Types', () => {
const path = 'mediaLibrary/' + fileName; const path = 'mediaLibrary/' + fileName;
const mimeType = "*/*"; const mimeType = "*/*";
await umbracoApi.media.ensureNameNotExists(fileItemName); await umbracoApi.media.ensureNameNotExists(fileItemName);
// Action // Action
await umbracoApi.media.createFileWithFile(fileItemName, fileName, path, mimeType); await umbracoApi.media.createFileWithFile(fileItemName, fileName, path, mimeType);
await umbracoUi.refreshMediaTree(); await umbracoUi.refreshMediaTree();
@@ -222,7 +222,7 @@ test.describe('media File Types', () => {
const childName = 'ChildFolder'; const childName = 'ChildFolder';
await umbracoApi.media.ensureNameNotExists(parentName); await umbracoApi.media.ensureNameNotExists(parentName);
await umbracoApi.media.ensureNameNotExists(childName); await umbracoApi.media.ensureNameNotExists(childName);
// Action // Action
await umbracoApi.media.createDefaultFolder(parentName); await umbracoApi.media.createDefaultFolder(parentName);
await umbracoUi.refreshMediaTree(); await umbracoUi.refreshMediaTree();
@@ -325,7 +325,7 @@ test.describe('media File Types', () => {
await umbracoApi.media.ensureNameNotExists(childName); await umbracoApi.media.ensureNameNotExists(childName);
}); });
}); });
test('Delete one of each Files in media', async ({page, umbracoApi, umbracoUi}) => { test('Delete one of each Files in media', async ({page, umbracoApi, umbracoUi}) => {
const articleName = 'ArticleToDelete'; const articleName = 'ArticleToDelete';
const audioName = 'AudioToDelete'; const audioName = 'AudioToDelete';
@@ -341,7 +341,7 @@ test.describe('media File Types', () => {
await page.reload(); await page.reload();
// Needs to close tours when page has reloaded // Needs to close tours when page has reloaded
await page.click('.umb-tour-step__close'); await page.click('.umb-tour-step__close');
// Takes all the child elements in folder-grid. // Takes all the child elements in folder-grid.
await page.locator(".umb-folder-grid").locator("xpath=/*", {hasText: folderName}).click({ await page.locator(".umb-folder-grid").locator("xpath=/*", {hasText: folderName}).click({
position: { position: {

View File

@@ -10,12 +10,12 @@ test.describe('Media', () => {
await umbracoApi.media.deleteAllMedia(); await umbracoApi.media.deleteAllMedia();
await umbracoApi.media.clearRecycleBin(); await umbracoApi.media.clearRecycleBin();
}); });
test.afterEach(async ({page, umbracoApi, umbracoUi}, testInfo) => { test.afterEach(async ({page, umbracoApi, umbracoUi}, testInfo) => {
await umbracoApi.media.deleteAllMedia(); await umbracoApi.media.deleteAllMedia();
await umbracoApi.media.clearRecycleBin(); await umbracoApi.media.clearRecycleBin();
}); });
test('move one of each Files into a Folder', async ({page, umbracoApi, umbracoUi}) => { test('move one of each Files into a Folder', async ({page, umbracoApi, umbracoUi}) => {
const articleName = 'ArticleToMove'; const articleName = 'ArticleToMove';
const audioName = 'AudioToMove'; const audioName = 'AudioToMove';
@@ -25,7 +25,7 @@ test.describe('Media', () => {
const vectorGraphicsName = 'VectorGraphicsToMove'; const vectorGraphicsName = 'VectorGraphicsToMove';
const videoName = 'VideoToMove'; const videoName = 'VideoToMove';
const folderToMoveTooName = 'MoveHere'; const folderToMoveTooName = 'MoveHere';
const mediaFileTypes = [ const mediaFileTypes = [
{fileTypeNames: articleName}, {fileTypeNames: articleName},
{fileTypeNames: audioName}, {fileTypeNames: audioName},
@@ -34,7 +34,7 @@ test.describe('Media', () => {
{fileTypeNames: vectorGraphicsName}, {fileTypeNames: vectorGraphicsName},
{fileTypeNames: videoName} {fileTypeNames: videoName}
]; ];
// Action // Action
await umbracoApi.media.createAllFileTypes(articleName, audioName, fileName, folderName, imageName, vectorGraphicsName, videoName); await umbracoApi.media.createAllFileTypes(articleName, audioName, fileName, folderName, imageName, vectorGraphicsName, videoName);
await umbracoApi.media.createDefaultFolder(folderToMoveTooName); await umbracoApi.media.createDefaultFolder(folderToMoveTooName);
@@ -53,7 +53,7 @@ test.describe('Media', () => {
await page.locator('[label-key="actions_move"]').click(); await page.locator('[label-key="actions_move"]').click();
await page.locator('[data-element="editor-container"] >> "' + folderToMoveTooName + '"').click(); await page.locator('[data-element="editor-container"] >> "' + folderToMoveTooName + '"').click();
await page.locator('[label-key="general_submit"]').click(); await page.locator('[label-key="general_submit"]').click();
// Assert // Assert
// Needs to wait before refreshing the media tree, otherwise the media files wont be moved to the folder yet // Needs to wait before refreshing the media tree, otherwise the media files wont be moved to the folder yet
await page.waitForTimeout(2500); await page.waitForTimeout(2500);
@@ -64,12 +64,12 @@ test.describe('Media', () => {
} }
await expect(page.locator(".umb-folder-grid", {hasText: folderName})).toBeVisible(); await expect(page.locator(".umb-folder-grid", {hasText: folderName})).toBeVisible();
}); });
test('sort by Name', async ({page, umbracoApi, umbracoUi}) => { test('sort by Name', async ({page, umbracoApi, umbracoUi}) => {
const FolderNameA = 'A'; const FolderNameA = 'A';
const FolderNameB = 'B'; const FolderNameB = 'B';
const FolderNameC = 'C'; const FolderNameC = 'C';
// Action // Action
await umbracoApi.media.createDefaultFolder(FolderNameC); await umbracoApi.media.createDefaultFolder(FolderNameC);
await umbracoApi.media.createDefaultFolder(FolderNameB); await umbracoApi.media.createDefaultFolder(FolderNameB);
@@ -130,4 +130,4 @@ test.describe('Media', () => {
// Assert // Assert
await expect(page.locator('[icon="icon-thumbnails-small"]')).toBeVisible(); await expect(page.locator('[icon="icon-thumbnails-small"]')).toBeVisible();
}); });
}); });

View File

@@ -33,7 +33,7 @@ test.describe('Modelsbuilder tests', () => {
.done() .done()
.build(); .build();
await umbracoApi.documentTypes.save(docType); await umbracoApi.documentTypes.save(docType);
await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument> @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@@ -81,7 +81,7 @@ test.describe('Modelsbuilder tests', () => {
.done() .done()
.build(); .build();
const savedDocType = await umbracoApi.documentTypes.save(docType); const savedDocType = await umbracoApi.documentTypes.save(docType);
await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument> @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@@ -150,7 +150,7 @@ test.describe('Modelsbuilder tests', () => {
.done() .done()
.build(); .build();
const savedDocType = await umbracoApi.documentTypes.save(docType); const savedDocType = await umbracoApi.documentTypes.save(docType);
await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument> @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@@ -183,7 +183,7 @@ test.describe('Modelsbuilder tests', () => {
// We only have to type out the opening tag, the editor adds the closing tag automatically. // We only have to type out the opening tag, the editor adds the closing tag automatically.
await editor.type("<p>Edited"); await editor.type("<p>Edited");
await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
await umbracoUi.isSuccessNotificationVisible({timeout:10000}); await umbracoUi.isSuccessNotificationVisible({timeout:10000});
await umbracoApi.content.verifyRenderedContent("/", "<h1>" + propertyValue + "</h1><p>Edited</p>", true); await umbracoApi.content.verifyRenderedContent("/", "<h1>" + propertyValue + "</h1><p>Edited</p>", true);
@@ -195,7 +195,7 @@ test.describe('Modelsbuilder tests', () => {
test('Can update view and document type', async ({page, umbracoApi, umbracoUi},testInfo) => { test('Can update view and document type', async ({page, umbracoApi, umbracoUi},testInfo) => {
await testInfo.slow(); await testInfo.slow();
const docTypeName = "TestDocument"; const docTypeName = "TestDocument";
const docTypeAlias = AliasHelper.toAlias(docTypeName); const docTypeAlias = AliasHelper.toAlias(docTypeName);
const propertyAlias = "title"; const propertyAlias = "title";
@@ -219,7 +219,7 @@ test.describe('Modelsbuilder tests', () => {
.done() .done()
.build(); .build();
const savedDocType = await umbracoApi.documentTypes.save(docType); const savedDocType = await umbracoApi.documentTypes.save(docType);
await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument> @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.Testdocument>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@@ -276,7 +276,7 @@ test.describe('Modelsbuilder tests', () => {
await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish));
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
await umbracoApi.content.verifyRenderedContent("/", "<h1>" + propertyValue + "</h1><p>Fancy body text</p>", true); await umbracoApi.content.verifyRenderedContent("/", "<h1>" + propertyValue + "</h1><p>Fancy body text</p>", true);
await umbracoApi.content.deleteAllContent(); await umbracoApi.content.deleteAllContent();

View File

@@ -37,18 +37,16 @@ test.describe('Partial Views', () => {
await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
//Assert //Assert
await umbracoUi.isSuccessNotificationVisible({timeout: 20000}); await umbracoUi.isSuccessNotificationVisible({timeout: 30000});
//Clean up //Clean up
await umbracoApi.partialViews.ensureNameNotExists('', fileName); await umbracoApi.partialViews.ensureNameNotExists('', fileName);
}); });
test('Create partial view from snippet', async ({page, umbracoApi, umbracoUi}) => { test('Create partial view from snippet', async ({page, umbracoApi, umbracoUi}) => {
const name = "TestPartialViewFromSnippet"; const name = "TestPartialViewFromSnippet";
const fileName = name + ".cshtml"; const fileName = name + ".cshtml";
await umbracoApi.partialViews.ensureNameNotExists('', fileName); await umbracoApi.partialViews.ensureNameNotExists('', fileName);
await openPartialViewsCreatePanel(page, umbracoUi); await openPartialViewsCreatePanel(page, umbracoUi);
await umbracoUi.clickElement(umbracoUi.getContextMenuAction("action-create")); await umbracoUi.clickElement(umbracoUi.getContextMenuAction("action-create"));
@@ -61,14 +59,13 @@ test.describe('Partial Views', () => {
// Save // Save
await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
// Assert // Assert
await umbracoUi.isSuccessNotificationVisible({timeout:20000}); await umbracoUi.isSuccessNotificationVisible({timeout:20000});
// Clean up // Clean up
await umbracoApi.partialViews.ensureNameNotExists('', fileName); await umbracoApi.partialViews.ensureNameNotExists('', fileName);
}); });
test('Partial view with no name', async ({page, umbracoApi, umbracoUi}) => { test('Partial view with no name', async ({page, umbracoApi, umbracoUi}) => {
await openPartialViewsCreatePanel(page, umbracoUi); await openPartialViewsCreatePanel(page, umbracoUi);
@@ -84,8 +81,8 @@ test.describe('Partial Views', () => {
// Asserts // Asserts
await umbracoUi.isErrorNotificationVisible(); await umbracoUi.isErrorNotificationVisible();
}); });
test('Delete partial view', async ({page, umbracoApi, umbracoUi}) => { test('Delete partial view', async ({page, umbracoApi, umbracoUi}) => {
const name = "TestDeletePartialView"; const name = "TestDeletePartialView";
const fileName = name + ".cshtml"; const fileName = name + ".cshtml";
@@ -112,8 +109,7 @@ test.describe('Partial Views', () => {
// Clean // Clean
await umbracoApi.partialViews.ensureNameNotExists('', fileName); await umbracoApi.partialViews.ensureNameNotExists('', fileName);
}); });
test('Edit partial view', async ({page, umbracoApi, umbracoUi}) => { test('Edit partial view', async ({page, umbracoApi, umbracoUi}) => {
const name = 'EditPartialView'; const name = 'EditPartialView';
const fileName = name + ".cshtml"; const fileName = name + ".cshtml";
@@ -136,7 +132,6 @@ test.describe('Partial Views', () => {
// Assert // Assert
await umbracoUi.isSuccessNotificationVisible({timeout:20000}); await umbracoUi.isSuccessNotificationVisible({timeout:20000});
// Clean // Clean
await umbracoApi.partialViews.ensureNameNotExists('', fileName); await umbracoApi.partialViews.ensureNameNotExists('', fileName);
}); });

View File

@@ -76,7 +76,9 @@ test.describe('Tours', () => {
} }
test('Backoffice introduction tour should run', async ({page, umbracoApi, umbracoUi}) => { test('Backoffice introduction tour should run', async ({page, umbracoApi, umbracoUi}, testInfo) => {
await testInfo.slow();
// We have to reload this page, as we already get a page context after login // We have to reload this page, as we already get a page context after login
// before we have reset a users tour data // before we have reset a users tour data
await expect(await umbracoUi.getGlobalHelp()).toBeVisible(); await expect(await umbracoUi.getGlobalHelp()).toBeVisible();
@@ -87,7 +89,9 @@ test.describe('Tours', () => {
await getPercentage(17, timeout, page); await getPercentage(17, timeout, page);
}); });
test('Backoffice introduction tour should run, then rerun', async ({page, umbracoApi, umbracoUi}) => { test('Backoffice introduction tour should run, then rerun', async ({page, umbracoApi, umbracoUi}, testInfo) => {
await testInfo.slow();
await expect(await umbracoUi.getGlobalHelp()).toBeVisible(); await expect(await umbracoUi.getGlobalHelp()).toBeVisible();
await umbracoUi.clickElement(umbracoUi.getGlobalHelp()); await umbracoUi.clickElement(umbracoUi.getGlobalHelp());
await runBackOfficeIntroTour(0, 'Start', timeout, page, umbracoUi); await runBackOfficeIntroTour(0, 'Start', timeout, page, umbracoUi);

View File

@@ -13,6 +13,7 @@ using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing; using Umbraco.Cms.Tests.Integration.Testing;
using static Umbraco.Cms.Core.Constants.Conventions;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories;
@@ -98,6 +99,45 @@ public class TagRepositoryTest : UmbracoIntegrationTest
} }
} }
[Test]
public void Can_Create_Tag_Relations_With_Mixed_Casing()
{
var provider = ScopeProvider;
using (ScopeProvider.CreateScope())
{
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template);
var contentType =
ContentTypeBuilder.CreateSimpleContentType("test", "Test", defaultTemplateId: template.Id);
ContentTypeRepository.Save(contentType);
var content1 = ContentBuilder.CreateSimpleContent(contentType);
var content2 = ContentBuilder.CreateSimpleContent(contentType);
DocumentRepository.Save(content1);
DocumentRepository.Save(content2);
var repository = CreateRepository(provider);
Tag[] tags1 = { new Tag { Text = "tag1", Group = "test" } };
repository.Assign(
content1.Id,
contentType.PropertyTypes.First().Id,
tags1,
false);
// Note the casing is different from tags1, but both should be considered equivalent
Tag[] tags2 = { new Tag { Text = "TAG1", Group = "test" } };
repository.Assign(
content2.Id,
contentType.PropertyTypes.First().Id,
tags2,
false);
// The template should have only one tag, despite case differences
Assert.AreEqual(1, repository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, "tag1").Count());
}
}
[Test] [Test]
public void Can_Append_Tag_Relations() public void Can_Append_Tag_Relations()
{ {

View File

@@ -7,66 +7,52 @@ using Umbraco.Cms.Imaging.ImageSharp.Media;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Media; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Media;
/// <summary>
/// Contains tests for all parameters for image generation options.
/// </summary>
[TestFixture] [TestFixture]
public class ImageSharpImageUrlGeneratorTests public class ImageSharpImageUrlGeneratorTests
{ {
private const string MediaPath = "/media/1005/img_0671.jpg"; private const string MediaPath = "/media/1005/img_0671.jpg";
private static readonly ImageUrlGenerationOptions.CropCoordinates s_crop = new(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); private static readonly ImageUrlGenerationOptions.CropCoordinates _sCrop = new(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m);
private static readonly ImageUrlGenerationOptions.FocalPointPosition _sFocus = new(0.96m, 0.80827067669172936m);
private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus1 = new(0.96m, 0.80827067669172936m); private static readonly ImageSharpImageUrlGenerator _sGenerator = new(Array.Empty<string>());
private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus2 = new(0.4275m, 0.41m);
private static readonly ImageSharpImageUrlGenerator s_generator = new(new string[0]);
/// <summary>
/// Tests that the media path is returned if no options are provided.
/// </summary>
[Test] [Test]
public void GetImageUrl_CropAliasTest() public void GivenMediaPath_AndNoOptions_ReturnsMediaPath()
{ {
var urlString = var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath));
s_generator.GetImageUrl( Assert.AreEqual(MediaPath, actual);
new ImageUrlGenerationOptions(MediaPath) { Crop = s_crop, Width = 100, Height = 100 });
Assert.AreEqual(
MediaPath + "?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100",
urlString);
} }
/// <summary>
/// Test that if options is null, the generated image URL is also null.
/// </summary>
[Test] [Test]
public void GetImageUrl_WidthHeightTest() public void GivenNullOptions_ReturnsNull()
{ {
var urlString = var actual = _sGenerator.GetImageUrl(null);
s_generator.GetImageUrl( Assert.IsNull(actual);
new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 200, Height = 300 });
Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300", urlString);
} }
/// <summary>
/// Test that if a null image url is given, null is returned.
/// </summary>
[Test] [Test]
public void GetImageUrl_FocalPointTest() public void GivenNullImageUrl_ReturnsNull()
{ {
var urlString = var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(null));
s_generator.GetImageUrl( Assert.IsNull(actual);
new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 100, Height = 100 });
Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=100&height=100", urlString);
}
[Test]
public void GetImageUrlFurtherOptionsTest()
{
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
FocalPoint = s_focus1,
Width = 200,
Height = 300,
FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff",
});
Assert.AreEqual(
MediaPath +
"?rxy=0.96,0.80827067669172936&width=200&height=300&filter=comic&roundedcorners=radius-26%7Cbgcolor-fff",
urlString);
} }
[Test] [Test]
public void GetImageUrlFurtherOptionsModeAndQualityTest() public void GetImageUrlFurtherOptionsModeAndQualityTest()
{ {
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) var urlString = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{ {
Quality = 10, Quality = 10,
FurtherOptions = "format=webp", FurtherOptions = "format=webp",
@@ -80,7 +66,7 @@ public class ImageSharpImageUrlGeneratorTests
[Test] [Test]
public void GetImageUrlFurtherOptionsWithModeAndQualityTest() public void GetImageUrlFurtherOptionsWithModeAndQualityTest()
{ {
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) var urlString = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{ {
FurtherOptions = "quality=10&format=webp", FurtherOptions = "quality=10&format=webp",
}); });
@@ -91,169 +77,171 @@ public class ImageSharpImageUrlGeneratorTests
} }
/// <summary> /// <summary>
/// Test that if options is null, the generated image URL is also null. /// Test that if an empty string image url is given, null is returned.
/// </summary> /// </summary>
[Test] [Test]
public void GetImageUrlNullOptionsTest() public void GivenEmptyStringImageUrl_ReturnsEmptyString()
{ {
var urlString = s_generator.GetImageUrl(null); var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty));
Assert.AreEqual(null, urlString); Assert.AreEqual(actual, string.Empty);
} }
/// <summary> /// <summary>
/// Test that if the image URL is null, the generated image URL is also null. /// Tests the correct query string is returned when given a crop.
/// </summary> /// </summary>
[Test] [Test]
public void GetImageUrlNullTest() public void GivenCrop_ReturnsExpectedQueryString()
{ {
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null)); const string expected = "?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386";
Assert.AreEqual(null, urlString); var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) { Crop = _sCrop });
Assert.AreEqual(expected, actual);
} }
/// <summary> /// <summary>
/// Test that if the image URL is empty, the generated image URL is empty. /// Tests the correct query string is returned when given a width.
/// </summary> /// </summary>
[Test] [Test]
public void GetImageUrlEmptyTest() public void GivenWidth_ReturnsExpectedQueryString()
{ {
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)); const string expected = "?width=200";
Assert.AreEqual(string.Empty, urlString); var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) { Width = 200 });
Assert.AreEqual(expected, actual);
} }
/// <summary> /// <summary>
/// Test the GetImageUrl method on the ImageCropDataSet Model /// Tests the correct query string is returned when given a height.
/// </summary> /// </summary>
[Test] [Test]
public void GetBaseCropUrlFromModelTest() public void GivenHeight_ReturnsExpectedQueryString()
{ {
var urlString = const string expected = "?height=200";
s_generator.GetImageUrl( var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) { Height = 200 });
new ImageUrlGenerationOptions(string.Empty) { Crop = s_crop, Width = 100, Height = 100 }); Assert.AreEqual(expected, actual);
Assert.AreEqual(
"?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100",
urlString);
} }
/// <summary> /// <summary>
/// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop /// Tests the correct query string is returned when provided a focal point.
/// </summary> /// </summary>
[Test] [Test]
public void GetImageUrl_SpecifiedCropModeTest() public void GivenFocalPoint_ReturnsExpectedQueryString()
{ {
var urlStringMin = const string expected = "?rxy=0.96,0.80827067669172936";
s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) { FocalPoint = _sFocus });
{ Assert.AreEqual(expected, actual);
ImageCropMode = ImageCropMode.Min,
Width = 300,
Height = 150,
});
var urlStringBoxPad =
s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
ImageCropMode = ImageCropMode.BoxPad,
Width = 300,
Height = 150,
});
var urlStringPad =
s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
ImageCropMode = ImageCropMode.Pad,
Width = 300,
Height = 150,
});
var urlStringMax =
s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
ImageCropMode = ImageCropMode.Max,
Width = 300,
Height = 150,
});
var urlStringStretch =
s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
ImageCropMode = ImageCropMode.Stretch,
Width = 300,
Height = 150,
});
Assert.AreEqual(MediaPath + "?rmode=min&width=300&height=150", urlStringMin);
Assert.AreEqual(MediaPath + "?rmode=boxpad&width=300&height=150", urlStringBoxPad);
Assert.AreEqual(MediaPath + "?rmode=pad&width=300&height=150", urlStringPad);
Assert.AreEqual(MediaPath + "?rmode=max&width=300&height=150", urlStringMax);
Assert.AreEqual(MediaPath + "?rmode=stretch&width=300&height=150", urlStringStretch);
} }
/// <summary> /// <summary>
/// Test for upload property type /// Tests the correct query string is returned when given further options.
/// There are a few edge case inputs here to ensure thorough testing in future versions.
/// </summary> /// </summary>
[Test] [TestCase("&filter=comic&roundedcorners=radius-26%7Cbgcolor-fff", "?filter=comic&roundedcorners=radius-26%7Cbgcolor-fff")]
public void GetImageUrl_UploadTypeTest() [TestCase("testoptions", "?testoptions=")]
[TestCase("&&&should=strip", "?should=strip")]
[TestCase("should=encode&$^%()", "?should=encode&$%5E%25()=")]
public void GivenFurtherOptions_ReturnsExpectedQueryString(string input, string expected)
{ {
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)
{ {
ImageCropMode = ImageCropMode.Crop, FurtherOptions = input,
ImageCropAnchor = ImageCropAnchor.Center,
Width = 100,
Height = 270,
}); });
Assert.AreEqual(MediaPath + "?rmode=crop&ranchor=center&width=100&height=270", urlString); Assert.AreEqual(expected, actual);
} }
/// <summary> /// <summary>
/// Test for preferFocalPoint when focal point is centered /// Test that the correct query string is returned for all image crop modes.
/// </summary> /// </summary>
[Test] [TestCase(ImageCropMode.Min, "?rmode=min")]
public void GetImageUrl_PreferFocalPointCenter() [TestCase(ImageCropMode.BoxPad, "?rmode=boxpad")]
[TestCase(ImageCropMode.Pad, "?rmode=pad")]
[TestCase(ImageCropMode.Max, "?rmode=max")]
[TestCase(ImageCropMode.Stretch, "?rmode=stretch")]
public void GivenCropMode_ReturnsExpectedQueryString(ImageCropMode cropMode, string expectedQueryString)
{ {
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 300, Height = 150 }); var cropUrl = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)
Assert.AreEqual(MediaPath + "?width=300&height=150", urlString);
}
/// <summary>
/// Test to check if crop ratio is ignored if useCropDimensions is true
/// </summary>
[Test]
public void GetImageUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore()
{
var urlString =
s_generator.GetImageUrl(
new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus2, Width = 270, Height = 161 });
Assert.AreEqual(MediaPath + "?rxy=0.4275,0.41&width=270&height=161", urlString);
}
/// <summary>
/// Test to check result when only a width parameter is passed, effectivly a resize only
/// </summary>
[Test]
public void GetImageUrl_WidthOnlyParameter()
{
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 200 });
Assert.AreEqual(MediaPath + "?width=200", urlString);
}
/// <summary>
/// Test to check result when only a height parameter is passed, effectivly a resize only
/// </summary>
[Test]
public void GetImageUrl_HeightOnlyParameter()
{
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Height = 200 });
Assert.AreEqual(MediaPath + "?height=200", urlString);
}
/// <summary>
/// Test to check result when using a background color with padding
/// </summary>
[Test]
public void GetImageUrl_BackgroundColorParameter()
{
var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{ {
ImageCropMode = ImageCropMode.Pad, ImageCropMode = cropMode,
Width = 400,
Height = 400,
FurtherOptions = "&bgcolor=fff",
}); });
Assert.AreEqual(MediaPath + "?rmode=pad&width=400&height=400&bgcolor=fff", urlString);
Assert.AreEqual(expectedQueryString, cropUrl);
}
/// <summary>
/// Test that the correct query string is returned for all image crop anchors.
/// </summary>
[TestCase(ImageCropAnchor.Bottom, "?ranchor=bottom")]
[TestCase(ImageCropAnchor.BottomLeft, "?ranchor=bottomleft")]
[TestCase(ImageCropAnchor.BottomRight, "?ranchor=bottomright")]
[TestCase(ImageCropAnchor.Center, "?ranchor=center")]
[TestCase(ImageCropAnchor.Left, "?ranchor=left")]
[TestCase(ImageCropAnchor.Right, "?ranchor=right")]
[TestCase(ImageCropAnchor.Top, "?ranchor=top")]
[TestCase(ImageCropAnchor.TopLeft, "?ranchor=topleft")]
[TestCase(ImageCropAnchor.TopRight, "?ranchor=topright")]
public void GivenCropAnchor_ReturnsExpectedQueryString(ImageCropAnchor imageCropAnchor, string expectedQueryString)
{
var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)
{
ImageCropAnchor = imageCropAnchor,
});
Assert.AreEqual(expectedQueryString, actual);
}
/// <summary>
/// Tests that the quality query string always returns the input number regardless of value.
/// </summary>
[TestCase(int.MinValue)]
[TestCase(-50)]
[TestCase(0)]
[TestCase(50)]
[TestCase(int.MaxValue)]
public void GivenQuality_ReturnsExpectedQueryString(int quality)
{
var expected = "?quality=" + quality;
var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)
{
Quality = quality,
});
Assert.AreEqual(expected, actual);
}
/// <summary>
/// Tests that the correct query string is returned for cache buster.
/// There are some edge case tests here to ensure thorough testing in future versions.
/// </summary>
[TestCase("test-buster", "?rnd=test-buster")]
[TestCase("test-buster&&^-value", "?rnd=test-buster%26%26%5E-value")]
public void GivenCacheBusterValue_ReturnsExpectedQueryString(string input, string expected)
{
var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)
{
CacheBusterValue = input,
});
Assert.AreEqual(expected, actual);
}
/// <summary>
/// Tests that an expected query string is returned when all options are given.
/// This will be a good test to see if something breaks with ordering of query string parameters.
/// </summary>
[Test]
public void GivenAllOptions_ReturnsExpectedQueryString()
{
const string expected =
"/media/1005/img_0671.jpg?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&rxy=0.96,0.80827067669172936&rmode=stretch&ranchor=right&width=200&height=200&quality=50&more=options&rnd=buster";
var actual = _sGenerator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
Quality = 50,
Crop = _sCrop,
FocalPoint = _sFocus,
CacheBusterValue = "buster",
FurtherOptions = "more=options",
Height = 200,
Width = 200,
ImageCropAnchor = ImageCropAnchor.Right,
ImageCropMode = ImageCropMode.Stretch,
});
Assert.AreEqual(expected, actual);
} }
} }