Skip to content

Commit

Permalink
feat(edit-content): drag and drop functionality (#30135)
Browse files Browse the repository at this point in the history
### Parent Issue

#29872 

### Proposed Changes
* Create new component for Image & File component
* Create new state to handle drag and drop functionality
* Work in Preview component
* Upload file as dotAsset

### Checklist
- [x] Tests
- [x] Translations
- [x] Security Implications Contemplated (add notes if applicable)

### Screenshots


https://github.com/user-attachments/assets/90f5deb2-a416-4e7f-bfcd-db68d9d51a76
  • Loading branch information
nicobytes authored Sep 27, 2024
1 parent 62799e6 commit b72fb24
Show file tree
Hide file tree
Showing 32 changed files with 1,997 additions and 315 deletions.
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { createHttpFactory, SpectatorHttp, SpyObject, mockProvider } from '@ngneat/spectator/jest';
import { of } from 'rxjs';

import { HttpClientTestingModule } from '@angular/common/http/testing';
import { DotWorkflowActionsFireService } from '@dotcms/data-access';

import { DotUploadFileService } from './dot-upload-file.service';

import { DotUploadService } from '../dot-upload/dot-upload.service';

describe('DotUploadFileService', () => {
let spectator: SpectatorService<DotUploadFileService>;
let service: DotUploadFileService;
let spectator: SpectatorHttp<DotUploadFileService>;
let dotWorkflowActionsFireService: SpyObject<DotWorkflowActionsFireService>;

const createService = createServiceFactory({
const createHttp = createHttpFactory({
service: DotUploadFileService,
imports: [HttpClientTestingModule],
providers: [DotUploadService]
providers: [DotUploadFileService, mockProvider(DotWorkflowActionsFireService)]
});

beforeEach(() => {
spectator = createService();
service = spectator.service;
spectator = createHttp();

dotWorkflowActionsFireService = spectator.inject(DotWorkflowActionsFireService);
});

it('should be created', () => {
expect(service).toBeTruthy();
expect(spectator.service).toBeTruthy();
});

describe('uploadDotAsset', () => {
it('should upload a file as a dotAsset', () => {
dotWorkflowActionsFireService.newContentlet.mockReturnValueOnce(
of({ entity: { identifier: 'test' } })
);

const file = new File([''], 'test.png', {
type: 'image/png'
});

spectator.service.uploadDotAsset(file).subscribe();

expect(dotWorkflowActionsFireService.newContentlet).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { from, Observable, throwError } from 'rxjs';

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';

import { catchError, pluck, switchMap } from 'rxjs/operators';

import { DotUploadService } from '@dotcms/data-access';
import { DotUploadService, DotWorkflowActionsFireService } from '@dotcms/data-access';
import { DotCMSContentlet, DotCMSTempFile } from '@dotcms/dotcms-models';

export enum FileStatus {
Expand All @@ -29,10 +29,10 @@ interface PublishContentProps {
*/
@Injectable()
export class DotUploadFileService {
constructor(
private http: HttpClient,
private dotUploadService: DotUploadService
) {}
readonly #BASE_URL = '/api/v1/workflow/actions/default';
readonly #httpClient = inject(HttpClient);
readonly #uploadService = inject(DotUploadService);
readonly #workflowActionsFireService = inject(DotWorkflowActionsFireService);

publishContent({
data,
Expand All @@ -59,17 +59,13 @@ export class DotUploadFileService {

statusCallback(FileStatus.IMPORT);

return this.http
.post(
'/api/v1/workflow/actions/default/fire/PUBLISH',
JSON.stringify({ contentlets }),
{
headers: {
Origin: window.location.hostname,
'Content-Type': 'application/json;charset=UTF-8'
}
return this.#httpClient
.post(`${this.#BASE_URL}/fire/PUBLISH`, JSON.stringify({ contentlets }), {
headers: {
Origin: window.location.hostname,
'Content-Type': 'application/json;charset=UTF-8'
}
)
})
.pipe(pluck('entity', 'results')) as Observable<DotCMSContentlet[]>;
}),
catchError((error) => throwError(error))
Expand All @@ -82,11 +78,27 @@ export class DotUploadFileService {
signal
}: PublishContentProps): Observable<DotCMSTempFile | DotCMSTempFile[]> {
return from(
this.dotUploadService.uploadFile({
this.#uploadService.uploadFile({
file,
maxSize,
signal
})
);
}

/**
* Uploads a file to dotCMS and creates a new dotAsset contentlet
* @param file the file to be uploaded
* @returns an Observable that emits the created contentlet
*/
uploadDotAsset(file: File) {
const formData = new FormData();
formData.append('file', file);

return this.#workflowActionsFireService.newContentlet<DotCMSContentlet>(
'dotAsset',
{ file: file.name },
formData
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,42 @@ describe('DotWorkflowActionsFireService', () => {
});
});

it('should SAVE and return a new contentlet with FormData', (done) => {
const mockResult = {
name: 'test'
};

const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });

const requestBody = {
contentlet: {
contentType: 'dotAsset',
file: file.name
}
};

const formData = new FormData();
formData.append('file', file);

spectator.service
.newContentlet('dotAsset', { file: file.name }, formData)
.subscribe((res) => {
expect(res).toEqual([mockResult]);
done();
});

const req = spectator.expectOne(
'/api/v1/workflow/actions/default/fire/NEW',
HttpMethod.PUT
);

expect(req.request.body.get('json')).toEqual(JSON.stringify(requestBody));

req.flush({
entity: [mockResult]
});
});

it('should EDIT and return the updated contentlet', (done) => {
const mockResult = {
inode: '123'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface DotActionRequestOptions {
data: { [key: string]: string };
action: ActionToFire;
individualPermissions?: { [key: string]: string[] };
formData?: FormData;
}

export interface DotFireActionOptions<T> {
Expand Down Expand Up @@ -86,8 +87,12 @@ export class DotWorkflowActionsFireService {
*
* @memberof DotWorkflowActionsFireService
*/
newContentlet<T>(contentType: string, data: { [key: string]: string }): Observable<T> {
return this.request<T>({ contentType, data, action: ActionToFire.NEW });
newContentlet<T>(
contentType: string,
data: { [key: string]: string },
formData?: FormData
): Observable<T> {
return this.request<T>({ contentType, data, action: ActionToFire.NEW, formData });
}

/**
Expand Down Expand Up @@ -171,21 +176,28 @@ export class DotWorkflowActionsFireService {
contentType,
data,
action,
individualPermissions
individualPermissions,
formData
}: DotActionRequestOptions): Observable<T> {
let url = `${this.BASE_URL}/actions/default/fire/${action}`;

const contentlet = contentType ? { contentType: contentType, ...data } : data;
const bodyRequest = individualPermissions
? { contentlet, individualPermissions }
: { contentlet };

if (data['inode']) {
url += `?inode=${data['inode']}`;
}

if (formData) {
formData.append('json', JSON.stringify(bodyRequest));
}

return this.httpClient
.put(
`${this.BASE_URL}/actions/default/fire/${action}${
data['inode'] ? `?inode=${data['inode']}` : ''
}`,
bodyRequest,
{ headers: this.defaultHeaders }
)
.put(url, formData ? formData : bodyRequest, {
headers: formData ? new HttpHeaders() : this.defaultHeaders
})
.pipe(take(1), pluck('entity'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface DotCMSContentlet {
url: string;
working: boolean;
body?: string;
content?: string;
contentTypeIcon?: string;
variant?: string;
__icon__?: string;
Expand Down
Loading

0 comments on commit b72fb24

Please sign in to comment.