Skip to content

Commit

Permalink
Fix #25782 Edit Contentlet: Create dropzone component
Browse files Browse the repository at this point in the history
* dev: create dot-drop-zone component

* dev: create dot drop zone value accessor directive

* dev: allow drop one file

* test: setup tests

* test: dot-drop-zone component

* dev: prevent DragOver behavior to allow drop event

* test: fix tests

* dev: emit dragStart and DrahStop events

* test: add tests for  directive

* dev: create dotDropZone Value Accesor Story

* dev: fix CodeQL warning

* dev: fix mimeType Validation

* dev: add addon-actions to story-book

* feedback: unify allowedExtensions & allowedMimeTypes

* feedback: emit error on drop

* feedback: rename dot-drop-zone @output

* feedback: rename @Ouput fileDrop to fileDropped

* dev: add validity object

* dev: add NG_VALIDATORS to the DotDropZoneValueAccessorDirective
  • Loading branch information
rjvelazco authored Aug 28, 2023
1 parent 7ad6f1d commit c7e15d2
Show file tree
Hide file tree
Showing 11 changed files with 754 additions and 3 deletions.
3 changes: 2 additions & 1 deletion core-web/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ module.exports = {
sourceLoaderOptions: null,
transcludeMarkdown: true
}
}
},
'@storybook/addon-actions'
],
stories: []
// uncomment the property below if you want to apply some webpack config globally
Expand Down
3 changes: 2 additions & 1 deletion core-web/apps/dotcms-ui/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ module.exports = {
'../src/**/*.stories.@(js|jsx|ts|tsx)',
'../../../libs/template-builder/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../../../libs/block-editor/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../../../libs/contenttype-fields/**/*.stories.@(js|jsx|ts|tsx|mdx)'
'../../../libs/contenttype-fields/**/*.stories.@(js|jsx|ts|tsx|mdx)',
'../../../libs/ui/**/*.stories.@(js|jsx|ts|tsx|mdx)'
],
addons: ['storybook-design-token', '@storybook/addon-essentials', ...rootMain.addons],
features: {
Expand Down
3 changes: 2 additions & 1 deletion core-web/apps/dotcms-ui/.storybook/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"../src/**/*",
"../../../**/template-builder/**/src/lib/**/*.stories.ts",
"../../../**/block-editor/**/src/lib/**/*.stories.ts",
"../../../**/contenttype-fields/**/src/lib/**/*.stories.ts"
"../../../**/contenttype-fields/**/src/lib/**/*.stories.ts",
"../../../**/ui/**/src/lib/**/*.stories.ts"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator';
import { MockComponent } from 'ng-mocks';

import { forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';

import { DotDropZoneValueAccessorDirective } from './dot-drop-zone-value-accessor.directive';

import { DotDropZoneComponent } from '../../dot-drop-zone.component';

describe('DotDropZoneValueAccessorDirective', () => {
let spectator: SpectatorDirective<DotDropZoneValueAccessorDirective>;

const createDirective = createDirectiveFactory({
directive: DotDropZoneValueAccessorDirective,
declarations: [MockComponent(DotDropZoneComponent)],
providers: [
{
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DotDropZoneValueAccessorDirective)
}
]
});

describe('Used with dot-drop-zone component', () => {
beforeEach(() => {
spectator = createDirective(`
<dot-drop-zone dotDropZoneValueAccessor>
<div id="dot-drop-zone__content" class="dot-drop-zone__content">
Content
</div>
</dot-drop-zone>`);
});

it('should create', () => {
expect(spectator.directive).toBeTruthy();
});
});

describe('Used without dot-drop-zone component', () => {
it('should throw error if not inside dot-drop-zone', () => {
expect(() => {
spectator = createDirective(`
<div dotDropZoneValueAccessor>
<div id="dot-drop-zone__content" class="dot-drop-zone__content">
Content
</div>
</div>`);
}).toThrowError(
'dot-drop-zone-value-accessor can only be used inside of a dot-drop-zone'
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { action } from '@storybook/addon-actions';
import { moduleMetadata, Story, Meta } from '@storybook/angular';

import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';

import { DotDropZoneValueAccessorDirective } from './dot-drop-zone-value-accessor.directive';

import { DotDropZoneComponent } from '../../dot-drop-zone.component';

/**
* This component is used to test the value accessor directive on Storybook
*
* @class DotDropZoneValueAccessorTestComponent
* @implements {OnInit}
*/
@Component({
selector: 'dot-drop-zone-value-accessor',
styles: [
`
.dot-drop-zone__content {
width: 100%;
height: 200px;
background: #f8f9fa;
display: flex;
justify-content: center;
flex-direction: column;
gap: 1rem;
align-items: center;
border: 1px dashed #ced4da;
border-radius: 5px;
}
`
],
template: `
<form [formGroup]="myForm">
<dot-drop-zone
[accept]="accept"
[maxFileSize]="maxFileSize"
formControlName="file"
dotDropZoneValueAccessor
>
<div class="dot-drop-zone__content" id="dot-drop-zone__content">
Drop files here.
<div *ngIf="accept.length"><strong>Allowed Type:</strong> {{ accept }}</div>
<div *ngIf="maxFileSize"><strong>Max File Size:</strong> {{ maxFileSize }}</div>
</div>
</dot-drop-zone>
</form>
`
})
class DotDropZoneValueAccessorTestComponent implements OnInit {
@Input() accept: string[];
@Input() maxFileSize: number;

@Output() formChanged = new EventEmitter();
@Output() formErrors = new EventEmitter();

myForm: FormGroup;

constructor(private fb: FormBuilder) {}

ngOnInit() {
this.myForm = this.fb.group({
file: null
});

this.myForm.valueChanges.subscribe((value) => {
// eslint-disable-next-line no-console
this.formChanged.emit(value);

if (this.myForm.invalid) {
this.formErrors.emit(this.myForm.errors);
}
});
}
}

export default {
title: 'Library/ui/Components/DropZone/ValueAccessor',
component: DotDropZoneValueAccessorTestComponent,
decorators: [
moduleMetadata({
imports: [FormsModule, ReactiveFormsModule, CommonModule, DotDropZoneComponent],
declarations: [DotDropZoneValueAccessorDirective]
})
],
parameters: {
// https://storybook.js.org/docs/6.5/angular/essentials/actions#action-event-handlers
actions: {
// detect if the component is emitting the correct HTML events
handles: ['formChanged', 'formErrors']
}
}
} as Meta<DotDropZoneComponent>;

const Template: Story<DotDropZoneComponent> = (args: DotDropZoneComponent) => ({
props: {
...args,
// https://storybook.js.org/docs/6.5/angular/essentials/actions#action-args
formChanged: action('formChanged'),
formErrors: action('formErrors')
},
template: `
<dot-drop-zone-value-accessor
[accept]="accept"
[maxFileSize]="maxFileSize"
(formChanged)="formChanged($event)"
(formErrors)="formErrors($event)"
></dot-drop-zone-value-accessor>
`
});

export const Base = Template.bind({});

Base.args = {
accept: [],
maxFileSize: 1000000
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Directive, Host, OnInit, Optional, forwardRef } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
Validator,
NG_VALIDATORS,
ValidationErrors
} from '@angular/forms';

import { DotDropZoneComponent, DropZoneFileEvent } from '../../dot-drop-zone.component';

@Directive({
selector: '[dotDropZoneValueAccessor]',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DotDropZoneValueAccessorDirective),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DotDropZoneValueAccessorDirective),
multi: true
}
]
})
export class DotDropZoneValueAccessorDirective implements ControlValueAccessor, Validator, OnInit {
private onChange: (value: File) => void;
private onTouched: () => void;

constructor(@Optional() @Host() private _dotDropZone: DotDropZoneComponent) {
if (!this._dotDropZone) {
throw new Error(
'dot-drop-zone-value-accessor can only be used inside of a dot-drop-zone'
);
}
}

ngOnInit() {
this._dotDropZone.fileDropped.subscribe(({ file }: DropZoneFileEvent) => {
this.onChange(file); // Only File
this.onTouched();
});
}

writeValue(_value: unknown) {
/*
We can set a value here by doing this._dotDropZone.setFile(value), if needed
*/
}

registerOnChange(fn: (value: unknown) => void) {
this.onChange = fn;
}

registerOnTouched(fn: () => void) {
this.onTouched = fn;
}

validate(_control: AbstractControl): ValidationErrors | null {
const validity = this._dotDropZone.validity;

if (validity.valid) {
return null;
}

const errors = Object.entries(validity).reduce((acc, [key, value]) => {
if (value === true) {
acc[key] = value;
}

return acc;
}, {});

return errors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<ng-content></ng-content>
Empty file.
Loading

0 comments on commit c7e15d2

Please sign in to comment.