Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c28e860
chore(mock): adds endpoint handler for allowed media types
iOvergaard Jan 22, 2025
15988c7
feat: adds new event `UmbDropzoneSubmittedEvent`
iOvergaard Jan 22, 2025
8926161
Merge branch 'v15/dev' into v15/bugfix/media-library-large-files
iOvergaard Jan 22, 2025
bbd9c5f
Merge remote-tracking branch 'origin/v15/dev' into v15/bugfix/media-l…
iOvergaard Jan 22, 2025
df1b698
Merge remote-tracking branch 'origin/v15/dev' into v15/bugfix/media-l…
iOvergaard Jan 24, 2025
67d9874
fix: do not await unnecessarily
iOvergaard Jan 24, 2025
aeef66d
fix: simplify error checking
iOvergaard Jan 24, 2025
39a3323
fix: only proceed if array contains elements
iOvergaard Jan 24, 2025
37582b7
feat: adds support to render an error state
iOvergaard Jan 24, 2025
1fb4dbf
fix: react to error state on temporary file badges
iOvergaard Jan 24, 2025
1e998c7
fix: cancel events and simplify error check and react to any status c…
iOvergaard Jan 24, 2025
a6ffd77
feat: adds new tryXhrRequest function
iOvergaard Jan 26, 2025
d0bf6f4
fix: use tryXhrRequest to upload all temporary files
iOvergaard Jan 26, 2025
d701874
fix: use error types from hey-api as a temporary solution
iOvergaard Jan 27, 2025
3f1c49a
Merge branch 'v15/dev' into v15/bugfix/media-library-large-files
iOvergaard Jan 27, 2025
c883a45
fix: changes limit from int32 to long (64-bit) to allow larger files …
iOvergaard Jan 27, 2025
bfbed24
fix: set default baseURL
iOvergaard Jan 27, 2025
1fabc9d
fix: use same unique
iOvergaard Jan 27, 2025
4ccba85
fix: do not overwrite status
iOvergaard Jan 27, 2025
35de74c
fix: adds progress callback for tinymce
iOvergaard Jan 27, 2025
3c723e0
generate openapi.json
iOvergaard Jan 27, 2025
1d5c839
Revert "generate openapi.json"
iOvergaard Jan 28, 2025
8a00021
Revert "fix: changes limit from int32 to long (64-bit) to allow large…
iOvergaard Jan 28, 2025
a79f547
chore: generate OpenApi.json
iOvergaard Jan 28, 2025
4176efe
Merge branch 'v15/dev' into v15/bugfix/media-library-large-files
iOvergaard Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/Umbraco.Cms.Api.Management/OpenApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -35978,14 +35978,11 @@
"mediaStartNodeIds",
"name",
"permissions",
"userGroupIds",
"userName"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"email": {
"type": "string"
},
Expand All @@ -35995,6 +35992,21 @@
"name": {
"type": "string"
},
"userGroupIds": {
"uniqueItems": true,
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ReferenceByIdModel"
}
]
}
},
"id": {
"type": "string",
"format": "uuid"
},
"languageIsoCode": {
"type": "string",
"nullable": true
Expand Down Expand Up @@ -37794,6 +37806,16 @@
"type": "string",
"format": "date-time",
"nullable": true
},
"scheduledPublishDate": {
"type": "string",
"format": "date-time",
"nullable": true
},
"scheduledUnpublishDate": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"additionalProperties": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export type UmbMockMediaTypeModel = MediaTypeResponseModel &
MediaTypeTreeItemResponseModel &
MediaTypeItemResponseModel;

export type UmbMockMediaTypeUnionModel =
| MediaTypeResponseModel
| MediaTypeTreeItemResponseModel
| MediaTypeItemResponseModel;

export const data: Array<UmbMockMediaTypeModel> = [
{
name: 'Media Type 1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import { UmbMockEntityFolderManager } from '../utils/entity/entity-folder.manage
import { UmbMockEntityTreeManager } from '../utils/entity/entity-tree.manager.js';
import { UmbMockEntityItemManager } from '../utils/entity/entity-item.manager.js';
import { UmbMockEntityDetailManager } from '../utils/entity/entity-detail.manager.js';
import type { UmbMockMediaTypeModel } from './media-type.data.js';
import type { UmbMockMediaTypeModel, UmbMockMediaTypeUnionModel } from './media-type.data.js';
import { data } from './media-type.data.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type {
AllowedMediaTypeModel,
CreateFolderRequestModel,
CreateMediaTypeRequestModel,
GetItemMediaTypeAllowedResponse,
MediaTypeItemResponseModel,
MediaTypeResponseModel,
MediaTypeSortModel,
MediaTypeTreeItemResponseModel,
PagedAllowedMediaTypeModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { umbDataTypeMockDb } from '../data-type/data-type.db.js';

class UmbMediaTypeMockDB extends UmbEntityMockDbBase<UmbMockMediaTypeModel> {
tree = new UmbMockEntityTreeManager<UmbMockMediaTypeModel>(this, mediaTypeTreeItemMapper);
Expand Down Expand Up @@ -45,6 +47,26 @@ class UmbMediaTypeMockDB extends UmbEntityMockDbBase<UmbMockMediaTypeModel> {
const mappedItems = mockItems.map((item) => allowedMediaTypeMapper(item));
return { items: mappedItems, total: mappedItems.length };
}

getAllowedByFileExtension(fileExtension: string): GetItemMediaTypeAllowedResponse {
const allowedTypes = this.data.filter((field) => {
const allProperties = field.properties.flat();

const fileUploadType = allProperties.find((prop) => prop.alias === 'umbracoFile');
if (!fileUploadType) return false;

const dataType = umbDataTypeMockDb.read(fileUploadType.dataType.id);
if (dataType?.editorAlias !== 'Umbraco.UploadField') return false;

const allowedFileExtensions = dataType.values.find((value) => value.alias === 'fileExtensions')?.value;
if (!allowedFileExtensions || !Array.isArray(allowedFileExtensions)) return false;

return allowedFileExtensions.includes(fileExtension);
});

const mappedTypes = allowedTypes.map(mediaTypeItemMapper);
return allowedExtensionMediaTypeMapper(mappedTypes, mappedTypes.length);
}
}

const createMockMediaTypeFolderMapper = (request: CreateFolderRequestModel): UmbMockMediaTypeModel => {
Expand Down Expand Up @@ -128,7 +150,7 @@ const mediaTypeTreeItemMapper = (item: UmbMockMediaTypeModel): MediaTypeTreeItem
};
};

const mediaTypeItemMapper = (item: UmbMockMediaTypeModel): MediaTypeItemResponseModel => {
const mediaTypeItemMapper = (item: UmbMockMediaTypeUnionModel): MediaTypeItemResponseModel => {
return {
id: item.id,
name: item.name,
Expand All @@ -145,4 +167,14 @@ const allowedMediaTypeMapper = (item: UmbMockMediaTypeModel): AllowedMediaTypeMo
};
};

const allowedExtensionMediaTypeMapper = (
items: Array<MediaTypeItemResponseModel>,
total: number,
): GetItemMediaTypeAllowedResponse => {
return {
items,
total,
};
};

export const umbMediaTypeMockDb = new UmbMediaTypeMockDB(data);
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ export const itemHandlers = [
const items = umbMediaTypeMockDb.item.getItems(ids);
return res(ctx.status(200), ctx.json(items));
}),

rest.get(umbracoPath(`/item${UMB_SLUG}/allowed`), (req, res, ctx) => {
const fileExtension = req.url.searchParams.get('fileExtension');
if (!fileExtension) return;

const response = umbMediaTypeMockDb.getAllowedByFileExtension(fileExtension);

return res(ctx.status(200), ctx.json(response));
}),
];
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@ export interface UmbDataSourceErrorResponse {
// TODO: we should not rely on the ApiError and CancelError types from the backend-api package
// We need to be able to return a generic error type that can be used in the frontend
// Example: the clipboard is getting is data from local storage, so it should not use the ApiError type
/**
* The error that occurred when fetching the data.
* The {ApiError} and {CancelError} types will change in the future to be a more generic error type.
*/
error?: ApiError | CancelError;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from './resource.controller.js';
export * from './tryExecute.function.js';
export * from './tryExecuteAndNotify.function.js';
export * from './tryXhrRequest.function.js';
export * from './extractUmbColorVariable.function.js';
export * from './apiTypeValidators.function.js';
export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { UMB_AUTH_CONTEXT } from '../auth/index.js';
import { isApiError, isCancelError, isCancelablePromise } from './apiTypeValidators.function.js';
import type { XhrRequestOptions } from './types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationOptions } from '@umbraco-cms/backoffice/notification';
import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
import {
ApiError,
CancelablePromise,
CancelError,
type ProblemDetails,
} from '@umbraco-cms/backoffice/external/backend-api';

export class UmbResourceController extends UmbControllerBase {
#promise: Promise<any>;
Expand Down Expand Up @@ -72,7 +78,7 @@ export class UmbResourceController extends UmbControllerBase {
// Cancelled - do nothing
return {};
} else {
console.group('ApiError caught in UmbResourceController');
console.groupCollapsed('ApiError caught in UmbResourceController');
console.error('Request failed', error.request);
console.error('Request body', error.body);
console.error('Error', error);
Expand Down Expand Up @@ -167,6 +173,117 @@ export class UmbResourceController extends UmbControllerBase {
return { data, error };
}

/**
* Make an XHR request.
* @param host The controller host for this controller to be appended to.
* @param options The options for the XHR request.
*/
static xhrRequest<T>(options: XhrRequestOptions): CancelablePromise<T> {
const baseUrl = options.baseUrl || '/umbraco';

const promise = new CancelablePromise<T>(async (resolve, reject, onCancel) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method, `${baseUrl}${options.url}`, true);

// Set default headers
if (options.token) {
const token = typeof options.token === 'function' ? await options.token() : options.token;
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
}

// Infer Content-Type header based on body type
if (options.body instanceof FormData) {
// Note: 'multipart/form-data' is automatically set by the browser for FormData
} else {
xhr.setRequestHeader('Content-Type', 'application/json');
}

// Set custom headers
if (options.headers) {
for (const [key, value] of Object.entries(options.headers)) {
xhr.setRequestHeader(key, value);
}
}

xhr.upload.onprogress = (event) => {
if (options.onProgress) {
options.onProgress(event);
}
};

xhr.onload = () => {
try {
if (xhr.status >= 200 && xhr.status < 300) {
if (options.responseHeader) {
const response = xhr.getResponseHeader(options.responseHeader);
resolve(response as T);
} else {
resolve(JSON.parse(xhr.responseText));
}
} else {
// TODO: [JOV] This has to be changed into our own error type, when we have a chance to introduce a breaking change in the future.
const error = new ApiError(
{
method: options.method,
url: `${baseUrl}${options.url}`,
},
{
body: xhr.responseText,
ok: false,
status: xhr.status,
statusText: xhr.statusText,
url: xhr.responseURL,
},
xhr.statusText,
);
reject(error);
}
} catch {
// This most likely happens when the response is not JSON
reject(new Error(`Failed to make request: ${xhr.statusText}`));
}
};

xhr.onerror = () => {
// TODO: [JOV] This has to be changed into our own error type, when we have a chance to introduce a breaking change in the future.
const error = new ApiError(
{
method: options.method,
url: `${baseUrl}${options.url}`,
},
{
body: xhr.responseText,
ok: false,
status: xhr.status,
statusText: xhr.statusText,
url: xhr.responseURL,
},
xhr.statusText,
);
reject(error);
};

if (!onCancel.isCancelled) {
// Handle body based on Content-Type
if (options.body instanceof FormData) {
xhr.send(options.body);
} else {
xhr.send(JSON.stringify(options.body));
}
}

onCancel(() => {
xhr.abort();
// TODO: [JOV] This has to be changed into our own error type, when we have a chance to introduce a breaking change in the future.
reject(new CancelError('Request was cancelled.'));
});
});

return promise;
}

/**
* Cancel all resources that are currently being executed by this controller if they are cancelable.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { UMB_AUTH_CONTEXT } from '../auth/auth.context.token.js';
import type { XhrRequestOptions } from './types.js';
import { UmbResourceController } from './resource.controller.js';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { OpenAPI, type CancelablePromise } from '@umbraco-cms/backoffice/external/backend-api';

/**
* Make an XHR request.
* @param host The controller host for this controller to be appended to.
* @param options The options for the XHR request.
*/
export function tryXhrRequest<T>(host: UmbControllerHost, options: XhrRequestOptions): CancelablePromise<T> {
return UmbResourceController.xhrRequest<T>({
...options,
baseUrl: OpenAPI.BASE,
async token() {
const contextConsumer = new UmbContextConsumerController(host, UMB_AUTH_CONTEXT).asPromise();
const authContext = await contextConsumer;
return authContext.getLatestToken();
},
});
}
10 changes: 10 additions & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface XhrRequestOptions {
baseUrl?: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
url: string;
body?: unknown;
token?: string | (() => string | Promise<string>);
headers?: Record<string, string>;
responseHeader?: string;
onProgress?: (event: ProgressEvent) => void;
}
Loading