mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
[Fix] Video filtering (#8765)
* feat: adds API for video count and updates video service Adds a new API endpoint to retrieve the count of videos. Updates the video service to use the new API endpoint for checking video availability. This improves performance by only retrieving the count instead of the entire list of videos. Adds update video API endpoint to update video metadata. * refactor: video service imports Moves RxJS operators import to be with other RxJS imports. This improves code readability and consistency. * refactor: permission Check Enhancements - Updated `VideoService` in `video.service.ts` to refine selection logic, prioritizing `selectedOrganization` over `organizationId`. - Enhanced `GetVideoCountQueryHandler` in `get-video-count.handler.ts` by introducing permission checks for `CHANGE_SELECTED_EMPLOYEE`, ensuring queries respect user permissions. * feat: enhance video retrieval with timezone-based date filtering - Introduced `moment-timezone` for converting start and end dates to UTC. - Updated query to dynamically filter videos based on provided `startDate`, `endDate`, and `timeZone`. - Implemented dynamic WHERE clause with optional employee filtering using `TypeORM`'s `In` and `Between` utilities. * feat: conditional date filtering in video query Modify the query to conditionally include the recordedAt filter only when both startDate and endDate are provided. This change enhances the query flexibility by avoiding unnecessary date filtering. * fix: hide date selector on detailled page * refactor: deleteVideo method in VideosController - Updated import statements for DTOs and models in videos.controller.ts - Changed the Delete method to accept an optional IDeleteVideo parameter * refactor: enhance UpdateVideo command structure - Updated the VideoEffects update logic for better state management - Refactored UpdateVideoHandler to include description in video updates - Modified UpdateVideoCommand and UpdateVideoDTO for simplified DTO structure - Revised VideosController to adapt to new UpdateVideoCommand constructor * fix: reset video store on filter change - Ensures that the video store is cleared before fetching new video data when the filter changes. * feat(video-page): modify filter component inputs - Disable log type, source, and activity level filters in the video page filter component. - This change simplifies the available filters for a streamlined user experience.
This commit is contained in:
@@ -105,7 +105,8 @@ export class VideoEffects {
|
||||
tap((video) => {
|
||||
const videos = this.videoQuery.videos;
|
||||
this.videoStore.update({
|
||||
videos: [...new Map([...videos, video].map((item) => [item.id, item])).values()]
|
||||
videos: [...new Map([...videos, video].map((item) => [item.id, item])).values()],
|
||||
video
|
||||
});
|
||||
this.toastrService.success('Video updated successfully');
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
[isTimeFormat]="true"
|
||||
[saveFilters]="(datePickerConfig$ | async).isSaveDatePicker"
|
||||
(filtersChange)="filtersChange($event)"
|
||||
[hasLogTypeFilter]="false"
|
||||
[hasSourceFilter]="false"
|
||||
[hasActivityLevelFilter]="false"
|
||||
>
|
||||
</ngx-gauzy-filters>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,7 @@ export class VideoPageComponent extends BaseSelectorFilterComponent implements O
|
||||
distinctUntilChange(),
|
||||
tap(() => {
|
||||
this.skip = 1;
|
||||
this.videoStore.update({ videos: [] });
|
||||
this.fetchVideos();
|
||||
this.hasNext = hasNext;
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { IPagination } from '@gauzy/contracts';
|
||||
import { API_PREFIX, toParams } from '@gauzy/ui-core/common';
|
||||
import { Store } from '@gauzy/ui-core/core';
|
||||
import { catchError, map, Observable, of } from 'rxjs';
|
||||
import { IVideo } from '../models/video.model';
|
||||
|
||||
@Injectable({
|
||||
@@ -10,7 +11,7 @@ import { IVideo } from '../models/video.model';
|
||||
})
|
||||
export class VideoService {
|
||||
private readonly API_ENDPOINT = `${API_PREFIX}/plugins/videos`;
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
constructor(private readonly http: HttpClient, private readonly store: Store) {}
|
||||
|
||||
public getAll<T>(params?: T): Observable<IPagination<IVideo>> {
|
||||
return this.http.get<IPagination<IVideo>>(this.API_ENDPOINT, {
|
||||
@@ -30,7 +31,24 @@ export class VideoService {
|
||||
return this.http.delete<void>(`${this.API_ENDPOINT}/${id}`);
|
||||
}
|
||||
|
||||
public getCount<T>(params?: T): Observable<number> {
|
||||
return this.http.get<number>(`${this.API_ENDPOINT}/count`, {
|
||||
params: toParams(params)
|
||||
});
|
||||
}
|
||||
|
||||
public get isAvailable$(): Observable<boolean> {
|
||||
return this.getAll({}).pipe(map(({ total }) => total > 0));
|
||||
const { id: selectedOrganizationId } = this.store.selectedOrganization;
|
||||
const organizationId = selectedOrganizationId || this.store.organizationId;
|
||||
const tenantId = this.store.tenantId;
|
||||
|
||||
if (!organizationId || !tenantId) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return this.getCount({ organizationId, tenantId }).pipe(
|
||||
map((count) => count > 0),
|
||||
catchError(() => of(false))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
title: 'Video',
|
||||
selectors: {
|
||||
date: true,
|
||||
date: false,
|
||||
employee: false,
|
||||
project: false,
|
||||
team: false
|
||||
|
||||
@@ -18,17 +18,15 @@ export class UpdateVideoHandler implements ICommandHandler<UpdateVideoCommand> {
|
||||
*/
|
||||
public async execute(command: UpdateVideoCommand): Promise<IVideo> {
|
||||
// Extract input data from the command
|
||||
const { input } = command;
|
||||
const { input, id } = command;
|
||||
|
||||
// Destructure the input fields for clarity
|
||||
const { id, title, size, file, duration } = input;
|
||||
const { title, description } = input;
|
||||
|
||||
// Update the video entity in the database using the provided ID and input fields
|
||||
await this.videosService.update(id, {
|
||||
title,
|
||||
size,
|
||||
duration,
|
||||
file: file?.key // Extract the file key if a file object is provided
|
||||
description
|
||||
});
|
||||
|
||||
// Fetch and return the updated video entity by its ID
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ICommand } from '@nestjs/cqrs';
|
||||
import { UpdateVideoDTO } from '../dto';
|
||||
import { ID } from '@gauzy/contracts';
|
||||
|
||||
export class UpdateVideoCommand implements ICommand {
|
||||
public static readonly type = '[Update] Video';
|
||||
constructor(public readonly input: UpdateVideoDTO) {}
|
||||
constructor(public readonly id: ID, public readonly input: UpdateVideoDTO) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsISO8601, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Entity Count Video DTO
|
||||
*
|
||||
* Represents the DTO for counting videos within a specific tenant and organization,
|
||||
* optionally filtered by a date range.
|
||||
*/
|
||||
export class CountVideoDTO {
|
||||
/**
|
||||
* The ID of the tenant.
|
||||
*
|
||||
* @example 'd3b07384-d9a0-4d5f-bf6d-f1b5b71e9a37'
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'The ID of the tenant.',
|
||||
example: 'd3b07384-d9a0-4d5f-bf6d-f1b5b71e9a37',
|
||||
type: String
|
||||
})
|
||||
@IsUUID('4', { message: 'tenantId must be a valid UUID' })
|
||||
tenantId: string;
|
||||
|
||||
/**
|
||||
* The ID of the organization within the tenant.
|
||||
*
|
||||
* @example 'a9e3fbc9-d0b7-4e85-b6f2-2eaf3a5d72dc'
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'The ID of the organization within the tenant.',
|
||||
example: 'a9e3fbc9-d0b7-4e85-b6f2-2eaf3a5d72dc',
|
||||
type: String
|
||||
})
|
||||
@IsUUID('4', { message: 'organizationId must be a valid UUID' })
|
||||
organizationId: string;
|
||||
|
||||
/**
|
||||
* The start date for filtering video records.
|
||||
* Can be provided as a string or a Date object.
|
||||
*
|
||||
* @example '2023-01-01T00:00:00.000Z'
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'The start date for filtering video records. Can be provided as a string or a Date object.',
|
||||
example: '2023-01-01T00:00:00.000Z',
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
nullable: true
|
||||
})
|
||||
@IsOptional()
|
||||
@IsISO8601({ strict: true }, { message: 'startDate must be a valid ISO 8601 date string' })
|
||||
startDate: Date | string;
|
||||
|
||||
/**
|
||||
* The end date for filtering video records.
|
||||
* Can be provided as a string or a Date object.
|
||||
*
|
||||
* @example '2023-12-31T23:59:59.999Z'
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'The end date for filtering video records. Can be provided as a string or a Date object.',
|
||||
example: '2023-12-31T23:59:59.999Z',
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
nullable: true
|
||||
})
|
||||
@IsOptional()
|
||||
@IsISO8601({ strict: true }, { message: 'endDate must be a valid ISO 8601 date string' })
|
||||
endDate: Date | string;
|
||||
}
|
||||
@@ -1,19 +1,3 @@
|
||||
import { ID } from '@gauzy/contracts';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNotEmpty, IsOptional, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { BaseVideoDTO } from './base-video.dto';
|
||||
import { FileDTO } from './file.dto';
|
||||
|
||||
export class UpdateVideoDTO extends BaseVideoDTO {
|
||||
@ApiProperty({ description: 'The ID of the video to update', example: '123e4567-e89b-12d3-a456-426614174000' })
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
id: ID;
|
||||
|
||||
@ApiProperty({ type: FileDTO })
|
||||
@ValidateNested()
|
||||
@Type(() => FileDTO)
|
||||
@IsOptional()
|
||||
file?: FileDTO;
|
||||
}
|
||||
export class UpdateVideoDTO extends BaseVideoDTO {}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IQuery } from '@nestjs/cqrs';
|
||||
import { CountVideoDTO } from '../dto/count-video.dto';
|
||||
|
||||
export class GetVideoCountQuery implements IQuery {
|
||||
public static readonly type = '[Video] Get Count';
|
||||
|
||||
constructor(public readonly options: CountVideoDTO) {}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { VideosService } from '../../services/videos.service';
|
||||
import { GetVideoCountQuery } from '../get-video-count.query';
|
||||
import { RequestContext } from '@gauzy/core';
|
||||
import { PermissionsEnum } from '@gauzy/contracts';
|
||||
|
||||
@QueryHandler(GetVideoCountQuery)
|
||||
export class GetVideoCountQueryHandler implements IQueryHandler<GetVideoCountQuery> {
|
||||
constructor(private readonly videosService: VideosService) {}
|
||||
|
||||
/**
|
||||
* Handles the `GetVideoCountQuery` to retrieve the count of video entities.
|
||||
*
|
||||
* @param query - The `GetVideoCountQuery` containing the ID of the video to be fetched and optional query options.
|
||||
*
|
||||
* @returns A promise resolving to the count of video entities (`number`).
|
||||
*
|
||||
*/
|
||||
public async execute(query: GetVideoCountQuery): Promise<number> {
|
||||
// Destructure the query to extract the video ID and options
|
||||
const { options } = query || {};
|
||||
// Fetch the video entity from the database
|
||||
const { organizationId, tenantId } = options;
|
||||
// Check if the current user has the permission
|
||||
const permission = RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE);
|
||||
// Fetch the count of video entities from the database
|
||||
return this.videosService.count({
|
||||
where: {
|
||||
...(!permission && { uploadedById: RequestContext.currentEmployeeId() }),
|
||||
organizationId,
|
||||
tenantId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { IPagination } from '@gauzy/contracts';
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { IVideo } from '../../video.model';
|
||||
import * as moment from 'moment-timezone';
|
||||
import { Between, In } from 'typeorm';
|
||||
import { VideosService } from '../../services/videos.service';
|
||||
import { IVideo } from '../../video.model';
|
||||
import { GetVideosQuery } from '../get-videos.query';
|
||||
|
||||
@QueryHandler(GetVideosQuery)
|
||||
@@ -19,7 +21,39 @@ export class GetVideosQueryHandler implements IQueryHandler<GetVideosQuery> {
|
||||
// Extract pagination and filter parameters from the query
|
||||
const { params } = query;
|
||||
|
||||
// Step 1: Fetch the paginated list of videos from the database
|
||||
return this.videosService.paginate(params);
|
||||
const {
|
||||
startDate,
|
||||
endDate,
|
||||
tenantId,
|
||||
organizationId,
|
||||
employeeIds = [],
|
||||
timeZone = 'UTC'
|
||||
} = (params || {}) as any;
|
||||
|
||||
// Build the dynamic WHERE clause for the query
|
||||
const where: Record<string, any> = {
|
||||
tenantId,
|
||||
organizationId
|
||||
};
|
||||
|
||||
// Add recordedAt only if startDate and endDate are provided
|
||||
if (startDate && endDate) {
|
||||
// Convert startDate and endDate to UTC based on the provided timeZone
|
||||
const startDateUtc = moment.tz(startDate, timeZone).utc().toDate();
|
||||
const endDateUtc = moment.tz(endDate, timeZone).utc().toDate();
|
||||
// Update the 'valueDate' property to filter records between the specified dates
|
||||
where.recordedAt = Between(startDateUtc, endDateUtc);
|
||||
}
|
||||
|
||||
// Add employee filter only if employeeIds is provided and non-empty
|
||||
if (employeeIds.length > 0) {
|
||||
where.uploadedById = In(employeeIds);
|
||||
}
|
||||
|
||||
// Fetch paginated videos from the service
|
||||
return this.videosService.paginate({
|
||||
...params,
|
||||
where
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GetVideoCountQueryHandler } from './get-video-count.handler';
|
||||
import { GetVideoQueryHandler } from './get-video.handler';
|
||||
import { GetVideosQueryHandler } from './get-videos.handler';
|
||||
|
||||
export const QueryHandlers = [GetVideoQueryHandler, GetVideosQueryHandler];
|
||||
export const QueryHandlers = [GetVideoQueryHandler, GetVideosQueryHandler, GetVideoCountQueryHandler];
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
import { FileStorageProviderEnum, ID, IPagination, PermissionsEnum } from '@gauzy/contracts';
|
||||
import {
|
||||
FileStorage,
|
||||
FileStorageFactory,
|
||||
LazyFileInterceptor,
|
||||
PaginationParams,
|
||||
PermissionGuard,
|
||||
Permissions,
|
||||
RequestContext,
|
||||
TenantPermissionGuard,
|
||||
UploadedFileStorage,
|
||||
UseValidationPipe,
|
||||
UUIDValidationPipe
|
||||
} from '@gauzy/core';
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
@@ -7,6 +21,7 @@ import {
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
@@ -16,27 +31,16 @@ import { ApiConsumes, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { FindOneOptions } from 'typeorm';
|
||||
import { FileStorageProviderEnum, ID, IPagination, PermissionsEnum } from '@gauzy/contracts';
|
||||
import {
|
||||
FileStorage,
|
||||
FileStorageFactory,
|
||||
PaginationParams,
|
||||
Permissions,
|
||||
PermissionGuard,
|
||||
UploadedFileStorage,
|
||||
TenantPermissionGuard,
|
||||
UseValidationPipe,
|
||||
RequestContext,
|
||||
UUIDValidationPipe,
|
||||
LazyFileInterceptor
|
||||
} from '@gauzy/core';
|
||||
import { IVideo } from './video.model';
|
||||
import { CreateVideoCommand } from './commands/create-video.command';
|
||||
import { DeleteVideoCommand } from './commands/delete-video.command';
|
||||
import { UpdateVideoCommand } from './commands/update-video.command';
|
||||
import { CreateVideoDTO, FileDTO, UpdateVideoDTO } from './dto';
|
||||
import { CountVideoDTO } from './dto/count-video.dto';
|
||||
import { Video } from './entities/video.entity';
|
||||
import { GetVideoCountQuery } from './queries/get-video-count.query';
|
||||
import { GetVideoQuery } from './queries/get-video.query';
|
||||
import { GetVideosQuery } from './queries/get-videos.query';
|
||||
import { CreateVideoDTO, DeleteVideoDTO, FileDTO } from './dto';
|
||||
import { Video } from './entities/video.entity';
|
||||
import { IDeleteVideo, IVideo } from './video.model';
|
||||
|
||||
@ApiTags('Video Plugin')
|
||||
@UseGuards(TenantPermissionGuard, PermissionGuard)
|
||||
@@ -158,6 +162,75 @@ export class VideosController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET video count in the same tenant.
|
||||
*
|
||||
* This endpoint retrieves the count of videos within a specific tenant.
|
||||
* It takes query parameters to filter the video count by certain criteria.
|
||||
*
|
||||
* @param options Query parameters to filter the video count.
|
||||
* @returns A promise resolving to the total count of videos in the tenant.
|
||||
*/
|
||||
@ApiOperation({ summary: 'Get video count in the same tenant' })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully retrieved the video count.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid query parameters. Please check your input.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: 'An error occurred while retrieving the video count.'
|
||||
})
|
||||
@Get('count')
|
||||
@UseValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true
|
||||
})
|
||||
async getCount(@Query() options: CountVideoDTO): Promise<number> {
|
||||
return this.queryBus.execute(new GetVideoCountQuery(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing video record.
|
||||
*
|
||||
* This endpoint allows authorized users to update an existing video record by providing its ID
|
||||
* and the necessary updated metadata.
|
||||
*
|
||||
* @param id - The UUID of the video to update.
|
||||
* @param input - The updated video metadata.
|
||||
* @returns A Promise that resolves with the details of the updated video.
|
||||
*/
|
||||
@ApiOperation({
|
||||
summary: 'Update a video by ID',
|
||||
description: 'Updates an existing video record based on the provided ID and metadata.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'The video has been successfully updated.',
|
||||
type: Video
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Video record not found.'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid input, The response body may contain clues as to what went wrong.'
|
||||
})
|
||||
@UseValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true
|
||||
})
|
||||
@Put(':id')
|
||||
public async update(@Param('id', UUIDValidationPipe) id: ID, @Body() input: UpdateVideoDTO): Promise<IVideo> {
|
||||
return this.commandBus.execute(new UpdateVideoCommand(id, input));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a video record by its ID.
|
||||
*
|
||||
@@ -215,13 +288,8 @@ export class VideosController {
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true
|
||||
})
|
||||
@UseValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true
|
||||
})
|
||||
@Delete(':id')
|
||||
public async delete(@Param('id', UUIDValidationPipe) id: ID, @Query() options: DeleteVideoDTO): Promise<void> {
|
||||
public async delete(@Param('id', UUIDValidationPipe) id: ID, @Query() options?: IDeleteVideo): Promise<void> {
|
||||
// Execute the delete video command
|
||||
return this.commandBus.execute(new DeleteVideoCommand({ id, options }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user