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

[Fix] Task Status Not Pre-filled During Edit, Causing Error on Save Without Re-selecting Status #8441

Merged
merged 5 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { AfterViewInit, Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { combineLatest, debounceTime, firstValueFrom, Subject } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, debounceTime, filter, firstValueFrom, map, Subject, tap } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import {
IOrganization,
IOrganizationProject,
IPagination,
ITaskStatus,
ITaskStatusFindInput,
TaskStatusEnum
} from '@gauzy/contracts';
import { distinctUntilChange, sluggable } from '@gauzy/ui-core/common';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Store, TaskStatusesService, ToastrService } from '@gauzy/ui-core/core';
import { ID, IOrganization, IPagination, ITaskStatus, ITaskStatusFindInput, TaskStatusEnum } from '@gauzy/contracts';
import { distinctUntilChange, sluggable } from '@gauzy/ui-core/common';
import { ErrorHandlingService, Store, TaskStatusesService } from '@gauzy/ui-core/core';
import { TranslationBaseComponent } from '@gauzy/ui-core/i18n';

@UntilDestroy({ checkProperties: true })
Expand All @@ -30,107 +21,88 @@ import { TranslationBaseComponent } from '@gauzy/ui-core/i18n';
]
})
export class TaskStatusSelectComponent extends TranslationBaseComponent implements AfterViewInit, OnInit, OnDestroy {
public organization: IOrganization;
private subject$: Subject<boolean> = new Subject();

/**
* Default global task statuses
* A BehaviorSubject to store and emit the latest list of task statuses.
*/
private _statuses: Array<{ name: string; value: TaskStatusEnum & any }> = [
{
name: TaskStatusEnum.OPEN,
value: sluggable(TaskStatusEnum.OPEN)
},
{
name: TaskStatusEnum.IN_PROGRESS,
value: sluggable(TaskStatusEnum.IN_PROGRESS)
},
{
name: TaskStatusEnum.READY_FOR_REVIEW,
value: sluggable(TaskStatusEnum.READY_FOR_REVIEW)
},
{
name: TaskStatusEnum.IN_REVIEW,
value: sluggable(TaskStatusEnum.IN_REVIEW)
},
{
name: TaskStatusEnum.BLOCKED,
value: sluggable(TaskStatusEnum.BLOCKED)
},
{
name: TaskStatusEnum.COMPLETED,
value: sluggable(TaskStatusEnum.COMPLETED)
}
];
public organization: IOrganization;
public statuses$: BehaviorSubject<ITaskStatus[]> = new BehaviorSubject([]);

/**
* Predefined task statuses with names and sluggable values.
*/
private _statuses: Array<{ name: string; value: string }> = [
{ name: TaskStatusEnum.OPEN, value: sluggable(TaskStatusEnum.OPEN) },
{ name: TaskStatusEnum.IN_PROGRESS, value: sluggable(TaskStatusEnum.IN_PROGRESS) },
{ name: TaskStatusEnum.READY_FOR_REVIEW, value: sluggable(TaskStatusEnum.READY_FOR_REVIEW) },
{ name: TaskStatusEnum.IN_REVIEW, value: sluggable(TaskStatusEnum.IN_REVIEW) },
{ name: TaskStatusEnum.BLOCKED, value: sluggable(TaskStatusEnum.BLOCKED) },
{ name: TaskStatusEnum.COMPLETED, value: sluggable(TaskStatusEnum.COMPLETED) }
];

/**
* EventEmitter to notify when a status is selected or changed.
*/
@Output() onChanged = new EventEmitter<ITaskStatus>();

constructor(
public readonly translateService: TranslateService,
public readonly store: Store,
public readonly taskStatusesService: TaskStatusesService,
private readonly toastrService: ToastrService
private readonly _store: Store,
private readonly _taskStatusesService: TaskStatusesService,
private readonly _errorHandlingService: ErrorHandlingService
) {
super(translateService);
}

/*
* Getter & Setter for selected organization project
/**
*
*/
private _projectId: IOrganizationProject['id'];
@Input() addTag: boolean = true;

get projectId(): IOrganizationProject['id'] {
return this._projectId;
}

@Input() set projectId(value: IOrganizationProject['id']) {
this._projectId = value;
this.subject$.next(true);
}

/*
* Getter & Setter for dynamic add tag option
/**
* The placeholder text to be displayed in the project selector.
* Provides guidance to the user on what action to take or what information to provide.
*
*/
private _addTag: boolean = true;

get addTag(): boolean {
return this._addTag;
}

@Input() set addTag(value: boolean) {
this._addTag = value;
}
@Input() placeholder: string | null = null;

/*
* Getter & Setter for dynamic placeholder
* Getter and Setter for the selected organization project ID.
* The setter updates the private _projectId and notifies any observers of the change.
*/
private _placeholder: string;

get placeholder(): string {
return this._placeholder;
private _projectId: ID;
@Input() set projectId(value: ID) {
this._projectId = value;
this.subject$.next(true); // Notify subscribers that the project ID has changed
}

@Input() set placeholder(value: string) {
this._placeholder = value;
get projectId(): ID {
return this._projectId;
}

/*
* Getter & Setter for status
*/
private _status: ITaskStatus;

get status(): ITaskStatus {
return this._status;
}

set status(val: ITaskStatus) {
this._status = val;
this.onChange(val);
this.onTouched(val);
this.onChange(val); // Notify form control value change
this.onTouched(); // Mark as touched in form control
}

onChange: any = () => {};
/**
* Callback function to notify changes in the form control.
*/
private onChange: (value: ITaskStatus) => void = () => {};

onTouched: any = () => {};
/**
* Callback function to notify touch events in the form control.
*/
private onTouched: () => void = () => {};

ngOnInit(): void {
this.subject$
Expand All @@ -143,8 +115,8 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent implemen
}

ngAfterViewInit(): void {
const storeOrganization$ = this.store.selectedOrganization$;
const storeProject$ = this.store.selectedProject$;
const storeOrganization$ = this._store.selectedOrganization$;
const storeProject$ = this._store.selectedProject$;
combineLatest([storeOrganization$, storeProject$])
.pipe(
distinctUntilChange(),
Expand All @@ -159,79 +131,111 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent implemen
.subscribe();
}

writeValue(value: ITaskStatus) {
/**
* Updates the status value for the component.
*
* @param value - The task status to be written to the component.
*/
writeValue(value: ITaskStatus): void {
this.status = value;
}

registerOnChange(fn: (rating: number) => void): void {
/**
* Registers a callback function to be called when the status changes.
*
* @param fn - The function that is triggered on status change.
*/
registerOnChange(fn: (status: ITaskStatus) => void): void {
this.onChange = fn;
}

/**
* Registers a callback function to be called when the component is touched.
*
* @param fn - The function that is triggered when the component is touched.
*/
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}

selectStatus(status: ITaskStatus) {
/**
* Emits the selected status when a task status is chosen.
*
* @param status - The selected task status.
*/
selectStatus(status: ITaskStatus): void {
this.onChanged.emit(status);
}

/**
* Get task statuses based organization & project
* Retrieves task statuses based on the organization and project.
* If a project ID is available, it filters statuses accordingly.
* Emits the list of statuses and sets the default status if none is selected.
*/
getStatuses() {
getStatuses(): void {
if (!this.organization) {
return;
}

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

this.taskStatusesService
// Fetch task statuses from the service
this._taskStatusesService
.get<ITaskStatusFindInput>({
tenantId,
organizationId,
...(this.projectId
? {
projectId: this.projectId
}
: {})
...(this.projectId ? { projectId: this.projectId } : {})
})
.pipe(
// Map the response to either the fetched statuses or a default set
map(({ items, total }: IPagination<ITaskStatus>) => (total > 0 ? items : this._statuses)),
tap((statuses: ITaskStatus[]) => this.statuses$.next(statuses)),
untilDestroyed(this)
// Update the observable with the fetched statuses
tap((statuses: ITaskStatus[]) => {
this.statuses$.next(statuses);

// Set default status if no status is currently selected
if (!this.status) {
const defaultStatus = statuses.find((status) => status.name === TaskStatusEnum.OPEN);
if (defaultStatus) {
this.status = defaultStatus;
this.onChange(defaultStatus);
}
}
}),
untilDestroyed(this) // Clean up the subscription when component is destroyed
)
.subscribe();
}

/**
* Create new status from ng-select tag
* Creates a new task status from the ng-select input.
*
* @param name
* @returns
* @param name - The name of the new status to be created.
* @returns A promise that resolves when the status is successfully created.
*/
createNew = async (name: string) => {
createNew = async (name: string): Promise<void> => {
if (!this.organization) {
return;
}

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

const source = this.taskStatusesService.create({
// Prepare the task status payload
const payload = {
tenantId,
organizationId,
name,
...(this.projectId
? {
projectId: this.projectId
}
: {})
});
await firstValueFrom(source);
...(this.projectId ? { projectId: this.projectId } : {})
};

// Create the new task status and wait for completion
await firstValueFrom(this._taskStatusesService.create(payload));
} catch (error) {
this.toastrService.error(error);
console.error('Error while creating new task status:', error);
this._errorHandlingService.handleError(error);
} finally {
// Notify observers after creation attempt
this.subject$.next(true);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,13 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { TaskStatusSelectComponent } from './task-status-select.component';
import { TranslateModule } from '@ngx-translate/core';
import { TaskStatusesService } from '@gauzy/ui-core/core';
import { PipesModule } from '../../pipes/pipes.module';
import { TaskBadgeViewComponentModule } from '../task-badge-view/task-badge-view.module';
import { TaskStatusSelectComponent } from './task-status-select.component';

@NgModule({
imports: [
CommonModule,
FormsModule,
NgSelectModule,
TranslateModule.forChild(),
PipesModule,
TaskBadgeViewComponentModule
],
imports: [CommonModule, FormsModule, NgSelectModule, TranslateModule.forChild(), TaskBadgeViewComponentModule],
declarations: [TaskStatusSelectComponent],
exports: [TaskStatusSelectComponent],
providers: [TaskStatusesService]
Expand Down
Loading