diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/effects/project.effects.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/effects/project.effects.ts index 59b41e1b1..cd4a8aacb 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/effects/project.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/effects/project.effects.ts @@ -285,8 +285,8 @@ export class ProjectEffects { }, 500); void this.router.navigate([ - `project/${project.id}/${project.slug}${ - action.moveTo ? `/kanban/${action.moveTo}` : '/overview' + `project/${project.id}/${project.slug}/kanban/${ + action.moveTo ? action.moveTo : project.workflows[0].slug }`, ]); diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.css b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.css index eb3473bf3..d88a69a5e 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.css @@ -20,6 +20,7 @@ Copyright (c) 2023-present Kaleidos INC color: var(--color-gray100); font-weight: 500; margin: 0; + padding-inline-start: var(--spacing-16); } .view-options-list { @@ -32,6 +33,7 @@ Copyright (c) 2023-present Kaleidos INC .kanban-header-breadcrumbs { padding-block-start: var(--spacing-4); + padding-inline-start: var(--spacing-16); } .workflow-form { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.ts index e668ce565..6cbb8d943 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.ts @@ -25,6 +25,7 @@ import { updateWorkflow, } from '~/app/modules/project/data-access/+state/actions/project.actions'; import { Store } from '@ngrx/store'; +import { KanbanStory } from '~/app/modules/project/feature-kanban/kanban.model'; @Component({ selector: 'tg-kanban-header', @@ -49,6 +50,7 @@ export class KanbanHeaderComponent { @Input({ required: true }) public project!: Project; @Input({ required: true }) public workflows: Workflow[] = []; @Input({ required: true }) public workflow!: Workflow; + @Input({ required: true }) public stories: KanbanStory[] = []; public openWorkflowOptions = false; public deleteWorkflowModal = false; @@ -57,6 +59,11 @@ export class KanbanHeaderComponent { constructor(private store: Store) {} public openDeleteWorkflowModal() { + if (this.workflow.statuses.length === 0 || this.stories.length === 0) { + this.submitDeleteWorkflow(undefined); + return; + } + this.deleteWorkflowModal = true; } diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.ts index 252cea9c7..03ded6867 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.ts @@ -368,9 +368,10 @@ export const reducer = createImmerReducer( return state; }), on(KanbanEventsActions.newStory, (state, { story }): KanbanState => { - state.stories[story.status.id].push(story); - - state.newEventStories.push(story.ref); + if (state.stories[story.status.id]) { + state.stories[story.status.id].push(story); + state.newEventStories.push(story.ref); + } return state; }), diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.html index 36a614567..910849af4 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.html @@ -24,7 +24,8 @@ *ngIf="vm.workflow" [project]="vm.project" [workflows]="vm.workflows" - [workflow]="vm.workflow"> + [workflow]="vm.workflow" + [stories]="vm.stories">
; + stories: KanbanStory[]; } @UntilDestroy() @@ -241,6 +242,13 @@ export class ProjectFeatureKanbanComponent { .subscribe((postponed) => { this.lockKanban = !!postponed; }); + + this.state.connect( + 'stories', + this.store + .select(selectStories) + .pipe(map((stories) => Object.values(stories).flat())) + ); } public setCloseShortcut() { @@ -390,50 +398,6 @@ export class ProjectFeatureKanbanComponent { this.store.dispatch(ProjectActions.fetchProjectMembers()); this.unassignRoleMembersWithoutPermissions(permissions as Role); }); - - this.wsService - .projectEvents<{ - workflow: Workflow & { stories: Story[] }; - targetWorkflow: Workflow; - }>('workflows.delete') - .pipe( - untilDestroyed(this), - concatLatestFrom(() => [ - this.store.select(selectCurrentWorkflowSlug).pipe(filterNil()), - ]) - ) - .subscribe(([eventResponse, workflow]) => { - const action = eventResponse.event.content; - const { stories, ...wf } = action.workflow; - this.store.dispatch( - ProjectActions.projectEventActions.deleteWorkflow({ - workflow: wf, - targetWorkflow: action.targetWorkflow, - hasOpenStory: this.state.get('showStoryDetail'), - }) - ); - - if (workflow === action.targetWorkflow?.slug) { - this.movedWorkflow.statuses = action.workflow.statuses; - - const statuses = action.workflow.statuses; - statuses.forEach((status) => { - this.store.dispatch( - KanbanEventsActions.moveStatus({ - status, - workflow: action.targetWorkflow.slug, - stories: stories.filter( - (story) => story.status.id === status.id - ), - }) - ); - }); - - setTimeout(() => { - this.movedWorkflow.statuses = null; - }, 500); - } - }); } private unassignRoleMembersWithoutPermissions(role: Role) { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell.component.ts index 183e12ef8..1db3f6466 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell.component.ts @@ -58,6 +58,11 @@ import { TranslocoDirective } from '@ngneat/transloco'; import { ContextNotificationComponent } from '@taiga/ui/context-notification/context-notification.component'; import { ProjectNavigationComponent } from '../feature-navigation/project-feature-navigation.component'; import * as ProjectActions from '~/app/modules/project/data-access/+state/actions/project.actions'; +import { concatLatestFrom } from '@ngrx/effects'; +import { selectCurrentWorkflowSlug } from '../feature-kanban/data-access/+state/selectors/kanban.selectors'; +import { KanbanEventsActions } from '../feature-kanban/data-access/+state/actions/kanban.actions'; +import { MovedWorkflowService } from '../feature-kanban/services/moved-workflow.service'; +import { selectStory } from '../story-detail/data-access/+state/selectors/story-detail.selectors'; @UntilDestroy() @Component({ @@ -117,11 +122,13 @@ export class ProjectFeatureShellComponent implements OnDestroy, AfterViewInit { private state: RxState<{ project: Project; showBannerOnRevoke: boolean; + selectedStory: StoryDetail | null; }>, private userStorageService: UserStorageService, private router: Router, private route: ActivatedRoute, - private appService: AppService + private appService: AppService, + private movedWorkflow: MovedWorkflowService ) { this.route.data.subscribe((data) => { this.store.dispatch( @@ -142,6 +149,8 @@ export class ProjectFeatureShellComponent implements OnDestroy, AfterViewInit { this.store.select(selectShowBannerOnRevoke).pipe(filterNil()) ); + this.state.connect('selectedStory', this.store.select(selectStory)); + const project$ = this.state .select('project') .pipe(distinctUntilChanged((prev, curr) => prev.id === curr.id)); @@ -499,6 +508,56 @@ export class ProjectFeatureShellComponent implements OnDestroy, AfterViewInit { .subscribe(() => { this.userLoseMembership(); }); + + this.wsService + .projectEvents<{ + workflow: Workflow & { stories: Story[] }; + targetWorkflow: Workflow; + }>('workflows.delete') + .pipe( + untilDestroyed(this), + concatLatestFrom(() => [ + this.store.select(selectCurrentWorkflowSlug).pipe(filterNil()), + ]) + ) + .subscribe(([eventResponse, workflow]) => { + const action = eventResponse.event.content; + const { stories, ...wf } = action.workflow; + + this.store.dispatch( + ProjectActions.projectEventActions.deleteWorkflow({ + workflow: wf, + targetWorkflow: action.targetWorkflow, + hasOpenStory: !!this.state.get('selectedStory'), + }) + ); + + if (workflow === action.targetWorkflow?.slug) { + this.movedWorkflow.statuses = action.workflow.statuses; + + const statuses = action.workflow.statuses; + + statuses.forEach((status) => { + const statusStories = stories.filter( + (story) => story.status.id === status.id + ); + + if (statusStories && statusStories.length > 0) { + this.store.dispatch( + KanbanEventsActions.moveStatus({ + status, + workflow: action.targetWorkflow.slug, + stories: statusStories, + }) + ); + } + }); + + setTimeout(() => { + this.movedWorkflow.statuses = null; + }, 500); + } + }); } public userLoseMembership() {