From 145c11394b7a70a5499e34bfb47fb23a20d499b5 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Fri, 6 Feb 2026 20:56:03 -0800
Subject: [PATCH] Ok lets just merge it all together
---
src-ui/src/app/app-routing.module.ts | 105 ++++----
src-ui/src/app/app.component.ts | 4 +-
.../app-frame/app-frame.component.html | 47 +---
.../app-frame/app-frame.component.ts | 25 ++
.../custom-fields.component.html | 12 -
.../custom-fields.component.spec.ts | 5 +-
.../custom-fields/custom-fields.component.ts | 2 -
.../document-attributes.component.html | 117 +++++++++
.../document-attributes.component.scss | 0
.../document-attributes.component.ts | 237 ++++++++++++++++++
.../management-list.component.html | 47 ----
.../src/app/guards/permissions.guard.spec.ts | 48 ++++
src-ui/src/app/guards/permissions.guard.ts | 8 +
13 files changed, 506 insertions(+), 151 deletions(-)
create mode 100644 src-ui/src/app/components/manage/document-attributes/document-attributes.component.html
create mode 100644 src-ui/src/app/components/manage/document-attributes/document-attributes.component.scss
create mode 100644 src-ui/src/app/components/manage/document-attributes/document-attributes.component.ts
diff --git a/src-ui/src/app/app-routing.module.ts b/src-ui/src/app/app-routing.module.ts
index f65514f74..66864a0d5 100644
--- a/src-ui/src/app/app-routing.module.ts
+++ b/src-ui/src/app/app-routing.module.ts
@@ -11,13 +11,9 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
-import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
-import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
-import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
+import { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
-import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
-import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { DirtyDocGuard } from './guards/dirty-doc.guard'
@@ -106,52 +102,76 @@ export const routes: Routes = [
},
},
{
- path: 'tags',
- component: TagListComponent,
+ path: 'attributes',
+ component: DocumentAttributesComponent,
canActivate: [PermissionsGuard],
data: {
- requiredPermission: {
- action: PermissionAction.View,
- type: PermissionType.Tag,
- },
- componentName: 'TagListComponent',
+ requiredPermissionAny: [
+ { action: PermissionAction.View, type: PermissionType.Tag },
+ {
+ action: PermissionAction.View,
+ type: PermissionType.Correspondent,
+ },
+ {
+ action: PermissionAction.View,
+ type: PermissionType.DocumentType,
+ },
+ { action: PermissionAction.View, type: PermissionType.StoragePath },
+ { action: PermissionAction.View, type: PermissionType.CustomField },
+ ],
+ componentName: 'DocumentAttributesComponent',
},
},
{
- path: 'documenttypes',
- component: DocumentTypeListComponent,
+ path: 'attributes/:section',
+ component: DocumentAttributesComponent,
canActivate: [PermissionsGuard],
data: {
- requiredPermission: {
- action: PermissionAction.View,
- type: PermissionType.DocumentType,
- },
- componentName: 'DocumentTypeListComponent',
+ requiredPermissionAny: [
+ { action: PermissionAction.View, type: PermissionType.Tag },
+ {
+ action: PermissionAction.View,
+ type: PermissionType.Correspondent,
+ },
+ {
+ action: PermissionAction.View,
+ type: PermissionType.DocumentType,
+ },
+ { action: PermissionAction.View, type: PermissionType.StoragePath },
+ { action: PermissionAction.View, type: PermissionType.CustomField },
+ ],
+ componentName: 'DocumentAttributesComponent',
},
},
+ {
+ path: 'documentproperties',
+ redirectTo: '/attributes',
+ pathMatch: 'full',
+ },
+ {
+ path: 'documentproperties/:section',
+ redirectTo: '/attributes/:section',
+ pathMatch: 'full',
+ },
+ {
+ path: 'tags',
+ redirectTo: '/attributes/tags',
+ pathMatch: 'full',
+ },
{
path: 'correspondents',
- component: CorrespondentListComponent,
- canActivate: [PermissionsGuard],
- data: {
- requiredPermission: {
- action: PermissionAction.View,
- type: PermissionType.Correspondent,
- },
- componentName: 'CorrespondentListComponent',
- },
+ redirectTo: '/attributes/correspondents',
+ pathMatch: 'full',
+ },
+ {
+ path: 'documenttypes',
+ redirectTo: '/attributes/documenttypes',
+ pathMatch: 'full',
},
{
path: 'storagepaths',
- component: StoragePathListComponent,
- canActivate: [PermissionsGuard],
- data: {
- requiredPermission: {
- action: PermissionAction.View,
- type: PermissionType.StoragePath,
- },
- componentName: 'StoragePathListComponent',
- },
+ redirectTo: '/attributes/storagepaths',
+ pathMatch: 'full',
},
{
path: 'logs',
@@ -239,15 +259,8 @@ export const routes: Routes = [
},
{
path: 'customfields',
- component: CustomFieldsComponent,
- canActivate: [PermissionsGuard],
- data: {
- requiredPermission: {
- action: PermissionAction.View,
- type: PermissionType.CustomField,
- },
- componentName: 'CustomFieldsComponent',
- },
+ redirectTo: '/attributes/customfields',
+ pathMatch: 'full',
},
{
path: 'workflows',
diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts
index 818f8eab0..543d2ecaa 100644
--- a/src-ui/src/app/app.component.ts
+++ b/src-ui/src/app/app.component.ts
@@ -195,8 +195,8 @@ export class AppComponent implements OnInit, OnDestroy {
},
{
anchorId: 'tour.tags',
- content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
- route: '/tags',
+ content: $localize`Attributes like tags, correspondents, document types, storage paths and custom fields can all be managed here. They can also be created from the document edit view.`,
+ route: '/attributes/tags',
backdropConfig: {
offset: 0,
},
diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html
index 62a2e16cc..db0e12607 100644
--- a/src-ui/src/app/components/app-frame/app-frame.component.html
+++ b/src-ui/src/app/components/app-frame/app-frame.component.html
@@ -175,44 +175,15 @@
Manage
- -
-
- Correspondents
-
-
- -
-
- Tags
-
-
- -
-
- Document Types
-
-
- -
-
- Storage Paths
-
-
- -
-
- Custom Fields
-
-
+ @if (canManageDocumentProperties) {
+ -
+
+ Attributes
+
+
+ }
-
-
-
-
-
diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts
index b86d476f3..3e86b841e 100644
--- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts
+++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts
@@ -110,10 +110,7 @@ describe('CustomFieldsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
- const createButton = fixture.debugElement
- .queryAll(By.css('button'))
- .find((btn) => btn.nativeElement.textContent.trim().includes('Add Field'))
- createButton.triggerEventHandler('click')
+ component.editField()
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts
index 8ecd713ef..5d6d5cf1c 100644
--- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts
+++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts
@@ -24,7 +24,6 @@ import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
-import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
@@ -32,7 +31,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
templateUrl: './custom-fields.component.html',
styleUrls: ['./custom-fields.component.scss'],
imports: [
- PageHeaderComponent,
IfPermissionsDirective,
NgbDropdownModule,
NgbPaginationModule,
diff --git a/src-ui/src/app/components/manage/document-attributes/document-attributes.component.html b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.html
new file mode 100644
index 000000000..b214e8c2c
--- /dev/null
+++ b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.html
@@ -0,0 +1,117 @@
+
+ @if (activeBulkList) {
+
+
+
+
+
+
+
+
+
+
+
+ Select:
+
+
+ @if (activeBulkList.selectedObjects.size > 0) {
+
+ }
+
+
+
+
+
+
+
+
+ } @else if (activeCustomFields) {
+
+ }
+
+
+
+
+
diff --git a/src-ui/src/app/components/manage/document-attributes/document-attributes.component.scss b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src-ui/src/app/components/manage/document-attributes/document-attributes.component.ts b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.ts
new file mode 100644
index 000000000..4951de0ad
--- /dev/null
+++ b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.ts
@@ -0,0 +1,237 @@
+import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import {
+ NgbDropdownModule,
+ NgbNavChangeEvent,
+ NgbNavModule,
+} from '@ng-bootstrap/ng-bootstrap'
+import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
+import { Subject, takeUntil } from 'rxjs'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import {
+ PermissionAction,
+ PermissionsService,
+ PermissionType,
+} from 'src/app/services/permissions.service'
+import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { CorrespondentListComponent } from '../correspondent-list/correspondent-list.component'
+import { CustomFieldsComponent } from '../custom-fields/custom-fields.component'
+import { DocumentTypeListComponent } from '../document-type-list/document-type-list.component'
+import { ManagementListComponent } from '../management-list/management-list.component'
+import { StoragePathListComponent } from '../storage-path-list/storage-path-list.component'
+import { TagListComponent } from '../tag-list/tag-list.component'
+
+enum DocumentAttributesNavIDs {
+ Tags = 1,
+ Correspondents = 2,
+ DocumentTypes = 3,
+ StoragePaths = 4,
+ CustomFields = 5,
+}
+
+@Component({
+ selector: 'pngx-document-attributes',
+ templateUrl: './document-attributes.component.html',
+ styleUrls: ['./document-attributes.component.scss'],
+ imports: [
+ PageHeaderComponent,
+ NgbNavModule,
+ NgbDropdownModule,
+ NgxBootstrapIconsModule,
+ IfPermissionsDirective,
+ ClearableBadgeComponent,
+ TagListComponent,
+ CorrespondentListComponent,
+ DocumentTypeListComponent,
+ StoragePathListComponent,
+ CustomFieldsComponent,
+ ],
+})
+export class DocumentAttributesComponent implements OnInit, OnDestroy {
+ private readonly permissionsService = inject(PermissionsService)
+ private readonly activatedRoute = inject(ActivatedRoute)
+ private readonly router = inject(Router)
+ private readonly unsubscribeNotifier = new Subject()
+
+ protected readonly DocumentAttributesNavIDs = DocumentAttributesNavIDs
+ protected readonly PermissionAction = PermissionAction
+ protected readonly PermissionType = PermissionType
+
+ @ViewChild(TagListComponent) private tagList?: TagListComponent
+ @ViewChild(CorrespondentListComponent)
+ private correspondentList?: CorrespondentListComponent
+ @ViewChild(DocumentTypeListComponent)
+ private documentTypeList?: DocumentTypeListComponent
+ @ViewChild(StoragePathListComponent)
+ private storagePathList?: StoragePathListComponent
+ @ViewChild(CustomFieldsComponent)
+ private customFields?: CustomFieldsComponent
+
+ activeNavID: number = null
+
+ get activeBulkList(): ManagementListComponent | null {
+ switch (this.activeNavID) {
+ case DocumentAttributesNavIDs.Tags:
+ return this.tagList ?? null
+ case DocumentAttributesNavIDs.Correspondents:
+ return this.correspondentList ?? null
+ case DocumentAttributesNavIDs.DocumentTypes:
+ return this.documentTypeList ?? null
+ case DocumentAttributesNavIDs.StoragePaths:
+ return this.storagePathList ?? null
+ default:
+ return null
+ }
+ }
+
+ get activeCustomFields(): CustomFieldsComponent | null {
+ return this.activeNavID === DocumentAttributesNavIDs.CustomFields
+ ? (this.customFields ?? null)
+ : null
+ }
+
+ get activeTabLabel(): string {
+ switch (this.activeNavID) {
+ case DocumentAttributesNavIDs.Tags:
+ return $localize`Tags`
+ case DocumentAttributesNavIDs.Correspondents:
+ return $localize`Correspondents`
+ case DocumentAttributesNavIDs.DocumentTypes:
+ return $localize`Document types`
+ case DocumentAttributesNavIDs.StoragePaths:
+ return $localize`Storage paths`
+ case DocumentAttributesNavIDs.CustomFields:
+ return $localize`Custom fields`
+ default:
+ return ''
+ }
+ }
+
+ get activeHeaderLoading(): boolean {
+ return (
+ this.activeBulkList?.loading ?? this.activeCustomFields?.loading ?? false
+ )
+ }
+
+ get canViewTags(): boolean {
+ return this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.Tag
+ )
+ }
+
+ get canViewCorrespondents(): boolean {
+ return this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.Correspondent
+ )
+ }
+
+ get canViewDocumentTypes(): boolean {
+ return this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.DocumentType
+ )
+ }
+
+ get canViewStoragePaths(): boolean {
+ return this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.StoragePath
+ )
+ }
+
+ get canViewCustomFields(): boolean {
+ return this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.CustomField
+ )
+ }
+
+ ngOnInit(): void {
+ this.activatedRoute.paramMap
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe((paramMap) => {
+ const section = paramMap.get('section')
+ const navIDFromSection =
+ this.getNavIDForSection(section) ?? this.getDefaultNavID()
+
+ if (navIDFromSection == null) {
+ this.router.navigate(['/dashboard'], { replaceUrl: true })
+ return
+ }
+
+ if (this.activeNavID !== navIDFromSection) {
+ this.activeNavID = navIDFromSection
+ }
+
+ if (!section || this.getNavIDForSection(section) == null) {
+ this.router.navigate(
+ ['attributes', this.getSectionForNavID(this.activeNavID)],
+ { replaceUrl: true }
+ )
+ }
+ })
+ }
+
+ ngOnDestroy(): void {
+ this.unsubscribeNotifier.next()
+ this.unsubscribeNotifier.complete()
+ }
+
+ onNavChange(navChangeEvent: NgbNavChangeEvent): void {
+ const nextSection = this.getSectionForNavID(navChangeEvent.nextId)
+ if (!nextSection) {
+ return
+ }
+ this.router.navigate(['attributes', nextSection])
+ }
+
+ private getDefaultNavID(): DocumentAttributesNavIDs | null {
+ if (this.canViewTags) return DocumentAttributesNavIDs.Tags
+ if (this.canViewCorrespondents)
+ return DocumentAttributesNavIDs.Correspondents
+ if (this.canViewDocumentTypes) return DocumentAttributesNavIDs.DocumentTypes
+ if (this.canViewStoragePaths) return DocumentAttributesNavIDs.StoragePaths
+ if (this.canViewCustomFields) return DocumentAttributesNavIDs.CustomFields
+ return null
+ }
+
+ private getNavIDForSection(section: string): DocumentAttributesNavIDs | null {
+ if (!section) return null
+ const navIDKey: string = Object.keys(DocumentAttributesNavIDs).find(
+ (navID) => navID.toLowerCase() === section.toLowerCase()
+ )
+ if (!navIDKey) return null
+
+ const navID = DocumentAttributesNavIDs[navIDKey]
+ if (!this.isNavIDAllowed(navID)) return null
+ return navID
+ }
+
+ private getSectionForNavID(navID: number): string | null {
+ if (!this.isNavIDAllowed(navID)) return null
+ const [foundNavIDKey] = Object.entries(DocumentAttributesNavIDs).find(
+ ([, navIDValue]) => navIDValue === navID
+ )
+ return foundNavIDKey?.toLowerCase() ?? null
+ }
+
+ private isNavIDAllowed(navID: number): boolean {
+ switch (navID) {
+ case DocumentAttributesNavIDs.Tags:
+ return this.canViewTags
+ case DocumentAttributesNavIDs.Correspondents:
+ return this.canViewCorrespondents
+ case DocumentAttributesNavIDs.DocumentTypes:
+ return this.canViewDocumentTypes
+ case DocumentAttributesNavIDs.StoragePaths:
+ return this.canViewStoragePaths
+ case DocumentAttributesNavIDs.CustomFields:
+ return this.canViewCustomFields
+ default:
+ return false
+ }
+ }
+}
diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html
index cb2f1010f..c35366736 100644
--- a/src-ui/src/app/components/manage/management-list/management-list.component.html
+++ b/src-ui/src/app/components/manage/management-list/management-list.component.html
@@ -1,50 +1,3 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- Select:
-
-
- @if (selectedObjects.size > 0) {
-
- }
-
-
-
-
-
-
-
-
-
-
diff --git a/src-ui/src/app/guards/permissions.guard.spec.ts b/src-ui/src/app/guards/permissions.guard.spec.ts
index c10232f2b..69191e1ad 100644
--- a/src-ui/src/app/guards/permissions.guard.spec.ts
+++ b/src-ui/src/app/guards/permissions.guard.spec.ts
@@ -96,4 +96,52 @@ describe('PermissionsGuard', () => {
expect(canActivate).toHaveProperty('root') // returns UrlTree
expect(toastSpy).toHaveBeenCalled()
})
+
+ it('should activate when any required permission is granted', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserCan')
+ .mockImplementation((action, type) => {
+ return type === PermissionType.Tag
+ })
+
+ const canActivate = guard.canActivate(
+ {
+ data: {
+ requiredPermissionAny: [
+ { action: PermissionAction.View, type: PermissionType.Tag },
+ {
+ action: PermissionAction.View,
+ type: PermissionType.DocumentType,
+ },
+ ],
+ },
+ } as any,
+ routerState.snapshot
+ )
+
+ expect(canActivate).toBeTruthy()
+ })
+
+ it('should not activate when no required permission is granted', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserCan')
+ .mockImplementation(() => false)
+
+ const canActivate = guard.canActivate(
+ {
+ data: {
+ requiredPermissionAny: [
+ { action: PermissionAction.View, type: PermissionType.Tag },
+ {
+ action: PermissionAction.View,
+ type: PermissionType.DocumentType,
+ },
+ ],
+ },
+ } as any,
+ routerState.snapshot
+ )
+
+ expect(canActivate).toHaveProperty('root')
+ })
})
diff --git a/src-ui/src/app/guards/permissions.guard.ts b/src-ui/src/app/guards/permissions.guard.ts
index ddac8e035..01d5e63cb 100644
--- a/src-ui/src/app/guards/permissions.guard.ts
+++ b/src-ui/src/app/guards/permissions.guard.ts
@@ -20,12 +20,20 @@ export class PermissionsGuard {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | UrlTree {
+ const requiredPermissionAny: { action: any; type: any }[] =
+ route.data.requiredPermissionAny
+
if (
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
(route.data.requiredPermission &&
!this.permissionsService.currentUserCan(
route.data.requiredPermission.action,
route.data.requiredPermission.type
+ )) ||
+ (Array.isArray(requiredPermissionAny) &&
+ requiredPermissionAny.length > 0 &&
+ !requiredPermissionAny.some((p) =>
+ this.permissionsService.currentUserCan(p.action, p.type)
))
) {
// Check if tour is running 1 = TourState.ON