Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a2a500b3 | ||
|
|
4b016317f9 | ||
|
|
1720692d3d | ||
|
|
fc815db80b | ||
|
|
98e0615338 | ||
|
|
26907f202f | ||
|
|
85176d1bf6 | ||
|
|
131c9cda6f | ||
|
|
f7854b8c95 |
+4
-1
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("File extension not allowed")
|
||||
.WithDetail("The file extension is not allowed.")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("The provided file name is not valid")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Key already used")
|
||||
.WithDetail("The specified key is already used.")
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the levels to truncate a date to.
|
||||
/// </summary>
|
||||
public enum DateTruncate
|
||||
{
|
||||
Year,
|
||||
@@ -25,33 +28,39 @@ public static class DateTimeExtensions
|
||||
public static string ToIsoString(this DateTime dt) =>
|
||||
dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Truncates the date to the specified level, i.e. if you pass in DateTruncate.Hour it will truncate the date to the hour.
|
||||
/// </summary>
|
||||
/// <param name="dt">The date.</param>
|
||||
/// <param name="truncateTo">The level to truncate the date to.</param>
|
||||
/// <returns>The truncated date.</returns>
|
||||
public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo)
|
||||
{
|
||||
if (truncateTo == DateTruncate.Year)
|
||||
{
|
||||
return new DateTime(dt.Year, 1, 1);
|
||||
return new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Month)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, 1);
|
||||
return new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Day)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day);
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Hour)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0);
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind);
|
||||
}
|
||||
|
||||
if (truncateTo == DateTruncate.Minute)
|
||||
{
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0);
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
|
||||
}
|
||||
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
|
||||
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ public enum TemporaryFileOperationStatus
|
||||
FileExtensionNotAllowed = 1,
|
||||
KeyAlreadyUsed = 2,
|
||||
NotFound = 3,
|
||||
UploadBlocked
|
||||
UploadBlocked = 4,
|
||||
InvalidFileName = 5,
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null);
|
||||
}
|
||||
|
||||
|
||||
await using Stream dataStream = createModel.OpenReadStream();
|
||||
dataStream.Seek(0, SeekOrigin.Begin);
|
||||
if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false)
|
||||
@@ -53,13 +52,12 @@ internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.UploadBlocked, null);
|
||||
}
|
||||
|
||||
|
||||
temporaryFileModel = new TemporaryFileModel
|
||||
{
|
||||
Key = createModel.Key,
|
||||
FileName = createModel.FileName,
|
||||
OpenReadStream = createModel.OpenReadStream,
|
||||
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime)
|
||||
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime),
|
||||
};
|
||||
|
||||
await _temporaryFileRepository.SaveAsync(temporaryFileModel);
|
||||
@@ -68,17 +66,29 @@ internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
}
|
||||
|
||||
private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel)
|
||||
=> IsAllowedFileExtension(temporaryFileModel) == false
|
||||
? TemporaryFileOperationStatus.FileExtensionNotAllowed
|
||||
: TemporaryFileOperationStatus.Success;
|
||||
|
||||
private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel)
|
||||
{
|
||||
var extension = Path.GetExtension(temporaryFileModel.FileName)[1..];
|
||||
if (IsAllowedFileExtension(temporaryFileModel.FileName) == false)
|
||||
{
|
||||
return TemporaryFileOperationStatus.FileExtensionNotAllowed;
|
||||
}
|
||||
|
||||
if (IsValidFileName(temporaryFileModel.FileName) == false)
|
||||
{
|
||||
return TemporaryFileOperationStatus.InvalidFileName;
|
||||
}
|
||||
|
||||
return TemporaryFileOperationStatus.Success;
|
||||
}
|
||||
|
||||
private bool IsAllowedFileExtension(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName)[1..];
|
||||
return _contentSettings.IsFileAllowedForUpload(extension);
|
||||
}
|
||||
|
||||
private static bool IsValidFileName(string fileName) =>
|
||||
!string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
|
||||
|
||||
public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key)
|
||||
{
|
||||
TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"version": "15.3.0-rc",
|
||||
"version": "15.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"version": "15.3.0-rc",
|
||||
"version": "15.3.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./src/packages/*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "15.3.0-rc",
|
||||
"version": "15.3.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
|
||||
@@ -374,8 +374,6 @@ export default {
|
||||
fileSecurityValidationFailure: 'One or more file security validations have failed',
|
||||
moveToSameFolderFailed: 'Parent and destination folders cannot be the same',
|
||||
uploadNotAllowed: 'Upload is not allowed in this location.',
|
||||
noticeExtensionsServerOverride:
|
||||
'Regardless of the allowed file types, the following limitations apply system-wide due to the server configuration:',
|
||||
},
|
||||
member: {
|
||||
'2fa': 'Two-Factor Authentication',
|
||||
@@ -899,7 +897,6 @@ export default {
|
||||
retrieve: 'Retrieve',
|
||||
retry: 'Retry',
|
||||
rights: 'Permissions',
|
||||
serverConfiguration: 'Server Configuration',
|
||||
scheduledPublishing: 'Scheduled Publishing',
|
||||
umbracoInfo: 'Umbraco info',
|
||||
search: 'Search',
|
||||
|
||||
+2
-1
@@ -48,8 +48,9 @@ export class UmbContextProviderController<
|
||||
|
||||
public override destroy(): void {
|
||||
if (this.#host) {
|
||||
this.#host.removeUmbController(this);
|
||||
const host = this.#host;
|
||||
(this.#host as unknown) = undefined;
|
||||
host.removeUmbController(this);
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
|
||||
// Note we are not removing the event listener in the hostDisconnected, therefor we do it here [NL].
|
||||
this.#eventTarget?.removeEventListener(UMB_CONTEXT_REQUEST_EVENT_TYPE, this.#handleContextRequest);
|
||||
this.#eventTarget?.removeEventListener(UMB_DEBUG_CONTEXT_EVENT_TYPE, this.#handleDebugContextRequest);
|
||||
// We want to call a destroy method on the instance, if it has one.
|
||||
(this.#instance as any)?.destroy?.();
|
||||
// We do not want to call a destroy method on the instance, because maybe it should be re-provided later on. [NL]
|
||||
this.#instance = undefined;
|
||||
(this.#eventTarget as unknown) = undefined;
|
||||
}
|
||||
|
||||
+6
-2
@@ -173,12 +173,13 @@ export class UmbPropertyEditorUIBlockListElement
|
||||
this.observe(
|
||||
this.#managerContext.layouts,
|
||||
(layouts) => {
|
||||
const validationMessagesToRemove: string[] = [];
|
||||
const contentKeys = layouts.map((x) => x.contentKey);
|
||||
this.#validationContext.messages.getMessagesOfPathAndDescendant('$.contentData').forEach((message) => {
|
||||
// get the KEY from this string: $.contentData[?(@.key == 'KEY')]
|
||||
const key = extractJsonQueryProps(message.path).key;
|
||||
if (key && contentKeys.indexOf(key) === -1) {
|
||||
this.#validationContext.messages.removeMessageByKey(message.key);
|
||||
validationMessagesToRemove.push(message.key);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -187,9 +188,12 @@ export class UmbPropertyEditorUIBlockListElement
|
||||
// get the key from this string: $.settingsData[?(@.key == 'KEY')]
|
||||
const key = extractJsonQueryProps(message.path).key;
|
||||
if (key && settingsKeys.indexOf(key) === -1) {
|
||||
this.#validationContext.messages.removeMessageByKey(message.key);
|
||||
validationMessagesToRemove.push(message.key);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the messages after the loop to prevent changing the array while iterating over it.
|
||||
this.#validationContext.messages.removeMessageByKeys(validationMessagesToRemove);
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@ export class UmbLocalizationRegistry {
|
||||
if (!translations.length) return;
|
||||
|
||||
if (diff.length) {
|
||||
const filteredTranslations = translations.filter((t) => diff.some((ext) => ext.meta.culture === t.$code));
|
||||
const filteredTranslations = translations.filter((t) =>
|
||||
diff.some((ext) => ext.meta.culture.toLowerCase() === t.$code),
|
||||
);
|
||||
umbLocalizationManager.registerManyLocalizations(filteredTranslations);
|
||||
}
|
||||
|
||||
|
||||
+39
@@ -33,6 +33,26 @@ export class UmbValidationMessagesManager {
|
||||
this.messages.subscribe((x) => console.log(logName, x));
|
||||
}
|
||||
|
||||
getMessages(): Array<UmbValidationMessage> {
|
||||
return this.#messages.getValue();
|
||||
}
|
||||
|
||||
#updateLock = 0;
|
||||
initiateChange() {
|
||||
this.#updateLock++;
|
||||
this.#messages.mute();
|
||||
// TODO: When ready enable this code will enable handling a finish automatically by this implementation 'using myState.initiatePropertyValueChange()' (Relies on TS support of Using) [NL]
|
||||
/*return {
|
||||
[Symbol.dispose]: this.finishPropertyValueChange,
|
||||
};*/
|
||||
}
|
||||
finishChange() {
|
||||
this.#updateLock--;
|
||||
if (this.#updateLock === 0) {
|
||||
this.#messages.unmute();
|
||||
}
|
||||
}
|
||||
|
||||
getHasAnyMessages(): boolean {
|
||||
return this.#messages.getValue().length !== 0;
|
||||
}
|
||||
@@ -75,7 +95,9 @@ export class UmbValidationMessagesManager {
|
||||
if (this.#messages.getValue().find((x) => x.type === type && x.path === path && x.body === body)) {
|
||||
return;
|
||||
}
|
||||
this.initiateChange();
|
||||
this.#messages.appendOne({ type, key, path, body: body });
|
||||
this.finishChange();
|
||||
}
|
||||
|
||||
addMessages(type: UmbValidationMessageType, path: string, bodies: Array<string>): void {
|
||||
@@ -86,27 +108,42 @@ export class UmbValidationMessagesManager {
|
||||
const newBodies = bodies.filter(
|
||||
(message) => existingMessages.find((x) => x.type === type && x.path === path && x.body === message) === undefined,
|
||||
);
|
||||
this.initiateChange();
|
||||
this.#messages.append(newBodies.map((body) => ({ type, key: UmbId.new(), path, body })));
|
||||
this.finishChange();
|
||||
}
|
||||
|
||||
removeMessageByKey(key: string): void {
|
||||
this.initiateChange();
|
||||
this.#messages.removeOne(key);
|
||||
this.finishChange();
|
||||
}
|
||||
removeMessageByKeys(keys: Array<string>): void {
|
||||
if (keys.length === 0) return;
|
||||
this.initiateChange();
|
||||
this.#messages.filter((x) => keys.indexOf(x.key) === -1);
|
||||
this.finishChange();
|
||||
}
|
||||
removeMessagesByType(type: UmbValidationMessageType): void {
|
||||
this.initiateChange();
|
||||
this.#messages.filter((x) => x.type !== type);
|
||||
this.finishChange();
|
||||
}
|
||||
removeMessagesByPath(path: string): void {
|
||||
this.initiateChange();
|
||||
this.#messages.filter((x) => x.path !== path);
|
||||
this.finishChange();
|
||||
}
|
||||
removeMessagesAndDescendantsByPath(path: string): void {
|
||||
this.initiateChange();
|
||||
this.#messages.filter((x) => MatchPathOrDescendantPath(x.path, path));
|
||||
this.finishChange();
|
||||
}
|
||||
removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void {
|
||||
//path = path.toLowerCase();
|
||||
this.initiateChange();
|
||||
this.#messages.filter((x) => !(x.type === type && x.path === path));
|
||||
this.finishChange();
|
||||
}
|
||||
|
||||
#translatePath(path: string): string | undefined {
|
||||
@@ -124,6 +161,7 @@ export class UmbValidationMessagesManager {
|
||||
|
||||
#translators: Array<UmbValidationMessageTranslator> = [];
|
||||
addTranslator(translator: UmbValidationMessageTranslator): void {
|
||||
this.initiateChange();
|
||||
if (this.#translators.indexOf(translator) === -1) {
|
||||
this.#translators.push(translator);
|
||||
}
|
||||
@@ -137,6 +175,7 @@ export class UmbValidationMessagesManager {
|
||||
this.#messages.updateOne(msg.key, { path: newPath });
|
||||
}
|
||||
}
|
||||
this.finishChange();
|
||||
}
|
||||
|
||||
removeTranslator(translator: UmbValidationMessageTranslator): void {
|
||||
|
||||
+7
-5
@@ -35,8 +35,6 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
|
||||
}
|
||||
});
|
||||
this.#control = formControl;
|
||||
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
|
||||
this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
@@ -82,12 +80,18 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
|
||||
|
||||
override hostConnected(): void {
|
||||
super.hostConnected();
|
||||
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
|
||||
this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
|
||||
if (this.#context) {
|
||||
this.#context.addValidator(this);
|
||||
}
|
||||
}
|
||||
override hostDisconnected(): void {
|
||||
super.hostDisconnected();
|
||||
if (this.#control) {
|
||||
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
|
||||
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
|
||||
}
|
||||
if (this.#context) {
|
||||
this.#context.removeValidator(this);
|
||||
// Remove any messages that this validator has added:
|
||||
@@ -99,11 +103,9 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
|
||||
}
|
||||
|
||||
override destroy(): void {
|
||||
super.destroy();
|
||||
if (this.#control) {
|
||||
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
|
||||
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
|
||||
this.#control = undefined as any;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
+33
-3
@@ -122,11 +122,12 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
this.observe(
|
||||
parent.messages.messagesOfPathAndDescendant(dataPath),
|
||||
(msgs) => {
|
||||
this.messages.initiateChange();
|
||||
//this.messages.appendMessages(msgs);
|
||||
if (this.#parentMessages) {
|
||||
// Remove the local messages that does not exist in the parent anymore:
|
||||
const toRemove = this.#parentMessages.filter((msg) => !msgs.find((m) => m.key === msg.key));
|
||||
this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key));
|
||||
this.messages.removeMessageByKeys(toRemove.map((msg) => msg.key));
|
||||
}
|
||||
this.#parentMessages = msgs;
|
||||
msgs.forEach((msg) => {
|
||||
@@ -139,6 +140,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
// Notice, the local message uses the same key. [NL]
|
||||
this.messages.addMessage(msg.type, path, msg.body, msg.key);
|
||||
});
|
||||
this.messages.finishChange();
|
||||
},
|
||||
'observeParentMessages',
|
||||
);
|
||||
@@ -147,6 +149,9 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
this.messages.messages,
|
||||
(msgs) => {
|
||||
if (!this.#parent) return;
|
||||
|
||||
this.#parent!.messages.initiateChange();
|
||||
|
||||
//this.messages.appendMessages(msgs);
|
||||
if (this.#localMessages) {
|
||||
// Remove the parent messages that does not exist locally anymore:
|
||||
@@ -165,6 +170,8 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
// Notice, the parent message uses the same key. [NL]
|
||||
this.#parent!.messages.addMessage(msg.type, path, msg.body, msg.key);
|
||||
});
|
||||
|
||||
this.#parent!.messages.finishChange();
|
||||
},
|
||||
'observeLocalMessages',
|
||||
);
|
||||
@@ -172,6 +179,19 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
// Notice skipHost ^^, this is because we do not want it to consume it self, as this would be a match for this consumption, instead we will look at the parent and above. [NL]
|
||||
}
|
||||
|
||||
override hostConnected(): void {
|
||||
super.hostConnected();
|
||||
if (this.#parent) {
|
||||
this.#parent.addValidator(this);
|
||||
}
|
||||
}
|
||||
override hostDisconnected(): void {
|
||||
super.hostDisconnected();
|
||||
if (this.#parent) {
|
||||
this.#parent.removeValidator(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this context is valid.
|
||||
* Notice this does not verify the validity.
|
||||
@@ -229,18 +249,27 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
() => false,
|
||||
);
|
||||
|
||||
if (!this.messages) {
|
||||
/*if (this.#validators.length === 0 && resultsStatus === false) {
|
||||
throw new Error('No validators to validate, but validation failed');
|
||||
}*/
|
||||
|
||||
if (this.messages === undefined) {
|
||||
// This Context has been destroyed while is was validating, so we should not continue.
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const hasMessages = this.messages.getHasAnyMessages();
|
||||
|
||||
// If we have any messages then we are not valid, otherwise lets check the validation results: [NL]
|
||||
// This enables us to keep client validations though UI is not present anymore — because the client validations got defined as messages. [NL]
|
||||
const isValid = this.messages.getHasAnyMessages() ? false : resultsStatus;
|
||||
const isValid = hasMessages ? false : resultsStatus;
|
||||
|
||||
this.#isValid = isValid;
|
||||
|
||||
if (isValid === false) {
|
||||
/*if (hasMessages === false && resultsStatus === false) {
|
||||
throw new Error('Missing validation messages to represent why a child validation context is invalid.');
|
||||
}*/
|
||||
// Focus first invalid element:
|
||||
this.focusFirstInvalidElement();
|
||||
return Promise.reject();
|
||||
@@ -278,6 +307,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
|
||||
}
|
||||
|
||||
override destroy(): void {
|
||||
this.#providerCtrl?.destroy();
|
||||
this.#providerCtrl = undefined;
|
||||
if (this.#parent) {
|
||||
this.#parent.removeValidator(this);
|
||||
|
||||
+54
-36
@@ -4,8 +4,13 @@
|
||||
* @param {string} path - the JSON path to the value that should be found
|
||||
* @returns {unknown} - the found value.
|
||||
*/
|
||||
export function GetValueByJsonPath(data: unknown, path: string): unknown {
|
||||
export function GetValueByJsonPath<ReturnType = unknown>(data: unknown, path: string): ReturnType | undefined {
|
||||
if (path === '$') return data as ReturnType;
|
||||
// strip $ from the path:
|
||||
if (path.startsWith('$[')) {
|
||||
return _GetNextArrayEntryFromPath(data as Array<unknown>, path.slice(2));
|
||||
}
|
||||
|
||||
const strippedPath = path.startsWith('$.') ? path.slice(2) : path;
|
||||
// get value from the path:
|
||||
return GetNextPropertyValueFromPath(data, strippedPath);
|
||||
@@ -33,49 +38,62 @@ function GetNextPropertyValueFromPath(data: any, path: string): any {
|
||||
const value = data[key];
|
||||
// if there is no rest of the path, return the value:
|
||||
if (rest === undefined) return value;
|
||||
|
||||
// if the value is an array, get the value at the index:
|
||||
if (Array.isArray(value)) {
|
||||
// get the value until the next ']', the value can be anything in between the brackets:
|
||||
const lookupEnd = rest.match(/\]/);
|
||||
if (!lookupEnd) return undefined;
|
||||
// get everything before the match:
|
||||
const entryPointer = rest.slice(0, lookupEnd.index);
|
||||
|
||||
// check if the entryPointer is a JSON Path Filter ( starting with ?( and ending with ) ):
|
||||
if (entryPointer.startsWith('?(') && entryPointer.endsWith(')')) {
|
||||
// get the filter from the entryPointer:
|
||||
// get the filter as a function:
|
||||
const jsFilter = JsFilterFromJsonPathFilter(entryPointer);
|
||||
// find the index of the value that matches the filter:
|
||||
const index = value.findIndex(jsFilter[0]);
|
||||
// if the index is -1, return undefined:
|
||||
if (index === -1) return undefined;
|
||||
// get the value at the index:
|
||||
const data = value[index];
|
||||
// Check for safety:
|
||||
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= rest.length) {
|
||||
return data;
|
||||
}
|
||||
// continue with the rest of the path:
|
||||
return GetNextPropertyValueFromPath(data, rest.slice(lookupEnd.index + 2)) ?? data;
|
||||
} else {
|
||||
// get the value at the index:
|
||||
const indexAsNumber = parseInt(entryPointer);
|
||||
if (isNaN(indexAsNumber)) return undefined;
|
||||
const data = value[indexAsNumber];
|
||||
// Check for safety:
|
||||
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= rest.length) {
|
||||
return data;
|
||||
}
|
||||
// continue with the rest of the path:
|
||||
return GetNextPropertyValueFromPath(data, rest.slice(lookupEnd.index + 2)) ?? data;
|
||||
}
|
||||
return _GetNextArrayEntryFromPath(value, rest);
|
||||
} else {
|
||||
// continue with the rest of the path:
|
||||
return GetNextPropertyValueFromPath(value, rest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} array - object to traverse for the value.
|
||||
* @param {string} path - the JSON path to the value that should be found, notice without the starting '['
|
||||
* @returns {unknown} - the found value.
|
||||
*/
|
||||
function _GetNextArrayEntryFromPath(array: Array<any>, path: string): any {
|
||||
if (!array) return undefined;
|
||||
|
||||
// get the value until the next ']', the value can be anything in between the brackets:
|
||||
const lookupEnd = path.match(/\]/);
|
||||
if (!lookupEnd) return undefined;
|
||||
// get everything before the match:
|
||||
const entryPointer = path.slice(0, lookupEnd.index);
|
||||
|
||||
// check if the entryPointer is a JSON Path Filter ( starting with ?( and ending with ) ):
|
||||
if (entryPointer.startsWith('?(') && entryPointer.endsWith(')')) {
|
||||
// get the filter from the entryPointer:
|
||||
// get the filter as a function:
|
||||
const jsFilter = JsFilterFromJsonPathFilter(entryPointer);
|
||||
// find the index of the value that matches the filter:
|
||||
const index = array.findIndex(jsFilter[0]);
|
||||
// if the index is -1, return undefined:
|
||||
if (index === -1) return undefined;
|
||||
// get the value at the index:
|
||||
const entryData = array[index];
|
||||
// Check for safety:
|
||||
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= path.length) {
|
||||
return entryData;
|
||||
}
|
||||
// continue with the rest of the path:
|
||||
return GetNextPropertyValueFromPath(entryData, path.slice(lookupEnd.index + 2)) ?? entryData;
|
||||
} else {
|
||||
// get the value at the index:
|
||||
const indexAsNumber = parseInt(entryPointer);
|
||||
if (isNaN(indexAsNumber)) return undefined;
|
||||
const entryData = array[indexAsNumber];
|
||||
// Check for safety:
|
||||
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= path.length) {
|
||||
return entryData;
|
||||
}
|
||||
// continue with the rest of the path:
|
||||
return GetNextPropertyValueFromPath(entryData, path.slice(lookupEnd.index + 2)) ?? entryData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filter - A JSON Query, limited to filtering features. Do not support other JSON PATH Query features.
|
||||
* @returns {Array<(queryFilter: any) => boolean>} - An array of methods that returns true if the given items property value matches the value of the query.
|
||||
|
||||
@@ -2,6 +2,14 @@ import { expect } from '@open-wc/testing';
|
||||
import { GetValueByJsonPath } from './json-path.function.js';
|
||||
|
||||
describe('UmbJsonPathFunctions', () => {
|
||||
it('retrieves root when path is root', () => {
|
||||
const data = { value: 'test' };
|
||||
const result = GetValueByJsonPath(data, '$') as any;
|
||||
|
||||
expect(result).to.eq(data);
|
||||
expect(result.value).to.eq('test');
|
||||
});
|
||||
|
||||
it('retrieve property value', () => {
|
||||
const result = GetValueByJsonPath({ value: 'test' }, '$.value');
|
||||
|
||||
@@ -34,4 +42,10 @@ describe('UmbJsonPathFunctions', () => {
|
||||
|
||||
expect(result).to.eq('test');
|
||||
});
|
||||
|
||||
it('query of array in root', () => {
|
||||
const result = GetValueByJsonPath([{ id: '123', value: 'test' }], "$[?(@.id == '123')].value");
|
||||
|
||||
expect(result).to.eq('test');
|
||||
});
|
||||
});
|
||||
|
||||
+6
@@ -19,4 +19,10 @@ describe('ReplaceStartOfPath', () => {
|
||||
|
||||
expect(result).to.eq('$');
|
||||
});
|
||||
|
||||
it('replaces the root character with root character', () => {
|
||||
const result = ReplaceStartOfPath('$.start.test', '$', '$');
|
||||
|
||||
expect(result).to.eq('$.start.test');
|
||||
});
|
||||
});
|
||||
|
||||
+13
-3
@@ -74,7 +74,6 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
|
||||
* @returns Promise that resolves to void when the validation is complete.
|
||||
*/
|
||||
public async validate(): Promise<Array<void>> {
|
||||
//return this.validation.validate();
|
||||
return Promise.all(this.#validationContexts.map((context) => context.validate()));
|
||||
}
|
||||
|
||||
@@ -97,7 +96,15 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
|
||||
async () => {
|
||||
onValid().then(this.#completeSubmit, this.#rejectSubmit);
|
||||
},
|
||||
async () => {
|
||||
async (/*error*/) => {
|
||||
/*if (error) {
|
||||
throw new Error(error);
|
||||
}*/
|
||||
// TODO: Implement developer-mode logging here. [NL]
|
||||
console.warn(
|
||||
'Validation failed because of these validation messages still begin present: ',
|
||||
this.#validationContexts.flatMap((x) => x.messages.getMessages()),
|
||||
);
|
||||
onInvalid().then(this.#resolveSubmit, this.#rejectSubmit);
|
||||
},
|
||||
);
|
||||
@@ -105,7 +112,10 @@ export abstract class UmbSubmittableWorkspaceContextBase<WorkspaceDataModelType>
|
||||
return this.#submitPromise;
|
||||
}
|
||||
|
||||
#rejectSubmit = () => {
|
||||
#rejectSubmit = (/*error: any*/) => {
|
||||
/*if (error) {
|
||||
throw new Error(error);
|
||||
}*/
|
||||
if (this.#submitPromise) {
|
||||
// TODO: Capture the validation contexts messages on open, and then reset to them in this case. [NL]
|
||||
|
||||
|
||||
+6
-4
@@ -11,15 +11,16 @@ import type {
|
||||
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
import '../components/input-multi-url/index.js';
|
||||
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
|
||||
/**
|
||||
* @element umb-property-editor-ui-multi-url-picker
|
||||
*/
|
||||
@customElement('umb-property-editor-ui-multi-url-picker')
|
||||
export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
@property({ type: Array })
|
||||
value: Array<UmbLinkPickerLink> = [];
|
||||
|
||||
export class UmbPropertyEditorUIMultiUrlPickerElement
|
||||
extends UmbFormControlMixin<Array<UmbLinkPickerLink>, typeof UmbLitElement, undefined>(UmbLitElement)
|
||||
implements UmbPropertyEditorUiElement
|
||||
{
|
||||
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
|
||||
if (!config) return;
|
||||
|
||||
@@ -81,6 +82,7 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl
|
||||
this,
|
||||
);
|
||||
}
|
||||
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-multi-url')!);
|
||||
}
|
||||
|
||||
#onChange(event: CustomEvent & { target: UmbInputMultiUrlElement }) {
|
||||
|
||||
+5
-46
@@ -1,6 +1,5 @@
|
||||
import { UmbPropertyEditorUIMultipleTextStringElement } from '../multiple-text-string/property-editor-ui-multiple-text-string.element.js';
|
||||
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { formatBytes } from '@umbraco-cms/backoffice/utils';
|
||||
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file';
|
||||
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
|
||||
import type { UmbTemporaryFileConfigurationModel } from '@umbraco-cms/backoffice/temporary-file';
|
||||
@@ -40,7 +39,8 @@ export class UmbPropertyEditorUIAcceptedUploadTypesElement
|
||||
}
|
||||
|
||||
#addValidators(config: UmbTemporaryFileConfigurationModel) {
|
||||
this._inputElement?.addValidator(
|
||||
const inputElement = this.shadowRoot?.querySelector('umb-input-multiple-text-string');
|
||||
inputElement?.addValidator(
|
||||
'badInput',
|
||||
() => {
|
||||
let message = this.localize.term('validation_invalidExtensions');
|
||||
@@ -53,7 +53,7 @@ export class UmbPropertyEditorUIAcceptedUploadTypesElement
|
||||
return message;
|
||||
},
|
||||
() => {
|
||||
const extensions = this._inputElement?.items;
|
||||
const extensions = inputElement?.items;
|
||||
if (!extensions) return false;
|
||||
if (
|
||||
config.allowedUploadedFileExtensions.length &&
|
||||
@@ -69,49 +69,8 @@ export class UmbPropertyEditorUIAcceptedUploadTypesElement
|
||||
);
|
||||
}
|
||||
|
||||
#renderAcceptedTypes() {
|
||||
if (!this._acceptedTypes.length && !this._disallowedTypes.length && !this._maxFileSize) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<uui-box id="notice" headline=${this.localize.term('general_serverConfiguration')}>
|
||||
<p><umb-localize key="media_noticeExtensionsServerOverride"></umb-localize></p>
|
||||
${when(
|
||||
this._acceptedTypes.length,
|
||||
() => html`
|
||||
<p>
|
||||
<umb-localize key="validation_allowedExtensions"></umb-localize>
|
||||
<strong>${this._acceptedTypes.join(', ')}</strong>
|
||||
</p>
|
||||
`,
|
||||
)}
|
||||
${when(
|
||||
this._disallowedTypes.length,
|
||||
() => html`
|
||||
<p>
|
||||
<umb-localize key="validation_disallowedExtensions"></umb-localize>
|
||||
<strong>${this._disallowedTypes.join(', ')}</strong>
|
||||
</p>
|
||||
`,
|
||||
)}
|
||||
${when(
|
||||
this._maxFileSize,
|
||||
() => html`
|
||||
<p>
|
||||
${this.localize.term('media_maxFileSize')}
|
||||
<strong title="${this.localize.number(this._maxFileSize!)} bytes"
|
||||
>${formatBytes(this._maxFileSize!, { decimals: 2 })}</strong
|
||||
>.
|
||||
</p>
|
||||
`,
|
||||
)}
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`${this.#renderAcceptedTypes()} ${super.render()}`;
|
||||
return html`${super.render()}`;
|
||||
}
|
||||
|
||||
static override readonly styles = [
|
||||
|
||||
+17
-44
@@ -1,12 +1,8 @@
|
||||
import { customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
|
||||
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { umbBindToValidation, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
|
||||
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
|
||||
import {
|
||||
UMB_SUBMITTABLE_WORKSPACE_CONTEXT,
|
||||
UmbSubmittableWorkspaceContextBase,
|
||||
} from '@umbraco-cms/backoffice/workspace';
|
||||
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
import type { UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/components';
|
||||
import type {
|
||||
@@ -18,10 +14,10 @@ import type {
|
||||
* @element umb-property-editor-ui-multiple-text-string
|
||||
*/
|
||||
@customElement('umb-property-editor-ui-multiple-text-string')
|
||||
export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
@property({ type: Array })
|
||||
value?: Array<string>;
|
||||
|
||||
export class UmbPropertyEditorUIMultipleTextStringElement
|
||||
extends UmbFormControlMixin<Array<string>, typeof UmbLitElement, undefined>(UmbLitElement)
|
||||
implements UmbPropertyEditorUiElement
|
||||
{
|
||||
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
|
||||
if (!config) return;
|
||||
|
||||
@@ -65,23 +61,12 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
|
||||
@state()
|
||||
private _max = Infinity;
|
||||
|
||||
@query('#input', true)
|
||||
protected _inputElement?: UmbInputMultipleTextStringElement;
|
||||
|
||||
protected _validationContext = new UmbValidationContext(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
|
||||
this._label = context.getLabel();
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (context) => {
|
||||
if (context instanceof UmbSubmittableWorkspaceContextBase) {
|
||||
context.addValidationContext(this._validationContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override firstUpdated() {
|
||||
@@ -91,6 +76,7 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
|
||||
this,
|
||||
);
|
||||
}
|
||||
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-multiple-text-string')!);
|
||||
}
|
||||
|
||||
#onChange(event: UmbChangeEvent) {
|
||||
@@ -100,31 +86,18 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement
|
||||
this.dispatchEvent(new UmbPropertyValueChangeEvent());
|
||||
}
|
||||
|
||||
// Prevent valid events from bubbling outside the message element
|
||||
#onValid(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Prevent invalid events from bubbling outside the message element
|
||||
#onInvalid(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<umb-form-validation-message id="validation-message" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
|
||||
<umb-input-multiple-text-string
|
||||
id="input"
|
||||
max=${this._max}
|
||||
min=${this._min}
|
||||
.items=${this.value ?? []}
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
?required=${this.required}
|
||||
@change=${this.#onChange}
|
||||
${umbBindToValidation(this)}>
|
||||
</umb-input-multiple-text-string>
|
||||
</umb-form-validation-message>
|
||||
<umb-input-multiple-text-string
|
||||
max=${this._max}
|
||||
min=${this._min}
|
||||
.items=${this.value ?? []}
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
?required=${this.required}
|
||||
@change=${this.#onChange}
|
||||
${umbBindToValidation(this)}>
|
||||
</umb-input-multiple-text-string>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -61,9 +61,8 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, '
|
||||
}
|
||||
|
||||
override set value(newValue: FormDataEntryValue | FormData) {
|
||||
if (newValue === this.value) return;
|
||||
super.value = newValue;
|
||||
const newContent = typeof newValue === 'string' ? newValue : '';
|
||||
super.value = newContent;
|
||||
|
||||
if (this.#editorRef && this.#editorRef.getContent() != newContent) {
|
||||
this.#editorRef.setContent(newContent);
|
||||
|
||||
+18
-23
@@ -1,6 +1,6 @@
|
||||
import type { UmbInputTinyMceElement } from '../../components/input-tiny-mce/input-tiny-mce.element.js';
|
||||
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbPropertyEditorUiRteElementBase, UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '@umbraco-cms/backoffice/rte';
|
||||
import { UmbPropertyEditorUiRteElementBase } from '@umbraco-cms/backoffice/rte';
|
||||
|
||||
import '../../components/input-tiny-mce/input-tiny-mce.element.js';
|
||||
|
||||
@@ -10,37 +10,32 @@ import '../../components/input-tiny-mce/input-tiny-mce.element.js';
|
||||
@customElement('umb-property-editor-ui-tiny-mce')
|
||||
export class UmbPropertyEditorUITinyMceElement extends UmbPropertyEditorUiRteElementBase {
|
||||
#onChange(event: CustomEvent & { target: UmbInputTinyMceElement }) {
|
||||
const value = typeof event.target.value === 'string' ? event.target.value : '';
|
||||
const markup = typeof event.target.value === 'string' ? event.target.value : '';
|
||||
|
||||
// If we don't get any markup clear the property editor value.
|
||||
if (value === '') {
|
||||
if (markup === '') {
|
||||
this.value = undefined;
|
||||
this._fireChangeEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone the DOM, to remove the classes and attributes on the original:
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = value;
|
||||
|
||||
// Loop through used, to remove the classes on these.
|
||||
const blockEls = div.querySelectorAll(`umb-rte-block, umb-rte-block-inline`);
|
||||
blockEls.forEach((blockEl) => {
|
||||
blockEl.removeAttribute('contenteditable');
|
||||
blockEl.removeAttribute('class');
|
||||
});
|
||||
|
||||
const markup = div.innerHTML;
|
||||
|
||||
// Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup.
|
||||
//const blockElements = editor.dom.select(`umb-rte-block, umb-rte-block-inline`);
|
||||
const usedContentKeys = Array.from(blockEls).map((blockElement) =>
|
||||
blockElement.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY),
|
||||
);
|
||||
const usedContentKeys: string[] = [];
|
||||
|
||||
if (super.value) {
|
||||
super.value = {
|
||||
...super.value,
|
||||
// Regex matching all block elements in the markup, and extracting the content key. It's the same as the one used on the backend.
|
||||
const regex = new RegExp(
|
||||
/<umb-rte-block(?:-inline)?(?: class="(?:.[^"]*)")? data-content-key="(?<key>.[^"]*)">(?:<!--Umbraco-Block-->)?<\/umb-rte-block(?:-inline)?>/gi,
|
||||
);
|
||||
let blockElement: RegExpExecArray | null;
|
||||
while ((blockElement = regex.exec(markup)) !== null) {
|
||||
if (blockElement.groups?.key) {
|
||||
usedContentKeys.push(blockElement.groups.key);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
this.value = {
|
||||
...this.value,
|
||||
markup: markup,
|
||||
};
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem
|
||||
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
this.addFormControlElement(this.shadowRoot?.querySelector('umb-input-tiptap') as UmbInputTiptapElement);
|
||||
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-tiptap') as UmbInputTiptapElement);
|
||||
}
|
||||
|
||||
#onChange(event: CustomEvent & { target: UmbInputTiptapElement }) {
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
using Bogus.DataSets;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -23,8 +22,8 @@ public class ContentPublishingServiceTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
private const string UnknownCulture = "ke-Ke";
|
||||
|
||||
private readonly DateTime _schedulePublishDate = DateTime.UtcNow.AddDays(1);
|
||||
private readonly DateTime _scheduleUnPublishDate = DateTime.UtcNow.AddDays(2);
|
||||
private readonly DateTime _schedulePublishDate = DateTime.UtcNow.AddDays(1).TruncateTo(DateTimeExtensions.DateTruncate.Second);
|
||||
private readonly DateTime _scheduleUnPublishDate = DateTime.UtcNow.AddDays(2).TruncateTo(DateTimeExtensions.DateTruncate.Second);
|
||||
|
||||
[SetUp]
|
||||
public new void Setup() => ContentRepositoryBase.ThrowOnWarning = true;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Attributes;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
|
||||
public class TemporaryFileServiceTests : UmbracoIntegrationTest
|
||||
{
|
||||
private ITemporaryFileService TemporaryFileService => GetRequiredService<ITemporaryFileService>();
|
||||
|
||||
public static void ConfigureAllowedUploadedFileExtensions(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<ContentSettings>(config =>
|
||||
config.AllowedUploadedFileExtensions = ["txt"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[ConfigureBuilder(ActionName = nameof(ConfigureAllowedUploadedFileExtensions))]
|
||||
public async Task Can_Create_Get_And_Delete_Temporary_File()
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
const string FileName = "test.txt";
|
||||
const string FileContents = "test";
|
||||
var model = new CreateTemporaryFileModel
|
||||
{
|
||||
FileName = FileName,
|
||||
Key = key,
|
||||
OpenReadStream = () =>
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write(FileContents);
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
var createAttempt = await TemporaryFileService.CreateAsync(model);
|
||||
Assert.IsTrue(createAttempt.Success);
|
||||
|
||||
TemporaryFileModel? fileModel = await TemporaryFileService.GetAsync(key);
|
||||
Assert.IsNotNull(fileModel);
|
||||
Assert.AreEqual(key, fileModel.Key);
|
||||
Assert.AreEqual(FileName, fileModel.FileName);
|
||||
|
||||
using (var reader = new StreamReader(fileModel.OpenReadStream()))
|
||||
{
|
||||
string fileContents = reader.ReadToEnd();
|
||||
Assert.AreEqual(FileContents, fileContents);
|
||||
}
|
||||
|
||||
var deleteAttempt = await TemporaryFileService.DeleteAsync(key);
|
||||
Assert.IsTrue(createAttempt.Success);
|
||||
|
||||
fileModel = await TemporaryFileService.GetAsync(key);
|
||||
Assert.IsNull(fileModel);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[ConfigureBuilder(ActionName = nameof(ConfigureAllowedUploadedFileExtensions))]
|
||||
public async Task Cannot_Create_File_Outside_Of_Temporary_Files_Root()
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
const string FileName = "../test.txt";
|
||||
var model = new CreateTemporaryFileModel
|
||||
{
|
||||
FileName = FileName,
|
||||
Key = key,
|
||||
OpenReadStream = () =>
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write(string.Empty);
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
var createAttempt = await TemporaryFileService.CreateAsync(model);
|
||||
Assert.IsFalse(createAttempt.Success);
|
||||
Assert.AreEqual(TemporaryFileOperationStatus.InvalidFileName, createAttempt.Status);
|
||||
}
|
||||
}
|
||||
@@ -527,7 +527,7 @@ public class LocksTests : UmbracoIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
[Retry(3)] // TODO make this test non-flaky.
|
||||
[NUnit.Framework.Ignore("This test is very flaky, and is stopping our nightlys")]
|
||||
[Test]
|
||||
public void Read_Lock_Waits_For_Write_Lock()
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "15.3.0-rc2",
|
||||
"version": "15.3.1",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user