Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] #8569 Tag Type #8720

Merged
merged 11 commits into from
Jan 16, 2025
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
samuelmbabhazi marked this conversation as resolved.
Show resolved Hide resolved
},
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
Loading