From 8d072016c8f7c9a1ff9dda23bee8c25dac325ca7 Mon Sep 17 00:00:00 2001 From: Gui Seek Date: Fri, 29 Nov 2024 18:27:45 -0300 Subject: [PATCH] =?UTF-8?q?feat(account):=20adiciona=20habilidades=20para?= =?UTF-8?q?=20usu=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closed #76 --- apps/devmx/public/icons/drag/handle.svg | 3 + apps/devmx/public/icons/drag/indicator.svg | 3 + packages/account/data-source/package.json | 3 +- .../account/data-source/src/lib/dtos/index.ts | 1 + .../data-source/src/lib/dtos/update-user.ts | 18 +++- .../data-source/src/lib/dtos/user-skill.ts | 16 +++ .../account/data-source/src/lib/dtos/user.ts | 5 + .../data-source/src/lib/schemas/user.ts | 13 ++- .../about-user/about-user.container.html | 17 ++++ .../about-user/about-user.container.scss | 15 +++ .../about-user/about-user.container.ts | 2 + .../containers/account/account.container.html | 2 + .../containers/account/account.container.ts | 3 + .../account/skills/skills.component.html | 45 +++++++++ .../account/skills/skills.component.scss | 68 +++++++++++++ .../account/skills/skills.component.ts | 98 +++++++++++++++++++ .../feature-shell/src/lib/forms/index.ts | 2 + .../feature-shell/src/lib/forms/user-skill.ts | 45 +++++++++ .../feature-shell/src/lib/forms/user.ts | 6 ++ .../src/lib/dtos/editable-user-skill.ts | 4 + .../api-interfaces/src/lib/dtos/index.ts | 1 + .../api-interfaces/src/lib/entities/user.ts | 3 + .../ui-global/icon/src/lib/types/drag.ts | 3 + .../ui-global/icon/src/lib/types/icon.ts | 4 +- 24 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 apps/devmx/public/icons/drag/handle.svg create mode 100644 apps/devmx/public/icons/drag/indicator.svg create mode 100644 packages/account/data-source/src/lib/dtos/user-skill.ts create mode 100644 packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html create mode 100644 packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss create mode 100644 packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts create mode 100644 packages/account/feature-shell/src/lib/forms/user-skill.ts create mode 100644 packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts create mode 100644 packages/shared/ui-global/icon/src/lib/types/drag.ts diff --git a/apps/devmx/public/icons/drag/handle.svg b/apps/devmx/public/icons/drag/handle.svg new file mode 100644 index 00000000..7646b71e --- /dev/null +++ b/apps/devmx/public/icons/drag/handle.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/devmx/public/icons/drag/indicator.svg b/apps/devmx/public/icons/drag/indicator.svg new file mode 100644 index 00000000..870d64f8 --- /dev/null +++ b/apps/devmx/public/icons/drag/indicator.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/account/data-source/package.json b/packages/account/data-source/package.json index ebd15d07..b2928c13 100644 --- a/packages/account/data-source/package.json +++ b/packages/account/data-source/package.json @@ -15,7 +15,8 @@ "passport-jwt": "^4.0.1", "@nestjs/passport": "^10.0.3", "@devmx/location-data-source": "0.0.1", - "@devmx/shared-util-data": "0.0.1" + "@devmx/shared-util-data": "0.0.1", + "@devmx/learn-data-source": "0.0.1" }, "type": "commonjs", "main": "./src/index.js", diff --git a/packages/account/data-source/src/lib/dtos/index.ts b/packages/account/data-source/src/lib/dtos/index.ts index a55d07f7..6af05e3e 100644 --- a/packages/account/data-source/src/lib/dtos/index.ts +++ b/packages/account/data-source/src/lib/dtos/index.ts @@ -12,6 +12,7 @@ export * from './update-user'; export * from './user-contact'; export * from './user-profile'; export * from './user-roles'; +export * from './user-skill'; export * from './user-social'; export * from './user'; export * from './validate-user-code'; diff --git a/packages/account/data-source/src/lib/dtos/update-user.ts b/packages/account/data-source/src/lib/dtos/update-user.ts index 04187cb4..f32f5d62 100644 --- a/packages/account/data-source/src/lib/dtos/update-user.ts +++ b/packages/account/data-source/src/lib/dtos/update-user.ts @@ -1,8 +1,14 @@ -import { IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { + IsArray, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import { UpdateProfileDto } from './update-profile'; import { UpdateContactDto } from './update-contact'; import { Exclude, Type } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { UserSocialDto } from './user-social'; import { RolesDto } from './roles'; import { @@ -10,6 +16,7 @@ import { EditableUser, UserPassword, } from '@devmx/shared-api-interfaces'; +import { UserSkillDto } from './user-skill'; export class UpdateUserDto implements EditableUser { @ApiProperty() @@ -30,6 +37,13 @@ export class UpdateUserDto implements EditableUser { @Exclude() roles: RolesDto; + @IsArray() + @IsOptional() + @ValidateNested() + @Type(() => UserSkillDto) + @ApiPropertyOptional({ type: () => [UserSkillDto] }) + skills: UserSkillDto[]; + @ApiProperty() @ValidateNested() @Type(() => UpdateProfileDto) diff --git a/packages/account/data-source/src/lib/dtos/user-skill.ts b/packages/account/data-source/src/lib/dtos/user-skill.ts new file mode 100644 index 00000000..d568fbf6 --- /dev/null +++ b/packages/account/data-source/src/lib/dtos/user-skill.ts @@ -0,0 +1,16 @@ +import { UserSkill } from '@devmx/shared-api-interfaces'; +import { IsNotEmpty, IsNumber } from 'class-validator'; +import { SkillDto } from '@devmx/learn-data-source'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class UserSkillDto implements UserSkill { + @IsNumber() + @IsNotEmpty() + @ApiProperty() + weight: number; + + @Type(() => SkillDto) + @ApiProperty({ type: () => SkillDto }) + skill: SkillDto; +} diff --git a/packages/account/data-source/src/lib/dtos/user.ts b/packages/account/data-source/src/lib/dtos/user.ts index 201d28cc..9707c898 100644 --- a/packages/account/data-source/src/lib/dtos/user.ts +++ b/packages/account/data-source/src/lib/dtos/user.ts @@ -5,6 +5,7 @@ import { UserProfileDto } from './user-profile'; import { ApiProperty } from '@nestjs/swagger'; import { UserSocialDto } from './user-social'; import { RolesDto } from './roles'; +import { UserSkillDto } from './user-skill'; export class UserDto implements User { @ApiProperty() @@ -23,6 +24,10 @@ export class UserDto implements User { @ApiProperty({ type: () => RolesDto }) roles: RolesDto; + @Type(() => UserSkillDto) + @ApiProperty({ type: () => [UserSkillDto] }) + skills: UserSkillDto[]; + @ApiProperty() @Type(() => UserContactDto) contact: UserContactDto; diff --git a/packages/account/data-source/src/lib/schemas/user.ts b/packages/account/data-source/src/lib/schemas/user.ts index 679eb8e7..2ad8e295 100644 --- a/packages/account/data-source/src/lib/schemas/user.ts +++ b/packages/account/data-source/src/lib/schemas/user.ts @@ -3,9 +3,10 @@ import { UserContactCollection, UserContactSchema } from './user-contact'; import { UserProfileCollection, UserProfileSchema } from './user-profile'; import { UserSocialCollection, UserSocialSchema } from './user-social'; import { UserCodeCollection, UserCodeSchema } from './user-code'; -import { Roles, User } from '@devmx/shared-api-interfaces'; +import { Roles, User, UserSkill } from '@devmx/shared-api-interfaces'; import { createSchema } from '@devmx/shared-data-source'; import { DEFAULT_ROLES } from '@devmx/shared-util-data'; +import { SkillSchema } from '@devmx/learn-data-source'; import { Prop, raw, Schema } from '@nestjs/mongoose'; import { Document } from 'mongoose'; @@ -25,6 +26,16 @@ export class UserCollection extends Document implements User { @Prop({ required: true, type: Object, default: DEFAULT_ROLES }) roles: Roles; + @Prop([ + { + type: raw({ + skill: { type: SkillSchema, required: true }, + weight: { type: Number, required: true }, + }), + }, + ]) + skills: UserSkill[]; + @Prop({ required: true, type: raw(UserContactSchema) }) contact: UserContactCollection; diff --git a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html index 0899fdd5..7e44f01f 100644 --- a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html +++ b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.html @@ -11,6 +11,23 @@

{{'@' + user.name}}

} + + +@if (user.skills?.length) { + + +
    + @for (item of user.skills; track item.skill.id) { +
  1. +

    {{item.skill.name}}

    + +
  2. + } +
+
+
+} + } diff --git a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss index ade9d6c9..5fc59fd0 100644 --- a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss +++ b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.scss @@ -11,4 +11,19 @@ display: flex; flex-direction: column; } + + ol { + padding-left: 1.4em; + + li { + margin-bottom: 1em; + p { + margin: 0; + } + } + } +} + +::ng-deep ol li .mdc-linear-progress .mdc-linear-progress__bar-inner { + border-color: #3BCE53; } diff --git a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts index 215827bd..43e0c167 100644 --- a/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts +++ b/packages/account/feature-about/src/lib/containers/about-user/about-user.container.ts @@ -1,5 +1,6 @@ import { PresentationCardListComponent } from '@devmx/presentation-ui-shared'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { PresentationFacade } from '@devmx/presentation-data-access'; import { MarkdownComponent } from '@devmx/shared-ui-global/editor'; import { AuthenticationFacade } from '@devmx/account-data-access'; @@ -23,6 +24,7 @@ import { filter, map, take } from 'rxjs'; MarkdownComponent, PresentationCardListComponent, EventCardListComponent, + MatProgressBarModule, AsyncPipe, ], standalone: true, diff --git a/packages/account/feature-shell/src/lib/containers/account/account.container.html b/packages/account/feature-shell/src/lib/containers/account/account.container.html index 6ebc8433..23b55344 100644 --- a/packages/account/feature-shell/src/lib/containers/account/account.container.html +++ b/packages/account/feature-shell/src/lib/containers/account/account.container.html @@ -6,6 +6,8 @@ + +
diff --git a/packages/account/feature-shell/src/lib/containers/account/account.container.ts b/packages/account/feature-shell/src/lib/containers/account/account.container.ts index 33d26571..c4063aa8 100644 --- a/packages/account/feature-shell/src/lib/containers/account/account.container.ts +++ b/packages/account/feature-shell/src/lib/containers/account/account.container.ts @@ -2,6 +2,7 @@ import { SelectFileComponent } from '@devmx/shared-ui-global/image'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ProfileComponent } from './profile/profile.component'; import { ContactComponent } from './contact/contact.component'; +import { SkillsComponent } from './skills/skills.component'; import { SocialComponent } from './social/social.component'; import { UserPhoto, provideUserPhoto } from '../../dialogs'; import { MatButtonModule } from '@angular/material/button'; @@ -23,6 +24,7 @@ import { AuthenticationFacade, } from '@devmx/account-data-access'; + @Component({ selector: 'devmx-account', templateUrl: './account.container.html', @@ -35,6 +37,7 @@ import { ProfileComponent, ContactComponent, SocialComponent, + SkillsComponent, SelectFileComponent, MatButtonModule, AvatarComponent, diff --git a/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html new file mode 100644 index 00000000..60cd0f4b --- /dev/null +++ b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.html @@ -0,0 +1,45 @@ +
+
    + @for (item of form.skills.controls; track item.value.skill?.id; let index = + $index) { +
  1. + + +
    +
    + {{ item.value.skill?.name }} +
    + + + + +
    + + {{ item.value.weight }}% +
  2. + } +
+
+ + + Adicionar habilidade + + + @if (skillFacade.response$ | async; as response) { @for (option of + response.data; track option.id) { + {{ option.name }} + } } + + diff --git a/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss new file mode 100644 index 00000000..09bb0274 --- /dev/null +++ b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.scss @@ -0,0 +1,68 @@ +:host { + display: flex; + flex-direction: column; + + .skill-list { + padding: 0; + + gap: 1em; + display: flex; + flex-direction: column; + + devmx-icon { + cursor: grab; + + &:active { + cursor: grabbing; + } + } + + li { + gap: 1.2em; + display: flex; + align-items: center; + + & > div { + flex: 1; + display: flex; + flex-direction: column; + + div { + padding-left: 0.5em; + } + } + } + } + + .skills-list { + width: 500px; + max-width: 100%; + overflow: hidden; + + + li { + min-height: 3em; + box-sizing: border-box; + cursor: move; + } + } + + .cdk-drag-preview { + border: none; + box-sizing: border-box; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); + } + + .cdk-drag-placeholder { + opacity: 0; + } + + .cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + } + + .skills-list.cdk-drop-list-dragging .skills-list li:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + } +} diff --git a/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts new file mode 100644 index 00000000..7f9e88a3 --- /dev/null +++ b/packages/account/feature-shell/src/lib/containers/account/skills/skills.component.ts @@ -0,0 +1,98 @@ +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { inject, Component, ChangeDetectorRef } from '@angular/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSliderModule } from '@angular/material/slider'; +import { UserForm, UserSkillForm } from '../../../forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { SkillFacade } from '@devmx/learn-data-access'; +import { Skill } from '@devmx/shared-api-interfaces'; +import { debounceTime, filter } from 'rxjs'; +import { AsyncPipe } from '@angular/common'; +import { + FormControl, + ControlContainer, + ReactiveFormsModule, +} from '@angular/forms'; +import { IconComponent } from '@devmx/shared-ui-global/icon'; +import { + CdkDragDrop, + DragDropModule, + moveItemInArray, +} from '@angular/cdk/drag-drop'; + +@Component({ + selector: 'devmx-account-skills', + templateUrl: './skills.component.html', + styleUrl: './skills.component.scss', + viewProviders: [ + { + provide: ControlContainer, + useFactory: () => inject(ControlContainer, { skipSelf: true }), + }, + ], + imports: [ + MatAutocompleteModule, + MatProgressBarModule, + ReactiveFormsModule, + MatFormFieldModule, + MatButtonModule, + MatSliderModule, + DragDropModule, + MatInputModule, + MatListModule, + IconComponent, + AsyncPipe, + ], +}) +export class SkillsComponent { + container = inject(ControlContainer); + + skillFacade = inject(SkillFacade); + + cdr = inject(ChangeDetectorRef); + + get form() { + return this.container.control as UserForm; + } + + searchControl = new FormControl(''); + + constructor() { + this.searchControl.valueChanges + .pipe( + filter((value) => typeof value === 'string'), + filter((value) => value.length > 1), + takeUntilDestroyed(), + debounceTime(600) + ) + .subscribe((name) => { + this.skillFacade.setFilter({ name }); + this.skillFacade.load(); + }); + } + + displayFn(skill: Skill) { + return skill && skill.name ? skill.name : ''; + } + + onOptionSelected(skill: Skill) { + this.form.skills.add({ skill, weight: 0 }); + this.searchControl.setValue(''); + } + + formatLabel(value: number) { + return `${value}%`; + } + + drop(event: CdkDragDrop) { + moveItemInArray( + this.form.skills.controls, + event.previousIndex, + event.currentIndex + ); + } +} diff --git a/packages/account/feature-shell/src/lib/forms/index.ts b/packages/account/feature-shell/src/lib/forms/index.ts index e285e204..dbd5a854 100644 --- a/packages/account/feature-shell/src/lib/forms/index.ts +++ b/packages/account/feature-shell/src/lib/forms/index.ts @@ -1,4 +1,6 @@ export * from './user-contact'; export * from './user-profile'; +export * from './user-roles'; +export * from './user-skill'; export * from './user-social'; export * from './user'; diff --git a/packages/account/feature-shell/src/lib/forms/user-skill.ts b/packages/account/feature-shell/src/lib/forms/user-skill.ts new file mode 100644 index 00000000..0b65aaaa --- /dev/null +++ b/packages/account/feature-shell/src/lib/forms/user-skill.ts @@ -0,0 +1,45 @@ +import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Skill, UserSkill } from '@devmx/shared-api-interfaces'; +import { TypedForm } from '@devmx/shared-ui-global/forms'; +import { SkillForm } from '@devmx/learn-ui-shared'; + +export class UserSkillForm extends FormGroup> { + constructor(value?: Partial) { + super({ + skill: new SkillForm(), + weight: new FormControl(0, { + nonNullable: true, + validators: [Validators.min(0), Validators.max(100)], + }), + }); + + if (value) { + this.patchValue(value); + } + } +} + +export class UserSkillsForm extends FormArray { + constructor() { + super([]); + } + + add(value?: UserSkill) { + this.push(new UserSkillForm(value)); + } + + has(skill?: Skill) { + return this.value.some((item) => item.skill === skill); + } + + override patchValue( + value: Partial[], + options?: { onlySelf?: boolean; emitEvent?: boolean } + ): void { + for (const item of value) { + if (!this.has(item.skill)) { + this.push(new UserSkillForm(item), options); + } + } + } +} diff --git a/packages/account/feature-shell/src/lib/forms/user.ts b/packages/account/feature-shell/src/lib/forms/user.ts index 35fa3858..c1a9b8e6 100644 --- a/packages/account/feature-shell/src/lib/forms/user.ts +++ b/packages/account/feature-shell/src/lib/forms/user.ts @@ -5,6 +5,7 @@ import { UserProfileForm } from './user-profile'; import { UserContactForm } from './user-contact'; import { UserSocialForm } from './user-social'; import { UserRolesForm } from './user-roles'; +import { UserSkillsForm } from './user-skill'; export class UserForm extends FormGroup> { constructor() { @@ -26,6 +27,7 @@ export class UserForm extends FormGroup> { social: new UserSocialForm(), active: new FormControl(), roles: new UserRolesForm(), + skills: new UserSkillsForm(), }); } @@ -45,6 +47,10 @@ export class UserForm extends FormGroup> { return this.controls.roles as UserRolesForm; } + get skills() { + return this.controls.skills as UserSkillsForm; + } + patch(value: Partial) { this.patchValue(value); diff --git a/packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts b/packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts new file mode 100644 index 00000000..3931e437 --- /dev/null +++ b/packages/shared/api-interfaces/src/lib/dtos/editable-user-skill.ts @@ -0,0 +1,4 @@ +import { EditableEntity } from '../types'; +import { UserSkill } from '../entities'; + +export type EditableUserSkill = EditableEntity; diff --git a/packages/shared/api-interfaces/src/lib/dtos/index.ts b/packages/shared/api-interfaces/src/lib/dtos/index.ts index 2e1247e3..28c18be9 100644 --- a/packages/shared/api-interfaces/src/lib/dtos/index.ts +++ b/packages/shared/api-interfaces/src/lib/dtos/index.ts @@ -18,6 +18,7 @@ export * from './editable-photo'; export * from './editable-presentation'; export * from './editable-skill'; export * from './editable-subject'; +export * from './editable-user-skill'; export * from './editable-user'; export * from './event-out'; export * from './event-page-schema'; diff --git a/packages/shared/api-interfaces/src/lib/entities/user.ts b/packages/shared/api-interfaces/src/lib/entities/user.ts index cb6a3225..711c45b8 100644 --- a/packages/shared/api-interfaces/src/lib/entities/user.ts +++ b/packages/shared/api-interfaces/src/lib/entities/user.ts @@ -3,6 +3,7 @@ import { UserCode } from './user-code'; import { UserContact } from './user-contact'; import { UserPassword } from './user-password'; import { UserProfile } from './user-profile'; +import { UserSkill } from './user-skill'; import { UserSocial } from './user-social'; export interface User { @@ -16,6 +17,8 @@ export interface User { roles: Roles; + skills?: UserSkill[]; + contact: UserContact; code?: UserCode; diff --git a/packages/shared/ui-global/icon/src/lib/types/drag.ts b/packages/shared/ui-global/icon/src/lib/types/drag.ts new file mode 100644 index 00000000..8d781536 --- /dev/null +++ b/packages/shared/ui-global/icon/src/lib/types/drag.ts @@ -0,0 +1,3 @@ +export type Drag = 'indicator' | 'handle'; + +export type DragIcon = `drag/${Drag}`; diff --git a/packages/shared/ui-global/icon/src/lib/types/icon.ts b/packages/shared/ui-global/icon/src/lib/types/icon.ts index f7378f51..f56a302e 100644 --- a/packages/shared/ui-global/icon/src/lib/types/icon.ts +++ b/packages/shared/ui-global/icon/src/lib/types/icon.ts @@ -12,6 +12,7 @@ import { FinanceIcon } from './finance'; import { TextIcon } from './text'; import { MessageIcon } from './message'; import { SocialIcon } from './social'; +import { DragIcon } from './drag'; type Root = | 'airplay' @@ -145,4 +146,5 @@ export type Icon = | FinanceIcon | TextIcon | MessageIcon - | SocialIcon; + | SocialIcon + | DragIcon;