From 7061e5a33aa90ce43a22e5c38b918da4ae034426 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 12 Sep 2023 13:42:20 +0200 Subject: [PATCH 01/16] feat(workflows): list workflows in main navigation bar --- ...roject-navigation-menu-styles.directive.ts | 36 +++ .../project-navigation-menu.component.css | 231 ++++++----------- .../project-navigation-menu.component.html | 240 +++++++++++++++--- .../project-navigation-menu.component.ts | 54 ++-- .../project-navigation-menu.directive.ts | 38 +++ .../project-feature-shell-routing.module.ts | 58 ++--- .../taiga/src/app/styles/shared/button.css | 28 +- .../src/app/styles/taiga-ui/tui-dropdown.css | 9 +- .../apps/taiga/src/assets/i18n/en-US.json | 4 + .../apps/taiga/src/assets/icons/kanban.svg | 5 +- .../libs/data/src/lib/project.model.mock.ts | 3 +- javascript/libs/data/src/lib/project.model.ts | 2 + 12 files changed, 459 insertions(+), 249 deletions(-) create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu-styles.directive.ts create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.directive.ts diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu-styles.directive.ts b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu-styles.directive.ts new file mode 100644 index 000000000..5e52808b4 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu-styles.directive.ts @@ -0,0 +1,36 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { Directive, ElementRef } from '@angular/core'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: '[kanbanWorkflowMenuStyles]', + standalone: true, +}) +export class ProjectNavWorkflowMenuStylesDirective { + constructor(private el: ElementRef) { + this.setStyles(); + } + + private setStyles() { + requestAnimationFrame(() => { + const tuiDropDown = (this.el.nativeElement as HTMLElement).closest( + 'tui-dropdown' + ) as HTMLElement; + if (tuiDropDown) { + tuiDropDown.style.setProperty('--tui-radius-m', '0 3px 3px 0'); + tuiDropDown.style.setProperty('--tui-base-04', 'var(--color-gray100)'); + tuiDropDown.style.setProperty( + '--tui-elevation-01', + 'var(--color-gray100)' + ); + } + }); + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css index 087198e18..8e1196e37 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css @@ -9,6 +9,7 @@ Copyright (c) 2023-present Kaleidos INC /* WORKSPACE */ @import url("tools/typography.css"); +@import url("taiga-ui/mixins/wrapper.css"); :host { --menu-size: 200px; @@ -133,7 +134,7 @@ ul { color: var(--color-gray40); display: flex; font-weight: var(--font-weight-regular); - padding: var(--spacing-4); + padding: var(--spacing-8); transition: padding var(--transition); } @@ -141,7 +142,7 @@ ul { aspect-ratio: 1/1; block-size: var(--spacing-24); inline-size: var(--spacing-24); - margin-inline-end: var(--spacing-16); + margin-inline-end: var(--spacing-8); } .menu-option-icon { @@ -152,11 +153,20 @@ ul { @mixin menu-option-item; &:hover, + &.active-section, &.active-dialog { background: var(--color-gray90); - color: var(--color-primary); + color: var(--color-gray20); font-weight: var(--font-weight-regular); } + + &:focus-visible { + outline: solid 2px var(--color-secondary50); + } + + &.has-add-workflow-button { + position: relative; + } } .menu-option { @@ -169,125 +179,53 @@ ul { font-weight: var(--font-weight-medium); } } - - & .menu-option-icon { - aspect-ratio: 1/1; - block-size: var(--spacing-24); - inline-size: var(--spacing-24); - margin-inline-end: var(--spacing-16); - } } -.menu-option-scrum { - & .scrum-button { - @mixin menu-option-item; - - align-items: center; - appearance: none; - background: none; - border: none; - cursor: pointer; - display: flex; - inline-size: 100%; - - &:focus-visible { - outline: solid 2px var(--color-secondary); - } +/* Submenu of a menu option */ - &:hover, - &.active-dialog { - background: var(--color-gray90); - color: var(--color-primary); - } +.submenu { + padding-block: var(--spacing-8); +} - & .scrum-button-icon { - @mixin menu-option-icon; - } +.submenu-option { + border-inline-start: 1px solid var(--color-gray90); + margin-block: 0; + margin-inline-end: var(--spacing-8); + margin-inline-start: var(--spacing-16); + padding-block: var(--spacing-4); + padding-inline-start: var(--spacing-8); +} - & .chevron { - block-size: var(--spacing-16); - color: var(--color-primary); - inline-size: var(--spacing-16); - margin-inline-start: auto; - transform: rotate(180deg); - transition: all 0.2s linear; - - &.active { - transform: rotate(0); - transition: all 0.2s linear; - } - } - } +.submenu-option-item { + @mixin menu-option-item; - &.scrum-active { - & .scrum-button { - color: var(--color-white); + block-size: var(--spacing-28); - &:hover { - color: var(--color-primary); - } - } + &:hover { + background: var(--color-gray90); + color: var(--color-gray20); } -} - -.menu-child-scrum { - background: var(--color-gray90); - padding: var(--spacing-8); - & .menu-child-option { - &:last-child { - margin-block-end: 0; - } + &:active, + &.submenu-option-item-active { + background: var(--color-secondary90); + color: var(--color-white); } - & .menu-child-option-item { - @mixin menu-option-item; - - padding-inline-start: var(--spacing-16); - - &:hover, - &.active { - color: var(--color-primary); - } - - &:hover { - background: var(--color-gray80); - } - - &.active { - background: var(--color-secondary80); - } + &:focus-visible { + outline: solid 2px var(--color-secondary50); } } -.secondary-menu { - & .menu-option-item { - &:hover { - color: var(--color-white); - - & .arrow { - opacity: 1; - } - } - - &:focus { - color: var(--color-gray40); - - & .arrow { - opacity: 1; - } - } - } - - & .arrow { - block-size: 1rem; - color: var(--color-gray60); - inline-size: 1rem; - margin-inline-start: auto; - opacity: 0; - } +/* Create workflow button */ +.create-workflow { + inset-block-start: var(--spacing-4); + inset-inline-end: var(--spacing-4); + position: absolute; } +/* Bottom area */ + .bottom-menu { & .project-settings { background: transparent; @@ -378,8 +316,7 @@ ul { } & .menu-option-item, - & .bottom-menu-option-item, - & .scrum-button { + & .bottom-menu-option-item { padding-inline-start: var(--spacing-4); transition: padding var(--transition); transition-delay: var(--transition-delay); @@ -389,21 +326,26 @@ ul { color: var(--color-white); inline-size: var(--inline-btn-size); } + + &.collapsed-kanban-button { + appearance: none; + background: none; + border: 0; + padding-inline: var(--spacing-4); + + &:hover { + cursor: pointer; + inline-size: auto; + } + } } - & .menu-option-icon, - & .scrum-button-icon { + & .menu-option-icon { margin-inline-end: 0; transition: margin var(--transition); transition-delay: var(--transition-delay); } - & .menu-option-scrum { - & .scrum-button-icon { - margin-inline-end: 0; - } - } - & .button-collapse { justify-content: start; padding: 0; @@ -420,40 +362,25 @@ ul { /* FLOATING DIALOG */ -.dialog-scrum { - box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.3); - - & .child-menu-option { - margin-block-end: 0; - } +.dialog-kanban { + background: var(--color-gray100); + color: var(--color-white); + inline-size: 170px; - & .child-menu-option-item { + & .dialog-kanban-title { + align-items: center; block-size: var(--spacing-32); - border-radius: 3px; - display: block; - font-weight: var(--font-weight-regular); - padding: var(--spacing-8); - - &:hover { - background: var(--color-gray80); - color: var(--color-primary); - } - - &.active { - background: var(--color-secondary80); - } - } - - & .child-menu-option-scrum { - block-size: auto; color: var(--color-white); - display: block; - margin-block-end: var(--spacing-8); - padding: 0; + display: flex; + padding-block: 0; + padding-inline: var(--spacing-12) var(--spacing-2); - &:hover { - background: none; - color: var(--color-white); + & .dialog-kanban-text { + @mixin ellipsis; + + font-size: var(--font-size-medium); + font-weight: var(--font-weight-regular); + text-decoration: none; } } } @@ -502,8 +429,14 @@ ul { padding-inline-start: var(--spacing-24); } - & .dialog-scrum { - color: var(--color-gray-40); - padding: var(--spacing-8); + & .dialog-kanban { + & .submenu { + background: var(--color-gray100); + margin: 0; + } + + & .submenu-option { + margin-inline-start: var(--spacing-4); + } } } diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html index 862f63a37..3f366bcd5 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html @@ -100,40 +100,190 @@ - + + +
+ + +
+
+ + + (pointerenter)="popup($event, 'kanban')" + (pointerleave)="out()"> + + + + {{ t('commons.kanban') }} + + + + + + +
@@ -201,18 +351,18 @@ [style.top.px]="dialog.top" [style.left.px]="dialog.left"> {{ dialog.text }} + + +
+ + + diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts index 507c60311..6af7f0f5c 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts @@ -21,11 +21,18 @@ import { RouterModule } from '@angular/router'; import { TranslocoDirective } from '@ngneat/transloco'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; -import { TuiSvgModule } from '@taiga-ui/core'; -import { Project } from '@taiga/data'; +import { + TuiButtonModule, + TuiHostedDropdownModule, + TuiSvgModule, +} from '@taiga-ui/core'; +import { Project, Workflow } from '@taiga/data'; import { AvatarComponent } from '@taiga/ui/avatar/avatar.component'; - +import { TooltipDirective } from '@taiga/ui/tooltip'; +import { TooltipComponent } from '@taiga/ui/tooltip/tooltip.component'; import { HasPermissionDirective } from '~/app/shared/directives/has-permissions/has-permission.directive'; +import { ProjectNavWorkflowMenuStylesDirective } from './project-navigation-menu-styles.directive'; +import { ProjectNavWorkflowMenuPositionDirective } from './project-navigation-menu.directive'; interface ProjectMenuDialog { hover: boolean; @@ -39,7 +46,7 @@ interface ProjectMenuDialog { mainLinkHeight: number; children: { text: string; - link: string[]; + link: string; }[]; } const cssValue = getComputedStyle(document.documentElement); @@ -57,6 +64,12 @@ const cssValue = getComputedStyle(document.documentElement); AvatarComponent, TranslocoDirective, CommonModule, + TooltipComponent, + TooltipDirective, + TuiButtonModule, + TuiHostedDropdownModule, + ProjectNavWorkflowMenuPositionDirective, + ProjectNavWorkflowMenuStylesDirective, ], animations: [ trigger('blockInitialRenderAnimation', [transition(':enter', [])]), @@ -100,7 +113,8 @@ export class ProjectNavigationMenuComponent { public projectSettingButton!: ElementRef; public collapseText = true; - public scrumChildMenuVisible = false; + public activeSection = false; + public openworkflowsDropdown = false; public dialog: ProjectMenuDialog = { open: false, @@ -133,7 +147,7 @@ export class ProjectNavigationMenuComponent { if (text) { const navigationBarWidth = 48; - if (type !== 'scrum' && el.querySelector('a')) { + if (el.querySelector('a')) { this.dialog.link = el.querySelector('a')!.getAttribute('href') ?? ''; } this.dialog.hover = false; @@ -185,32 +199,12 @@ export class ProjectNavigationMenuComponent { this.out(); } - public popupScrum(event: MouseEvent | FocusEvent) { - if (!this.collapsed) { - return; - } - - this.initDialog(event.target as HTMLElement, 'scrum' /* children */); - } - - public toggleScrumChildMenu() { - if (this.collapsed) { - (this.backlogSubmenuEl.nativeElement as HTMLElement).focus(); - } else { - this.scrumChildMenuVisible = !this.scrumChildMenuVisible; - } - } - public getCollapseIcon() { return this.collapsed ? 'collapse-right' : 'collapse-left'; } public toggleCollapse() { this.collapseMenu.next(); - - if (this.collapsed) { - this.scrumChildMenuVisible = false; - } } public openSettings() { @@ -218,4 +212,12 @@ export class ProjectNavigationMenuComponent { this.dialog.open = false; this.dialog.type = ''; } + + public createWorkflow() { + console.log('create workflow'); + } + + public trackById(_index: number, workflow: Workflow) { + return workflow.id; + } } diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.directive.ts b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.directive.ts new file mode 100644 index 000000000..0d51c9896 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.directive.ts @@ -0,0 +1,38 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { Directive, ElementRef, Inject } from '@angular/core'; +import { + TuiPoint, + TuiPositionAccessor, + tuiAsPositionAccessor, +} from '@taiga-ui/core'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: '[kanbanWorkflowMenuPosition]', + providers: [tuiAsPositionAccessor(ProjectNavWorkflowMenuPositionDirective)], + standalone: true, +}) +export class ProjectNavWorkflowMenuPositionDirective extends TuiPositionAccessor { + public readonly type = 'dropdown'; + + constructor( + @Inject(ElementRef) private readonly el: ElementRef + ) { + super(); + } + + public getPosition(): TuiPoint { + const { right, top } = this.el.nativeElement.getBoundingClientRect(); + + const spacing = 4; //4px + + return [top + spacing, right + spacing * 2]; + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts index 0faff9bdd..eaf42252c 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts @@ -8,10 +8,10 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { CanDeactivateGuard } from '~/app/shared/can-deactivate/can-deactivate.guard'; import { ProjectAdminResolver } from './project-admin.resolver.service'; import { ProjectFeatureShellResolverService } from './project-feature-shell-resolver.service'; import { ProjectFeatureShellComponent } from './project-feature-shell.component'; -import { CanDeactivateGuard } from '~/app/shared/can-deactivate/can-deactivate.guard'; const routes: Routes = [ { @@ -22,18 +22,34 @@ const routes: Routes = [ }, children: [ { - path: ':slug/kanban', + path: '', + loadChildren: () => + import( + '~/app/modules/project/feature-overview/project-feature-overview.module' + ).then((m) => m.ProjectFeatureOverviewModule), + }, + { + path: 'kanban', loadChildren: () => import( '~/app/modules/project/feature-view-setter/project-feature-view-setter.module' ).then((m) => m.ProjectFeatureViewSetterModule), canDeactivate: [CanDeactivateGuard], - data: { - kanban: true, - }, }, { - path: 'kanban', + path: ':slug', + loadChildren: () => + import( + '~/app/modules/project/feature-overview/project-feature-overview.module' + ).then((m) => m.ProjectFeatureOverviewModule), + }, + { + path: ':slug/kanban', + redirectTo: ':slug/kanban/main', + pathMatch: 'full', + }, + { + path: ':slug/kanban/:workflow', loadChildren: () => import( '~/app/modules/project/feature-view-setter/project-feature-view-setter.module' @@ -44,18 +60,18 @@ const routes: Routes = [ }, }, { - path: ':slug/stories/:storyRef', + path: 'stories/:storyRef', loadChildren: () => import( '~/app/modules/project/feature-view-setter/project-feature-view-setter.module' ).then((m) => m.ProjectFeatureViewSetterModule), canDeactivate: [CanDeactivateGuard], data: { - stories: true, + kanban: true, }, }, { - path: 'stories/:storyRef', + path: ':slug/stories/:storyRef', loadChildren: () => import( '~/app/modules/project/feature-view-setter/project-feature-view-setter.module' @@ -66,7 +82,7 @@ const routes: Routes = [ }, }, { - path: ':slug/settings', + path: 'settings', loadChildren: () => import( '~/app/modules/project/settings/feature-settings/feature-settings.module' @@ -79,7 +95,7 @@ const routes: Routes = [ }, }, { - path: 'settings', + path: ':slug/settings', loadChildren: () => import( '~/app/modules/project/settings/feature-settings/feature-settings.module' @@ -91,26 +107,6 @@ const routes: Routes = [ project: ProjectAdminResolver, }, }, - { - path: 'overview', - loadChildren: () => - import( - '~/app/modules/project/feature-overview/project-feature-overview.module' - ).then((m) => m.ProjectFeatureOverviewModule), - data: { - overview: true, - }, - }, - { - path: ':slug/overview', - loadChildren: () => - import( - '~/app/modules/project/feature-overview/project-feature-overview.module' - ).then((m) => m.ProjectFeatureOverviewModule), - data: { - overview: true, - }, - }, ], }, ]; diff --git a/javascript/apps/taiga/src/app/styles/shared/button.css b/javascript/apps/taiga/src/app/styles/shared/button.css index 97e7fe5c2..5033bf3a0 100644 --- a/javascript/apps/taiga/src/app/styles/shared/button.css +++ b/javascript/apps/taiga/src/app/styles/shared/button.css @@ -419,6 +419,7 @@ Used only in comment list sort button & [tuiWrapper] { background: none; + border-radius: 2px; color: var(--color-gray60); font-weight: var(--font-weight-medium); padding: var(--spacing-4) !important; @@ -430,12 +431,37 @@ Used only in comment list sort button } @mixin wrapper-focus { + --tui-focus: var(--color-secondary); + + color: var(--color-secondary); + &::after { - border-color: var(--color-secondary); border-width: 1px; } } } + + &[variant="dark"] { + & [tuiWrapper] { + color: var(--color-gray40); + + @mixin wrapper-hover { + background: var(--color-gray80); + color: var(--color-secondary40); + } + + @mixin wrapper-focus { + --tui-focus: var(--color-secondary50); + + color: var(--color-secondary50); + + /* stylelint-disable-next-line max-nesting-depth */ + &::after { + border-width: 2px; + } + } + } + } } [data-appearance="action-button-2"]:is(button, a) { diff --git a/javascript/apps/taiga/src/app/styles/taiga-ui/tui-dropdown.css b/javascript/apps/taiga/src/app/styles/taiga-ui/tui-dropdown.css index 00a94b676..feec438b5 100644 --- a/javascript/apps/taiga/src/app/styles/taiga-ui/tui-dropdown.css +++ b/javascript/apps/taiga/src/app/styles/taiga-ui/tui-dropdown.css @@ -9,12 +9,19 @@ Copyright (c) 2023-present Kaleidos INC tui-dropdown { --tui-radius-m: 3px; + --tui-base-04: var(--color-gray50); + --tui-elevation-01: var(--color-white); - border: 1px solid var(--color-gray50) !important; box-shadow: 0.25rem 0.25rem 0.5rem 0 rgba(46, 52, 64, 0.1) !important; margin-block-start: -4px; & .t-checkmark { visibility: hidden; } + + &:has(.dialog-kanban) { + --tui-radius-m: 0 3px 3px 0; + --tui-base-04: var(--color-gray100); + --tui-elevation-01: var(--color-gray100); + } } diff --git a/javascript/apps/taiga/src/assets/i18n/en-US.json b/javascript/apps/taiga/src/assets/i18n/en-US.json index 241dc7978..3be2f0238 100644 --- a/javascript/apps/taiga/src/assets/i18n/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/en-US.json @@ -112,6 +112,10 @@ "choose_image_no_svg_tip": "Allowed formats: gif, png, jpg and webp", "format_no_svg_error": "The file format is not allowed. Allowed formats: gif, png, jpg or webp" }, + "workflow": { + "create": "Create new workflow", + "workflow_list": "Workflow list" + }, "delete_modal_title": "Delete {{projectTitle}}?", "delete_modal_warning": "Warning: This action cannot be undone.", "delete_modal_text": "This action will delete the project, along with all the stories and attachments in it.", diff --git a/javascript/apps/taiga/src/assets/icons/kanban.svg b/javascript/apps/taiga/src/assets/icons/kanban.svg index 8043d1270..c9d9b5f05 100644 --- a/javascript/apps/taiga/src/assets/icons/kanban.svg +++ b/javascript/apps/taiga/src/assets/icons/kanban.svg @@ -1,4 +1,3 @@ - - + + diff --git a/javascript/libs/data/src/lib/project.model.mock.ts b/javascript/libs/data/src/lib/project.model.mock.ts index 0f034e565..20d118d9c 100644 --- a/javascript/libs/data/src/lib/project.model.mock.ts +++ b/javascript/libs/data/src/lib/project.model.mock.ts @@ -16,7 +16,7 @@ import { randUuid, randWord, } from '@ngneat/falso'; -import { ProjectCreation, Workspace } from '..'; +import { ProjectCreation, WorkflowMockFactory, Workspace } from '..'; import { Project } from './project.model'; import { WorkspaceMockFactory } from './workspace.model.mock'; @@ -30,6 +30,7 @@ export const ProjectMockFactory = ( color: randNumber(), description: randParagraph({ length: 3 }).join('\n'), workspace: workspace ?? WorkspaceMockFactory(), + workflows: [WorkflowMockFactory()], logo: randImg(), logoSmall: randImg(), logoLarge: randImg(), diff --git a/javascript/libs/data/src/lib/project.model.ts b/javascript/libs/data/src/lib/project.model.ts index 836d04811..240dcd824 100644 --- a/javascript/libs/data/src/lib/project.model.ts +++ b/javascript/libs/data/src/lib/project.model.ts @@ -9,6 +9,7 @@ import type { Merge } from 'type-fest'; import { Membership } from './membership.model'; import { Story } from './story.model'; +import { Workflow } from './workflow.model'; import { Workspace } from './workspace.model'; export interface Project { @@ -21,6 +22,7 @@ export interface Project { description: string | null; color: number; workspace: Pick; + workflows: Workflow[]; userIsAdmin: boolean; userIsMember: boolean; userPermissions: string[]; From 039e0120359461a5f908e265dd9442c042b4223a Mon Sep 17 00:00:00 2001 From: Xaviju Date: Thu, 14 Sep 2023 09:27:08 +0200 Subject: [PATCH 02/16] feat(workflows): navigate to workflows --- .../kanban-create-status.component.ts | 14 +-- .../edit-status/edit-status.component.ts | 20 ++-- .../kanban-scroll-manager.service.ts | 4 +- .../+state/actions/kanban.actions.ts | 6 +- .../+state/effects/kanban.effects.ts | 45 ++++----- .../+state/reducers/kanban.reducer.ts | 92 ++++++++----------- .../+state/selectors/kanban.selectors.ts | 12 +-- .../project-feature-kanban.component.html | 17 ++-- .../project-feature-kanban.component.ts | 61 ++++++------ .../services/a11yDrag.service.ts | 7 +- .../project-feature-shell-routing.module.ts | 1 + .../+state/effects/story-detail.effects.ts | 38 +++----- .../story-detail/story-detail.component.ts | 7 +- .../src/lib/project/project-api.service.ts | 4 +- 14 files changed, 144 insertions(+), 184 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/create-status/kanban-create-status.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/create-status/kanban-create-status.component.ts index 51ad1d2a2..1899322d2 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/create-status/kanban-create-status.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/create-status/kanban-create-status.component.ts @@ -6,24 +6,24 @@ * Copyright (c) 2023-present Kaleidos INC */ +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, - Output, Input, OnInit, + Output, } from '@angular/core'; +import { TranslocoDirective } from '@ngneat/transloco'; import { Store } from '@ngrx/store'; +import { TuiButtonModule } from '@taiga-ui/core'; import { KanbanActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions'; -import { selectCurrentWorkflow } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors'; +import { selectWorkflow } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors'; import { EditStatus } from '~/app/modules/project/feature-kanban/models/edit-status.model'; -import { TuiButtonModule } from '@taiga-ui/core'; -import { EditStatusComponent } from '../edit-status/edit-status.component'; -import { CommonModule } from '@angular/common'; -import { TranslocoDirective } from '@ngneat/transloco'; import { RestoreFocusTargetDirective } from '~/app/shared/directives/restore-focus/restore-focus-target.directive'; +import { EditStatusComponent } from '../edit-status/edit-status.component'; @Component({ selector: 'tg-kanban-create-status', @@ -49,7 +49,7 @@ export class KanbanCreateStatusComponent implements OnInit { @Output() public closeForm = new EventEmitter(); - public workflow = this.store.selectSignal(selectCurrentWorkflow); + public workflow = this.store.selectSignal(selectWorkflow); public showAddForm = false; public columnSize = 292; diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/edit-status/edit-status.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/edit-status/edit-status.component.ts index 1a5b2f2ea..392910985 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/edit-status/edit-status.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/edit-status/edit-status.component.ts @@ -6,6 +6,7 @@ * Copyright (c) 2023-present Kaleidos INC */ +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -20,21 +21,20 @@ import { import { FormBuilder, FormGroup, - Validators, ReactiveFormsModule, + Validators, } from '@angular/forms'; +import { TranslocoDirective } from '@ngneat/transloco'; import { Store } from '@ngrx/store'; -import { Status } from '@taiga/data'; -import { selectCurrentWorkflow } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors'; -import { EditStatus } from '~/app/modules/project/feature-kanban/models/edit-status.model'; -import { UtilsService } from '~/app/shared/utils/utils-service.service'; import { TuiButtonModule } from '@taiga-ui/core'; -import { CommonModule } from '@angular/common'; -import { TranslocoDirective } from '@ngneat/transloco'; +import { Status } from '@taiga/data'; import { InputsModule } from '@taiga/ui/inputs'; -import { RestoreFocusDirective } from '~/app/shared/directives/restore-focus/restore-focus.directive'; -import { OutsideClickDirective } from '~/app/shared/directives/outside-click/outside-click.directive'; +import { selectWorkflow } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors'; +import { EditStatus } from '~/app/modules/project/feature-kanban/models/edit-status.model'; import { AutoFocusDirective } from '~/app/shared/directives/auto-focus/auto-focus.directive'; +import { OutsideClickDirective } from '~/app/shared/directives/outside-click/outside-click.directive'; +import { RestoreFocusDirective } from '~/app/shared/directives/restore-focus/restore-focus.directive'; +import { UtilsService } from '~/app/shared/utils/utils-service.service'; @Component({ selector: 'tg-edit-status', @@ -76,7 +76,7 @@ export class EditStatusComponent implements OnInit { this.cancelEdit(); } - public workflow = this.store.selectSignal(selectCurrentWorkflow); + public workflow = this.store.selectSignal(selectWorkflow); public statusForm!: FormGroup; public statusMaxLength = 30; diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/custom-scroll-strategy/kanban-scroll-manager.service.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/custom-scroll-strategy/kanban-scroll-manager.service.ts index 4d6380e9f..0fdc25582 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/custom-scroll-strategy/kanban-scroll-manager.service.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/custom-scroll-strategy/kanban-scroll-manager.service.ts @@ -24,8 +24,8 @@ import { filterNil } from '~/app/shared/utils/operators'; import { KanbanStatusComponent } from '../components/status/kanban-status.component'; import { KanbanWorkflowComponent } from '../components/workflow/kanban-workflow.component'; import { - selectCurrentWorkflow, selectStory, + selectWorkflow, } from '../data-access/+state/selectors/kanban.selectors'; import { KanbanStory } from '../kanban.model'; @@ -131,7 +131,7 @@ export class KanbanScrollManagerService { private moveToStatus(status: Status) { return new Observable((subscriber) => { this.store - .select(selectCurrentWorkflow) + .select(selectWorkflow) .pipe( filterNil(), take(1), diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts index 5db1323f9..41a3bbd46 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts @@ -8,18 +8,18 @@ import { createActionGroup, emptyProps, props } from '@ngrx/store'; import { Membership, Status, Story, Workflow } from '@taiga/data'; +import { DropCandidate } from '@taiga/ui/drag/drag.model'; import { KanbanReorderEvent, KanbanStory, KanbanStoryA11y, PartialStory, } from '~/app/modules/project/feature-kanban/kanban.model'; -import { DropCandidate } from '@taiga/ui/drag/drag.model'; export const KanbanActions = createActionGroup({ source: 'Kanban', events: { - 'Init Kanban': emptyProps(), + 'Init Kanban': props<{ workflow: Workflow['slug'] }>(), 'Open Create Story form': props<{ status: Status['id'] }>(), 'Close Create Story form': emptyProps(), 'Create Story': props<{ @@ -120,7 +120,7 @@ export const KanbanActions = createActionGroup({ export const KanbanApiActions = createActionGroup({ source: 'Kanban Api', events: { - 'Fetch Workflows Success': props<{ workflows: Workflow[] }>(), + 'Fetch Workflow Success': props<{ workflow: Workflow }>(), 'Fetch Stories Success': props<{ stories: Story[]; offset: number; diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts index 10ca94e7c..760212afd 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts @@ -36,9 +36,8 @@ import { KanbanEventsActions, } from '../actions/kanban.actions'; import { - selectCurrentWorkflow, selectCurrentWorkflowSlug, - selectWorkflows, + selectWorkflow, } from '../selectors/kanban.selectors'; @Injectable() @@ -51,11 +50,13 @@ export class KanbanEffects { ]), fetch({ run: (action, project) => { - return this.projectApiService.getWorkflows(project.id).pipe( - map((workflows) => { - return KanbanApiActions.fetchWorkflowsSuccess({ workflows }); - }) - ); + return this.projectApiService + .getWorkflow(project.id, action.workflow) + .pipe( + map((workflow) => { + return KanbanApiActions.fetchWorkflowSuccess({ workflow }); + }) + ); }, onError: (action, error: HttpErrorResponse) => { return this.appService.errorManagement(error); @@ -69,22 +70,24 @@ export class KanbanEffects { ofType(KanbanActions.initKanban), concatLatestFrom(() => [ this.store.select(selectCurrentProject).pipe(filterNil()), - this.store.select(selectWorkflows), + this.store.select(selectWorkflow), ]), fetch({ run: (action, project) => { - return this.projectApiService.getAllStories(project.id, 'main').pipe( - map(({ stories, offset, complete }) => { - return KanbanApiActions.fetchStoriesSuccess({ - stories, - offset, - complete, - }); - }), - finalize(() => { - return KanbanActions.loadStoriesComplete(); - }) - ); + return this.projectApiService + .getAllStories(project.id, action.workflow) + .pipe( + map(({ stories, offset, complete }) => { + return KanbanApiActions.fetchStoriesSuccess({ + stories, + offset, + complete, + }); + }), + finalize(() => { + return KanbanActions.loadStoriesComplete(); + }) + ); }, onError: (action, error: HttpErrorResponse) => { return this.appService.errorManagement(error); @@ -437,7 +440,7 @@ export class KanbanEffects { filter(({ candidate }) => !!candidate), concatLatestFrom(() => [ this.store.select(selectCurrentProjectId).pipe(filterNil()), - this.store.select(selectCurrentWorkflow).pipe(filterNil()), + this.store.select(selectWorkflow).pipe(filterNil()), ]), pessimisticUpdate({ run: (action, project, workflow) => { 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 a3e6d290d..c010f7ac6 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 @@ -8,6 +8,7 @@ import { createFeature, createSelector, on } from '@ngrx/store'; import { Status, Story, Workflow } from '@taiga/data'; +import { DropCandidate } from '@taiga/ui/drag/drag.model'; import { projectEventActions } from '~/app/modules/project/data-access/+state/actions/project.actions'; import { KanbanStory, @@ -15,7 +16,7 @@ import { PartialStory, } from '~/app/modules/project/feature-kanban/kanban.model'; import { StoryDetailActions } from '~/app/modules/project/story-detail/data-access/+state/actions/story-detail.actions'; -import { DropCandidate } from '@taiga/ui/drag/drag.model'; +import { moveItemArray } from '~/app/shared/utils/move-item-array'; import { pick } from '~/app/shared/utils/pick'; import { createImmerReducer } from '~/app/shared/utils/store'; import { @@ -31,12 +32,11 @@ import { replaceStory, setIntialPosition, } from './kanban.reducer.helpers'; -import { moveItemArray } from '~/app/shared/utils/move-item-array'; export interface KanbanState { - loadingWorkflows: boolean; + loadingWorkflow: boolean; loadingStories: boolean; - workflows: null | Workflow[]; + workflow: null | Workflow; currentWorkflowSlug: Workflow['slug']; stories: Record; createStoryForm: Status['id']; @@ -67,9 +67,9 @@ export interface KanbanState { } export const initialKanbanState: KanbanState = { - loadingWorkflows: false, + loadingWorkflow: false, loadingStories: false, - workflows: null, + workflow: null, currentWorkflowSlug: 'main', stories: {}, createStoryForm: '', @@ -272,22 +272,20 @@ export const reducer = createImmerReducer( } ), on( - KanbanApiActions.fetchWorkflowsSuccess, - (state, { workflows }): KanbanState => { - state.workflows = workflows; - state.loadingWorkflows = false; - - workflows.forEach((workflow) => { - workflow.statuses.forEach((status) => { - if (!state.stories[status.id]) { - state.stories[status.id] = []; - } - }); + KanbanApiActions.fetchWorkflowSuccess, + (state, { workflow }): KanbanState => { + state.workflow = workflow; + state.loadingWorkflow = false; + + workflow.statuses.forEach((status) => { + if (!state.stories[status.id]) { + state.stories[status.id] = []; + } }); if (state.empty !== null && state.empty) { // open the first form if the kanban is empty and there is at least one status - state.createStoryForm = state.workflows[0].statuses?.[0]?.id; + state.createStoryForm = state.workflow.statuses?.[0]?.id; } return state; @@ -310,9 +308,9 @@ export const reducer = createImmerReducer( state.empty = !stories.length; } - if (state.empty && state.workflows) { + if (state.empty && state.workflow) { // open the first form if the kanban is empty and there is at least one status - state.createStoryForm = state.workflows[0].statuses?.[0]?.id; + state.createStoryForm = state.workflow.statuses?.[0]?.id; } return state; @@ -448,8 +446,7 @@ export const reducer = createImmerReducer( state = removeStory(state, (it) => !!it._shadow || !!it._dragging); if (story) { - // TODO: current workflow - const statusObj = state.workflows![0].statuses.find( + const statusObj = state.workflow?.statuses.find( (it) => it.id === status ); @@ -533,8 +530,8 @@ export const reducer = createImmerReducer( const oldStory = findStory(state, (it) => it.ref === story.ref); // TODO: current workflow - if (oldStory?.status.id !== story.status && state.workflows) { - const status = state.workflows[0].statuses.find( + if (oldStory?.status.id !== story.status && state.workflow) { + const status = state.workflow.statuses.find( (it) => it.id === story.status ); @@ -669,12 +666,10 @@ export const reducer = createImmerReducer( on( KanbanApiActions.createStatusSuccess, KanbanEventsActions.updateStatus, - (state, { status, workflow }): KanbanState => { + (state, { status }): KanbanState => { state.loadingStatus = false; - const workflowIndex = state.workflows!.findIndex( - (it) => it.slug === workflow - ); - state.workflows![workflowIndex].statuses.push(status); + + state.workflow?.statuses.push(status); state.stories[status.id] = []; return state; @@ -683,29 +678,22 @@ export const reducer = createImmerReducer( on( KanbanActions.editStatus, KanbanEventsActions.editStatus, - (state, { status, workflow }): KanbanState => { - const workflowIndex = state.workflows!.findIndex( - (it) => it.slug === workflow - ); - const statusIndex = state.workflows![workflowIndex].statuses.findIndex( + (state, { status }): KanbanState => { + const statusIndex = state.workflow!.statuses.findIndex( (it) => it.id === status.id ); - state.workflows![workflowIndex].statuses[statusIndex].name = status.name; + state.workflow!.statuses[statusIndex].name = status.name; return state; } ), on( KanbanApiActions.editStatusError, - (state, { undo, status, workflow }): KanbanState => { - const workflowIndex = state.workflows!.findIndex( - (it) => it.slug === workflow - ); - const statusIndex = state.workflows![workflowIndex].statuses.findIndex( + (state, { undo, status }): KanbanState => { + const statusIndex = state.workflow!.statuses.findIndex( (it) => it.id === status.id ); - state.workflows![workflowIndex].statuses[statusIndex].name = - undo.status.name; + state.workflow!.statuses[statusIndex].name = undo.status.name; return state; } @@ -713,11 +701,8 @@ export const reducer = createImmerReducer( on( KanbanApiActions.deleteStatusSuccess, KanbanEventsActions.statusDeleted, - (state, { status, workflow, moveToStatus }): KanbanState => { - const workflowIndex = state.workflows!.findIndex( - (it) => it.slug === workflow - ); - const statuses = state.workflows![workflowIndex].statuses; + (state, { status, moveToStatus }): KanbanState => { + const statuses = state.workflow!.statuses; const statusIndex = statuses.findIndex((it) => it.id === status); if (moveToStatus) { const storiesToMove = state.stories[status]; @@ -733,7 +718,7 @@ export const reducer = createImmerReducer( } ), on(KanbanActions.statusDragStart, (state, { id }): KanbanState => { - const currentWorkflow = state.workflows ? state.workflows[0] : undefined; + const currentWorkflow = state.workflow; if (currentWorkflow) { state.dragType = 'status'; @@ -759,7 +744,7 @@ export const reducer = createImmerReducer( KanbanActions.statusDropped, projectEventActions.statusReorder, (state, { id, candidate }): KanbanState => { - const currentWorkflow = state.workflows ? state.workflows[0] : undefined; + const currentWorkflow = state.workflow; if (!currentWorkflow) { return state; @@ -811,19 +796,18 @@ export const kanbanFeature = createFeature({ name: 'kanban', reducer, extraSelectors: ({ - selectWorkflows, + selectWorkflow, selectDraggingStatus, selectStatusDropCandidate, }) => ({ selectColums: createSelector( - selectWorkflows, + selectWorkflow, selectDraggingStatus, selectStatusDropCandidate, - (workflows, currentStatus, statusDropCandidate) => { - if (!workflows || !workflows.length) { + (workflow, currentStatus, statusDropCandidate) => { + if (!workflow) { return []; } - const workflow = workflows[0]; if (!statusDropCandidate) { return workflow.statuses.map((it) => { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors.ts index b7e2e81e7..0b73db39a 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors.ts @@ -13,9 +13,9 @@ import { findStory } from '../reducers/kanban.reducer.helpers'; export const { selectKanbanState, - selectLoadingWorkflows, + selectLoadingWorkflow, selectLoadingStories, - selectWorkflows, + selectWorkflow, selectStories, selectCreateStoryForm, selectScrollToStory, @@ -57,11 +57,3 @@ export const selectStory = (ref: number) => { return findStory(state, (it) => it.ref === ref); }); }; - -export const selectCurrentWorkflow = createSelector( - selectWorkflows, - selectCurrentWorkflowSlug, - (wokflows, slug) => { - return wokflows?.find((it) => it.slug === slug); - } -); 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 260a598d3..1a3482bb3 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,19 +24,16 @@ {{ t('kanban.title') }}
- - - - + +
{{ t('kanban.empty') }}
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts index 8a1779a81..0c2d520f0 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts @@ -6,10 +6,11 @@ * Copyright (c) 2023-present Kaleidos INC */ -import { Location, CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; +import { TranslocoDirective } from '@ngneat/transloco'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { concatLatestFrom } from '@ngrx/effects'; import { Store } from '@ngrx/store'; @@ -21,13 +22,14 @@ import { Permissions, Project, Role, + Status, Story, StoryDetail, StoryView, - WorkflowStatus, - Status, Workflow, + WorkflowStatus, } from '@taiga/data'; +import { ModalComponent } from '@taiga/ui/modal/components'; import { combineLatest, filter, map, merge, pairwise, take } from 'rxjs'; import * as ProjectActions from '~/app/modules/project/data-access/+state/actions/project.actions'; import { @@ -37,9 +39,12 @@ import { import { AppService } from '~/app/services/app.service'; import { PermissionsService } from '~/app/services/permissions.service'; import { WsService } from '~/app/services/ws'; +import { InviteUserModalComponent } from '~/app/shared/invite-user-modal/invite-user-modal.component'; import { PermissionUpdateNotificationService } from '~/app/shared/permission-update-notification/permission-update-notification.service'; +import { ResizedDirective } from '~/app/shared/resize/resize.directive'; import { ResizedEvent } from '~/app/shared/resize/resize.model'; import { RouteHistoryService } from '~/app/shared/route-history/route-history.service'; +import { TitleComponent } from '~/app/shared/title/title.component'; import { filterNil } from '~/app/shared/utils/operators'; import { pick } from '~/app/shared/utils/pick'; import { ProjectFeatureStoryWrapperModalViewComponent } from '../feature-story-wrapper-modal-view/project-feature-story-wrapper-modal-view.component'; @@ -48,6 +53,7 @@ import { selectStory, selectStoryView, } from '../story-detail/data-access/+state/selectors/story-detail.selectors'; +import { KanbanWorkflowComponent } from './components/workflow/kanban-workflow.component'; import { KanbanActions, KanbanEventsActions, @@ -57,20 +63,14 @@ import { kanbanFeature, } from './data-access/+state/reducers/kanban.reducer'; import { - selectLoadingWorkflows, - selectWorkflows, + selectLoadingWorkflow, + selectWorkflow, } from './data-access/+state/selectors/kanban.selectors'; import { KanbanReorderEvent } from './kanban.model'; -import { KanbanWorkflowComponent } from './components/workflow/kanban-workflow.component'; -import { TranslocoDirective } from '@ngneat/transloco'; -import { ModalComponent } from '@taiga/ui/modal/components'; -import { InviteUserModalComponent } from '~/app/shared/invite-user-modal/invite-user-modal.component'; -import { ResizedDirective } from '~/app/shared/resize/resize.directive'; -import { TitleComponent } from '~/app/shared/title/title.component'; interface ComponentState { - loadingWorkflows: KanbanState['loadingWorkflows']; - workflows: KanbanState['workflows']; + loadingWorkflow: KanbanState['loadingWorkflow']; + workflow: KanbanState['workflow']; invitePeopleModal: boolean; showStoryDetail: boolean; storyView: StoryView; @@ -109,19 +109,7 @@ export class ProjectFeatureKanbanComponent { public invitePeopleModal = false; public kanbanWidth = 0; - public model$ = this.state.select().pipe( - map((state) => { - const hasStatuses = - state.workflows?.find((workflow) => { - return workflow.statuses.length; - }) ?? true; - - return { - ...state, - isEmpty: !hasStatuses, - }; - }) - ); + public model$ = this.state.select(); public project$ = this.store.select(selectCurrentProject); constructor( @@ -129,8 +117,8 @@ export class ProjectFeatureKanbanComponent { private state: RxState, private wsService: WsService, private permissionService: PermissionsService, - private router: Router, private route: ActivatedRoute, + private router: Router, private appService: AppService, private location: Location, public shortcutsService: ShortcutsService, @@ -145,11 +133,22 @@ export class ProjectFeatureKanbanComponent { void this.router.navigate(['403']); return; } - this.store.dispatch(KanbanActions.initKanban()); + + this.route.paramMap.subscribe((params) => { + const workflowSlug = params.get('workflow') ?? 'main'; + this.store.dispatch(KanbanActions.initKanban({ workflow: workflowSlug })); + }); + + // Load on init kanban page. Not on every reload + // const workflowSlug = this.route.snapshot.params['workflow']; + // this.store.dispatch( + // KanbanActions.initKanban({ workflow: workflowSlug }) + // ); + this.checkInviteModalStatus(); this.state.connect( - 'loadingWorkflows', - this.store.select(selectLoadingWorkflows) + 'loadingWorkflow', + this.store.select(selectLoadingWorkflow) ); this.state.connect( @@ -185,7 +184,7 @@ export class ProjectFeatureKanbanComponent { } ); this.state.connect('storyView', this.store.select(selectStoryView)); - this.state.connect('workflows', this.store.select(selectWorkflows)); + this.state.connect('workflow', this.store.select(selectWorkflow)); this.state.connect( 'columns', this.store.select(kanbanFeature.selectColums) diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/services/a11yDrag.service.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/services/a11yDrag.service.ts index 77920bb71..f6c7f816d 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/services/a11yDrag.service.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/services/a11yDrag.service.ts @@ -28,6 +28,7 @@ import { KanbanStory, KanbanStoryA11y, } from '~/app/modules/project/feature-kanban/kanban.model'; +import { KanbanState } from '../data-access/+state/reducers/kanban.reducer'; @Injectable({ providedIn: 'root', @@ -166,11 +167,9 @@ export class A11yDragService { this.store .select(selectKanbanState) .pipe(take(1)) - .subscribe((state) => { + .subscribe((state: KanbanState) => { const story = findStory(state, (it) => it.ref === this.storyRef); - const workflow = state.workflows?.find((it) => { - return it.slug === state.currentWorkflowSlug; - }); + const workflow = state.workflow; if (!workflow || !story) { return; diff --git a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts index eaf42252c..170a8efc0 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts @@ -57,6 +57,7 @@ const routes: Routes = [ canDeactivate: [CanDeactivateGuard], data: { kanban: true, + reuseComponent: false, }, }, { diff --git a/javascript/apps/taiga/src/app/modules/project/story-detail/data-access/+state/effects/story-detail.effects.ts b/javascript/apps/taiga/src/app/modules/project/story-detail/data-access/+state/effects/story-detail.effects.ts index b7f48de94..f169508ca 100644 --- a/javascript/apps/taiga/src/app/modules/project/story-detail/data-access/+state/effects/story-detail.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/story-detail/data-access/+state/effects/story-detail.effects.ts @@ -11,13 +11,15 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { TranslocoService } from '@ngneat/transloco'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; -import { Store } from '@ngrx/store'; import { fetch, pessimisticUpdate } from '@ngrx/router-store/data-persistence'; +import { Store } from '@ngrx/store'; import { TuiNotification } from '@taiga-ui/core'; import { ProjectApiService } from '@taiga/api'; -import { catchError, concatMap, EMPTY, map, of, tap } from 'rxjs'; +import { EMPTY, catchError, concatMap, map, of, tap } from 'rxjs'; +import { projectEventActions } from '~/app/modules/project/data-access/+state/actions/project.actions'; import { selectCurrentProject } from '~/app/modules/project/data-access/+state/selectors/project.selectors'; -import { selectWorkflows } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors'; +import { KanbanActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions'; +import { selectWorkflow } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors'; import { AppService } from '~/app/services/app.service'; import { LocalStorageService } from '~/app/shared/local-storage/local-storage.service'; import { filterNil } from '~/app/shared/utils/operators'; @@ -25,12 +27,7 @@ import { StoryDetailActions, StoryDetailApiActions, } from '../actions/story-detail.actions'; -import { - selectStory, - selectWorkflow, -} from '../selectors/story-detail.selectors'; -import { KanbanActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions'; -import { projectEventActions } from '~/app/modules/project/data-access/+state/actions/project.actions'; +import { selectStory } from '../selectors/story-detail.selectors'; @Injectable() export class StoryDetailEffects { @@ -59,25 +56,18 @@ export class StoryDetailEffects { return this.actions$.pipe( ofType(StoryDetailApiActions.fetchStorySuccess), concatLatestFrom(() => [ - this.store.select(selectWorkflows), - this.store.select(selectWorkflow), + this.store.select(selectWorkflow).pipe(filterNil()), this.store.select(selectCurrentProject).pipe(filterNil()), ]), - concatMap(([action, workflows, loadedWorkflow, project]) => { - const workflow = - loadedWorkflow ?? - workflows?.find( - (workflow) => workflow.slug === action.story.workflow.slug - ); - - if (workflow?.slug === action.story.workflow.slug) { + concatMap(([action, workflow, project]) => { + if (workflow && workflow.slug === action.story.workflow.slug) { return of( StoryDetailApiActions.fetchWorkflowSuccess({ workflow, }) ); } - + console.log(action.story.workflow.slug); return this.projectApiService .getWorkflow(project?.id, action.story.workflow.slug) .pipe( @@ -261,12 +251,8 @@ export class StoryDetailEffects { public updatesWorkflowStatusAfterDragAndDrop$ = createEffect(() => { return this.actions$.pipe( ofType(KanbanActions.statusDropped, projectEventActions.statusReorder), - concatLatestFrom(() => [this.store.select(selectWorkflows)]), - map(([action, workflows]) => { - const workflow = workflows?.find((workflow) => - workflow.statuses.find((status) => status.id === action.candidate?.id) - ); - + concatLatestFrom(() => [this.store.select(selectWorkflow)]), + map(([, workflow]) => { if (workflow) { return StoryDetailActions.newStatusOrderAfterDrag({ workflow, diff --git a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts index b5a23315d..9290957b1 100644 --- a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts @@ -26,10 +26,10 @@ import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; import { TuiButtonComponent, + TuiButtonModule, TuiNotification, TuiScrollbarComponent, TuiSvgModule, - TuiButtonModule, } from '@taiga-ui/core'; import { Attachment, @@ -47,7 +47,6 @@ import { Workflow, } from '@taiga/data'; import { map, merge, pairwise, startWith, take } from 'rxjs'; - import { v4 } from 'uuid'; import { selectUser } from '~/app/modules/auth/data-access/+state/selectors/auth.selectors'; import { selectCurrentProject } from '~/app/modules/project/data-access/+state/selectors/project.selectors'; @@ -187,7 +186,7 @@ export class StoryDetailComponent { // taiga ui in the modal has a focus trap that makes the focus on the element, so we need to delay the focus one tick requestAnimationFrame(() => { - this.setInitilFocus(); + this.setInitialFocus(); }); } } @@ -443,7 +442,7 @@ export class StoryDetailComponent { } } - public setInitilFocus() { + public setInitialFocus() { const locationState = this.location.getState() as null | { nextStoryNavigation?: boolean; previousStoryNavigation?: boolean; diff --git a/javascript/libs/api/src/lib/project/project-api.service.ts b/javascript/libs/api/src/lib/project/project-api.service.ts index 34404b680..a0b0cedf3 100644 --- a/javascript/libs/api/src/lib/project/project-api.service.ts +++ b/javascript/libs/api/src/lib/project/project-api.service.ts @@ -239,10 +239,10 @@ export class ProjectApiService { public getWorkflow( projectId: Project['id'], - slug: string + workflow: Workflow['slug'] ): Observable { return this.http.get( - `${this.config.apiUrl}/projects/${projectId}/workflows/${slug}` + `${this.config.apiUrl}/projects/${projectId}/workflows/${workflow}` ); } From 6fd6073e0ad4941c47bcac62bbc73304bf708cad Mon Sep 17 00:00:00 2001 From: Xaviju Date: Mon, 18 Sep 2023 17:16:11 +0200 Subject: [PATCH 03/16] feat(workflows): create workflow page --- .../+state/actions/project.actions.ts | 9 ++ .../+state/actions/kanban.actions.ts | 6 ++ .../+state/effects/kanban.effects.ts | 42 +++++++++- .../project-feature-kanban.component.ts | 35 +++++--- .../project-navigation-menu.component.html | 6 +- .../project-navigation-menu.component.ts | 4 - .../new-workflow-form.component.css | 7 ++ .../new-workflow-form.component.html | 60 +++++++++++++ .../new-workflow-form.component.ts | 84 +++++++++++++++++++ ...ect-feature-new-workflow-routing.module.ts | 21 +++++ ...project-feature-new-workflow.component.css | 7 ++ ...roject-feature-new-workflow.component.html | 12 +++ .../project-feature-new-workflow.component.ts | 38 +++++++++ .../project-feature-new-workflow.module.ts | 25 ++++++ .../project-feature-shell-routing.module.ts | 31 +++++++ .../project-feature-shell.component.ts | 14 +++- .../taiga/src/assets/i18n/kanban/en-US.json | 6 ++ .../src/lib/project/project-api.service.ts | 12 +++ 18 files changed, 396 insertions(+), 23 deletions(-) create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.html create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow-routing.module.ts create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.module.ts diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts index 3bc15d2ff..c9e7d1407 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts @@ -16,6 +16,7 @@ import { StoryDetail, User, UserComment, + Workflow, } from '@taiga/data'; import { DropCandidate } from '@taiga/ui/drag/drag.model'; @@ -67,6 +68,14 @@ export const updateStoryShowView = createAction( }>() ); +export const createWorkflow = createAction( + '[Project] Create Workflow', + props<{ + project: Project; + name: Workflow['name']; + }> +); + export const newProjectMembers = createAction( '[Project][ws] New Project Members' ); diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts index 41a3bbd46..729ce45a8 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts @@ -20,6 +20,8 @@ export const KanbanActions = createActionGroup({ source: 'Kanban', events: { 'Init Kanban': props<{ workflow: Workflow['slug'] }>(), + 'Load Workflow kanban': props<{ workflow: Workflow['slug'] }>(), + 'Create Workflow': props<{ name: Workflow['name'] }>(), 'Open Create Story form': props<{ status: Status['id'] }>(), 'Close Create Story form': emptyProps(), 'Create Story': props<{ @@ -126,6 +128,10 @@ export const KanbanApiActions = createActionGroup({ offset: number; complete: boolean; }>(), + 'Create Workflow Success': props<{ + workflow: Workflow; + }>(), + 'create Workflow Error': emptyProps(), 'Create Story Success': props<{ story: Story; tmpId: PartialStory['tmpId']; diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts index 760212afd..48a852207 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts @@ -8,6 +8,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; import { TranslocoService } from '@ngneat/transloco'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; import { @@ -44,7 +45,7 @@ import { export class KanbanEffects { public loadKanbanWorkflows$ = createEffect(() => { return this.actions$.pipe( - ofType(KanbanActions.initKanban), + ofType(KanbanActions.initKanban, KanbanActions.loadWorkflowKanban), concatLatestFrom(() => [ this.store.select(selectCurrentProject).pipe(filterNil()), ]), @@ -67,7 +68,7 @@ export class KanbanEffects { public loadKanbanStories$ = createEffect(() => { return this.actions$.pipe( - ofType(KanbanActions.initKanban), + ofType(KanbanActions.initKanban, KanbanActions.loadWorkflowKanban), concatLatestFrom(() => [ this.store.select(selectCurrentProject).pipe(filterNil()), this.store.select(selectWorkflow), @@ -96,6 +97,42 @@ export class KanbanEffects { ); }); + public createWorkflow$ = createEffect(() => { + return this.actions$.pipe( + ofType(KanbanActions.createWorkflow), + concatLatestFrom(() => [ + this.store.select(selectCurrentProject).pipe(filterNil()), + ]), + pessimisticUpdate({ + run: (workflow, project) => { + return this.projectApiService + .createWorkflow(workflow.name, project.id) + .pipe( + map((newWorkflow) => { + console.log({ newWorkflow }); + void this.router.navigate([ + '/project', + project.id, + project.slug, + newWorkflow.slug, + ]); + return KanbanApiActions.createWorkflowSuccess({ + workflow: newWorkflow, + }); + }) + ); + }, + onError: (action, httpResponse: HttpErrorResponse) => { + if (httpResponse.status !== 403) { + this.appService.errorManagement(httpResponse); + } + + return KanbanApiActions.createWorkflowError(); + }, + }) + ); + }); + public createStory$ = createEffect(() => { return this.actions$.pipe( ofType(KanbanActions.createStory), @@ -480,6 +517,7 @@ export class KanbanEffects { private actions$: Actions, private store: Store, private projectApiService: ProjectApiService, + private router: Router, private kanbanScrollManagerService: KanbanScrollManagerService, private translocoService: TranslocoService ) {} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts index 0c2d520f0..3ed2ceb0d 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.ts @@ -30,7 +30,16 @@ import { WorkflowStatus, } from '@taiga/data'; import { ModalComponent } from '@taiga/ui/modal/components'; -import { combineLatest, filter, map, merge, pairwise, take } from 'rxjs'; +import { + combineLatest, + distinctUntilChanged, + filter, + map, + merge, + pairwise, + skip, + take, +} from 'rxjs'; import * as ProjectActions from '~/app/modules/project/data-access/+state/actions/project.actions'; import { selectCurrentProject, @@ -134,16 +143,22 @@ export class ProjectFeatureKanbanComponent { return; } - this.route.paramMap.subscribe((params) => { - const workflowSlug = params.get('workflow') ?? 'main'; - this.store.dispatch(KanbanActions.initKanban({ workflow: workflowSlug })); - }); - // Load on init kanban page. Not on every reload - // const workflowSlug = this.route.snapshot.params['workflow']; - // this.store.dispatch( - // KanbanActions.initKanban({ workflow: workflowSlug }) - // ); + const workflow: Workflow['slug'] = + (this.route.snapshot.params['workflow'] as Workflow['slug']) ?? 'main'; + this.store.dispatch(KanbanActions.initKanban({ workflow })); + + this.route.paramMap + .pipe( + map((params) => { + return params.get('workflow') ?? 'main'; + }), + distinctUntilChanged(), + skip(1) + ) + .subscribe((workflow: Workflow['slug']) => { + this.store.dispatch(KanbanActions.loadWorkflowKanban({ workflow })); + }); this.checkInviteModalStatus(); this.state.connect( diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html index 3f366bcd5..96f4fe7f9 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html @@ -394,7 +394,8 @@
- + icon="plus"> diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts index 6af7f0f5c..66b24fe5e 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.ts @@ -213,10 +213,6 @@ export class ProjectNavigationMenuComponent { this.dialog.type = ''; } - public createWorkflow() { - console.log('create workflow'); - } - public trackById(_index: number, workflow: Workflow) { return workflow.id; } diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css new file mode 100644 index 000000000..711742e35 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css @@ -0,0 +1,7 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright (c) 2023-present Kaleidos INC +*/ diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.html b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.html new file mode 100644 index 000000000..5983e495e --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.html @@ -0,0 +1,60 @@ + + + +
+
+ + + + + {{ t('kanban.create_workflow.workflow_empty') }} + + + +
+
+ {{ t('form_errors.max_length') }} +
+ +
+ + +
+
+
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts new file mode 100644 index 000000000..e96d6b7a3 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts @@ -0,0 +1,84 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + OnInit, + Output, +} from '@angular/core'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { TranslocoDirective } from '@ngneat/transloco'; +import { TuiButtonModule } from '@taiga-ui/core'; +import { Workflow } from '@taiga/data'; +import { InputsModule } from '@taiga/ui/inputs'; + +@Component({ + selector: 'tg-new-workflow-form', + templateUrl: './new-workflow-form.component.html', + styleUrls: ['./new-workflow-form.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + TranslocoDirective, + ReactiveFormsModule, + InputsModule, + TuiButtonModule, + ], +}) +export class NewWorkflowFormComponent implements OnInit { + @Output() + public createWorkflow = new EventEmitter(); + + @Output() + public cancelCreateWorkflow = new EventEmitter(); + + public newWorkflowForm!: FormGroup; + + public workflowNameMaxLength = 40; + public submitted = false; + + constructor(private fb: FormBuilder) {} + + public ngOnInit() { + this.initForm(); + } + + public initForm() { + this.newWorkflowForm = this.fb.group({ + name: [ + '', + [ + Validators.required, + Validators.maxLength(this.workflowNameMaxLength), + //avoid only white spaces + Validators.pattern(/^(\s+\S+\s*)*(?!\s).*$/), + ], + ], + }); + } + + public submitCreateWorkflow() { + this.submitted = true; + const workflowName = this.newWorkflowForm.get('name') + ?.value as Workflow['name']; + this.createWorkflow.emit(workflowName); + } + + public cancelEdit() { + this.cancelCreateWorkflow.emit(); + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow-routing.module.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow-routing.module.ts new file mode 100644 index 000000000..a84be6f30 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow-routing.module.ts @@ -0,0 +1,21 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ProjectFeatureNewWorkflowComponent } from './project-feature-new-workflow.component'; + +const routes: Routes = [ + { path: '', component: ProjectFeatureNewWorkflowComponent }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ProjectFeatureNewWorkflowRoutingModule {} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css new file mode 100644 index 000000000..711742e35 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css @@ -0,0 +1,7 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright (c) 2023-present Kaleidos INC +*/ diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html new file mode 100644 index 000000000..790fd5931 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html @@ -0,0 +1,12 @@ + + +
New Kanban Works!
+ diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts new file mode 100644 index 000000000..2fbeb0076 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts @@ -0,0 +1,38 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Workflow } from '@taiga/data'; +import { KanbanActions } from '../feature-kanban/data-access/+state/actions/kanban.actions'; +import { NewWorkflowFormComponent } from './components/new-workflow-form/new-workflow-form.component'; + +@Component({ + selector: 'tg-project-feature-new-workflow', + templateUrl: './project-feature-new-workflow.component.html', + styleUrls: ['./project-feature-new-workflow.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [NewWorkflowFormComponent], +}) +export class ProjectFeatureNewWorkflowComponent { + constructor(private store: Store) {} + + public createWorkflow(workflow: Workflow['name']) { + console.log({ workflow }); + this.store.dispatch( + KanbanActions.createWorkflow({ + name: workflow, + }) + ); + } + + public cancelCreateWorkflow() { + console.log('routelink to main'); + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.module.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.module.ts new file mode 100644 index 000000000..5b7218c87 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.module.ts @@ -0,0 +1,25 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TRANSLOCO_SCOPE } from '@ngneat/transloco'; +import { ProjectFeatureNewWorkflowRoutingModule } from './project-feature-new-workflow-routing.module'; + +@NgModule({ + imports: [CommonModule, ProjectFeatureNewWorkflowRoutingModule], + declarations: [], + providers: [ + { + provide: TRANSLOCO_SCOPE, + useValue: 'kanban', + }, + ], + exports: [], +}) +export class ProjectFeatureNewWorkflowModule {} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts index 170a8efc0..5236a4f4d 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-shell/project-feature-shell-routing.module.ts @@ -28,6 +28,26 @@ const routes: Routes = [ '~/app/modules/project/feature-overview/project-feature-overview.module' ).then((m) => m.ProjectFeatureOverviewModule), }, + { + path: 'overview', + loadChildren: () => + import( + '~/app/modules/project/feature-overview/project-feature-overview.module' + ).then((m) => m.ProjectFeatureOverviewModule), + data: { + overview: true, + }, + }, + { + path: ':slug/overview', + loadChildren: () => + import( + '~/app/modules/project/feature-overview/project-feature-overview.module' + ).then((m) => m.ProjectFeatureOverviewModule), + data: { + overview: true, + }, + }, { path: 'kanban', loadChildren: () => @@ -48,6 +68,17 @@ const routes: Routes = [ redirectTo: ':slug/kanban/main', pathMatch: 'full', }, + { + path: ':slug/new-workflow', + loadChildren: () => + import( + '~/app/modules/project/feature-new-workflow/project-feature-new-workflow.module' + ).then((m) => m.ProjectFeatureNewWorkflowModule), + canDeactivate: [CanDeactivateGuard], + data: { + newKanban: true, + }, + }, { path: ':slug/kanban/:workflow', loadChildren: () => 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 a54d0fd27..894af565f 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 @@ -15,15 +15,15 @@ import { OnDestroy, } from '@angular/core'; import { - Router, - RouterOutlet, ActivatedRoute, ActivatedRouteSnapshot, + Router, + RouterOutlet, } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; -import { TuiNotification, TuiButtonModule } from '@taiga-ui/core'; +import { TuiButtonModule, TuiNotification } from '@taiga-ui/core'; import { Attachment, Membership, @@ -54,9 +54,9 @@ import { } from '../data-access/+state/actions/project.actions'; import { setNotificationClosed } from '../feature-overview/data-access/+state/actions/project-overview.actions'; -import { ProjectNavigationComponent } from '../feature-navigation/project-feature-navigation.component'; import { TranslocoDirective } from '@ngneat/transloco'; import { ContextNotificationComponent } from '@taiga/ui/context-notification/context-notification.component'; +import { ProjectNavigationComponent } from '../feature-navigation/project-feature-navigation.component'; @UntilDestroy() @Component({ @@ -153,12 +153,18 @@ export class ProjectFeatureShellComponent implements OnDestroy, AfterViewInit { active.routeConfig?.component?.name === 'ProjectFeatureOverviewComponent'; const isSettings = !!active.data.settings; + const isNewKanban = !!active.data.newKanban; if (isKanban) { void this.router.navigate( [`project/${project.id}/${project.slug}/kanban`], { replaceUrl: true } ); + } else if (isNewKanban) { + void this.router.navigate( + [`project/${project.id}/${project.slug}/new-workflow`], + { replaceUrl: true } + ); } else if (isOverview) { void this.router.navigate( [`project/${project.id}/${project.slug}/overview`], diff --git a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json index 404066fca..c763d709c 100644 --- a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json @@ -42,5 +42,11 @@ "delete_stories": "Delete the stories too", "stories_placed_below": "The stories will be placed below the existing ones.", "status": "Status" + }, + "create_workflow": { + "save": "Create Workflow", + "workflow_name_aria": "Workflow name (Maximum 40 Characters only)", + "write_workflow_name": "Write a workflow name", + "workflow_empty": "Workflow name can’t be empty." } } diff --git a/javascript/libs/api/src/lib/project/project-api.service.ts b/javascript/libs/api/src/lib/project/project-api.service.ts index a0b0cedf3..41d4d1337 100644 --- a/javascript/libs/api/src/lib/project/project-api.service.ts +++ b/javascript/libs/api/src/lib/project/project-api.service.ts @@ -246,6 +246,18 @@ export class ProjectApiService { ); } + public createWorkflow( + workflow: Workflow['name'], + project: Project['id'] + ): Observable { + return this.http.post( + `${this.config.apiUrl}/projects/${project}/workflows`, + { + name: workflow, + } + ); + } + public updateInvitationRole( id: string, userData: { id: string; roleSlug: string } From 9b0bcfc2f8d09e7d85e90d4bd96d7879e369e02c Mon Sep 17 00:00:00 2001 From: Juanfran Date: Tue, 19 Sep 2023 14:38:08 +0200 Subject: [PATCH 04/16] feat: init kanban header --- .../kanban-header/kanban-header.component.css | 28 ++++++++ .../kanban-header.component.html | 66 +++++++++++++++++++ .../kanban-header/kanban-header.component.ts | 36 ++++++++++ .../project-feature-kanban.component.css | 9 ++- .../project-feature-kanban.component.html | 6 +- .../project-feature-kanban.component.ts | 2 + .../taiga/src/assets/i18n/kanban/en-US.json | 2 + 7 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.css create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.ts 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 new file mode 100644 index 000000000..e18395623 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.css @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright (c) 2023-present Kaleidos INC +*/ + +@import url("tools/typography.css"); +@import url("shared/option-list.css"); + +:host { + align-items: center; + display: flex; + gap: var(--spacing-4); +} + +h1 { + margin: 0; +} + +.view-options-list { + @mixin option-list; +} + +.separator { + @mixin separator; +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html new file mode 100644 index 000000000..989d57cec --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html @@ -0,0 +1,66 @@ + + + +

+ {{ t('kanban.title') }} +

+ + + + + + + + +
+ +
+
+
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 new file mode 100644 index 000000000..a58e38a94 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.ts @@ -0,0 +1,36 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TranslocoDirective } from '@ngneat/transloco'; +import { + TuiButtonModule, + TuiDataListModule, + TuiHostedDropdownModule, + TuiSvgModule, +} from '@taiga-ui/core'; + +@Component({ + selector: 'tg-kanban-header', + templateUrl: './kanban-header.component.html', + styleUrls: ['./kanban-header.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + TranslocoDirective, + TuiHostedDropdownModule, + TuiButtonModule, + TuiDataListModule, + TuiSvgModule, + ], +}) +export class KanbanHeaderComponent { + public openWorkflowOptions = false; +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.css b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.css index 86504a3c3..c33898014 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/project-feature-kanban.component.css @@ -14,11 +14,6 @@ Copyright (c) 2023-present Kaleidos INC padding: 0; } -h1 { - margin: 0; - margin-block-end: var(--spacing-20); -} - .empty { border: 1px solid var(--color-gray20); color: var(--color-gray70); @@ -41,3 +36,7 @@ h1 { padding-block-start: var(--spacing-16); padding-inline-start: var(--spacing-16); } + +tg-kanban-header { + margin-block-end: var(--spacing-20); +} 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 1a3482bb3..5642c0de7 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 @@ -18,11 +18,7 @@ (resized)="onResized($event)" class="kanban-wrapper kanban-cdk-area">
-

- {{ t('kanban.title') }} -

+
Date: Tue, 19 Sep 2023 15:11:49 +0200 Subject: [PATCH 05/16] feat(workflows): create workflows API --- .../data-access/+state/reducers/project.reducer.ts | 12 ++++++++++++ .../data-access/+state/actions/kanban.actions.ts | 3 ++- .../data-access/+state/effects/kanban.effects.ts | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts index 159fe223a..84f5d6064 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts @@ -8,6 +8,7 @@ import { createFeature, on } from '@ngrx/store'; import { Membership, Project } from '@taiga/data'; +import { KanbanApiActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions'; import { editProject, editProjectSuccess, @@ -102,6 +103,17 @@ export const reducer = createImmerReducer( return state; }), + on( + KanbanApiActions.createWorkflowSuccess, + (state, { projectId, workflow }): ProjectState => { + state.projects[projectId].workflows.push(workflow); + + return state; + } + ), + // on(KanbanApiActions.createWorkflowSuccess, (state, {workflow})): ProjectState => { + + // }), on( ProjectActions.permissionsUpdateSuccess, (state, { project }): ProjectState => { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts index 729ce45a8..481957f5d 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts @@ -7,7 +7,7 @@ */ import { createActionGroup, emptyProps, props } from '@ngrx/store'; -import { Membership, Status, Story, Workflow } from '@taiga/data'; +import { Membership, Project, Status, Story, Workflow } from '@taiga/data'; import { DropCandidate } from '@taiga/ui/drag/drag.model'; import { KanbanReorderEvent, @@ -129,6 +129,7 @@ export const KanbanApiActions = createActionGroup({ complete: boolean; }>(), 'Create Workflow Success': props<{ + projectId: Project['id']; workflow: Workflow; }>(), 'create Workflow Error': emptyProps(), diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts index 48a852207..8b5b93543 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts @@ -109,14 +109,15 @@ export class KanbanEffects { .createWorkflow(workflow.name, project.id) .pipe( map((newWorkflow) => { - console.log({ newWorkflow }); void this.router.navigate([ '/project', project.id, project.slug, + 'kanban', newWorkflow.slug, ]); return KanbanApiActions.createWorkflowSuccess({ + projectId: project.id, workflow: newWorkflow, }); }) From 48b3a9d1c2faefa4eaa5cbcdef379085f9141a7f Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 19 Sep 2023 15:27:10 +0200 Subject: [PATCH 06/16] feat(workflows): cancel workflow creation --- .../+state/reducers/project.reducer.ts | 3 -- .../project-feature-new-workflow.component.ts | 30 ++++++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts index 84f5d6064..cb219dfeb 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts @@ -111,9 +111,6 @@ export const reducer = createImmerReducer( return state; } ), - // on(KanbanApiActions.createWorkflowSuccess, (state, {workflow})): ProjectState => { - - // }), on( ProjectActions.permissionsUpdateSuccess, (state, { project }): ProjectState => { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts index 2fbeb0076..1c3440080 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts @@ -7,8 +7,12 @@ */ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { Workflow } from '@taiga/data'; +import { RxState } from '@rx-angular/state'; +import { Project, Workflow } from '@taiga/data'; +import { filterNil } from '~/app/shared/utils/operators'; +import { selectCurrentProject } from '../data-access/+state/selectors/project.selectors'; import { KanbanActions } from '../feature-kanban/data-access/+state/actions/kanban.actions'; import { NewWorkflowFormComponent } from './components/new-workflow-form/new-workflow-form.component'; @@ -21,10 +25,20 @@ import { NewWorkflowFormComponent } from './components/new-workflow-form/new-wor imports: [NewWorkflowFormComponent], }) export class ProjectFeatureNewWorkflowComponent { - constructor(private store: Store) {} + constructor( + private state: RxState<{ + project: Project; + }>, + private store: Store, + private router: Router + ) { + this.state.connect( + 'project', + this.store.select(selectCurrentProject).pipe(filterNil()) + ); + } public createWorkflow(workflow: Workflow['name']) { - console.log({ workflow }); this.store.dispatch( KanbanActions.createWorkflow({ name: workflow, @@ -33,6 +47,14 @@ export class ProjectFeatureNewWorkflowComponent { } public cancelCreateWorkflow() { - console.log('routelink to main'); + const project = this.state.get('project'); + const firstworkflow = project.workflows[0]; + void this.router.navigate([ + '/project', + project.id, + project.slug, + 'kanban', + firstworkflow.slug, + ]); } } From e405dc66abea91316df5e0f24c5b051847fb65dc Mon Sep 17 00:00:00 2001 From: Juanfran Date: Wed, 20 Sep 2023 09:03:02 +0200 Subject: [PATCH 07/16] feat(workflow): breadcrumb placeholder --- .../delete-workflow.component.css | 69 ++++++++++ .../delete-workflow.component.html | 109 +++++++++++++++ .../delete-workflow.component.ts | 129 ++++++++++++++++++ .../kanban-header.component.html | 17 +++ .../kanban-header/kanban-header.component.ts | 17 ++- .../project-feature-kanban.component.html | 5 +- .../project-feature-kanban.component.ts | 13 ++ 7 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.css create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html create mode 100644 javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.css b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.css new file mode 100644 index 000000000..7b468cc2f --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.css @@ -0,0 +1,69 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright (c) 2023-present Kaleidos INC +*/ + +@import url("tools/typography.css"); + +.actions { + display: flex; + gap: 2rem; + justify-content: flex-end; + margin-block-start: var(--spacing-24); +} + +.title { + @mixin font-heading-3; + + margin-block: 0 var(--spacing-24); +} + +.project-form { + display: flex; + flex-direction: column; + gap: var(--spacing-24); +} + +.confirm-close-description { + margin-block-end: var(--spacing-32); +} + +.warning-notification { + display: block; + + &::ng-deep .wrapper { + inline-size: 100%; + } + + &::ng-deep .bold { + font-weight: var(--font-weight-medium); + } +} + +.text { + @mixin font-paragraph; + + color: var(--color-gray80); + margin-block-end: 0; + padding-block: var(--spacing-24); + padding-block-end: var(--spacing-16); + padding-inline: 0; + + &.is-last-workflow { + padding-block-end: 0; + } +} + +.select-workflow { + margin-block: var(--spacing-8); + margin-inline-start: var(--spacing-24); +} + +.select-info { + display: flex; + margin-block-end: var(--spacing-24); + margin-inline-start: var(--spacing-24); +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html new file mode 100644 index 000000000..7ff8ad833 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html @@ -0,0 +1,109 @@ + + + + +
+

+ {{ + t('kanban.delete_status_modal.title', { + statusName: currentWorkflow.name + }) + }} +

+ + + +

+ {{ + isLastWorkflow + ? t('kanban.delete_status_modal.stories_will_be_deleted') + : t('kanban.delete_status_modal.what_to_do_stories') + }} +

+
+ + + + + + + + + + + {{ t('kanban.delete_status_modal.stories_placed_below') }} + + + +
+ +
+ + +
+
+
+
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts new file mode 100644 index 000000000..372adde36 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts @@ -0,0 +1,129 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + OnInit, + signal, + inject, + WritableSignal, + computed, + Signal, +} from '@angular/core'; +import { TranslocoModule } from '@ngneat/transloco'; +import { TuiAutoFocusModule } from '@taiga-ui/cdk'; +import { TuiButtonModule, TuiLinkModule } from '@taiga-ui/core'; +import { Status, Workflow } from '@taiga/data'; + +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { Observable, map } from 'rxjs'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ContextNotificationComponent } from '@taiga/ui/context-notification/context-notification.component'; +import { InputsModule } from '@taiga/ui/inputs'; +import { ModalComponent } from '@taiga/ui/modal/components'; +import { trackByProp } from '~/app/shared/utils/track-by-prop'; + +@Component({ + selector: 'tg-delete-workflow', + templateUrl: './delete-workflow.component.html', + styleUrls: ['./delete-workflow.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + TranslocoModule, + TuiButtonModule, + TuiLinkModule, + TuiAutoFocusModule, + CommonModule, + ContextNotificationComponent, + InputsModule, + ModalComponent, + ], +}) +export class DeleteWorkflowComponent implements OnInit { + @Input({ required: true }) + public currentWorkflow!: Workflow; + + @Input() + public show = false; + + @Output() + public closeModal = new EventEmitter(); + + @Output() + public submitDelete = new EventEmitter(); + + @Input({ required: true }) + public set workflows(workflows: Workflow[]) { + this.workflowsList.set(workflows); + } + + public get isLastWorkflow() { + return this.workflowsList().length === 1; + } + + public form!: FormGroup; + public fb = inject(FormBuilder); + public workflowsList: WritableSignal = signal([]); + public filteredWorkflwos: Signal = computed(() => { + return this.workflowsList().filter( + (it) => it.id !== this.currentWorkflow.id + ); + }); + public valueContent!: Signal; + public trackById = trackByProp('id'); + + public get statusesFormControl() { + return this.form.get('statuses') as FormControl; + } + + constructor() { + if (!this.isLastWorkflow) { + this.form = this.fb.group({ + statuses: ['move'], + workflow: [''], + }); + + const valueContent$ = this.form.get('statuses')?.valueChanges.pipe( + map((value) => { + return this.workflowsList().find((it) => it.id === value)?.name ?? ''; + }) + ) as Observable; + + this.valueContent = toSignal(valueContent$, { + initialValue: '', + }); + } + } + + public ngOnInit() { + if (this.filteredWorkflwos().length) { + (this.form.get('workflow') as FormControl).setValue( + this.filteredWorkflwos()[0].id + ); + } + } + + public submit() { + this.close(); + const moveToWorkflow: Workflow['id'] | undefined = + !this.isLastWorkflow && this.form.get('statuses')!.value === 'move' + ? (this.form.get('workflow')!.value as Workflow['id']) + : undefined; + this.submitDelete.next(moveToWorkflow); + } + + public close() { + this.closeModal.next(); + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html index 989d57cec..d0fd79fa3 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html @@ -9,10 +9,17 @@

{{ t('kanban.title') }}

+ +
- -
+ + diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts index e96d6b7a3..7be86f939 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.ts @@ -24,6 +24,7 @@ import { TranslocoDirective } from '@ngneat/transloco'; import { TuiButtonModule } from '@taiga-ui/core'; import { Workflow } from '@taiga/data'; import { InputsModule } from '@taiga/ui/inputs'; +import { AutoFocusDirective } from '~/app/shared/directives/auto-focus/auto-focus.directive'; @Component({ selector: 'tg-new-workflow-form', @@ -37,6 +38,7 @@ import { InputsModule } from '@taiga/ui/inputs'; ReactiveFormsModule, InputsModule, TuiButtonModule, + AutoFocusDirective, ], }) export class NewWorkflowFormComponent implements OnInit { @@ -72,10 +74,13 @@ export class NewWorkflowFormComponent implements OnInit { } public submitCreateWorkflow() { + this.newWorkflowForm.markAllAsTouched(); this.submitted = true; - const workflowName = this.newWorkflowForm.get('name') - ?.value as Workflow['name']; - this.createWorkflow.emit(workflowName); + if (this.newWorkflowForm.valid) { + const workflowName = this.newWorkflowForm.get('name') + ?.value as Workflow['name']; + this.createWorkflow.emit(workflowName); + } } public cancelEdit() { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.css b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.css new file mode 100644 index 000000000..41784eae3 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.css @@ -0,0 +1,52 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright (c) 2023-present Kaleidos INC +*/ + +:host { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + inline-size: 288px; + padding: var(--spacing-16); +} + +.skeleton-text { + border-radius: 8px; +} + +.skeleton-header { + display: flex; + gap: var(--spacing-12); + padding-block-end: var(--spacing-16); + + & :is(.skeleton-text, .skeleton-avatar) { + background-color: var(--color-gray30); + } + + & .skeleton-avatar-end { + margin-inline-start: auto; + } +} + +.skeleton-cards { + display: flex; + flex-direction: column; + gap: var(--spacing-8); +} + +.skeleton-card { + background-color: var(--color-white); + display: flex; + flex-direction: column; + gap: var(--spacing-8); + padding: var(--spacing-16); + + & .skeleton-text { + background-color: var(--color-gray20); + block-size: var(--spacing-12); + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.html b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.html new file mode 100644 index 000000000..2fe73b04a --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.html @@ -0,0 +1,22 @@ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.ts new file mode 100644 index 000000000..43dfabcf2 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-skeleton/new-workflow-skeleton.component.ts @@ -0,0 +1,18 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + imports: [], + selector: 'tg-new-workflow-skeleton', + templateUrl: 'new-workflow-skeleton.component.html', + styleUrls: ['./new-workflow-skeleton.component.css'], +}) +export class NewWorkflowSkeletonComponent {} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css index 711742e35..5d6923404 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.css @@ -5,3 +5,18 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. Copyright (c) 2023-present Kaleidos INC */ + +:host { + background-color: var(--color-gray10); + block-size: 100%; + display: flex; + flex-direction: column; + gap: var(--spacing-16); + padding: var(--spacing-16); +} + +.skeleton-wrapper { + display: flex; + flex: 1; + gap: var(--spacing-8); +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html index 790fd5931..6bd6565c4 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.html @@ -6,7 +6,13 @@ Copyright (c) 2023-present Kaleidos INC --> -
New Kanban Works!
+
+ + + + +
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts index 1c3440080..26a596a29 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts @@ -15,6 +15,7 @@ import { filterNil } from '~/app/shared/utils/operators'; import { selectCurrentProject } from '../data-access/+state/selectors/project.selectors'; import { KanbanActions } from '../feature-kanban/data-access/+state/actions/kanban.actions'; import { NewWorkflowFormComponent } from './components/new-workflow-form/new-workflow-form.component'; +import { NewWorkflowSkeletonComponent } from './components/new-workflow-skeleton/new-workflow-skeleton.component'; @Component({ selector: 'tg-project-feature-new-workflow', @@ -22,7 +23,7 @@ import { NewWorkflowFormComponent } from './components/new-workflow-form/new-wor styleUrls: ['./project-feature-new-workflow.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [NewWorkflowFormComponent], + imports: [NewWorkflowFormComponent, NewWorkflowSkeletonComponent], }) export class ProjectFeatureNewWorkflowComponent { constructor( diff --git a/javascript/apps/taiga/src/app/styles/tools/skeleton.css b/javascript/apps/taiga/src/app/styles/tools/skeleton.css index 21d3fe87f..da897feca 100644 --- a/javascript/apps/taiga/src/app/styles/tools/skeleton.css +++ b/javascript/apps/taiga/src/app/styles/tools/skeleton.css @@ -63,6 +63,21 @@ Copyright (c) 2023-present Kaleidos INC @mixin square 20; @mixin square 24; +/* mixin cicle - auto generates utility classes */ +@define-mixin circle $size { + .skeleton-circle-$(size) { + /* stylelint-disable-next-line custom-property-pattern */ + block-size: var(--spacing-$size); + border-radius: 50%; + /* stylelint-disable-next-line custom-property-pattern */ + inline-size: var(--spacing-$size); + + @mixin-content; + } +} + +@mixin circle 12; + /* skeleton-animation for menu or content */ .skeleton-animation { & * { From b45165686002525be77b624cae5433bda1790339 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Thu, 21 Sep 2023 08:01:33 +0200 Subject: [PATCH 09/16] feat(workflow): delete workflow modal --- .../delete-status/delete-status.component.ts | 3 +- .../delete-workflow.component.html | 36 +++++----- .../delete-workflow.component.ts | 66 +++++++++---------- .../taiga/src/assets/i18n/kanban/en-US.json | 11 ++++ 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-status/delete-status.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-status/delete-status.component.ts index dee9f7a51..1a5b20001 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-status/delete-status.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-status/delete-status.component.ts @@ -27,7 +27,7 @@ import { Status } from '@taiga/data'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { Observable, map } from 'rxjs'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ContextNotificationComponent } from '@taiga/ui/context-notification/context-notification.component'; import { InputsModule } from '@taiga/ui/inputs'; import { ModalComponent } from '@taiga/ui/modal/components'; @@ -90,6 +90,7 @@ export class DeleteStatusComponent implements OnInit { }); const valueContent$ = this.form.get('status')?.valueChanges.pipe( + takeUntilDestroyed(), map((value) => { return this.statusesList().find((it) => it.id === value)?.name ?? ''; }) diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html index 7ff8ad833..cb668f511 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.html @@ -20,8 +20,8 @@ class="title" id="delete-status"> {{ - t('kanban.delete_status_modal.title', { - statusName: currentWorkflow.name + t('kanban.delete_workflow_modal.title', { + name: currentWorkflow.name }) }} @@ -33,26 +33,28 @@

+ [class.is-last-workflow]="isLastWorkflow()"> {{ - isLastWorkflow - ? t('kanban.delete_status_modal.stories_will_be_deleted') - : t('kanban.delete_status_modal.what_to_do_stories') + isLastWorkflow() + ? t('kanban.delete_workflow_modal.stories_will_be_deleted') + : t('kanban.delete_workflow_modal.what_to_do_statuses') }}

- {{ t('kanban.delete_status_modal.stories_placed_below') }} + {{ t('kanban.delete_workflow_modal.statuses_placed_after') }}
@@ -93,7 +95,7 @@ (click)="close()" data-test="cancel-delete-status" tuiLink> - {{ t('kanban.delete_status_modal.cancel') }} + {{ t('kanban.delete_workflow_modal.cancel') }}
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts index 372adde36..17086a0e7 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/delete-workflow/delete-workflow.component.ts @@ -14,7 +14,6 @@ import { Output, OnInit, signal, - inject, WritableSignal, computed, Signal, @@ -22,17 +21,18 @@ import { import { TranslocoModule } from '@ngneat/transloco'; import { TuiAutoFocusModule } from '@taiga-ui/cdk'; import { TuiButtonModule, TuiLinkModule } from '@taiga-ui/core'; -import { Status, Workflow } from '@taiga/data'; +import { Workflow } from '@taiga/data'; -import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { FormControl, FormGroup } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import { Observable, map } from 'rxjs'; -import { toSignal } from '@angular/core/rxjs-interop'; +import { map } from 'rxjs'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ContextNotificationComponent } from '@taiga/ui/context-notification/context-notification.component'; import { InputsModule } from '@taiga/ui/inputs'; import { ModalComponent } from '@taiga/ui/modal/components'; import { trackByProp } from '~/app/shared/utils/track-by-prop'; +/* TODO: Workflout shoud have id?, if not, change the type */ @Component({ selector: 'tg-delete-workflow', templateUrl: './delete-workflow.component.html', @@ -68,18 +68,20 @@ export class DeleteWorkflowComponent implements OnInit { this.workflowsList.set(workflows); } - public get isLastWorkflow() { - return this.workflowsList().length === 1; - } - - public form!: FormGroup; - public fb = inject(FormBuilder); + public form = new FormGroup({ + statuses: new FormControl('move', { nonNullable: true }), + workflow: new FormControl('', { nonNullable: true }), + }); public workflowsList: WritableSignal = signal([]); - public filteredWorkflwos: Signal = computed(() => { + public filteredWorkflows = computed(() => { return this.workflowsList().filter( - (it) => it.id !== this.currentWorkflow.id + (it) => it.slug !== this.currentWorkflow.slug ); }); + public isLastWorkflow = computed(() => { + return this.workflowsList().length === 1; + }); + public valueContent!: Signal; public trackById = trackByProp('id'); @@ -88,37 +90,33 @@ export class DeleteWorkflowComponent implements OnInit { } constructor() { - if (!this.isLastWorkflow) { - this.form = this.fb.group({ - statuses: ['move'], - workflow: [''], - }); - - const valueContent$ = this.form.get('statuses')?.valueChanges.pipe( - map((value) => { - return this.workflowsList().find((it) => it.id === value)?.name ?? ''; - }) - ) as Observable; - - this.valueContent = toSignal(valueContent$, { - initialValue: '', - }); + if (!this.isLastWorkflow()) { + this.valueContent = toSignal( + this.statusesFormControl.valueChanges.pipe( + takeUntilDestroyed(), + map((value) => { + return ( + this.workflowsList().find((it) => it.id === value)?.name ?? '' + ); + }), + map((value) => value ?? '') + ), + { initialValue: '' } + ); } } public ngOnInit() { - if (this.filteredWorkflwos().length) { - (this.form.get('workflow') as FormControl).setValue( - this.filteredWorkflwos()[0].id - ); + if (this.filteredWorkflows().length) { + this.form.get('workflow')?.setValue(this.filteredWorkflows()[0].slug); } } public submit() { this.close(); const moveToWorkflow: Workflow['id'] | undefined = - !this.isLastWorkflow && this.form.get('statuses')!.value === 'move' - ? (this.form.get('workflow')!.value as Workflow['id']) + !this.isLastWorkflow() && this.form.get('statuses')!.value === 'move' + ? this.form.value.workflow : undefined; this.submitDelete.next(moveToWorkflow); } diff --git a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json index ab011568e..e1d005fec 100644 --- a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json @@ -45,6 +45,17 @@ "stories_placed_below": "The stories will be placed below the existing ones.", "status": "Status" }, + "delete_workflow_modal": { + "title": "Delete {{ name }} workflow?", + "what_to_do_statuses": "What do you want to do with the statuses inside them?", + "stories_will_be_deleted": "The stories from this status will be automatically deleted.", + "cancel": "Never mind, keep workflow", + "confirm": "Yes, delete workflow", + "move_stories_another_workflow": "Move them to another workflow", + "delete_all": "Delete the stories and statuses too", + "statuses_placed_after": "The statuses will be placed after the existing ones.", + "workflow": "Workflow" + }, "create_workflow": { "save": "Create Workflow", "workflow_name_aria": "Workflow name (Maximum 40 Characters only)", From e1ea980f3a592659fb0fd66057a4301a76c217e9 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Fri, 22 Sep 2023 10:32:15 +0200 Subject: [PATCH 10/16] feat(workflows): workflow events --- .../data-access/+state/actions/project.actions.ts | 1 + .../data-access/+state/reducers/project.reducer.ts | 12 +++++++----- .../data-access/+state/actions/kanban.actions.ts | 3 +-- .../data-access/+state/effects/kanban.effects.ts | 1 - .../+state/reducers/kanban.reducer.spec.ts | 12 ++++++------ .../feature-shell/project-feature-shell.component.ts | 9 +++++++++ 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts index c9e7d1407..4477220f9 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts @@ -142,6 +142,7 @@ export const projectEventActions = createActionGroup({ }>(), 'Remove Member': props<{ membership: Membership; workspace: string }>(), 'Update Member': props<{ membership: Membership }>(), + 'Create Workflow': props<{ workflow: Workflow }>(), 'Create comment': props<{ storyRef: Story['ref']; comment: UserComment }>(), 'Status reorder': props<{ id: Status['id']; diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts index cb219dfeb..c0fc18a04 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts @@ -17,7 +17,6 @@ import * as RolesPermissionsActions from '~/app/modules/project/settings/feature import { invitationProjectActions } from '~/app/shared/invite-user-modal/data-access/+state/actions/invitation.action'; import { createImmerReducer } from '~/app/shared/utils/store'; import * as ProjectActions from '../actions/project.actions'; -import { projectEventActions } from '../actions/project.actions'; export const projectFeatureKey = 'project'; @@ -105,8 +104,11 @@ export const reducer = createImmerReducer( }), on( KanbanApiActions.createWorkflowSuccess, - (state, { projectId, workflow }): ProjectState => { - state.projects[projectId].workflows.push(workflow); + ProjectActions.projectEventActions.createWorkflow, + (state, { workflow }): ProjectState => { + if (state.currentProjectId) { + state.projects[state.currentProjectId].workflows.push(workflow); + } return state; } @@ -144,7 +146,7 @@ export const reducer = createImmerReducer( } ), on( - projectEventActions.removeMember, + ProjectActions.projectEventActions.removeMember, (state, { membership }): ProjectState => { state.members = state.members.filter( (members) => members.user.username !== membership.user.username @@ -154,7 +156,7 @@ export const reducer = createImmerReducer( } ), on( - projectEventActions.updateMember, + ProjectActions.projectEventActions.updateMember, (state, { membership }): ProjectState => { state.members = state.members.map((member) => { if (member.user.username === membership.user.username) { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts index 481957f5d..729ce45a8 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts @@ -7,7 +7,7 @@ */ import { createActionGroup, emptyProps, props } from '@ngrx/store'; -import { Membership, Project, Status, Story, Workflow } from '@taiga/data'; +import { Membership, Status, Story, Workflow } from '@taiga/data'; import { DropCandidate } from '@taiga/ui/drag/drag.model'; import { KanbanReorderEvent, @@ -129,7 +129,6 @@ export const KanbanApiActions = createActionGroup({ complete: boolean; }>(), 'Create Workflow Success': props<{ - projectId: Project['id']; workflow: Workflow; }>(), 'create Workflow Error': emptyProps(), diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts index 8b5b93543..c161f4abd 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts @@ -117,7 +117,6 @@ export class KanbanEffects { newWorkflow.slug, ]); return KanbanApiActions.createWorkflowSuccess({ - projectId: project.id, workflow: newWorkflow, }); }) diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.spec.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.spec.ts index 40d558be9..58c81fcc6 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.spec.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/reducers/kanban.reducer.spec.ts @@ -34,17 +34,17 @@ describe('Kanban reducer', () => { { ...initialKanbanState, empty: true, - loadingWorkflows: true, - workflows: [workflow], + loadingWorkflow: true, + workflow, }, - KanbanApiActions.fetchWorkflowsSuccess({ - workflows: [workflow], + KanbanApiActions.fetchWorkflowSuccess({ + workflow, }) ); expect(state.stories[workflow.statuses[0].id]).toEqual([]); expect(state.createStoryForm).toEqual(workflow.statuses[0].id); - expect(state.loadingWorkflows).toEqual(false); + expect(state.loadingWorkflow).toEqual(false); }); it('load stories', () => { @@ -76,7 +76,7 @@ describe('Kanban reducer', () => { { ...initialKanbanState, loadingStories: true, - workflows: [workflow], + workflow, }, KanbanApiActions.fetchStoriesSuccess({ stories: [], 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 894af565f..fe114f866 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 @@ -269,6 +269,15 @@ export class ProjectFeatureShellComponent implements OnDestroy, AfterViewInit { ); }); + this.wsService + .projectEvents<{ workflow: Workflow }>('workflows.create') + .pipe(untilDestroyed(this)) + .subscribe((eventResponse) => { + this.store.dispatch( + projectEventActions.createWorkflow(eventResponse.event.content) + ); + }); + this.wsService .projectEvents<{ story: StoryDetail }>('stories.update') .pipe(untilDestroyed(this)) From 13aa10f477ff4f536da65d15734d067c8b46a99e Mon Sep 17 00:00:00 2001 From: Xaviju Date: Fri, 22 Sep 2023 13:53:30 +0200 Subject: [PATCH 11/16] feat(workflows): workflow flow --- .../+state/actions/project.actions.ts | 29 +++++++++-- .../+state/effects/project.effects.ts | 48 +++++++++++++++++-- .../+state/reducers/project.reducer.ts | 3 +- .../+state/actions/kanban.actions.ts | 6 +-- .../+state/effects/kanban.effects.ts | 36 -------------- .../project-navigation-menu.component.css | 6 +-- .../project-navigation-menu.component.html | 8 ++-- .../project-feature-new-workflow.component.ts | 38 ++++++++++----- .../taiga/src/assets/i18n/kanban/en-US.json | 5 +- 9 files changed, 108 insertions(+), 71 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts index 4477220f9..d4acbeef6 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts @@ -6,7 +6,12 @@ * Copyright (c) 2023-present Kaleidos INC */ -import { createAction, createActionGroup, props } from '@ngrx/store'; +import { + createAction, + createActionGroup, + emptyProps, + props, +} from '@ngrx/store'; import { Attachment, Membership, @@ -68,12 +73,18 @@ export const updateStoryShowView = createAction( }>() ); +// export const createWorkflow = createAction( +// '[Project] Create Workflow', +// props<{ +// name: Workflow['name']; +// }> +// ); + export const createWorkflow = createAction( - '[Project] Create Workflow', + '[Project] create workflow', props<{ - project: Project; name: Workflow['name']; - }> + }>() ); export const newProjectMembers = createAction( @@ -113,6 +124,16 @@ export const deleteProjectSuccess = createAction( }>() ); +export const projectApiActions = createActionGroup({ + source: 'Project Api', + events: { + 'Create Workflow Success': props<{ + workflow: Workflow; + }>(), + 'create Workflow Error': emptyProps(), + }, +}); + export const projectEventActions = createActionGroup({ source: 'Project ws', events: { 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 07b79d70e..661eae60f 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 @@ -10,8 +10,8 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; -import { Store } from '@ngrx/store'; import { fetch, pessimisticUpdate } from '@ngrx/router-store/data-persistence'; +import { Store } from '@ngrx/store'; import { TuiNotification } from '@taiga-ui/core'; import { ProjectApiService } from '@taiga/api'; import { EMPTY, of } from 'rxjs'; @@ -23,7 +23,9 @@ import { switchMap, tap, } from 'rxjs/operators'; +import { selectUser } from '~/app/modules/auth/data-access/+state/selectors/auth.selectors'; import * as ProjectOverviewActions from '~/app/modules/project/feature-overview/data-access/+state/actions/project-overview.actions'; +import { selectUrl } from '~/app/router-selectors'; import { AppService } from '~/app/services/app.service'; import { RevokeInvitationService } from '~/app/services/revoke-invitation.service'; import { invitationProjectActions } from '~/app/shared/invite-user-modal/data-access/+state/actions/invitation.action'; @@ -35,8 +37,6 @@ import { selectCurrentProject, selectMembers, } from '../selectors/project.selectors'; -import { selectUser } from '~/app/modules/auth/data-access/+state/selectors/auth.selectors'; -import { selectUrl } from '~/app/router-selectors'; @Injectable() export class ProjectEffects { public loadProject$ = createEffect(() => { @@ -169,6 +169,48 @@ export class ProjectEffects { { dispatch: false } ); + public createWorkflow$ = createEffect(() => { + return this.actions$.pipe( + ofType(ProjectActions.createWorkflow), + concatLatestFrom(() => [ + this.store.select(selectCurrentProject).pipe(filterNil()), + ]), + pessimisticUpdate({ + run: (action, project) => { + return this.projectApiService + .createWorkflow(action.name, project.id) + .pipe( + map((newWorkflow) => { + void this.router.navigate([ + '/project', + project.id, + project.slug, + 'kanban', + newWorkflow.slug, + ]); + return ProjectActions.projectApiActions.createWorkflowSuccess({ + workflow: newWorkflow, + }); + }) + ); + }, + onError: (_, httpResponse: HttpErrorResponse) => { + if (httpResponse.status === 400) { + this.appService.toastNotification({ + message: 'create_workflow.max_workflow_created', + status: TuiNotification.Error, + scope: 'kanban', + closeOnNavigation: false, + }); + return ProjectActions.projectApiActions.createWorkflowError(); + } else { + return this.appService.errorManagement(httpResponse); + } + }, + }) + ); + }); + public initAssignUser$ = createEffect(() => { return this.actions$.pipe( ofType(ProjectActions.initAssignUser), diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts index c0fc18a04..ab1c060a5 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts @@ -8,7 +8,6 @@ import { createFeature, on } from '@ngrx/store'; import { Membership, Project } from '@taiga/data'; -import { KanbanApiActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions'; import { editProject, editProjectSuccess, @@ -103,7 +102,7 @@ export const reducer = createImmerReducer( return state; }), on( - KanbanApiActions.createWorkflowSuccess, + ProjectActions.projectApiActions.createWorkflowSuccess, ProjectActions.projectEventActions.createWorkflow, (state, { workflow }): ProjectState => { if (state.currentProjectId) { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts index 729ce45a8..16a66b670 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions.ts @@ -21,7 +21,6 @@ export const KanbanActions = createActionGroup({ events: { 'Init Kanban': props<{ workflow: Workflow['slug'] }>(), 'Load Workflow kanban': props<{ workflow: Workflow['slug'] }>(), - 'Create Workflow': props<{ name: Workflow['name'] }>(), 'Open Create Story form': props<{ status: Status['id'] }>(), 'Close Create Story form': emptyProps(), 'Create Story': props<{ @@ -128,10 +127,7 @@ export const KanbanApiActions = createActionGroup({ offset: number; complete: boolean; }>(), - 'Create Workflow Success': props<{ - workflow: Workflow; - }>(), - 'create Workflow Error': emptyProps(), + 'Create Story Success': props<{ story: Story; tmpId: PartialStory['tmpId']; diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts index c161f4abd..2bfc41aa4 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/data-access/+state/effects/kanban.effects.ts @@ -97,42 +97,6 @@ export class KanbanEffects { ); }); - public createWorkflow$ = createEffect(() => { - return this.actions$.pipe( - ofType(KanbanActions.createWorkflow), - concatLatestFrom(() => [ - this.store.select(selectCurrentProject).pipe(filterNil()), - ]), - pessimisticUpdate({ - run: (workflow, project) => { - return this.projectApiService - .createWorkflow(workflow.name, project.id) - .pipe( - map((newWorkflow) => { - void this.router.navigate([ - '/project', - project.id, - project.slug, - 'kanban', - newWorkflow.slug, - ]); - return KanbanApiActions.createWorkflowSuccess({ - workflow: newWorkflow, - }); - }) - ); - }, - onError: (action, httpResponse: HttpErrorResponse) => { - if (httpResponse.status !== 403) { - this.appService.errorManagement(httpResponse); - } - - return KanbanApiActions.createWorkflowError(); - }, - }) - ); - }); - public createStory$ = createEffect(() => { return this.actions$.pipe( ofType(KanbanActions.createStory), diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css index 8e1196e37..1b681658d 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css @@ -163,10 +163,10 @@ ul { &:focus-visible { outline: solid 2px var(--color-secondary50); } +} - &.has-add-workflow-button { - position: relative; - } +.has-add-workflow-button { + position: relative; } .menu-option { diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html index 96f4fe7f9..5fec153db 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.html @@ -102,7 +102,7 @@ diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts index 26a596a29..a1550d55a 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/project-feature-new-workflow.component.ts @@ -8,15 +8,21 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Router } from '@angular/router'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Actions, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { RxState } from '@rx-angular/state'; import { Project, Workflow } from '@taiga/data'; +import { RouteHistoryService } from '~/app/shared/route-history/route-history.service'; import { filterNil } from '~/app/shared/utils/operators'; +import { + createWorkflow, + projectApiActions, +} from '../data-access/+state/actions/project.actions'; import { selectCurrentProject } from '../data-access/+state/selectors/project.selectors'; -import { KanbanActions } from '../feature-kanban/data-access/+state/actions/kanban.actions'; import { NewWorkflowFormComponent } from './components/new-workflow-form/new-workflow-form.component'; import { NewWorkflowSkeletonComponent } from './components/new-workflow-skeleton/new-workflow-skeleton.component'; - +@UntilDestroy() @Component({ selector: 'tg-project-feature-new-workflow', templateUrl: './project-feature-new-workflow.component.html', @@ -31,31 +37,39 @@ export class ProjectFeatureNewWorkflowComponent { project: Project; }>, private store: Store, - private router: Router + private router: Router, + private actions$: Actions, + private routeHistoryService: RouteHistoryService ) { this.state.connect( 'project', this.store.select(selectCurrentProject).pipe(filterNil()) ); + this.actions$ + .pipe(ofType(projectApiActions.createWorkflowError), untilDestroyed(this)) + .subscribe(() => { + this.cancelCreateWorkflow(); + }); } public createWorkflow(workflow: Workflow['name']) { this.store.dispatch( - KanbanActions.createWorkflow({ + createWorkflow({ name: workflow, }) ); } public cancelCreateWorkflow() { + void this.router.navigate(this.getPreviousUrl); + } + + public get getPreviousUrl(): string[] { + const previousUrl = this.routeHistoryService.getPreviousUrl(); const project = this.state.get('project'); - const firstworkflow = project.workflows[0]; - void this.router.navigate([ - '/project', - project.id, - project.slug, - 'kanban', - firstworkflow.slug, - ]); + console.log({ previousUrl }); + return previousUrl + ? [previousUrl] + : [`/project/${project.id}${project.slug}/overview`]; } } diff --git a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json index e1d005fec..3618cf1d3 100644 --- a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json @@ -60,6 +60,7 @@ "save": "Create Workflow", "workflow_name_aria": "Workflow name (Maximum 40 Characters only)", "write_workflow_name": "Write a workflow name", - "workflow_empty": "Workflow name can’t be empty." + "workflow_empty": "Workflow name can’t be empty.", + "max_workflow_created": "You reached the max number of workflows (8)." } -} +} \ No newline at end of file From d0ed4df14fbb382a5eccb15ac540527fef088535 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Fri, 22 Sep 2023 15:02:38 +0200 Subject: [PATCH 12/16] feat(workflows): workflow breadcrumb --- .../kanban-header/kanban-header.component.css | 6 +++- .../kanban-header.component.html | 14 +++++--- .../kanban-header/kanban-header.component.ts | 7 ++-- .../project-feature-kanban.component.html | 1 + .../project-navigation-menu.component.css | 3 ++ .../new-workflow-form.component.css | 2 +- .../story-detail-assign.component.css | 1 - .../story-detail/story-detail.component.css | 6 ++++ .../story-detail/story-detail.component.html | 23 +++++++----- .../story-detail/story-detail.component.ts | 9 +++-- .../apps/taiga/src/assets/i18n/en-US.json | 2 +- .../taiga/src/assets/i18n/kanban/en-US.json | 3 ++ .../lib/breadcrumb/breadcrumb.component.css | 23 ++++++++++++ .../lib/breadcrumb/breadcrumb.component.html | 35 +++++++++++++++++++ .../lib/breadcrumb/breadcrumb.component.ts | 33 +++++++++++++++++ 15 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.css create mode 100644 javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.html create mode 100644 javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.ts 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 e18395623..e5da13529 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 @@ -15,7 +15,11 @@ Copyright (c) 2023-present Kaleidos INC gap: var(--spacing-4); } -h1 { +.kanban-title { + @mixin font-inline; + + color: var(--color-gray100); + font-weight: 500; margin: 0; } diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html index d0fd79fa3..af2f4f472 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html @@ -9,16 +9,20 @@

{{ t('kanban.title') }}

- + +
diff --git a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css index 1b681658d..528d1688a 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.component.css @@ -198,8 +198,11 @@ ul { .submenu-option-item { @mixin menu-option-item; + @mixin ellipsis; block-size: var(--spacing-28); + display: inline-block; + inline-size: 100%; &:hover { background: var(--color-gray90); diff --git a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css index 728f00e3f..d6ee9fec4 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css +++ b/javascript/apps/taiga/src/app/modules/project/feature-new-workflow/components/new-workflow-form/new-workflow-form.component.css @@ -10,7 +10,7 @@ Copyright (c) 2023-present Kaleidos INC background: var(--color-white); border: 1px solid var(--color-gray30); border-radius: 3px; - box-shadow: 3px 4px 14px 0px rgba(0, 138, 168, 0.15); + box-shadow: 3px 4px 14px 0 rgba(0, 138, 168, 0.15); display: block; inline-size: 100%; max-inline-size: 586px; diff --git a/javascript/apps/taiga/src/app/modules/project/story-detail/components/story-detail-assign/story-detail-assign.component.css b/javascript/apps/taiga/src/app/modules/project/story-detail/components/story-detail-assign/story-detail-assign.component.css index c9f98f780..b013ee5cf 100644 --- a/javascript/apps/taiga/src/app/modules/project/story-detail/components/story-detail-assign/story-detail-assign.component.css +++ b/javascript/apps/taiga/src/app/modules/project/story-detail/components/story-detail-assign/story-detail-assign.component.css @@ -20,7 +20,6 @@ Copyright (c) 2023-present Kaleidos INC --hover-bg-color: var(--color-gray20); inline-size: 100%; - margin-block-start: var(--spacing-16); max-inline-size: 450px; & .add-assignee { diff --git a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.css b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.css index d4f1848eb..ed07386e7 100644 --- a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.css +++ b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.css @@ -136,6 +136,12 @@ Copyright (c) 2023-present Kaleidos INC --no-user-avatar-size: 24px; } +.story-breadcrumb-modal, +tg-story-detail-status, +tg-story-detail-assign { + margin-block-end: var(--spacing-16); +} + /* stylelint-disable selector-max-compound-selectors, selector-max-type */ .field-edit { & tg-story-detail-status, diff --git a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.html b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.html index 5db79a4ef..7e54bd243 100644 --- a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.html +++ b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.html @@ -78,12 +78,6 @@
- - + + + + diff --git a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts index 9290957b1..4eaf378e2 100644 --- a/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts +++ b/javascript/apps/taiga/src/app/modules/project/story-detail/story-detail.component.ts @@ -47,6 +47,11 @@ import { Workflow, } from '@taiga/data'; import { map, merge, pairwise, startWith, take } from 'rxjs'; + +import { TuiScrollbarModule } from '@taiga-ui/core/components/scrollbar'; +import { BreadcrumbComponent } from '@taiga/ui/breadcrumb/breadcrumb.component'; +import { InputsModule } from '@taiga/ui/inputs'; +import { ModalComponent } from '@taiga/ui/modal/components'; import { v4 } from 'uuid'; import { selectUser } from '~/app/modules/auth/data-access/+state/selectors/auth.selectors'; import { selectCurrentProject } from '~/app/modules/project/data-access/+state/selectors/project.selectors'; @@ -79,9 +84,6 @@ import { StoryDetailAssignComponent } from './components/story-detail-assign/sto import { StoryDetailStatusComponent } from './components/story-detail-status/story-detail-status.component'; import { StoryDetailTitleComponent } from './components/story-detail-title/story-detail-title.component'; import { StoryCommentsPaginationDirective } from './directives/story-comments-pagination.directive'; -import { TuiScrollbarModule } from '@taiga-ui/core/components/scrollbar'; -import { InputsModule } from '@taiga/ui/inputs'; -import { ModalComponent } from '@taiga/ui/modal/components'; import { AttachmentsComponent } from '~/app/shared/attachments/attachments.component'; import { CommentsAutoScrollDirective } from '~/app/shared/comments/directives/comments-auto-scroll.directive'; import { DiscardChangesModalComponent } from '~/app/shared/discard-changes-modal/discard-changes-modal.component'; @@ -164,6 +166,7 @@ export interface StoryDetailForm { DiscardChangesModalComponent, DatePipe, DateDistancePipe, + BreadcrumbComponent, ], }) export class StoryDetailComponent { diff --git a/javascript/apps/taiga/src/assets/i18n/en-US.json b/javascript/apps/taiga/src/assets/i18n/en-US.json index 3be2f0238..44a18ef32 100644 --- a/javascript/apps/taiga/src/assets/i18n/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/en-US.json @@ -235,4 +235,4 @@ "action_undo": "Undo" } } -} +} \ No newline at end of file diff --git a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json index 3618cf1d3..3592b9ac8 100644 --- a/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/kanban/en-US.json @@ -1,5 +1,8 @@ { + "workflow": "workflow", + "workflows": "workflows", "page_title": "{{projectName}} kanban", + "workflow_title": "[{{projectName}}] kanban workflow", "title": "Kanban", "empty": "The project administrator has not set any status yet.", "status_label": "Status {{ statusName }}, Tap left and right arrows to move between statuses.", diff --git a/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.css b/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.css new file mode 100644 index 000000000..7418e4e7e --- /dev/null +++ b/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.css @@ -0,0 +1,23 @@ +@import url("tools/typography.css"); + +:host { + @mixin font-inline; + + color: var(--color-gray80); + display: block; +} + +.crumb { + padding: var(--spacing4); +} + +.accent { + color: var(--color-gray100); + font-weight: 500; +} + +.collapsed-crumb-icon { + block-size: var(--spacing-16); + color: var(--color-gray40); + inline-size: var(--spacing-16); +} diff --git a/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.html b/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.html new file mode 100644 index 000000000..ca88868ad --- /dev/null +++ b/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.html @@ -0,0 +1,35 @@ + + + + + + + {{ crumb }} + + + > + + + + + + + {{ crumbs.at(-1) }} + + + diff --git a/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.ts b/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.ts new file mode 100644 index 000000000..af7d4b1ae --- /dev/null +++ b/javascript/libs/ui/src/lib/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,33 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) 2023-present Kaleidos INC + */ + +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { TranslocoDirective } from '@ngneat/transloco'; +import { TuiSvgModule } from '@taiga-ui/core'; + +type BreadCrumbType = 'expanded' | 'collapsed'; + +@Component({ + selector: 'tg-ui-breadcrumb', + templateUrl: './breadcrumb.component.html', + styleUrls: ['./breadcrumb.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, TranslocoDirective, TuiSvgModule], +}) +export class BreadcrumbComponent { + @Input({ required: true }) public crumbs: string[] = []; + @Input() public icon = 'kanban'; + @Input() public accent = false; + @Input() public type: BreadCrumbType = 'expanded'; + + public trackByIndex(index: number) { + return index; + } +} From 10dba0f8894e93ccbaaec59d7f5db65148ffcad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Tue, 26 Sep 2023 07:41:33 +0200 Subject: [PATCH 13/16] feat(workflows): update workflow name --- .../+state/actions/project.actions.ts | 19 ++++++---- .../+state/effects/project.effects.ts | 37 ++++++++++++++++++- .../+state/reducers/project.reducer.ts | 12 ++++++ .../kanban-header/kanban-header.component.css | 14 +++++++ .../kanban-header.component.html | 12 +++++- .../kanban-header/kanban-header.component.ts | 22 +++++++++++ .../+state/reducers/kanban.reducer.ts | 17 +++++++++ .../project-feature-kanban.component.css | 1 + .../new-workflow-form.component.html | 2 +- .../new-workflow-form.component.ts | 5 ++- .../apps/taiga/src/assets/i18n/en-US.json | 2 +- .../taiga/src/assets/i18n/kanban/en-US.json | 2 +- .../src/lib/project/project-api.service.ts | 13 +++++++ .../lib/breadcrumb/breadcrumb.component.css | 8 ++++ .../lib/breadcrumb/breadcrumb.component.html | 16 +++++++- .../lib/breadcrumb/breadcrumb.component.ts | 1 + 16 files changed, 168 insertions(+), 15 deletions(-) diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts index d4acbeef6..b52ce1af1 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/actions/project.actions.ts @@ -73,13 +73,6 @@ export const updateStoryShowView = createAction( }>() ); -// export const createWorkflow = createAction( -// '[Project] Create Workflow', -// props<{ -// name: Workflow['name']; -// }> -// ); - export const createWorkflow = createAction( '[Project] create workflow', props<{ @@ -87,6 +80,14 @@ export const createWorkflow = createAction( }>() ); +export const updateWorkflow = createAction( + '[Project] update workflow', + props<{ + name: Workflow['name']; + slug: Workflow['slug']; + }>() +); + export const newProjectMembers = createAction( '[Project][ws] New Project Members' ); @@ -131,6 +132,10 @@ export const projectApiActions = createActionGroup({ workflow: Workflow; }>(), 'create Workflow Error': emptyProps(), + 'Update Workflow Success': props<{ + workflow: Workflow; + oldSlug: Workflow['slug']; + }>(), }, }); 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 661eae60f..d3d1f4b31 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 @@ -6,6 +6,7 @@ * Copyright (c) 2023-present Kaleidos INC */ +import { Location } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; @@ -211,6 +212,39 @@ export class ProjectEffects { ); }); + public updateWorkflow$ = createEffect(() => { + return this.actions$.pipe( + ofType(ProjectActions.updateWorkflow), + concatLatestFrom(() => [ + this.store.select(selectCurrentProject).pipe(filterNil()), + ]), + pessimisticUpdate({ + run: (action, project) => { + return this.projectApiService + .updateWorkflow(action.name, action.slug, project.id) + .pipe( + map((updatedWorkflow) => { + // void this.router.navigate( + // [`project/${project.id}/${project.slug}/kanban/${updatedWorkflow.slug}`], + // { replaceUrl: true } + // ); + this.location.go( + `project/${project.id}/${project.slug}/kanban/${updatedWorkflow.slug}` + ); + return ProjectActions.projectApiActions.updateWorkflowSuccess({ + workflow: updatedWorkflow, + oldSlug: action.slug, + }); + }) + ); + }, + onError: (_, httpResponse: HttpErrorResponse) => { + return this.appService.errorManagement(httpResponse); + }, + }) + ); + }); + public initAssignUser$ = createEffect(() => { return this.actions$.pipe( ofType(ProjectActions.initAssignUser), @@ -463,6 +497,7 @@ export class ProjectEffects { private appService: AppService, private router: Router, private revokeInvitationService: RevokeInvitationService, - private store: Store + private store: Store, + private location: Location ) {} } diff --git a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts index ab1c060a5..84d595eab 100644 --- a/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts +++ b/javascript/apps/taiga/src/app/modules/project/data-access/+state/reducers/project.reducer.ts @@ -112,6 +112,18 @@ export const reducer = createImmerReducer( return state; } ), + on( + ProjectActions.projectApiActions.updateWorkflowSuccess, + (state, { workflow, oldSlug }): ProjectState => { + if (state.currentProjectId) { + const workflows = state.projects[state.currentProjectId].workflows; + const workflowIndex = workflows.findIndex((it) => it.slug === oldSlug); + workflows[workflowIndex].name = workflow.name; + workflows[workflowIndex].slug = workflow.slug; + } + return state; + } + ), on( ProjectActions.permissionsUpdateSuccess, (state, { project }): ProjectState => { 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 e5da13529..ce5b490e8 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 @@ -30,3 +30,17 @@ Copyright (c) 2023-present Kaleidos INC .separator { @mixin separator; } + +.workflow-form { + &::ng-deep { + background: transparent; + border: 0; + box-shadow: none; + margin-block-start: var(--spacing-4); + padding: 0; + + &.input-wrapper { + max-inline-size: 210px; + } + } +} diff --git a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html index af2f4f472..c23886ec5 100644 --- a/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html +++ b/javascript/apps/taiga/src/app/modules/project/feature-kanban/components/kanban-header/kanban-header.component.html @@ -20,11 +20,13 @@ + type="button" + (click)="toggleEditWorkflowForm()">