mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
feat(projectFolders): improve code quality and extract translations
This commit is contained in:
parent
9332bd4701
commit
7dc91c1b6a
18 changed files with 341 additions and 425 deletions
|
|
@ -1,89 +1,60 @@
|
|||
import { expect, test } from '../../fixtures/test.fixture';
|
||||
|
||||
test.describe('Project Folders', () => {
|
||||
test('should create and display project folders in navigation', async ({
|
||||
page,
|
||||
workViewPage,
|
||||
}) => {
|
||||
// Wait for work view to be ready
|
||||
test('can create a project folder via navigation', async ({ page, workViewPage }) => {
|
||||
await workViewPage.waitForTaskList();
|
||||
|
||||
// Take screenshot to see the initial navigation structure
|
||||
await page.screenshot({
|
||||
path: 'e2e-results/project-folders-before.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// Check if navigation is visible
|
||||
const navigation = page.locator('nav.nav-sidenav, .nav-sidenav');
|
||||
await expect(navigation).toBeVisible();
|
||||
|
||||
// Look for projects section in navigation
|
||||
const projectsSection = page.locator('text="Projects"');
|
||||
const projectsSectionExists = (await projectsSection.count()) > 0;
|
||||
console.log(`Projects section found: ${projectsSectionExists}`);
|
||||
const projectsHeader = page
|
||||
.locator('.g-multi-btn-wrapper')
|
||||
.filter({ hasText: 'Projects' })
|
||||
.first();
|
||||
await expect(projectsHeader).toBeVisible();
|
||||
|
||||
// If projects section exists, check if we can expand it
|
||||
if (projectsSectionExists) {
|
||||
// Take screenshot after finding projects section
|
||||
await page.screenshot({
|
||||
path: 'e2e-results/project-folders-projects-found.png',
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
const createFolderBtn = projectsHeader
|
||||
.locator('button.additional-btn')
|
||||
.filter({ has: page.locator('mat-icon', { hasText: 'create_new_folder' }) })
|
||||
.first();
|
||||
await createFolderBtn.click();
|
||||
|
||||
// For now, just verify the navigation exists and has basic structure
|
||||
const navItems = page.locator('.nav-sidenav li, nav-item');
|
||||
const navCount = await navItems.count();
|
||||
expect(navCount).toBeGreaterThan(0);
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
const folderName = 'E2E Folder';
|
||||
await dialog.locator('input[formcontrolname="title"]').fill(folderName);
|
||||
await dialog.locator('button[type="submit"]').click();
|
||||
|
||||
await expect(dialog).toHaveCount(0);
|
||||
|
||||
const createdFolderNavItem = page
|
||||
.locator('nav-item, .nav-child-item')
|
||||
.filter({ hasText: folderName })
|
||||
.first();
|
||||
await expect(createdFolderNavItem).toBeVisible();
|
||||
});
|
||||
|
||||
test('should test project creation flow', async ({ page, workViewPage }) => {
|
||||
// Wait for work view to be ready
|
||||
test('opens create project dialog from navigation', async ({ page, workViewPage }) => {
|
||||
await workViewPage.waitForTaskList();
|
||||
|
||||
// Find and click "Create Project" button
|
||||
const createProjectBtn = page.locator('text="Create Project"');
|
||||
await expect(createProjectBtn).toBeVisible();
|
||||
|
||||
// Take screenshot before clicking
|
||||
await page.screenshot({
|
||||
path: 'e2e-results/project-folders-before-create.png',
|
||||
fullPage: true,
|
||||
});
|
||||
const projectsHeader = page
|
||||
.locator('.g-multi-btn-wrapper')
|
||||
.filter({ hasText: 'Projects' })
|
||||
.first();
|
||||
await expect(projectsHeader).toBeVisible();
|
||||
|
||||
const createProjectBtn = projectsHeader
|
||||
.locator('button.additional-btn')
|
||||
.filter({ has: page.locator('mat-icon', { hasText: 'add' }) })
|
||||
.first();
|
||||
await createProjectBtn.click();
|
||||
|
||||
// Wait for dialog or new project form to appear
|
||||
await page.waitForTimeout(1000);
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.locator('input[formcontrolname="title"]')).toBeVisible();
|
||||
|
||||
// Take screenshot after clicking to see what appears
|
||||
await page.screenshot({
|
||||
path: 'e2e-results/project-folders-after-create-click.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// Look for project creation dialog
|
||||
const dialog = page.locator(
|
||||
'mat-dialog-container, .mat-dialog-container, [role="dialog"]',
|
||||
);
|
||||
const hasDialog = (await dialog.count()) > 0;
|
||||
|
||||
console.log(`Project creation dialog appeared: ${hasDialog}`);
|
||||
|
||||
// For now, just verify the create project button works
|
||||
expect(hasDialog).toBeTruthy();
|
||||
|
||||
// Close dialog if it opened
|
||||
if (hasDialog) {
|
||||
const closeBtn = page.locator(
|
||||
'button[mat-dialog-close], .mat-dialog-close, button:has-text("Cancel")',
|
||||
);
|
||||
if ((await closeBtn.count()) > 0) {
|
||||
await closeBtn.click();
|
||||
} else {
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
}
|
||||
await dialog.getByRole('button', { name: 'Cancel' }).click();
|
||||
await expect(dialog).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
208
package-lock.json
generated
208
package-lock.json
generated
|
|
@ -43,9 +43,6 @@
|
|||
"@angular/router": "^20.1.6",
|
||||
"@angular/service-worker": "^20.1.6",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.7",
|
||||
"@capacitor/android": "^7.3.0",
|
||||
"@capacitor/app": "^7.0.1",
|
||||
"@capacitor/cli": "^7.3.0",
|
||||
|
|
@ -2858,61 +2855,6 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/atlassian-context": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/atlassian-context/-/atlassian-context-0.5.0.tgz",
|
||||
"integrity": "sha512-ui8J50lnr7I8i8yq+nqupZqEQ3awJzWa3PsGUR+BY/vuM/A/UzNMA3EQApBItyeOPv49nov8lOIidtU0VnzLxw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/ds-lib": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/ds-lib/-/ds-lib-5.0.1.tgz",
|
||||
"integrity": "sha512-vP71lCCXSM1S1TkGIpWDsNRs5IwQnM99BGa4Dsx14odTXRr/c9k/0ywL7voibHfofCPYgxn9I+iiq4T8KiQ1Ew==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/platform-feature-flags": "^1.1.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"bind-event-listener": "^3.0.0",
|
||||
"react-uid": "^2.2.0",
|
||||
"tiny-invariant": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/feature-gate-js-client": {
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/feature-gate-js-client/-/feature-gate-js-client-5.5.5.tgz",
|
||||
"integrity": "sha512-uuF/gK35LZoyEUtoVcjxXtbls9fb0RiF5kxp9kYfVwuT6XkZxrYAGuQMPq8tnHKpZE3m70tdMuANyIlMddLzqw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/atlassian-context": "^0.5.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@statsig/client-core": "^3.21.1",
|
||||
"@statsig/js-client": "^3.21.1",
|
||||
"eventemitter3": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/platform-feature-flags": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/platform-feature-flags/-/platform-feature-flags-1.1.2.tgz",
|
||||
"integrity": "sha512-PM+fVkV4Yn4/0keiN6ioAap2Y+odTyw1Z4uf+TA0zMmg3/lp/rVNJEc4a24dvd8DfVs5m9bxa/hbO1qJarhCBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/feature-gate-js-client": "^5.5.0",
|
||||
"@babel/runtime": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.7.tgz",
|
||||
|
|
@ -2925,63 +2867,6 @@
|
|||
"raf-schd": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/pragmatic-drag-and-drop-auto-scroll": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-2.1.2.tgz",
|
||||
"integrity": "sha512-6BgAUxSNbQFiG3uqNxf53cDQADn5mSeh/JsQzCHo46GPQnVWIJk77zWC8yZ++0Mfg1ECy02zNrbniF7SgHAhXQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.0",
|
||||
"@babel/runtime": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/pragmatic-drag-and-drop-hitbox": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-hitbox/-/pragmatic-drag-and-drop-hitbox-1.1.0.tgz",
|
||||
"integrity": "sha512-JWt6eVp6Br2FPHRM8s0dUIHQk/jFInGP1f3ti5CdtM1Ji5/pt8Akm44wDC063Gv2i5RGseixtbW0z/t6RYtbdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.6.0",
|
||||
"@babel/runtime": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/-/pragmatic-drag-and-drop-react-drop-indicator-3.2.7.tgz",
|
||||
"integrity": "sha512-QdJJuBnERhnR/iG9TDXJEc1HudIsPVjP3BKuFE14NSVVs8fnSkFsfUrpVxVQPJT6fYZdfEHXREoU+DdLtEI7Fg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@atlaskit/tokens": "^6.3.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@compiled/react": "^0.18.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@atlaskit/tokens": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tokens/-/tokens-6.3.1.tgz",
|
||||
"integrity": "sha512-acIM5WwrETYp9OuGEO/kh2MZOrCzQoYYz3Hv6HK7Pt5VwxStmAhZw9II3IE81uLRsac+gzo0NgDtuaBJHA4a1g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/ds-lib": "^5.0.0",
|
||||
"@atlaskit/platform-feature-flags": "^1.1.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.20.0",
|
||||
"bind-event-listener": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
|
|
@ -4859,19 +4744,6 @@
|
|||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@compiled/react": {
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@compiled/react/-/react-0.18.6.tgz",
|
||||
"integrity": "sha512-Mt6sJOwykeoToEBFbOUNR4xABi2gOr/+X5QSGqGEYiCBMh+XPDAclG2UX94zveiYJXO4AUJIQBCVwa4/lwPMBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@conventional-changelog/git-client": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
|
|
@ -9153,23 +9025,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@statsig/client-core": {
|
||||
"version": "3.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@statsig/client-core/-/client-core-3.25.2.tgz",
|
||||
"integrity": "sha512-Yznv8wj5VkGxxj2QVUSEYZMnvWPPNfXe0tUXSJswwrmH+Jy5s+91TSjle1j+faypKJIQLKSuuw3rwdxb9B45dA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@statsig/js-client": {
|
||||
"version": "3.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@statsig/js-client/-/js-client-3.25.2.tgz",
|
||||
"integrity": "sha512-kydziYuIjo1jEQDH+0rR5sNBxb5UKENTiiYv+kiF7YEn1F63FJZ5ZwO7SVYS4knJpJjsLXS4SJGSd5n449Sz7A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@statsig/client-core": "3.25.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@super-productivity/plugin-api": {
|
||||
"resolved": "packages/plugin-api",
|
||||
"link": true
|
||||
|
|
@ -12962,13 +12817,6 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/custom-event": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
|
|
@ -18989,20 +18837,6 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.2",
|
||||
"dev": true,
|
||||
|
|
@ -22313,42 +22147,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-uid": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-uid/-/react-uid-2.4.0.tgz",
|
||||
"integrity": "sha512-+MVs/25NrcZuGrmlVRWPOSsbS8y72GJOBsR7d68j3/wqOrRBF52U29XAw4+XSelw0Vm6s5VmGH5mCbTCPGVCVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/read-binary-file-arch": {
|
||||
"version": "1.0.6",
|
||||
"dev": true,
|
||||
|
|
@ -24984,12 +24782,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -162,9 +162,6 @@
|
|||
"@angular/router": "^20.1.6",
|
||||
"@angular/service-worker": "^20.1.6",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.7",
|
||||
"@capacitor/android": "^7.3.0",
|
||||
"@capacitor/app": "^7.0.1",
|
||||
"@capacitor/cli": "^7.3.0",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
(click)="editFolder()"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>Edit</span>
|
||||
<span>{{ T.G.EDIT | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -11,5 +11,5 @@
|
|||
(click)="deleteFolder()"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>Delete</span>
|
||||
<span>{{ T.G.DELETE | translate }}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -9,22 +9,27 @@ import { MatMenuItem } from '@angular/material/menu';
|
|||
import { MatIcon } from '@angular/material/icon';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
import { ProjectFolder } from '../../features/project-folder/store/project-folder.model';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { T } from '../../t.const';
|
||||
|
||||
@Component({
|
||||
selector: 'folder-context-menu',
|
||||
templateUrl: './folder-context-menu.component.html',
|
||||
styleUrls: ['./folder-context-menu.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MatMenuItem, MatIcon],
|
||||
imports: [MatMenuItem, MatIcon, TranslateModule],
|
||||
standalone: true,
|
||||
})
|
||||
export class FolderContextMenuComponent {
|
||||
private readonly _matDialog = inject(MatDialog);
|
||||
private readonly _projectFolderService = inject(ProjectFolderService);
|
||||
private readonly _projectService = inject(ProjectService);
|
||||
private readonly _translateService = inject(TranslateService);
|
||||
|
||||
@Input() folderId!: string;
|
||||
|
||||
readonly T = T;
|
||||
|
||||
async editFolder(): Promise<void> {
|
||||
const folder = await this._loadFolder(this.folderId);
|
||||
|
||||
|
|
@ -43,12 +48,16 @@ export class FolderContextMenuComponent {
|
|||
|
||||
if (!folder) return;
|
||||
|
||||
const message = this._translateService.instant(T.F.PROJECT_FOLDER.CONFIRM_DELETE, {
|
||||
title: folder.title,
|
||||
});
|
||||
|
||||
const isConfirmed = await new Promise<boolean>((resolve) => {
|
||||
this._matDialog
|
||||
.open(DialogConfirmComponent, {
|
||||
restoreFocus: true,
|
||||
data: {
|
||||
message: `Are you sure you want to delete the folder "${folder.title}"? All projects in this folder will be moved to the root level.`,
|
||||
message,
|
||||
},
|
||||
})
|
||||
.afterClosed()
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { toggleHideFromMenu } from '../../features/project/store/project.actions
|
|||
import { NavConfig, NavItem, NavWorkContextItem } from './magic-side-nav.model';
|
||||
import { TODAY_TAG } from '../../features/tag/tag.const';
|
||||
import { PluginBridgeService } from '../../plugins/plugin-bridge.service';
|
||||
import { Log } from '../../core/log';
|
||||
import { lsGetBoolean, lsSetItem } from '../../util/ls-util';
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -131,13 +130,13 @@ export class MagicNavConfigService {
|
|||
{
|
||||
id: 'project-visibility',
|
||||
icon: 'visibility',
|
||||
tooltip: 'Show/Hide Projects',
|
||||
tooltip: T.F.PROJECT_FOLDER.TOOLTIP_VISIBILITY,
|
||||
action: () => this._openProjectVisibilityMenu(),
|
||||
},
|
||||
{
|
||||
id: 'add-project-folder',
|
||||
icon: 'create_new_folder',
|
||||
tooltip: 'Create Project Folder',
|
||||
tooltip: T.F.PROJECT_FOLDER.TOOLTIP_CREATE,
|
||||
action: () => this._openCreateProjectFolder(),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -65,111 +65,112 @@
|
|||
</button>
|
||||
}
|
||||
|
||||
<!-- Navigation Items -->
|
||||
<ul
|
||||
class="nav-list"
|
||||
role="list"
|
||||
>
|
||||
@for (item of config().items; track item.id) {
|
||||
@switch (item.type) {
|
||||
@case ('separator') {
|
||||
<hr
|
||||
class="nav-separator"
|
||||
[style.margin-top]="item.mtAuto && 'auto'"
|
||||
/>
|
||||
}
|
||||
@case ('group') {
|
||||
<li
|
||||
class="nav-item has-children"
|
||||
role="listitem"
|
||||
>
|
||||
<nav-list-tree
|
||||
<!-- Navigation Items -->
|
||||
<ul
|
||||
class="nav-list"
|
||||
role="list"
|
||||
>
|
||||
@for (item of config().items; track item.id) {
|
||||
@switch (item.type) {
|
||||
@case ('separator') {
|
||||
<hr
|
||||
class="nav-separator"
|
||||
[style.margin-top]="item.mtAuto && 'auto'"
|
||||
/>
|
||||
}
|
||||
@case ('group') {
|
||||
<li
|
||||
class="nav-item has-children"
|
||||
role="listitem"
|
||||
>
|
||||
<nav-list-tree
|
||||
[item]="item"
|
||||
[showLabels]="showText()"
|
||||
[isExpanded]="isGroupExpanded(item)"
|
||||
[activeWorkContextId]="activeWorkContextId()"
|
||||
(itemClick)="onItemClick($event)"
|
||||
></nav-list-tree>
|
||||
</li>
|
||||
}
|
||||
@case ('menu') {
|
||||
<nav-mat-menu
|
||||
[item]="item"
|
||||
[showLabels]="showText()"
|
||||
[isExpanded]="isGroupExpanded(item)"
|
||||
[activeWorkContextId]="activeWorkContextId()"
|
||||
(itemClick)="onItemClick($event)"
|
||||
></nav-list-tree>
|
||||
</li>
|
||||
}
|
||||
@case ('menu') {
|
||||
<nav-mat-menu
|
||||
[item]="item"
|
||||
[showLabels]="showText()"
|
||||
(itemClick)="onItemClick($event)"
|
||||
></nav-mat-menu>
|
||||
}
|
||||
@default {
|
||||
<li
|
||||
class="nav-item"
|
||||
role="listitem"
|
||||
>
|
||||
@switch (item.type) {
|
||||
@case ('workContext') {
|
||||
<nav-item
|
||||
[workContext]="item.workContext"
|
||||
[type]="item.workContextType"
|
||||
[defaultIcon]="item.defaultIcon"
|
||||
[activeWorkContextId]="activeWorkContextId() || ''"
|
||||
[variant]="'nav'"
|
||||
[showLabels]="showText()"
|
||||
[showMoreButton]="showText()"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
></nav-mat-menu>
|
||||
}
|
||||
@default {
|
||||
<li
|
||||
class="nav-item"
|
||||
role="listitem"
|
||||
>
|
||||
@switch (item.type) {
|
||||
@case ('workContext') {
|
||||
<nav-item
|
||||
[workContext]="item.workContext"
|
||||
[type]="item.workContextType"
|
||||
[defaultIcon]="item.defaultIcon"
|
||||
[activeWorkContextId]="activeWorkContextId() || ''"
|
||||
[variant]="'nav'"
|
||||
[showLabels]="showText()"
|
||||
[showMoreButton]="showText()"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('route') {
|
||||
<nav-item
|
||||
[container]="'route'"
|
||||
[navRoute]="item.route"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('href') {
|
||||
<nav-item
|
||||
[container]="'href'"
|
||||
[navHref]="item.href"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('action') {
|
||||
<nav-item
|
||||
[container]="'action'"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('plugin') {
|
||||
<nav-item
|
||||
[container]="'action'"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
}
|
||||
@case ('route') {
|
||||
<nav-item
|
||||
[container]="'route'"
|
||||
[navRoute]="item.route"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('href') {
|
||||
<nav-item
|
||||
[container]="'href'"
|
||||
[navHref]="item.href"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('action') {
|
||||
<nav-item
|
||||
[container]="'action'"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('plugin') {
|
||||
<nav-item
|
||||
[container]="'action'"
|
||||
[icon]="item.icon"
|
||||
[svgIcon]="item.svgIcon"
|
||||
[label]="item.label"
|
||||
[showLabels]="showText()"
|
||||
[tourClass]="item.tourClass"
|
||||
(clicked)="onItemClick(item)"
|
||||
></nav-item>
|
||||
}
|
||||
}
|
||||
</li>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</nav>}
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
<!-- Resize Handle (moved outside scrolling container) -->
|
||||
@if (config().resizable && !isMobile()) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { MagicNavConfigService } from '../magic-nav-config.service';
|
|||
import { T } from '../../../t.const';
|
||||
import { WorkContextType } from '../../../features/work-context/work-context.model';
|
||||
import { ProjectFolderService } from '../../../features/project-folder/project-folder.service';
|
||||
import { ProjectFolder } from '../../../features/project-folder/store/project-folder.model';
|
||||
import { ProjectService } from '../../../features/project/project.service';
|
||||
import { TagService } from '../../../features/tag/tag.service';
|
||||
import { TODAY_TAG } from '../../../features/tag/tag.const';
|
||||
|
|
@ -187,21 +188,41 @@ export class NavListTreeComponent {
|
|||
|
||||
private _persistProjectFolderRelationships(
|
||||
folderProjectMap: Map<string, string[]>,
|
||||
rootProjectIds: string[],
|
||||
rootLayout: string[],
|
||||
folderParentMap: Map<string, string | null>,
|
||||
orderedFolderIds: string[],
|
||||
): void {
|
||||
const currentFolders = this._projectFolders();
|
||||
const nextFolderById = new Map<string, ProjectFolder>();
|
||||
|
||||
let didChange = false;
|
||||
const updatedFolders = currentFolders.map((folder) => {
|
||||
|
||||
currentFolders.forEach((folder) => {
|
||||
const nextProjectIds = folderProjectMap.get(folder.id) ?? [];
|
||||
if (!didChange && !this._areArraysEqual(folder.projectIds, nextProjectIds)) {
|
||||
didChange = true;
|
||||
const nextParentId = folderParentMap.has(folder.id)
|
||||
? (folderParentMap.get(folder.id) ?? null)
|
||||
: folder.parentId;
|
||||
if (!didChange) {
|
||||
didChange =
|
||||
!this._areArraysEqual(folder.projectIds, nextProjectIds) ||
|
||||
folder.parentId !== nextParentId;
|
||||
}
|
||||
return {
|
||||
nextFolderById.set(folder.id, {
|
||||
...folder,
|
||||
projectIds: nextProjectIds,
|
||||
};
|
||||
parentId: nextParentId,
|
||||
});
|
||||
});
|
||||
|
||||
const orderedFolders: ProjectFolder[] = orderedFolderIds
|
||||
.map((id) => nextFolderById.get(id))
|
||||
.filter((folder): folder is ProjectFolder => !!folder);
|
||||
|
||||
const included = new Set(orderedFolders.map((folder) => folder.id));
|
||||
currentFolders.forEach((folder) => {
|
||||
if (!included.has(folder.id)) {
|
||||
orderedFolders.push(folder);
|
||||
}
|
||||
});
|
||||
|
||||
const layoutChanged = !this._areArraysEqual(this._rootProjectIdsSig(), rootLayout);
|
||||
|
|
@ -211,7 +232,7 @@ export class NavListTreeComponent {
|
|||
}
|
||||
|
||||
this._projectFolderService.updateProjectFolderRelationships(
|
||||
updatedFolders,
|
||||
orderedFolders,
|
||||
rootLayout,
|
||||
);
|
||||
}
|
||||
|
|
@ -299,10 +320,20 @@ export class NavListTreeComponent {
|
|||
}
|
||||
|
||||
private _applyProjectTreeChanges(nodes: TreeNode<NavGroupItem>[]): void {
|
||||
const { folderProjectMap, rootProjectIds, orderedProjectIds, rootLayout } =
|
||||
this._collectProjectStructure(nodes);
|
||||
const {
|
||||
folderProjectMap,
|
||||
orderedProjectIds,
|
||||
rootLayout,
|
||||
folderParentMap,
|
||||
orderedFolderIds,
|
||||
} = this._collectProjectStructure(nodes);
|
||||
|
||||
this._persistProjectFolderRelationships(folderProjectMap, rootProjectIds, rootLayout);
|
||||
this._persistProjectFolderRelationships(
|
||||
folderProjectMap,
|
||||
rootLayout,
|
||||
folderParentMap,
|
||||
orderedFolderIds,
|
||||
);
|
||||
|
||||
const allProjects = this._navConfigService.allProjectsExceptInbox();
|
||||
const visibleProjects = allProjects.filter(
|
||||
|
|
@ -317,7 +348,9 @@ export class NavListTreeComponent {
|
|||
folderProjectMap.forEach((projectIds, folderId) => {
|
||||
projectIds.forEach((projectId) => desiredAssignment.set(projectId, folderId));
|
||||
});
|
||||
rootProjectIds.forEach((projectId) => desiredAssignment.set(projectId, null));
|
||||
rootLayout
|
||||
.filter((entry) => entry.startsWith('project:'))
|
||||
.forEach((entry) => desiredAssignment.set(entry.replace('project:', ''), null));
|
||||
|
||||
desiredAssignment.forEach((targetFolderId, projectId) => {
|
||||
const project = projectLookup.get(projectId);
|
||||
|
|
@ -373,14 +406,16 @@ export class NavListTreeComponent {
|
|||
|
||||
private _collectProjectStructure(nodes: TreeNode<NavGroupItem>[]): {
|
||||
folderProjectMap: Map<string, string[]>;
|
||||
rootProjectIds: string[];
|
||||
orderedProjectIds: string[];
|
||||
rootLayout: string[];
|
||||
folderParentMap: Map<string, string | null>;
|
||||
orderedFolderIds: string[];
|
||||
} {
|
||||
const folderProjectMap = new Map<string, string[]>();
|
||||
const rootProjectIds: string[] = [];
|
||||
const orderedProjectIds: string[] = [];
|
||||
const rootLayout: string[] = [];
|
||||
const folderParentMap = new Map<string, string | null>();
|
||||
const orderedFolderIds: string[] = [];
|
||||
|
||||
const visit = (
|
||||
node: TreeNode<NavGroupItem>,
|
||||
|
|
@ -388,6 +423,8 @@ export class NavListTreeComponent {
|
|||
level: number,
|
||||
): void => {
|
||||
if (node.isFolder) {
|
||||
folderParentMap.set(node.id, parentFolderId);
|
||||
orderedFolderIds.push(node.id);
|
||||
if (level === 0) {
|
||||
rootLayout.push(`folder:${node.id}`);
|
||||
}
|
||||
|
|
@ -412,7 +449,6 @@ export class NavListTreeComponent {
|
|||
if (projectId) {
|
||||
orderedProjectIds.push(projectId);
|
||||
if (parentFolderId === null) {
|
||||
rootProjectIds.push(projectId);
|
||||
rootLayout.push(`project:${projectId}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -420,7 +456,13 @@ export class NavListTreeComponent {
|
|||
|
||||
nodes.forEach((node) => visit(node, null, 0));
|
||||
|
||||
return { folderProjectMap, rootProjectIds, orderedProjectIds, rootLayout };
|
||||
return {
|
||||
folderProjectMap,
|
||||
orderedProjectIds,
|
||||
rootLayout,
|
||||
folderParentMap,
|
||||
orderedFolderIds,
|
||||
};
|
||||
}
|
||||
|
||||
private _extractProjectId(nodeId: string): string | null {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
<h2 mat-dialog-title>{{ isEdit ? 'Edit Folder' : 'Create New Folder' }}</h2>
|
||||
<h2 mat-dialog-title>
|
||||
{{
|
||||
(isEdit
|
||||
? T.F.PROJECT_FOLDER.DIALOG.EDIT_TITLE
|
||||
: T.F.PROJECT_FOLDER.DIALOG.CREATE_TITLE
|
||||
) | translate
|
||||
}}
|
||||
</h2>
|
||||
|
||||
<form
|
||||
[formGroup]="form"
|
||||
|
|
@ -9,15 +16,17 @@
|
|||
appearance="outline"
|
||||
class="full-width"
|
||||
>
|
||||
<mat-label>Folder Name</mat-label>
|
||||
<mat-label>{{ T.F.PROJECT_FOLDER.DIALOG.NAME_LABEL | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="title"
|
||||
placeholder="Enter folder name"
|
||||
[placeholder]="T.F.PROJECT_FOLDER.DIALOG.NAME_PLACEHOLDER | translate"
|
||||
required
|
||||
/>
|
||||
@if (form.get('title')?.errors?.['required']) {
|
||||
<mat-error>Folder name is required</mat-error>
|
||||
<mat-error>
|
||||
{{ T.F.PROJECT_FOLDER.DIALOG.NAME_REQUIRED | translate }}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
|
|
@ -25,9 +34,11 @@
|
|||
appearance="outline"
|
||||
class="full-width"
|
||||
>
|
||||
<mat-label>Parent Folder</mat-label>
|
||||
<mat-label>{{ T.F.PROJECT_FOLDER.DIALOG.PARENT_LABEL | translate }}</mat-label>
|
||||
<mat-select formControlName="parentId">
|
||||
<mat-option [value]="null">No parent (root level)</mat-option>
|
||||
<mat-option [value]="null">
|
||||
{{ T.F.PROJECT_FOLDER.DIALOG.NO_PARENT | translate }}
|
||||
</mat-option>
|
||||
@for (folder of availableFolders$ | async; track folder.id) {
|
||||
@if (!isEdit || folder.id !== data?.folder?.id) {
|
||||
<mat-option [value]="folder.id">{{ folder.title }}</mat-option>
|
||||
|
|
@ -43,7 +54,7 @@
|
|||
mat-button
|
||||
(click)="cancel()"
|
||||
>
|
||||
Cancel
|
||||
{{ T.G.CANCEL | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -51,7 +62,7 @@
|
|||
color="primary"
|
||||
[disabled]="form.invalid"
|
||||
>
|
||||
{{ isEdit ? 'Save' : 'Create' }}
|
||||
{{ (isEdit ? T.G.SAVE : T.G.ADD) | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { CommonModule } from '@angular/common';
|
|||
|
||||
import { ProjectFolder } from '../../store/project-folder.model';
|
||||
import { ProjectFolderService } from '../../project-folder.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { T } from '../../../../t.const';
|
||||
|
||||
export interface DialogCreateEditProjectFolderData {
|
||||
folder?: ProjectFolder;
|
||||
|
|
@ -27,6 +29,7 @@ export interface DialogCreateEditProjectFolderData {
|
|||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatButtonModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class DialogCreateEditProjectFolderComponent {
|
||||
|
|
@ -45,6 +48,8 @@ export class DialogCreateEditProjectFolderComponent {
|
|||
parentId: [null],
|
||||
});
|
||||
|
||||
readonly T = T;
|
||||
|
||||
constructor() {
|
||||
if (this.isEdit && this.data?.folder) {
|
||||
const folder = this.data.folder;
|
||||
|
|
|
|||
|
|
@ -5,17 +5,25 @@ import { ProjectFolderService } from '../../project-folder.service';
|
|||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { T } from '../../../../t.const';
|
||||
|
||||
@Component({
|
||||
selector: 'formly-field-project-folder-select',
|
||||
template: `
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ to.label }}</mat-label>
|
||||
<mat-label>
|
||||
{{ to.label || (T.F.PROJECT_FOLDER.SELECT.LABEL | translate) }}
|
||||
</mat-label>
|
||||
<mat-select
|
||||
[formControl]="formControl"
|
||||
[placeholder]="to.placeholder || 'Select folder'"
|
||||
[placeholder]="
|
||||
to.placeholder ?? (T.F.PROJECT_FOLDER.SELECT.PLACEHOLDER | translate)
|
||||
"
|
||||
>
|
||||
<mat-option [value]="null">No folder (root level)</mat-option>
|
||||
<mat-option [value]="null">
|
||||
{{ T.F.PROJECT_FOLDER.SELECT.NO_PARENT | translate }}
|
||||
</mat-option>
|
||||
@for (folder of projectFolderService.projectFolders$ | async; track folder.id) {
|
||||
<mat-option [value]="folder.id">{{ folder.title }}</mat-option>
|
||||
}
|
||||
|
|
@ -24,8 +32,15 @@ import { MatOptionModule } from '@angular/material/core';
|
|||
`,
|
||||
styleUrls: ['./project-folder-select.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, MatFormFieldModule, MatSelectModule, MatOptionModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class ProjectFolderSelectComponent extends FieldType<FieldTypeConfig> {
|
||||
readonly projectFolderService = inject(ProjectFolderService);
|
||||
readonly T = T;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,8 +65,14 @@ export class ProjectFolderService {
|
|||
.pipe(take(1), withLatestFrom(this.rootProjectIds$))
|
||||
.subscribe(([folders, rootProjectIds]) => {
|
||||
const updatedFolders = folders.filter((folder) => folder.id !== id);
|
||||
const updatedRootLayout = rootProjectIds.filter(
|
||||
(entry) => entry !== `folder:${id}`,
|
||||
);
|
||||
this._store.dispatch(
|
||||
updateProjectFolders({ projectFolders: updatedFolders, rootProjectIds }),
|
||||
updateProjectFolders({
|
||||
projectFolders: updatedFolders,
|
||||
rootProjectIds: updatedRootLayout,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -75,8 +81,25 @@ export class ProjectFolderService {
|
|||
this.projectFolders$
|
||||
.pipe(take(1), withLatestFrom(this.rootProjectIds$))
|
||||
.subscribe(([folders, rootProjectIds]) => {
|
||||
const folderMap = Object.fromEntries(folders.map((f) => [f.id, f]));
|
||||
const reorderedFolders = newIds.map((id) => folderMap[id]).filter(Boolean);
|
||||
const folderMap = new Map(folders.map((f) => [f.id, f] as const));
|
||||
const seen = new Set<string>();
|
||||
const reorderedFolders: ProjectFolder[] = [];
|
||||
|
||||
newIds.forEach((id) => {
|
||||
const folder = folderMap.get(id);
|
||||
if (folder && !seen.has(id)) {
|
||||
reorderedFolders.push(folder);
|
||||
seen.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (!seen.has(folder.id)) {
|
||||
reorderedFolders.push(folder);
|
||||
seen.add(folder.id);
|
||||
}
|
||||
});
|
||||
|
||||
this._store.dispatch(
|
||||
updateProjectFolders({ projectFolders: reorderedFolders, rootProjectIds }),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import { Injectable, inject } from '@angular/core';
|
||||
import { Actions } from '@ngrx/effects';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectFolderEffects {
|
||||
private readonly actions$ = inject(Actions);
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ import {
|
|||
projectFolderFeatureKey,
|
||||
projectFolderReducer,
|
||||
} from '../features/project-folder/store/project-folder.reducer';
|
||||
import { ProjectFolderEffects } from '../features/project-folder/store/project-folder.effects';
|
||||
import {
|
||||
SIMPLE_COUNTER_FEATURE_NAME,
|
||||
simpleCounterReducer,
|
||||
|
|
@ -144,7 +143,6 @@ import { PluginHooksEffects } from '../plugins/plugin-hooks.effects';
|
|||
EffectsModule.forFeature([ProjectEffects]),
|
||||
|
||||
StoreModule.forFeature(projectFolderFeatureKey, projectFolderReducer),
|
||||
EffectsModule.forFeature([ProjectFolderEffects]),
|
||||
|
||||
StoreModule.forFeature(SIMPLE_COUNTER_FEATURE_NAME, simpleCounterReducer),
|
||||
EffectsModule.forFeature([SimpleCounterEffects]),
|
||||
|
|
|
|||
|
|
@ -860,6 +860,25 @@ const T = {
|
|||
UPDATED: 'F.PROJECT.S.UPDATED',
|
||||
},
|
||||
},
|
||||
PROJECT_FOLDER: {
|
||||
DIALOG: {
|
||||
CREATE_TITLE: 'F.PROJECT_FOLDER.DIALOG.CREATE_TITLE',
|
||||
EDIT_TITLE: 'F.PROJECT_FOLDER.DIALOG.EDIT_TITLE',
|
||||
NAME_LABEL: 'F.PROJECT_FOLDER.DIALOG.NAME_LABEL',
|
||||
NAME_PLACEHOLDER: 'F.PROJECT_FOLDER.DIALOG.NAME_PLACEHOLDER',
|
||||
NAME_REQUIRED: 'F.PROJECT_FOLDER.DIALOG.NAME_REQUIRED',
|
||||
PARENT_LABEL: 'F.PROJECT_FOLDER.DIALOG.PARENT_LABEL',
|
||||
NO_PARENT: 'F.PROJECT_FOLDER.DIALOG.NO_PARENT',
|
||||
},
|
||||
SELECT: {
|
||||
LABEL: 'F.PROJECT_FOLDER.SELECT.LABEL',
|
||||
PLACEHOLDER: 'F.PROJECT_FOLDER.SELECT.PLACEHOLDER',
|
||||
NO_PARENT: 'F.PROJECT_FOLDER.SELECT.NO_PARENT',
|
||||
},
|
||||
CONFIRM_DELETE: 'F.PROJECT_FOLDER.CONFIRM_DELETE',
|
||||
TOOLTIP_CREATE: 'F.PROJECT_FOLDER.TOOLTIP_CREATE',
|
||||
TOOLTIP_VISIBILITY: 'F.PROJECT_FOLDER.TOOLTIP_VISIBILITY',
|
||||
},
|
||||
QUICK_HISTORY: {
|
||||
NO_DATA: 'F.QUICK_HISTORY.NO_DATA',
|
||||
PAGE_TITLE: 'F.QUICK_HISTORY.PAGE_TITLE',
|
||||
|
|
|
|||
|
|
@ -336,6 +336,9 @@ export class TreeDndComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
private flashJustDropped(id: string): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
this.justDroppedId.set(id);
|
||||
if (this._dropFlashTimer) clearTimeout(this._dropFlashTimer);
|
||||
this._dropFlashTimer = window.setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -812,6 +812,25 @@
|
|||
"UPDATED": "Projekteinstellungen aktualisiert"
|
||||
}
|
||||
},
|
||||
"PROJECT_FOLDER": {
|
||||
"DIALOG": {
|
||||
"CREATE_TITLE": "Ordner erstellen",
|
||||
"EDIT_TITLE": "Ordner bearbeiten",
|
||||
"NAME_LABEL": "Ordnername",
|
||||
"NAME_PLACEHOLDER": "Ordnernamen eingeben",
|
||||
"NAME_REQUIRED": "Ordnername ist erforderlich",
|
||||
"PARENT_LABEL": "Übergeordneter Ordner",
|
||||
"NO_PARENT": "Kein übergeordneter Ordner (Root-Ebene)"
|
||||
},
|
||||
"SELECT": {
|
||||
"LABEL": "Ordner",
|
||||
"PLACEHOLDER": "Ordner wählen",
|
||||
"NO_PARENT": "Kein Ordner (Root-Ebene)"
|
||||
},
|
||||
"CONFIRM_DELETE": "Möchtest du den Ordner \"{{title}}\" wirklich löschen? Alle Projekte in diesem Ordner werden auf die oberste Ebene verschoben.",
|
||||
"TOOLTIP_CREATE": "Projektordner erstellen",
|
||||
"TOOLTIP_VISIBILITY": "Projekte anzeigen/ausblenden"
|
||||
},
|
||||
"QUICK_HISTORY": {
|
||||
"NO_DATA": "Keine Daten für das laufende Jahr",
|
||||
"PAGE_TITLE": "Schnellverlauf",
|
||||
|
|
|
|||
|
|
@ -846,6 +846,25 @@
|
|||
"UPDATED": "Updated project settings"
|
||||
}
|
||||
},
|
||||
"PROJECT_FOLDER": {
|
||||
"DIALOG": {
|
||||
"CREATE_TITLE": "Create folder",
|
||||
"EDIT_TITLE": "Edit folder",
|
||||
"NAME_LABEL": "Folder name",
|
||||
"NAME_PLACEHOLDER": "Enter folder name",
|
||||
"NAME_REQUIRED": "Folder name is required",
|
||||
"PARENT_LABEL": "Parent folder",
|
||||
"NO_PARENT": "No parent (root level)"
|
||||
},
|
||||
"SELECT": {
|
||||
"LABEL": "Folder",
|
||||
"PLACEHOLDER": "Select folder",
|
||||
"NO_PARENT": "No folder (root level)"
|
||||
},
|
||||
"CONFIRM_DELETE": "Are you sure you want to delete the folder \"{{title}}\"? All projects in this folder will be moved to the root level.",
|
||||
"TOOLTIP_CREATE": "Create project folder",
|
||||
"TOOLTIP_VISIBILITY": "Show/hide projects"
|
||||
},
|
||||
"QUICK_HISTORY": {
|
||||
"NO_DATA": "No data for current year",
|
||||
"PAGE_TITLE": "Quick History",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue