Merge branch 'main' into v17/dev

This commit is contained in:
Andy Butland
2025-10-15 09:48:05 +02:00
13 changed files with 261 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
@@ -407,7 +407,7 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
}
// This this method gets aliases across documents, members, and media, so it covers it all
private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().Contains(alias);
private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().InvariantContains(alias);
private bool ContentTypeAliasCanBeUsedFor(string alias, Guid key)
{

View File

@@ -29,6 +29,14 @@ export const manifests: Array<ManifestUfmFilter> = [
api: () => import('./strip-html.filter.js'),
meta: { alias: 'strip-html' },
},
// TODO: Remove in V18 - replaced by camelCase alias below for UFMJS compatibility
{
type: 'ufmFilter',
alias: 'Umb.Filter.StripHtmlCamelCase',
name: 'Strip HTML UFM Filter (camelCase)',
api: () => import('./strip-html.filter.js'),
meta: { alias: 'stripHtml' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.TitleCase',
@@ -36,6 +44,14 @@ export const manifests: Array<ManifestUfmFilter> = [
api: () => import('./title-case.filter.js'),
meta: { alias: 'title-case' },
},
// TODO: Remove in V18 - replaced by camelCase alias below for UFMJS compatibility
{
type: 'ufmFilter',
alias: 'Umb.Filter.TitleCaseCamelCase',
name: 'Title Case UFM Filter (camelCase)',
api: () => import('./title-case.filter.js'),
meta: { alias: 'titleCase' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.Truncate',
@@ -57,4 +73,12 @@ export const manifests: Array<ManifestUfmFilter> = [
api: () => import('./word-limit.filter.js'),
meta: { alias: 'word-limit' },
},
// TODO: Remove in V18 - replaced by camelCase alias below for UFMJS compatibility
{
type: 'ufmFilter',
alias: 'Umb.Filter.WordLimitCamelCase',
name: 'Word Limit UFM Filter (camelCase)',
api: () => import('./word-limit.filter.js'),
meta: { alias: 'wordLimit' },
},
];

View File

@@ -0,0 +1,47 @@
import { expect } from '@open-wc/testing';
import { UmbUfmStripHtmlFilterApi } from './strip-html.filter.js';
describe('UmbUfmStripHtmlFilter', () => {
let filter: UmbUfmStripHtmlFilterApi;
beforeEach(() => {
filter = new UmbUfmStripHtmlFilterApi();
});
describe('filter', () => {
it('should strip HTML tags from string', () => {
const result = filter.filter('<p>Hello <strong>World</strong></p>');
expect(result).to.equal('Hello World');
});
it('should handle empty string', () => {
const result = filter.filter('');
expect(result).to.equal('');
});
it('should handle null input', () => {
const result = filter.filter(null);
expect(result).to.equal('');
});
it('should handle undefined input', () => {
const result = filter.filter(undefined);
expect(result).to.equal('');
});
it('should handle markup object', () => {
const result = filter.filter({ markup: '<p>Test</p>' });
expect(result).to.equal('Test');
});
it('should strip complex HTML', () => {
const result = filter.filter('<div><h1>Title</h1><p>Paragraph with <a href="#">link</a></p></div>');
expect(result).to.equal('TitleParagraph with link');
});
it('should handle plain text without HTML', () => {
const result = filter.filter('Plain text');
expect(result).to.equal('Plain text');
});
});
});

View File

@@ -13,3 +13,4 @@ class UmbUfmStripHtmlFilterApi extends UmbUfmFilterBase {
}
export { UmbUfmStripHtmlFilterApi as api };
export { UmbUfmStripHtmlFilterApi };

View File

@@ -0,0 +1,25 @@
{
"$schema": "../../umbraco-package-schema.json",
"name": "My.WelcomePackage",
"version": "0.1.0",
"extensions": [
{
"type": "dashboard",
"alias": "my.welcome.dashboard",
"name": "My Welcome Dashboard",
"element": "/App_Plugins/welcome-dashboard/welcome-dashboard.js",
"elementName": "my-welcome-dashboard",
"weight": 30,
"meta": {
"label": "Welcome Dashboard",
"pathname": "welcome-dashboard"
},
"conditions": [
{
"alias": "Umb.Condition.SectionAlias",
"match": "Umb.Section.Content"
}
]
}
]
}

View File

@@ -0,0 +1,38 @@
import { css as n, customElement as c, html as d } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement as p } from "@umbraco-cms/backoffice/lit-element";
var i = Object.getOwnPropertyDescriptor, h = (r, s, l, a) => {
for (var e = a > 1 ? void 0 : a ? i(s, l) : s, o = r.length - 1, m; o >= 0; o--)
(m = r[o]) && (e = m(e) || e);
return e;
};
let t = class extends p {
render() {
return d`
<h1>Welcome Dashboard</h1>
<div>
<p>
This is the Backoffice. From here, you can modify the content,
media, and settings of your website.
</p>
<p>© Sample Company 20XX</p>
</div>
`;
}
};
t.styles = [
n`
:host {
display: block;
padding: 24px;
}
`
];
t = h([
c("my-welcome-dashboard")
], t);
const b = t;
export {
t as MyWelcomeDashboardElement,
b as default
};
//# sourceMappingURL=welcome-dashboard.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"welcome-dashboard.js","sources":["../../welcome-dashboard/src/welcome-dashboard.element.ts"],"sourcesContent":["import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';\r\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\r\n\r\n@customElement('my-welcome-dashboard')\r\nexport class MyWelcomeDashboardElement extends UmbLitElement {\r\n\r\n override render() {\r\n return html`\r\n <h1>Welcome Dashboard</h1>\r\n <div>\r\n <p>\r\n This is the Backoffice. From here, you can modify the content,\r\n media, and settings of your website.\r\n </p>\r\n <p>© Sample Company 20XX</p>\r\n </div>\r\n `;\r\n }\r\n\r\n static override readonly styles = [\r\n css`\r\n :host {\r\n display: block;\r\n padding: 24px;\r\n }\r\n `,\r\n ];\r\n}\r\n\r\nexport default MyWelcomeDashboardElement;\r\n\r\ndeclare global {\r\n interface HTMLElementTagNameMap {\r\n 'my-welcome-dashboard': MyWelcomeDashboardElement;\r\n }\r\n}"],"names":["MyWelcomeDashboardElement","UmbLitElement","html","css","__decorateClass","customElement","MyWelcomeDashboardElement$1"],"mappings":";;;;;;;AAIO,IAAMA,IAAN,cAAwCC,EAAc;AAAA,EAEhD,SAAS;AACd,WAAOC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUX;AAUJ;AAvBaF,EAegB,SAAS;AAAA,EAC9BG;AAAA;AAAA;AAAA;AAAA;AAAA;AAMJ;AAtBSH,IAANI,EAAA;AAAA,EADNC,EAAc,sBAAsB;AAAA,GACxBL,CAAA;AAyBb,MAAAM,IAAeN;"}

View File

@@ -0,0 +1,24 @@
{
"$schema": "../../umbraco-package-schema.json",
"name": "My workspace",
"version": "0.1.0",
"extensions": [
{
"type": "workspaceView",
"alias": "My.WorkspaceView",
"name": "My Workspace View",
"element": "/App_Plugins/workspace-view/workspace-view.js",
"meta": {
"label": "My Workspace View",
"pathname": "/my-workspace-view",
"icon": "icon-add"
},
"conditions": [
{
"alias": "Umb.Condition.WorkspaceAlias",
"match": "Umb.Workspace.Document"
}
]
}
]
}

View File

@@ -0,0 +1,28 @@
import { LitElement as n, html as a, css as c, customElement as p } from "@umbraco-cms/backoffice/external/lit";
import { UmbElementMixin as u } from "@umbraco-cms/backoffice/element-api";
var w = Object.getOwnPropertyDescriptor, v = (o, s, i, l) => {
for (var e = l > 1 ? void 0 : l ? w(s, i) : s, r = o.length - 1, m; r >= 0; r--)
(m = o[r]) && (e = m(e) || e);
return e;
};
let t = class extends u(n) {
render() {
return a`
<uui-box headline="Workspace View">
Welcome to my newly created workspace view.
</uui-box>
`;
}
};
t.styles = c`
uui-box {
margin: 20px;
}
`;
t = v([
p("my-workspaceview")
], t);
export {
t as default
};
//# sourceMappingURL=workspace-view.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"workspace-view.js","sources":["../../workspace-view/src/my-element.ts"],"sourcesContent":["import { LitElement, html, customElement, css } from \"@umbraco-cms/backoffice/external/lit\";\nimport { UmbElementMixin } from \"@umbraco-cms/backoffice/element-api\";\n\n@customElement('my-workspaceview')\nexport default class MyWorkspaceViewElement extends UmbElementMixin(LitElement) {\n\n render() {\n return html` \n <uui-box headline=\"Workspace View\">\n Welcome to my newly created workspace view.\n </uui-box> \n `\n }\n\n static styles = css`\n uui-box {\n margin: 20px;\n }\n `\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'my-workspaceview': MyWorkspaceViewElement\n }\n}\n"],"names":["MyWorkspaceViewElement","UmbElementMixin","LitElement","html","css","__decorateClass","customElement"],"mappings":";;;;;;;AAIA,IAAqBA,IAArB,cAAoDC,EAAgBC,CAAU,EAAE;AAAA,EAE5E,SAAS;AACL,WAAOC;AAAA;AAAA;AAAA;AAAA;AAAA,EAKX;AAOJ;AAfqBH,EAUV,SAASI;AAAA;AAAA;AAAA;AAAA;AAVCJ,IAArBK,EAAA;AAAA,EADCC,EAAc,kBAAkB;AAAA,GACZN,CAAA;"}

View File

@@ -0,0 +1,22 @@
import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
// Dashboard
const dashboardName = 'Welcome Dashboard';
test('can see the custom dashboard in content section', async ({umbracoUi}) => {
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Assert
await umbracoUi.content.isDashboardTabWithNameVisible(dashboardName, true);
});
test('can not see the custom dashboard in media section', async ({umbracoUi}) => {
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.media);
// Assert
await umbracoUi.content.isDashboardTabWithNameVisible(dashboardName, false);
});

View File

@@ -0,0 +1,44 @@
import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
// Content
const contentName = 'TestContent';
// DocumentType
const documentTypeName = 'TestDocumentTypeForContent';
// DataType
const dataTypeName = 'Textstring';
// Media
const mediaName = 'TestMedia';
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.media.ensureNameNotExists(mediaName);
});
test('can see the custom workspace view in the content section', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, 'Test content', dataTypeName);
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Assert
await umbracoUi.content.isWorkspaceViewTabWithAliasVisible('My.WorkspaceView', true);
});
test('cannot see the custom workspace view in the media section', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.media.createDefaultMediaWithImage(mediaName);
// Act
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.media);
await umbracoUi.media.goToMediaWithName(mediaName);
// Assert
await umbracoUi.media.isWorkspaceViewTabWithAliasVisible('My.WorkspaceView', false);
});

View File

@@ -861,14 +861,15 @@ internal sealed partial class ContentTypeEditingServiceTests
Assert.AreEqual(ContentTypeOperationStatus.InvalidAlias, result.Status);
}
[Test]
public async Task Cannot_Use_Existing_Alias()
[TestCase("test")] // Matches alias case sensitively.
[TestCase("Test")] // Matches alias case insensitively.
public async Task Cannot_Use_Existing_Alias(string newAlias)
{
var createModel = ContentTypeCreateModel("Test", "test");
var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
createModel = ContentTypeCreateModel("Test 2", "test");
createModel = ContentTypeCreateModel("Test 2", newAlias);
result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentTypeOperationStatus.DuplicateAlias, result.Status);