mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-07 23:42:46 -06:00
Ok lets just merge it all together
This commit is contained in:
@@ -11,13 +11,9 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
|
|||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DocumentListComponent } from './components/document-list/document-list.component'
|
import { DocumentListComponent } from './components/document-list/document-list.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
import { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component'
|
||||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
|
||||||
import { MailComponent } from './components/manage/mail/mail.component'
|
import { MailComponent } from './components/manage/mail/mail.component'
|
||||||
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.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 { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
@@ -106,52 +102,76 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tags',
|
path: 'attributes',
|
||||||
component: TagListComponent,
|
component: DocumentAttributesComponent,
|
||||||
canActivate: [PermissionsGuard],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
requiredPermission: {
|
requiredPermissionAny: [
|
||||||
action: PermissionAction.View,
|
{ action: PermissionAction.View, type: PermissionType.Tag },
|
||||||
type: PermissionType.Tag,
|
|
||||||
},
|
|
||||||
componentName: 'TagListComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'documenttypes',
|
|
||||||
component: DocumentTypeListComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
componentName: 'DocumentTypeListComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'correspondents',
|
|
||||||
component: CorrespondentListComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
action: PermissionAction.View,
|
||||||
type: PermissionType.Correspondent,
|
type: PermissionType.Correspondent,
|
||||||
},
|
},
|
||||||
componentName: 'CorrespondentListComponent',
|
{
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
||||||
|
],
|
||||||
|
componentName: 'DocumentAttributesComponent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'storagepaths',
|
path: 'attributes/:section',
|
||||||
component: StoragePathListComponent,
|
component: DocumentAttributesComponent,
|
||||||
canActivate: [PermissionsGuard],
|
canActivate: [PermissionsGuard],
|
||||||
data: {
|
data: {
|
||||||
requiredPermission: {
|
requiredPermissionAny: [
|
||||||
|
{ action: PermissionAction.View, type: PermissionType.Tag },
|
||||||
|
{
|
||||||
action: PermissionAction.View,
|
action: PermissionAction.View,
|
||||||
type: PermissionType.StoragePath,
|
type: PermissionType.Correspondent,
|
||||||
},
|
},
|
||||||
componentName: 'StoragePathListComponent',
|
{
|
||||||
|
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',
|
||||||
|
redirectTo: '/attributes/correspondents',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documenttypes',
|
||||||
|
redirectTo: '/attributes/documenttypes',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'storagepaths',
|
||||||
|
redirectTo: '/attributes/storagepaths',
|
||||||
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
@@ -239,15 +259,8 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'customfields',
|
path: 'customfields',
|
||||||
component: CustomFieldsComponent,
|
redirectTo: '/attributes/customfields',
|
||||||
canActivate: [PermissionsGuard],
|
pathMatch: 'full',
|
||||||
data: {
|
|
||||||
requiredPermission: {
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.CustomField,
|
|
||||||
},
|
|
||||||
componentName: 'CustomFieldsComponent',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflows',
|
path: 'workflows',
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchorId: 'tour.tags',
|
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.`,
|
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: '/tags',
|
route: '/attributes/tags',
|
||||||
backdropConfig: {
|
backdropConfig: {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -175,44 +175,15 @@
|
|||||||
<span i18n>Manage</span>
|
<span i18n>Manage</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
<li class="nav-item app-link"
|
@if (canManageDocumentProperties) {
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
<li class="nav-item app-link" tourAnchor="tour.tags">
|
||||||
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
<i-bs class="me-1" name=""></i-bs><span> <ng-container i18n>Attributes</ng-container></span>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
|
||||||
tourAnchor="tour.tags">
|
|
||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link"
|
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
|
||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
|
||||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
|
||||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
|
||||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
|
||||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
|
||||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom Fields</ng-container></span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||||
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
|
|||||||
@@ -167,6 +167,31 @@ export class AppFrameComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canManageDocumentProperties(): boolean {
|
||||||
|
return (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Tag
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Correspondent
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.DocumentType
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.StoragePath
|
||||||
|
) ||
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.CustomField
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
<pngx-page-header
|
|
||||||
title="Custom Fields"
|
|
||||||
i18n-title
|
|
||||||
info="Customize the data fields that can be attached to documents."
|
|
||||||
i18n-info
|
|
||||||
infoLink="usage/#custom-fields"
|
|
||||||
>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
|
||||||
</button>
|
|
||||||
</pngx-page-header>
|
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
|
|||||||
@@ -110,10 +110,7 @@ describe('CustomFieldsComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reload')
|
const reloadSpy = jest.spyOn(component, 'reload')
|
||||||
|
|
||||||
const createButton = fixture.debugElement
|
component.editField()
|
||||||
.queryAll(By.css('button'))
|
|
||||||
.find((btn) => btn.nativeElement.textContent.trim().includes('Add Field'))
|
|
||||||
createButton.triggerEventHandler('click')
|
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
|
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
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 { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/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'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -32,7 +31,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
templateUrl: './custom-fields.component.html',
|
templateUrl: './custom-fields.component.html',
|
||||||
styleUrls: ['./custom-fields.component.scss'],
|
styleUrls: ['./custom-fields.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
PageHeaderComponent,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<pngx-page-header
|
||||||
|
title="Attributes"
|
||||||
|
i18n-title
|
||||||
|
info="Manage tags, correspondents, document types, storage paths, and custom fields."
|
||||||
|
i18n-info
|
||||||
|
[subTitle]="activeTabLabel"
|
||||||
|
[loading]="activeHeaderLoading"
|
||||||
|
>
|
||||||
|
@if (activeBulkList) {
|
||||||
|
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
||||||
|
<i-bs name="text-indent-left"></i-bs>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||||
|
@if (activeBulkList.selectedObjects.size > 0) {
|
||||||
|
<pngx-clearable-badge [selected]="activeBulkList.selectedObjects.size > 0" [number]="activeBulkList.selectedObjects.size" (cleared)="activeBulkList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
||||||
|
<button ngbDropdownItem (click)="activeBulkList.selectNone()" i18n>Select none</button>
|
||||||
|
<button ngbDropdownItem (click)="activeBulkList.selectPage(true)" i18n>Select page</button>
|
||||||
|
<button ngbDropdownItem (click)="activeBulkList.selectAll()" i18n>Select all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none d-sm-flex flex-fill me-3">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text border-0" i18n>Select:</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm flex-nowrap">
|
||||||
|
@if (activeBulkList.selectedObjects.size > 0) {
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="activeBulkList.selectNone()">
|
||||||
|
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="activeBulkList.selectPage(true)">
|
||||||
|
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="activeBulkList.selectAll()">
|
||||||
|
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeBulkList.setPermissions()"
|
||||||
|
[disabled]="!activeBulkList.userCanBulkEdit(PermissionAction.Change) || activeBulkList.selectedObjects.size === 0">
|
||||||
|
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="activeBulkList.delete()"
|
||||||
|
[disabled]="!activeBulkList.userCanBulkEdit(PermissionAction.Delete) || activeBulkList.selectedObjects.size === 0">
|
||||||
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="activeBulkList.openCreateDialog()"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.Add, type: activeBulkList.permissionType }">
|
||||||
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
||||||
|
</button>
|
||||||
|
} @else if (activeCustomFields) {
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="activeCustomFields.editField()"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
||||||
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</pngx-page-header>
|
||||||
|
|
||||||
|
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs">
|
||||||
|
@if (canViewTags) {
|
||||||
|
<li [ngbNavItem]="DocumentAttributesNavIDs.Tags">
|
||||||
|
<a ngbNavLink>
|
||||||
|
<ng-container i18n>Tags</ng-container>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<pngx-tag-list></pngx-tag-list>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (canViewCorrespondents) {
|
||||||
|
<li [ngbNavItem]="DocumentAttributesNavIDs.Correspondents">
|
||||||
|
<a ngbNavLink>
|
||||||
|
</i-bs><ng-container i18n>Correspondents</ng-container>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<pngx-correspondent-list></pngx-correspondent-list>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (canViewDocumentTypes) {
|
||||||
|
<li [ngbNavItem]="DocumentAttributesNavIDs.DocumentTypes">
|
||||||
|
<a ngbNavLink>
|
||||||
|
</i-bs><ng-container i18n>Document types</ng-container>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<pngx-document-type-list></pngx-document-type-list>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (canViewStoragePaths) {
|
||||||
|
<li [ngbNavItem]="DocumentAttributesNavIDs.StoragePaths">
|
||||||
|
<a ngbNavLink>
|
||||||
|
</i-bs><ng-container i18n>Storage paths</ng-container>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<pngx-storage-path-list></pngx-storage-path-list>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (canViewCustomFields) {
|
||||||
|
<li [ngbNavItem]="DocumentAttributesNavIDs.CustomFields">
|
||||||
|
<a ngbNavLink>
|
||||||
|
</i-bs><ng-container i18n>Custom fields</ng-container>
|
||||||
|
</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<pngx-custom-fields></pngx-custom-fields>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
@@ -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<void>()
|
||||||
|
|
||||||
|
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<any> | 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,3 @@
|
|||||||
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions" [loading]="loading">
|
|
||||||
|
|
||||||
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
|
||||||
<i-bs name="text-indent-left"></i-bs>
|
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
|
||||||
@if (selectedObjects.size > 0) {
|
|
||||||
<pngx-clearable-badge [selected]="selectedObjects.size > 0" [number]="selectedObjects.size" (cleared)="selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
|
||||||
<button ngbDropdownItem (click)="selectNone()" i18n>Select none</button>
|
|
||||||
<button ngbDropdownItem (click)="selectPage(true)" i18n>Select page</button>
|
|
||||||
<button ngbDropdownItem (click)="selectAll()" i18n>Select all</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-none d-sm-flex flex-fill me-3">
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<span class="input-group-text border-0" i18n>Select:</span>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group btn-group-sm flex-nowrap">
|
|
||||||
@if (selectedObjects.size > 0) {
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="selectNone()">
|
|
||||||
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="selectPage(true)">
|
|
||||||
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="selectAll()">
|
|
||||||
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
|
||||||
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
|
|
||||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
|
||||||
</button>
|
|
||||||
</pngx-page-header>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col mb-2 mb-xl-0">
|
<div class="col mb-2 mb-xl-0">
|
||||||
<div class="form-inline d-flex align-items-center">
|
<div class="form-inline d-flex align-items-center">
|
||||||
|
|||||||
@@ -96,4 +96,52 @@ describe('PermissionsGuard', () => {
|
|||||||
expect(canActivate).toHaveProperty('root') // returns UrlTree
|
expect(canActivate).toHaveProperty('root') // returns UrlTree
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,12 +20,20 @@ export class PermissionsGuard {
|
|||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot
|
state: RouterStateSnapshot
|
||||||
): boolean | UrlTree {
|
): boolean | UrlTree {
|
||||||
|
const requiredPermissionAny: { action: any; type: any }[] =
|
||||||
|
route.data.requiredPermissionAny
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
(route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
|
||||||
(route.data.requiredPermission &&
|
(route.data.requiredPermission &&
|
||||||
!this.permissionsService.currentUserCan(
|
!this.permissionsService.currentUserCan(
|
||||||
route.data.requiredPermission.action,
|
route.data.requiredPermission.action,
|
||||||
route.data.requiredPermission.type
|
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
|
// Check if tour is running 1 = TourState.ON
|
||||||
|
|||||||
Reference in New Issue
Block a user