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..e2aaa6278 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,22 @@ ul { @mixin menu-option-item; &:hover, + &.active-section, &.active-dialog { background: var(--color-gray90); color: var(--color-primary); font-weight: var(--font-weight-regular); } + + &:focus-visible { + outline: solid 2px var(--color-secondary50); + } + + &.menu-option-item-kanban { + appearance: none; + background: none; + border: 0; + } } .menu-option { @@ -169,125 +181,57 @@ 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; - } - } - } +.menu-option-kanban { + position: relative; +} - & .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 +322,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); @@ -391,19 +334,18 @@ ul { } } - & .menu-option-icon, - & .scrum-button-icon { + & .menu-option-item-kanban { + &:hover { + inline-size: auto; + } + } + + & .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 af1abff30..a8330542c 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 @@ -95,40 +95,197 @@ - + + +
+ + +
+
+ + + (pointerenter)="popup($event, 'kanban')" + (pointerleave)="out()"> + + + + {{ t('commons.kanban') }} + + + + + + + + + + +
@@ -196,11 +353,7 @@ [style.top.px]="dialog.top" [style.left.px]="dialog.left"> 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 e14dabd12..e793a2771 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 @@ -19,11 +19,13 @@ import { import { RouterModule } from '@angular/router'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; -import { TuiSvgModule } from '@taiga-ui/core'; -import { Project } from '@taiga/data'; +import { TuiHostedDropdownModule, TuiSvgModule } from '@taiga-ui/core'; +import { Project, Workflow } from '@taiga/data'; import { AvatarModule } from '@taiga/ui/avatar'; +import { ToolTipModule } from '@taiga/ui/tooltip'; import { CommonTemplateModule } from '~/app/shared/common-template.module'; import { HasPermissionDirective } from '~/app/shared/directives/has-permissions/has-permission.directive'; +import { TopRightProjectNavMenuDirective } from './project-navigation-menu.directive'; interface ProjectMenuDialog { hover: boolean; @@ -37,7 +39,7 @@ interface ProjectMenuDialog { mainLinkHeight: number; children: { text: string; - link: string[]; + link: string; }[]; } const cssValue = getComputedStyle(document.documentElement); @@ -54,6 +56,9 @@ const cssValue = getComputedStyle(document.documentElement); AvatarModule, RouterModule, HasPermissionDirective, + ToolTipModule, + TuiHostedDropdownModule, + TopRightProjectNavMenuDirective, ], animations: [ trigger('blockInitialRenderAnimation', [transition(':enter', [])]), @@ -97,7 +102,8 @@ export class ProjectNavigationMenuComponent { public projectSettingButton!: ElementRef; public collapseText = true; - public scrumChildMenuVisible = false; + public activeSection = false; + public openworkflowsDropdown = false; public dialog: ProjectMenuDialog = { open: false, @@ -130,7 +136,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; @@ -182,32 +188,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() { @@ -215,4 +201,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..d3a0d2c13 --- /dev/null +++ b/javascript/apps/taiga/src/app/modules/project/feature-navigation/components/project-navigation-menu/project-navigation-menu.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, 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(TopRightProjectNavMenuDirective)], + standalone: true, +}) +export class TopRightProjectNavMenuDirective extends TuiPositionAccessor { + public readonly type = 'dropdown'; + + constructor( + @Inject(ElementRef) private readonly el: ElementRef + ) { + super(); + } + + public getPosition(): TuiPoint { + const { right, top } = this.el.nativeElement.getBoundingClientRect(); + + return [top + 5, right]; + } +} 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 b8f963ab0..fdb7396b1 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,11 +22,11 @@ const routes: Routes = [ }, children: [ { - path: ':slug/kanban', + path: '', loadChildren: () => import( - '~/app/modules/project/feature-view-setter/project-feature-view-setter.module' - ).then((m) => m.ProjectFeatureViewSetterModule), + '~/app/modules/project/feature-overview/project-feature-overview.module' + ).then((m) => m.ProjectFeatureOverviewModule), }, { path: 'kanban', @@ -37,12 +37,23 @@ const routes: Routes = [ canDeactivate: [CanDeactivateGuard], }, { - path: ':slug/stories/:storyRef', + 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' ).then((m) => m.ProjectFeatureViewSetterModule), - canDeactivate: [CanDeactivateGuard], }, { path: 'stories/:storyRef', @@ -53,14 +64,12 @@ const routes: Routes = [ canDeactivate: [CanDeactivateGuard], }, { - path: ':slug/settings', + path: ':slug/stories/:storyRef', loadChildren: () => import( - '~/app/modules/project/settings/feature-settings/feature-settings.module' - ).then((m) => m.ProjectSettingsFeatureSettingsModule), - resolve: { - project: ProjectAdminResolver, - }, + '~/app/modules/project/feature-view-setter/project-feature-view-setter.module' + ).then((m) => m.ProjectFeatureViewSetterModule), + canDeactivate: [CanDeactivateGuard], }, { path: 'settings', @@ -73,18 +82,14 @@ const routes: Routes = [ }, }, { - path: '', - loadChildren: () => - import( - '~/app/modules/project/feature-overview/project-feature-overview.module' - ).then((m) => m.ProjectFeatureOverviewModule), - }, - { - path: ':slug', + path: ':slug/settings', loadChildren: () => import( - '~/app/modules/project/feature-overview/project-feature-overview.module' - ).then((m) => m.ProjectFeatureOverviewModule), + '~/app/modules/project/settings/feature-settings/feature-settings.module' + ).then((m) => m.ProjectSettingsFeatureSettingsModule), + resolve: { + project: ProjectAdminResolver, + }, }, ], }, 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..f48781e29 100644 --- a/javascript/apps/taiga/src/assets/i18n/en-US.json +++ b/javascript/apps/taiga/src/assets/i18n/en-US.json @@ -112,6 +112,9 @@ "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" + }, "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/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[];