Skip to content

Commit

Permalink
feat(front-admin-users)#131: implement Users table view (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
spy4x authored Dec 19, 2022
1 parent d62f824 commit a3d0318
Show file tree
Hide file tree
Showing 61 changed files with 1,446 additions and 134 deletions.
13 changes: 11 additions & 2 deletions libs/back/api/shared/src/lib/cqrs/queries/user/usersFind.query.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { IsOptional } from 'class-validator';
import { IsEnum, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { PaginationRequestDTO } from '../../../dtos';
import { PAGINATION_DEFAULTS } from '@seed/shared/constants';
import { UserRole } from '@prisma/client';

export class UsersFindQuery extends PaginationRequestDTO {
@ApiProperty({ type: String, required: false })
@IsOptional()
@Type(() => String)
public search?: string;

constructor(page = PAGINATION_DEFAULTS.page, limit = PAGINATION_DEFAULTS.limit, search?: string) {
@ApiProperty({ enum: UserRole, enumName: 'UserRole', required: false })
@IsOptional()
@IsEnum(UserRole, {
message: `Field 'role' should be one of values: [${Object.keys(UserRole).join(', ')}]`,
})
public role?: UserRole;

constructor(page = PAGINATION_DEFAULTS.page, limit = PAGINATION_DEFAULTS.limit, search?: string, role?: UserRole) {
super(page, limit);
this.search = search;
this.role = role;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { PaginationResponseDTO, PrismaService, UsersFindQuery } from '@seed/back
import { UsersFindQueryHandler } from './usersFind.queryHandler';
import { mockUsers } from '@seed/shared/mock-data';
import { ONE, PAGINATION_DEFAULTS } from '@seed/shared/constants';
import { User } from '@prisma/client';
import { User, UserRole } from '@prisma/client';

describe(UsersFindQueryHandler.name, () => {
//region VARIABLES
let handler: UsersFindQueryHandler;
const page = 3;
const limit = 50;
const role = UserRole.ADMIN;
const findManyMockResult = mockUsers;
const countMockResult = mockUsers.length;
const findManyMock = jest.fn().mockReturnValue(findManyMockResult);
Expand Down Expand Up @@ -46,8 +47,9 @@ describe(UsersFindQueryHandler.name, () => {
pageArg = PAGINATION_DEFAULTS.page,
limitArg = PAGINATION_DEFAULTS.limit,
search?: string,
role?: UserRole,
): UsersFindQuery {
return new UsersFindQuery(pageArg, limitArg, search);
return new UsersFindQuery(pageArg, limitArg, search, role);
}
//endregion

Expand All @@ -63,29 +65,35 @@ describe(UsersFindQueryHandler.name, () => {
expect(result).toEqual(new PaginationResponseDTO<User>(findManyMockResult, page, limit, countMockResult));
});

it('should call prisma.user.findMany(), prisma.user.count() with basic params + search query condition for single word', async () => {
const query = getQuery(page, limit);
query.search = 'John';
it('should call prisma.user.findMany(), prisma.user.count() with basic params + search query condition for single word + role', async () => {
const query = getQuery(page, limit, 'John', role);
const result = await handler.execute(query);
const where = {
OR: [
{
userName: {
contains: 'John',
mode: 'insensitive',
},
},
AND: [
{
firstName: {
contains: 'John',
mode: 'insensitive',
},
OR: [
{
userName: {
contains: 'John',
mode: 'insensitive',
},
},
{
firstName: {
contains: 'John',
mode: 'insensitive',
},
},
{
lastName: {
contains: 'John',
mode: 'insensitive',
},
},
],
},
{
lastName: {
contains: 'John',
mode: 'insensitive',
},
role,
},
],
};
Expand All @@ -104,43 +112,48 @@ describe(UsersFindQueryHandler.name, () => {
query.search = 'John Wick';
const result = await handler.execute(query);
const where = {
OR: [
{
userName: {
contains: 'John',
mode: 'insensitive',
},
},
{
firstName: {
contains: 'John',
mode: 'insensitive',
},
},
{
lastName: {
contains: 'John',
mode: 'insensitive',
},
},
{
userName: {
contains: 'Wick',
mode: 'insensitive',
},
},
{
firstName: {
contains: 'Wick',
mode: 'insensitive',
},
},
AND: [
{
lastName: {
contains: 'Wick',
mode: 'insensitive',
},
OR: [
{
userName: {
contains: 'John',
mode: 'insensitive',
},
},
{
firstName: {
contains: 'John',
mode: 'insensitive',
},
},
{
lastName: {
contains: 'John',
mode: 'insensitive',
},
},
{
userName: {
contains: 'Wick',
mode: 'insensitive',
},
},
{
firstName: {
contains: 'Wick',
mode: 'insensitive',
},
},
{
lastName: {
contains: 'Wick',
mode: 'insensitive',
},
},
],
},
{ role: undefined },
],
};
expect(findManyMock).toBeCalledWith({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class UsersFindQueryHandler extends ListHandler implements IQueryHandler<

async execute(query: UsersFindQuery): Promise<PaginationResponseDTO<User>> {
return this.logger.trackSegment(this.execute.name, async logSegment => {
const { search } = query;
const { search, role } = query;
const { page, limit } = this.getPaginationData(query);
const usernameSplit = search?.split(' ');

Expand All @@ -40,8 +40,15 @@ export class UsersFindQueryHandler extends ListHandler implements IQueryHandler<

return acc;
}, new Array<Prisma.UserWhereInput>()) || [];
const where: { OR: Prisma.UserWhereInput[] } = {
OR: [...conditions],
const where: Prisma.UserWhereInput = {
AND: [
{
OR: [...conditions],
},
{
role,
},
],
};

logSegment.log('Searching for users with filter:', where);
Expand All @@ -61,6 +68,6 @@ export class UsersFindQueryHandler extends ListHandler implements IQueryHandler<
}

hasCondition = (query: UsersFindQuery): boolean => {
return !!query.search;
return !!query.search || !!query.role;
};
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AuthSelectors, AuthUIActions } from '@seed/front/shared/auth/state';
import { map } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '@prisma/client';

@Component({
Expand Down
12 changes: 4 additions & 8 deletions libs/front/admin/core/src/lib/protected/protected.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div *ngIf="user$ | async as user">
<div *ngIf="user$ | async as user" class="h-full">
<!-- TODO: implement show/hide animations using "@angular/animations" -->
<!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
<div *ngIf="isMobileMenuOpened" class="relative z-40 md:hidden" role="dialog" aria-modal="true">
Expand Down Expand Up @@ -157,7 +157,7 @@
</div>
</div>
</div>
<div class="flex flex-1 flex-col md:pl-64">
<div class="flex h-full flex-1 flex-col overflow-hidden md:pl-64">
<div class="sticky top-0 z-10 bg-gray-100 pl-1 pt-1 sm:pl-3 sm:pt-3 md:hidden">
<button
(click)="toggleMobileMenu(true)"
Expand All @@ -179,12 +179,8 @@
</svg>
</button>
</div>
<main class="flex-1">
<div class="py-6">
<div class="mx-auto max-w-7xl px-4 sm:px-6 md:px-8">
<router-outlet></router-outlet>
</div>
</div>
<main class="max-h-[calc(100%-50px)] flex-1 md:max-h-full">
<router-outlet></router-outlet>
</main>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { AuthSelectors } from '@seed/front/shared/auth/state';
import { map } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '@prisma/client';
import { Store } from '@ngrx/store';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AuthSelectors, AuthUIActions } from '@seed/front/shared/auth/state';
import { map } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '@prisma/client';

@Component({
Expand Down
4 changes: 2 additions & 2 deletions libs/front/admin/users/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
"error",
{
"type": "attribute",
"prefix": "adminUsers",
"prefix": "seedAdminUsers",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "admin-users",
"prefix": "seed-admin-users",
"style": "kebab-case"
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>user-details works!</p>
22 changes: 22 additions & 0 deletions libs/front/admin/users/src/lib/detail/detail.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { DetailComponent } from './detail.component';

describe('UserDetailsComponent', () => {
let component: DetailComponent;
let fixture: ComponentFixture<DetailComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DetailComponent],
}).compileComponents();

fixture = TestBed.createComponent(DetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
9 changes: 9 additions & 0 deletions libs/front/admin/users/src/lib/detail/detail.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'seed-admin-users-details',
templateUrl: './detail.component.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DetailComponent {}
44 changes: 44 additions & 0 deletions libs/front/admin/users/src/lib/list/filters/filters.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!-- Tabs -->
<div [ngClass]="{ 'pointer-events-none opacity-50': isLoading }">
<!-- Mobile -->
<div class="p-4 sm:hidden">
<label for="tabs" class="sr-only">Select a tab</label>
<select
id="tabs"
name="tabs"
class="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-purple-500 focus:outline-none focus:ring-purple-500 sm:text-sm"
[formControl]="tabControl"
>
<option *ngFor="let r of roleTitles" [ngValue]="r.value">
{{ r.title }} {{ !isLoading && r.value === role ? '(' + total + ')' : '' }}
</option>
</select>
</div>

<!-- Desktop -->
<div class="hidden sm:block">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8 px-4" aria-label="Tabs">
<a
*ngFor="let r of roleTitles"
(click)="changeRole(r.value)"
href="javascript:;"
class="whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium"
[ngClass]="{
'border-purple-500 text-purple-600': r.value === role,
'border-transparent text-gray-500 hover:border-gray-200 hover:text-gray-700': r.value !== role
}"
>
{{ r.title }}
<span
*ngIf="r.value === role"
class="ml-2 inline-flex rounded-full bg-purple-100 py-0.5 px-2.5 text-xs font-medium text-purple-600"
>
<i *ngIf="isLoading" class="feather-loader animate-spin"></i>
<span *ngIf="!isLoading">{{ total }}</span>
</span>
</a>
</nav>
</div>
</div>
</div>
Loading

0 comments on commit a3d0318

Please sign in to comment.