Skip to content

Commit

Permalink
Merge pull request #8720 from ever-co/enhancement/tag-type
Browse files Browse the repository at this point in the history
[Feat] #8569 Tag Type
  • Loading branch information
rahul-rocket authored Jan 16, 2025
2 parents 87aad24 + 5b8b22d commit 1b7baef
Show file tree
Hide file tree
Showing 30 changed files with 318 additions and 97 deletions.
2 changes: 1 addition & 1 deletion apps/gauzy/src/app/pages/tags/tags.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<nb-list-item
*ngFor="let option of filterOptions"
class="filter-item"
(click)="selectedFilterOption(option.property)"
(click)="selectedFilterOption(option.value)"
>
{{ option.displayName }}
</nb-list-item>
Expand Down
86 changes: 52 additions & 34 deletions apps/gauzy/src/app/pages/tags/tags.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { debounceTime, filter, tap } from 'rxjs/operators';
import { Subject, firstValueFrom } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { ITag, IOrganization, ComponentLayoutStyleEnum } from '@gauzy/contracts';
import { ComponentEnum, distinctUntilChange, splitCamelCase } from '@gauzy/ui-core/common';
import { Store, TagsService, ToastrService } from '@gauzy/ui-core/core';
import { ITag, IOrganization, ComponentLayoutStyleEnum, ITagType } from '@gauzy/contracts';
import { ComponentEnum, distinctUntilChange } from '@gauzy/ui-core/common';
import { Store, TagsService, TagTypesService, ToastrService } from '@gauzy/ui-core/core';
import {
DeleteConfirmationComponent,
IPaginationBase,
Expand All @@ -35,6 +35,7 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
dataLayoutStyle = ComponentLayoutStyleEnum.TABLE;
componentLayoutStyleEnum = ComponentLayoutStyleEnum;
tags: ITag[] = [];
tagTypes: ITagType[] = [];

private organization: IOrganization;
tags$: Subject<any> = this.subject$;
Expand All @@ -44,6 +45,7 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
constructor(
private readonly dialogService: NbDialogService,
private readonly tagsService: TagsService,
private readonly tagTypesService: TagTypesService,
public readonly translateService: TranslateService,
private readonly toastrService: ToastrService,
private readonly store: Store,
Expand All @@ -61,6 +63,7 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
debounceTime(300),
tap(() => (this.loading = true)),
tap(() => this.getTags()),
tap(() => this.getTagTypes()),
tap(() => this.clearItem()),
untilDestroyed(this)
)
Expand Down Expand Up @@ -232,10 +235,17 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
instance.value = cell.getValue();
}
},
tagTypeName: {
title: this.getTranslation('TAGS_PAGE.TAGS_TYPE'),
type: 'string',
width: '20%',
isFilterable: false
},
description: {
title: this.getTranslation('TAGS_PAGE.TAGS_DESCRIPTION'),
type: 'string',
width: '70%'
width: '70%',
isFilterable: false
},
counter: {
title: this.getTranslation('Counter'),
Expand Down Expand Up @@ -277,23 +287,53 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
return counter;
};

async getTagTypes() {
this.loading = true;
const { tenantId } = this.store.user;
const { id: organizationId } = this.organization;

try {
const { items } = await this.tagTypesService.getTagTypes({
tenantId,
organizationId
});

this.tagTypes = items;

this.filterOptions.push(
...this.tagTypes.map((tagType) => {
return {
value: tagType.id,
displayName: tagType.type
};
})
);
this.loading = false;
} catch (error) {
this.loading = false;
this.toastrService.danger('TAGS_PAGE.TAGS_FETCH_FAILED', 'Error fetching tag types');
}
}

async getTags() {
this.allTags = [];
this.filterOptions = [{ property: 'all', displayName: 'All' }];
this.filterOptions = [{ value: '', displayName: 'All' }];

const { tenantId } = this.store.user;
const { id: organizationId } = this.organization;

const { items } = await this.tagsService.getTags({
tenantId,
organizationId
});
const { items } = await this.tagsService.getTags(
{
tenantId,
organizationId
},
['tagType']
);

const { activePage, itemsPerPage } = this.getPagination();

this.allTags = items;

this._generateUniqueTags(this.allTags);
this.smartTableSource.setPaging(activePage, itemsPerPage, false);
if (!this._isFiltered) {
this.smartTableSource.load(this.allTags);
Expand Down Expand Up @@ -327,43 +367,21 @@ export class TagsComponent extends PaginationFilterBaseComponent implements Afte
* @returns
*/
selectedFilterOption(value: string) {
if (value === 'all') {
if (value === '') {
this._isFiltered = false;
this._refresh$.next(true);
this.tags$.next(true);
return;
}
if (value) {
const tags = this.allTags.filter((tag) => tag[value] && parseInt(tag[value]) > 0);
const tags = this.allTags.filter((tag) => tag.tagTypeId === value);
this._isFiltered = true;
this._refresh$.next(true);
this.smartTableSource.load(tags);
this.tags$.next(true);
}
}

/**
* Generate Unique Tags
*
* @param tags
*/
private _generateUniqueTags(tags: any[]) {
tags.forEach((tag) => {
for (const property in tag) {
const substring = '_counter';
if (property.includes(substring) && parseInt(tag[property]) > 0) {
const options = this.filterOptions.find((option) => option.property === property);
if (!options) {
this.filterOptions.push({
property,
displayName: splitCamelCase(property.replace(substring, ''))
});
}
}
}
});
}

private _applyTranslationOnSmartTable() {
this.translateService.onLangChange
.pipe(
Expand Down
13 changes: 10 additions & 3 deletions packages/contracts/src/lib/tag.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export interface ITag extends IBasePerTenantAndOrganizationEntityModel, IRelatio
/**
* Input interface for finding tags with optional filters.
*/
export interface ITagFindInput extends IBasePerTenantAndOrganizationEntityModel, Partial<
Pick<ITag, 'name' | 'color' | 'textColor' | 'description' | 'isSystem' | 'tagTypeId' | 'organizationTeamId'>
> {}
export interface ITagFindInput
extends IBasePerTenantAndOrganizationEntityModel,
Partial<
Pick<ITag, 'name' | 'color' | 'textColor' | 'description' | 'isSystem' | 'tagTypeId' | 'organizationTeamId'>
> {}

/**
* Input interface for creating a tag.
Expand Down Expand Up @@ -56,6 +58,11 @@ export interface ITagTypeCreateInput extends Omit<ITagType, 'createdAt' | 'updat
*/
export interface ITagTypeUpdateInput extends Partial<ITagTypeCreateInput> {}

/**
* Input interface for finding tag Type with optional filters.
*/
export interface ITagTypesFindInput extends IBasePerTenantAndOrganizationEntityModel, Partial<Pick<ITagType, 'type'>> {}

/**
* Enum for default task tags.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/lib/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ import { SubscriptionModule } from '../subscription/subscription.module';
import { DashboardModule } from '../dashboard/dashboard.module';
import { DashboardWidgetModule } from '../dashboard/dashboard-widget/dashboard-widget.module';
import { TenantApiKeyModule } from '../tenant-api-key/tenant-api-key.module';
import { TagTypeModule } from '../tag-type';

const { unleashConfig } = environment;

Expand Down Expand Up @@ -389,6 +390,7 @@ if (environment.THROTTLE_ENABLED) {
TenantModule,
TenantSettingModule,
TagModule,
TagTypeModule,
SkillModule,
LanguageModule,
InvoiceModule,
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/lib/core/seeds/seed-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ import { createDefaultPriorities } from './../../tasks/priorities/priority.seed'
import { createDefaultSizes } from './../../tasks/sizes/size.seed';
import { createDefaultIssueTypes } from './../../tasks/issue-type/issue-type.seed';
import { getDBType } from './../../core/utils';
import { createRandomOrganizationTagTypes, createTagTypes } from '../../tag-type/tag-type.seed';

export enum SeederTypeEnum {
ALL = 'all',
Expand Down Expand Up @@ -574,6 +575,8 @@ export class SeedDataService {

await this.tryExecute('Default Tags', createDefaultTags(this.dataSource, this.tenant, this.organizations));

await this.tryExecute('Default Tag Types', createTagTypes(this.dataSource, this.tenant, this.organizations));

// Organization level inserts which need connection, tenant, role, organizations
const categories = await this.tryExecute(
'Default Expense Categories',
Expand Down Expand Up @@ -1010,6 +1013,11 @@ export class SeedDataService {
createRandomOrganizationTags(this.dataSource, this.randomTenants, this.randomTenantOrganizationsMap)
);

await this.tryExecute(
'Random Organization Tag Types',
createRandomOrganizationTagTypes(this.dataSource, this.randomTenants, this.randomTenantOrganizationsMap)
);

await this.tryExecute(
'Random Organization Documents',
createRandomOrganizationDocuments(this.dataSource, this.randomTenants, this.randomTenantOrganizationsMap)
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/lib/tag-type/tag-type.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ export class TagTypeController extends CrudController<TagType> {
})
@Permissions(PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_TAG_TYPES_VIEW)
@Get('/')
async findAll(
@Query(new ValidationPipe()) options: PaginationParams<TagType>
): Promise<IPagination<TagType>> {
async findAll(@Query(new ValidationPipe()) options: PaginationParams<TagType>): Promise<IPagination<TagType>> {
return await this.tagTypesService.findAll(options);
}

Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/lib/tag-type/tag-type.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,61 @@ export const createTagTypes = async (
return insertTagTypes(dataSource, tagTypes);
};

/**
* Creates random organization tag types for given tenants and their organizations.
*
* @function createRandomOrganizationTagTypes
* @async
* @param {DataSource} dataSource - The TypeORM `DataSource` instance used for database operations.
* @param {ITenant[]} tenants - An array of tenant entities for which random tag types are being created.
* @param {Map<ITenant, IOrganization[]>} tenantOrganizationsMap - A map linking each tenant to its associated organizations.
* @returns {Promise<ITagType[]>} - A promise that resolves to an array of created and saved `ITagType` entities.
*
* @description
* This function generates random tag types for multiple tenants and their organizations.
* For each tenant, it retrieves the associated organizations from the `tenantOrganizationsMap`.
* It iterates over the organizations and creates `TagType` entities based on predefined
* `DEFAULT_TAG_TYPES`. The generated entities are saved in bulk into the database.
*
* If a tenant does not have any organizations, the function logs a warning and skips the tenant.
*
* @throws Will throw an error if the database save operation fails.
*/
export const createRandomOrganizationTagTypes = async (
dataSource: DataSource,
tenants: ITenant[],
tenantOrganizationsMap: Map<ITenant, IOrganization[]>
): Promise<ITagType[]> => {
let tagTypes: TagType[] = [];

for (const tenant of tenants) {
// Fetch organizations for the current tenant
const organizations = tenantOrganizationsMap.get(tenant);

if (!organizations || organizations.length === 0) {
console.warn(`No organizations found for tenant ID: ${tenant.id}`);
continue; // Skip to the next tenant if no organizations are found
}

for (const organization of organizations) {
// Create TagType instances for the current organization
const organizationTagTypes: TagType[] = DEFAULT_TAG_TYPES.map(({ type }) => {
const tagType = new TagType();
tagType.type = type;
tagType.organization = organization;
tagType.tenantId = tenant.id;
return tagType;
});

// Add the new TagType entities to the tagTypes array
tagTypes.push(...organizationTagTypes);
}
}

// Bulk save all created tag types into the database
return await dataSource.manager.save(tagTypes);
};

/**
* Inserts an array of tag types into the database.
*
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lib/tags/commands/tag.list.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export class TagListCommand implements ICommand {

constructor(
public readonly input: FindOptionsWhere<Tag>,
public readonly relations: string[] | FindOptionsRelations<Tag>,
) { }
public readonly relations: string[] | FindOptionsRelations<Tag>
) {}
}
10 changes: 6 additions & 4 deletions packages/core/src/lib/tags/dto/create-tag.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { ITagCreateInput } from '@gauzy/contracts';
import { TenantOrganizationBaseDTO } from './../../core/dto';
import { Tag } from './../tag.entity';

export class CreateTagDTO extends IntersectionType(
PartialType(TenantOrganizationBaseDTO),
PickType(Tag, ['name', 'description', 'color', 'textColor', 'icon', 'organizationTeamId'])
) implements ITagCreateInput { }
export class CreateTagDTO
extends IntersectionType(
PartialType(TenantOrganizationBaseDTO),
PickType(Tag, ['name', 'description', 'color', 'textColor', 'icon', 'organizationTeamId', 'tagTypeId'])
)
implements ITagCreateInput {}
12 changes: 8 additions & 4 deletions packages/core/src/lib/tags/dto/update-tag.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { ITagUpdateInput } from '@gauzy/contracts';
import { TenantOrganizationBaseDTO } from './../../core/dto';
import { Tag } from './../tag.entity';

export class UpdateTagDTO extends IntersectionType(
PartialType(TenantOrganizationBaseDTO),
PartialType(PickType(Tag, ['name', 'description', 'color', 'textColor', 'icon', 'organizationTeamId'])),
) implements ITagUpdateInput { }
export class UpdateTagDTO
extends IntersectionType(
PartialType(TenantOrganizationBaseDTO),
PartialType(
PickType(Tag, ['name', 'description', 'color', 'textColor', 'icon', 'organizationTeamId', 'tagTypeId'])
)
)
implements ITagUpdateInput {}
10 changes: 9 additions & 1 deletion packages/core/src/lib/tags/tag.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export class TagService extends TenantAwareCrudService<Tag> {
query.setFindOptions({
...(relations ? { relations: relations } : {})
});

// Left join all relational tables with tag table
query.leftJoin(`${query.alias}.tagType`, 'tagType');
query.leftJoin(`${query.alias}.candidates`, 'candidate');
query.leftJoin(`${query.alias}.employees`, 'employee');
query.leftJoin(`${query.alias}.employeeLevels`, 'employeeLevel');
Expand Down Expand Up @@ -100,6 +102,8 @@ export class TagService extends TenantAwareCrudService<Tag> {

// Add new selection to the SELECT query
query.select(`${query.alias}.*`);

query.addSelect(p(`"tagType"."type"`), `tagTypeName`);
// Add the select statement for counting, and cast it to integer
query.addSelect(p(`CAST(COUNT("candidate"."id") AS INTEGER)`), `candidate_counter`);
query.addSelect(p(`CAST(COUNT("employee"."id") AS INTEGER)`), `employee_counter`);
Expand Down Expand Up @@ -145,6 +149,7 @@ export class TagService extends TenantAwareCrudService<Tag> {

// Adds GROUP BY condition in the query builder.
query.addGroupBy(`${query.alias}.id`);
query.addGroupBy(`tagType.type`);
// Additionally you can add parameters used in where expression.
query.where((qb: SelectQueryBuilder<Tag>) => {
this.getFilterTagQuery(qb, input);
Expand Down Expand Up @@ -184,7 +189,10 @@ export class TagService extends TenantAwareCrudService<Tag> {
// Optional organization filter
query.andWhere(
new Brackets((qb) => {
qb.where(`${query.alias}.organizationId IS NULL`).orWhere(`${query.alias}.organizationId = :organizationId`, { organizationId });
qb.where(`${query.alias}.organizationId IS NULL`).orWhere(
`${query.alias}.organizationId = :organizationId`,
{ organizationId }
);
})
);

Expand Down
Loading

0 comments on commit 1b7baef

Please sign in to comment.