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
4 changes: 2 additions & 2 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ PLATFORM_WEBSITE_DOWNLOAD_URL=https://gauzy.co/downloads
DB_ORM=typeorm

# DB_TYPE: sqlite | postgres | better-sqlite3 | mysql
DB_TYPE=better-sqlite3
DB_TYPE=postgres
DB_SYNCHRONIZE=false

# DB Connection Parameters
Expand All @@ -66,7 +66,7 @@ DB_PORT=5432
DB_NAME=gauzy
## DB Username. The default for PostgreSQL is 'postgres', for MySQL it's 'root'
DB_USER=postgres
DB_PASS=root
DB_PASS=postgres
samuelmbabhazi marked this conversation as resolved.
Show resolved Hide resolved
DB_LOGGING=all
DB_POOL_SIZE=40
DB_POOL_SIZE_KNEX=10
Expand Down
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
88 changes: 54 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: 'Type',
type: 'string',
width: '20%',
isFilterable: false
},
samuelmbabhazi marked this conversation as resolved.
Show resolved Hide resolved
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,55 @@ 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;
console.log(error);

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 +369,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 @@ -1005,6 +1008,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 {}
Loading
Loading