mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-07 23:42:46 -06:00
Compare commits
18 Commits
dependabot
...
feature-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a02a1dd6f | ||
|
|
651a0c72f1 | ||
|
|
ac6f38ed63 | ||
|
|
271adfdd60 | ||
|
|
5714e3b2e1 | ||
|
|
df0f96c3a9 | ||
|
|
b0b34048c2 | ||
|
|
47a4868ddd | ||
|
|
1ac52d1d68 | ||
|
|
4a102da2db | ||
|
|
793fdeb0d2 | ||
|
|
8ef048e3ba | ||
|
|
1ba9f346f9 | ||
|
|
97d56941fe | ||
|
|
c8820b1c9a | ||
|
|
2852a3af56 | ||
|
|
cee5d3b84e | ||
|
|
ee8f6b3ec9 |
@@ -52,11 +52,11 @@ test('dashboard saved view document links', async ({ page }) => {
|
|||||||
test('test slim sidebar', async ({ page }) => {
|
test('test slim sidebar', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await page.locator('#sidebarMenu').getByRole('button').click()
|
await page.locator('.sidebar-slim-toggler').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
await page.locator('#sidebarMenu').getByRole('button').click()
|
await page.locator('.sidebar-slim-toggler').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Correspondents' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/correspondents')
|
await page.goto('/attributes/correspondents')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -44,8 +44,10 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
|||||||
test('should not allow user to view tags', async ({ page }) => {
|
test('should not allow user to view tags', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
|
await expect(
|
||||||
await page.goto('/tags')
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
|
).not.toBeAttached()
|
||||||
|
await page.goto('/attributes/tags')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -55,9 +57,9 @@ test('should not allow user to view document types', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Document Types' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/documenttypes')
|
await page.goto('/attributes/documenttypes')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
@@ -67,9 +69,9 @@ test('should not allow user to view storage paths', async ({ page }) => {
|
|||||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
await page.goto('/dashboard')
|
await page.goto('/dashboard')
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Storage Paths' })
|
page.getByRole('link', { name: 'Attributes' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/storagepaths')
|
await page.goto('/attributes/storagepaths')
|
||||||
await expect(page.locator('body')).toHaveText(
|
await expect(page.locator('body')).toHaveText(
|
||||||
/You don't have permissions to do that/i
|
/You don't have permissions to do that/i
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,60 @@
|
|||||||
<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 (canManageAttributes) {
|
||||||
*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()"
|
<div class="d-flex align-items-center attributes-row">
|
||||||
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
<a class="nav-link flex-fill" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()"
|
||||||
|
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="stack"></i-bs><span> <ng-container i18n>Attributes</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
@if (!slimSidebarEnabled) {
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
<button
|
||||||
tourAnchor="tour.tags">
|
type="button"
|
||||||
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
|
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
||||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
(click)="toggleAttributesSections($event)"
|
||||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
|
||||||
|
i18n-aria-label
|
||||||
|
>
|
||||||
|
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="attributes-submenu ms-2"
|
||||||
|
[ngbCollapse]="slimSidebarEnabled || attributesSectionsCollapsed"
|
||||||
|
>
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
|
||||||
|
<a class="nav-link py-1" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
<i-bs class="me-1" name="tags"></i-bs><span> <ng-container i18n>Tags</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link"
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
||||||
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
|
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
</a>
|
||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
</li>
|
||||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document Types</ng-container></span>
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
||||||
|
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
||||||
|
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document types</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
|
||||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link py-1" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
||||||
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage paths</ng-container></span>
|
||||||
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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link py-1" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
|
||||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom fields</ng-container></span>
|
||||||
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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
|||||||
@@ -177,6 +177,15 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attributes-row .attributes-expand-btn {
|
||||||
|
opacity: 0.2;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-row:hover .attributes-expand-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ import {
|
|||||||
DjangoMessagesService,
|
DjangoMessagesService,
|
||||||
} from 'src/app/services/django-messages.service'
|
} from 'src/app/services/django-messages.service'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import {
|
||||||
|
PermissionType,
|
||||||
|
PermissionsService,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SearchService } from 'src/app/services/rest/search.service'
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
@@ -258,7 +261,7 @@ describe('AppFrameComponent', () => {
|
|||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
component.toggleSlimSidebar()
|
component.toggleSlimSidebar()
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
.match(`${environment.apiBaseUrl}ui_settings/`)[0]
|
||||||
.flush('error', {
|
.flush('error', {
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: 'error',
|
statusText: 'error',
|
||||||
@@ -373,4 +376,36 @@ describe('AppFrameComponent', () => {
|
|||||||
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
|
||||||
expect(maybeRefreshSpy).toHaveBeenCalled()
|
expect(maybeRefreshSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should indicate attributes management availability when any permission is granted', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserCan')
|
||||||
|
.mockImplementation((action, type) => {
|
||||||
|
return type === PermissionType.Tag
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(component.canManageAttributes).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should persist attributes section collapse state', () => {
|
||||||
|
const setSpy = jest.spyOn(settingsService, 'set')
|
||||||
|
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
||||||
|
|
||||||
|
component.attributesSectionsCollapsed = true
|
||||||
|
|
||||||
|
expect(setSpy).toHaveBeenCalledWith(
|
||||||
|
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
['attributes']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should collapse attributes sections when enabling slim sidebar', () => {
|
||||||
|
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
||||||
|
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
|
||||||
|
settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false)
|
||||||
|
|
||||||
|
component.toggleSlimSidebar()
|
||||||
|
|
||||||
|
expect(component.attributesSectionsCollapsed).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Observable } from 'rxjs'
|
|||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { CollapsibleSection, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
|
||||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||||
@@ -141,11 +141,20 @@ export class AppFrameComponent
|
|||||||
toggleSlimSidebar(): void {
|
toggleSlimSidebar(): void {
|
||||||
this.slimSidebarAnimating = true
|
this.slimSidebarAnimating = true
|
||||||
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
||||||
|
if (this.slimSidebarEnabled) {
|
||||||
|
this.attributesSectionsCollapsed = true
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.slimSidebarAnimating = false
|
this.slimSidebarAnimating = false
|
||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleAttributesSections(event?: Event): void {
|
||||||
|
event?.preventDefault()
|
||||||
|
event?.stopPropagation()
|
||||||
|
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
|
||||||
|
}
|
||||||
|
|
||||||
get versionString(): string {
|
get versionString(): string {
|
||||||
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
||||||
}
|
}
|
||||||
@@ -167,6 +176,31 @@ export class AppFrameComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canManageAttributes(): 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)
|
||||||
}
|
}
|
||||||
@@ -186,6 +220,31 @@ export class AppFrameComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get attributesSectionsCollapsed(): boolean {
|
||||||
|
return this.settingsService
|
||||||
|
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
|
||||||
|
?.includes(CollapsibleSection.ATTRIBUTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
set attributesSectionsCollapsed(collapsed: boolean) {
|
||||||
|
// TODO: refactor to be able to toggle individual sections, if implemented
|
||||||
|
this.settingsService.set(
|
||||||
|
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
collapsed ? [CollapsibleSection.ATTRIBUTES] : []
|
||||||
|
)
|
||||||
|
this.settingsService
|
||||||
|
.storeSettings()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe({
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred while saving settings.`
|
||||||
|
)
|
||||||
|
console.warn(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -26,9 +26,9 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
|||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../common/page-header/page-header.component'
|
||||||
import { CustomFieldsComponent } from './custom-fields.component'
|
import { CustomFieldsComponent } from './custom-fields.component'
|
||||||
|
|
||||||
const fields: CustomField[] = [
|
const fields: CustomField[] = [
|
||||||
@@ -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
|
||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { delay, takeUntil, tap } from 'rxjs'
|
import { delay, takeUntil, tap } from 'rxjs'
|
||||||
|
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||||
|
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
||||||
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
|
||||||
import {
|
import {
|
||||||
CustomFieldQueryLogicalOperator,
|
CustomFieldQueryLogicalOperator,
|
||||||
@@ -21,18 +25,12 @@ import { DocumentService } from 'src/app/services/rest/document.service'
|
|||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
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({
|
@Component({
|
||||||
selector: 'pngx-custom-fields',
|
selector: 'pngx-custom-fields',
|
||||||
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,78 @@
|
|||||||
|
<pngx-page-header
|
||||||
|
[title]="activeTabLabel"
|
||||||
|
info="Manage tags, correspondents, document types, storage paths, and custom fields."
|
||||||
|
i18n-info
|
||||||
|
[infoLink]="activeInfoLink"
|
||||||
|
[loading]="activeHeaderLoading"
|
||||||
|
>
|
||||||
|
@if (activeAttributeList) {
|
||||||
|
<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 (activeAttributeList.selectedObjects.size > 0) {
|
||||||
|
<pngx-clearable-badge [selected]="activeAttributeList.selectedObjects.size > 0" [number]="activeAttributeList.selectedObjects.size" (cleared)="activeAttributeList.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
||||||
|
<button ngbDropdownItem (click)="activeAttributeList.selectNone()" i18n>Select none</button>
|
||||||
|
<button ngbDropdownItem (click)="activeAttributeList.selectPage(true)" i18n>Select page</button>
|
||||||
|
<button ngbDropdownItem (click)="activeAttributeList.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 (activeAttributeList.selectedObjects.size > 0) {
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="activeAttributeList.selectNone()">
|
||||||
|
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="activeAttributeList.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)="activeAttributeList.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)="activeAttributeList.setPermissions()"
|
||||||
|
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Change) || activeAttributeList.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)="activeAttributeList.delete()"
|
||||||
|
[disabled]="!activeAttributeList.userCanBulkEdit(PermissionAction.Delete) || activeAttributeList.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)="activeAttributeList.openCreateDialog()"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.Add, type: activeAttributeList.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-underline">
|
||||||
|
@for (section of visibleSections; track section.id) {
|
||||||
|
<li [ngbNavItem]="section.id">
|
||||||
|
<a ngbNavLink >
|
||||||
|
<i-bs class="me-2" [name]="section.icon"></i-bs>{{ section.label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="my-3 shadow-sm">
|
||||||
|
<ng-container
|
||||||
|
[ngComponentOutlet]="activeSection?.component"
|
||||||
|
#activeOutlet="ngComponentOutlet"
|
||||||
|
></ng-container>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// .attributes-tabs {
|
||||||
|
// border-bottom-color: rgba(var(--bs-body-color-rgb), 0.12);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .attributes-tabs .nav-link {
|
||||||
|
// position: relative;
|
||||||
|
// padding-bottom: 0.45rem;
|
||||||
|
// color: rgba(var(--bs-body-color-rgb));
|
||||||
|
// transition: color 140ms ease;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .attributes-tabs .nav-link::after {
|
||||||
|
// content: '';
|
||||||
|
// position: absolute;
|
||||||
|
// left: 0.2rem;
|
||||||
|
// right: 0.2rem;
|
||||||
|
// bottom: 0;
|
||||||
|
// height: var(--bs-nav-underline-border-width);
|
||||||
|
// border-radius: 1px;
|
||||||
|
// background: transparent;
|
||||||
|
// transition: background-color 140ms ease;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .attributes-tabs .nav-link:hover {
|
||||||
|
// color: rgba(var(--bs-body-color-rgb), 0.95);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .attributes-tabs .nav-link.active {
|
||||||
|
// color: var(--bs-primary);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .attributes-tabs .nav-link.active::after {
|
||||||
|
// background: var(--bs-primary);
|
||||||
|
// }
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
convertToParamMap,
|
||||||
|
ParamMap,
|
||||||
|
Router,
|
||||||
|
} from '@angular/router'
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { Subject } from 'rxjs'
|
||||||
|
import {
|
||||||
|
PermissionAction,
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
import { DocumentAttributesComponent } from './document-attributes.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-dummy-section',
|
||||||
|
template: '',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
class DummySectionComponent {}
|
||||||
|
|
||||||
|
describe('DocumentAttributesComponent', () => {
|
||||||
|
let component: DocumentAttributesComponent
|
||||||
|
let fixture: ComponentFixture<DocumentAttributesComponent>
|
||||||
|
let router: Router
|
||||||
|
let paramMapSubject: Subject<ParamMap>
|
||||||
|
let permissionsService: PermissionsService
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
paramMapSubject = new Subject<ParamMap>()
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
RouterTestingModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
DocumentAttributesComponent,
|
||||||
|
DummySectionComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
paramMap: paramMapSubject.asObservable(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PermissionsService,
|
||||||
|
useValue: {
|
||||||
|
currentUserCan: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DocumentAttributesComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
router = TestBed.inject(Router)
|
||||||
|
permissionsService = TestBed.inject(PermissionsService)
|
||||||
|
|
||||||
|
jest.spyOn(router, 'navigate').mockResolvedValue(true)
|
||||||
|
;(component as any).sections = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
path: 'tags',
|
||||||
|
label: 'Tags',
|
||||||
|
icon: 'tags',
|
||||||
|
permissionType: PermissionType.Tag,
|
||||||
|
kind: 'attributeList',
|
||||||
|
component: DummySectionComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
path: 'customfields',
|
||||||
|
label: 'Custom fields',
|
||||||
|
icon: 'ui-radios',
|
||||||
|
permissionType: PermissionType.CustomField,
|
||||||
|
kind: 'customFields',
|
||||||
|
component: DummySectionComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate to default section when no section is provided', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
||||||
|
(action, type) => {
|
||||||
|
return action === PermissionAction.View && type === PermissionType.Tag
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({}))
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'tags'], {
|
||||||
|
replaceUrl: true,
|
||||||
|
})
|
||||||
|
expect(component.activeNavID).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set active section from route param when valid', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
||||||
|
(action, type) => {
|
||||||
|
return (
|
||||||
|
action === PermissionAction.View &&
|
||||||
|
type === PermissionType.CustomField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
||||||
|
|
||||||
|
expect(component.activeNavID).toBe(2)
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect to dashboard when no sections are visible', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(false)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({}))
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/dashboard'], {
|
||||||
|
replaceUrl: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should navigate when a nav change occurs', () => {
|
||||||
|
;(permissionsService.currentUserCan as jest.Mock).mockImplementation(
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
paramMapSubject.next(convertToParamMap({ section: 'tags' }))
|
||||||
|
|
||||||
|
component.onNavChange({ nextId: 2 } as any)
|
||||||
|
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'customfields'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import { NgComponentOutlet } from '@angular/common'
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Type,
|
||||||
|
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 { CustomFieldsComponent } from './custom-fields/custom-fields.component'
|
||||||
|
import { CorrespondentListComponent } from './management-list/correspondent-list/correspondent-list.component'
|
||||||
|
import { DocumentTypeListComponent } from './management-list/document-type-list/document-type-list.component'
|
||||||
|
import { ManagementListComponent } from './management-list/management-list.component'
|
||||||
|
import { StoragePathListComponent } from './management-list/storage-path-list/storage-path-list.component'
|
||||||
|
import { TagListComponent } from './management-list/tag-list/tag-list.component'
|
||||||
|
|
||||||
|
enum DocumentAttributesNavIDs {
|
||||||
|
Tags = 1,
|
||||||
|
Correspondents = 2,
|
||||||
|
DocumentTypes = 3,
|
||||||
|
StoragePaths = 4,
|
||||||
|
CustomFields = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentAttributesSectionKind = 'attributeList' | 'customFields'
|
||||||
|
|
||||||
|
interface DocumentAttributesSection {
|
||||||
|
id: DocumentAttributesNavIDs
|
||||||
|
path: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
infoLink?: string
|
||||||
|
permissionType: PermissionType
|
||||||
|
kind: DocumentAttributesSectionKind
|
||||||
|
component: Type<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-document-attributes',
|
||||||
|
templateUrl: './document-attributes.component.html',
|
||||||
|
styleUrls: ['./document-attributes.component.scss'],
|
||||||
|
imports: [
|
||||||
|
PageHeaderComponent,
|
||||||
|
NgbNavModule,
|
||||||
|
NgbDropdownModule,
|
||||||
|
NgComponentOutlet,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
ClearableBadgeComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
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 PermissionAction = PermissionAction
|
||||||
|
protected readonly PermissionType = PermissionType
|
||||||
|
|
||||||
|
readonly sections: DocumentAttributesSection[] = [
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.Tags,
|
||||||
|
path: 'tags',
|
||||||
|
label: $localize`Tags`,
|
||||||
|
icon: 'tags',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.Tag,
|
||||||
|
kind: 'attributeList',
|
||||||
|
component: TagListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.Correspondents,
|
||||||
|
path: 'correspondents',
|
||||||
|
label: $localize`Correspondents`,
|
||||||
|
icon: 'person',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.Correspondent,
|
||||||
|
kind: 'attributeList',
|
||||||
|
component: CorrespondentListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.DocumentTypes,
|
||||||
|
path: 'documenttypes',
|
||||||
|
label: $localize`Document types`,
|
||||||
|
icon: 'hash',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.DocumentType,
|
||||||
|
kind: 'attributeList',
|
||||||
|
component: DocumentTypeListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.StoragePaths,
|
||||||
|
path: 'storagepaths',
|
||||||
|
label: $localize`Storage paths`,
|
||||||
|
icon: 'folder',
|
||||||
|
infoLink: 'usage/#terms-and-definitions',
|
||||||
|
permissionType: PermissionType.StoragePath,
|
||||||
|
kind: 'attributeList',
|
||||||
|
component: StoragePathListComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentAttributesNavIDs.CustomFields,
|
||||||
|
path: 'customfields',
|
||||||
|
label: $localize`Custom fields`,
|
||||||
|
icon: 'ui-radios',
|
||||||
|
infoLink: 'usage/#custom-fields',
|
||||||
|
permissionType: PermissionType.CustomField,
|
||||||
|
kind: 'customFields',
|
||||||
|
component: CustomFieldsComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@ViewChild('activeOutlet', { read: NgComponentOutlet })
|
||||||
|
private activeOutlet?: NgComponentOutlet
|
||||||
|
|
||||||
|
activeNavID: number = null
|
||||||
|
|
||||||
|
get visibleSections(): DocumentAttributesSection[] {
|
||||||
|
return this.sections.filter((section) =>
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
section.permissionType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeSection(): DocumentAttributesSection | null {
|
||||||
|
return (
|
||||||
|
this.visibleSections.find((section) => section.id === this.activeNavID) ??
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeAttributeList(): ManagementListComponent<any> | null {
|
||||||
|
if (this.activeSection?.kind !== 'attributeList') return null
|
||||||
|
const instance = this.activeOutlet?.componentInstance
|
||||||
|
return instance instanceof ManagementListComponent ? instance : null
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeCustomFields(): CustomFieldsComponent | null {
|
||||||
|
if (this.activeSection?.kind !== 'customFields') return null
|
||||||
|
const instance = this.activeOutlet?.componentInstance
|
||||||
|
return instance instanceof CustomFieldsComponent ? instance : null
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeTabLabel(): string {
|
||||||
|
return this.activeSection?.label ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeInfoLink(): string {
|
||||||
|
return this.activeSection?.infoLink ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeHeaderLoading(): boolean {
|
||||||
|
return (
|
||||||
|
this.activeAttributeList?.loading ??
|
||||||
|
this.activeCustomFields?.loading ??
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return this.visibleSections[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNavIDForSection(section: string): DocumentAttributesNavIDs | null {
|
||||||
|
const path = section?.toLowerCase()
|
||||||
|
if (!path) return null
|
||||||
|
|
||||||
|
const found = this.visibleSections.find((s) => s.path === path)
|
||||||
|
return found?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSectionForNavID(navID: number): string | null {
|
||||||
|
const section = this.visibleSections.find((s) => s.id === navID)
|
||||||
|
return section?.path ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { CorrespondentListComponent } from './correspondent-list.component'
|
import { CorrespondentListComponent } from './correspondent-list.component'
|
||||||
|
|
||||||
describe('CorrespondentListComponent', () => {
|
describe('CorrespondentListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { CorrespondentEditDialogComponent } from 'src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@@ -14,21 +15,16 @@ import { SortableDirective } from 'src/app/directives/sortable.directive'
|
|||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-correspondent-list',
|
selector: 'pngx-correspondent-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
providers: [{ provide: CustomDatePipe }],
|
providers: [{ provide: CustomDatePipe }],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
@@ -37,7 +33,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { DocumentTypeListComponent } from './document-type-list.component'
|
import { DocumentTypeListComponent } from './document-type-list.component'
|
||||||
|
|
||||||
describe('DocumentTypeListComponent', () => {
|
describe('DocumentTypeListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,25 +7,21 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { DocumentTypeEditDialogComponent } from 'src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-document-type-list',
|
selector: 'pngx-document-type-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -35,7 +31,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
||||||
@@ -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">
|
||||||
@@ -44,12 +44,12 @@ import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-fil
|
|||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
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 { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogComponent } from '../../../common/edit-dialog/edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../common/page-header/page-header.component'
|
||||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
import { PermissionsDialogComponent } from '../../../common/permissions-dialog/permissions-dialog.component'
|
||||||
import { TagListComponent } from '../tag-list/tag-list.component'
|
|
||||||
import { ManagementListComponent } from './management-list.component'
|
import { ManagementListComponent } from './management-list.component'
|
||||||
|
import { TagListComponent } from './tag-list/tag-list.component'
|
||||||
|
|
||||||
const tags: Tag[] = [
|
const tags: Tag[] = [
|
||||||
{
|
{
|
||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs/operators'
|
} from 'rxjs/operators'
|
||||||
|
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
|
||||||
|
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
||||||
import {
|
import {
|
||||||
MATCH_AUTO,
|
MATCH_AUTO,
|
||||||
MATCH_NONE,
|
MATCH_NONE,
|
||||||
@@ -40,10 +44,6 @@ import {
|
|||||||
} from 'src/app/services/rest/abstract-name-filter-service'
|
} from 'src/app/services/rest/abstract-name-filter-service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
|
||||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
|
||||||
|
|
||||||
export interface ManagementListColumn {
|
export interface ManagementListColumn {
|
||||||
key: string
|
key: string
|
||||||
@@ -10,7 +10,7 @@ import { StoragePath } from 'src/app/data/storage-path'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { StoragePathListComponent } from './storage-path-list.component'
|
import { StoragePathListComponent } from './storage-path-list.component'
|
||||||
|
|
||||||
describe('StoragePathListComponent', () => {
|
describe('StoragePathListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,25 +7,21 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { StoragePathEditDialogComponent } from 'src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-storage-path-list',
|
selector: 'pngx-storage-path-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -35,7 +31,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
||||||
@@ -9,7 +9,7 @@ import { of } from 'rxjs'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../../../common/page-header/page-header.component'
|
||||||
import { TagListComponent } from './tag-list.component'
|
import { TagListComponent } from './tag-list.component'
|
||||||
|
|
||||||
describe('TagListComponent', () => {
|
describe('TagListComponent', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
@@ -7,25 +7,21 @@ import {
|
|||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { TagEditDialogComponent } from 'src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ManagementListComponent } from '../management-list.component'
|
||||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-tag-list',
|
selector: 'pngx-tag-list',
|
||||||
templateUrl: './../management-list/management-list.component.html',
|
templateUrl: './../management-list.component.html',
|
||||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
styleUrls: ['./../management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
PageHeaderComponent,
|
|
||||||
TitleCasePipe,
|
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -35,7 +31,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
ClearableBadgeComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TagListComponent extends ManagementListComponent<Tag> {
|
export class TagListComponent extends ManagementListComponent<Tag> {
|
||||||
@@ -19,6 +19,10 @@ export enum GlobalSearchType {
|
|||||||
TITLE_CONTENT = 'title-content',
|
TITLE_CONTENT = 'title-content',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CollapsibleSection {
|
||||||
|
ATTRIBUTES = 'attributes',
|
||||||
|
}
|
||||||
|
|
||||||
export const PAPERLESS_GREEN_HEX = '#17541f'
|
export const PAPERLESS_GREEN_HEX = '#17541f'
|
||||||
|
|
||||||
export const SETTINGS_KEYS = {
|
export const SETTINGS_KEYS = {
|
||||||
@@ -51,6 +55,8 @@ export const SETTINGS_KEYS = {
|
|||||||
NOTES_ENABLED: 'general-settings:notes-enabled',
|
NOTES_ENABLED: 'general-settings:notes-enabled',
|
||||||
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
||||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||||
|
ATTRIBUTES_SECTIONS_COLLAPSED:
|
||||||
|
'general-settings:attributes-sections-collapsed',
|
||||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||||
UPDATE_CHECKING_BACKEND_SETTING:
|
UPDATE_CHECKING_BACKEND_SETTING:
|
||||||
'general-settings:update-checking:backend-setting',
|
'general-settings:update-checking:backend-setting',
|
||||||
@@ -112,6 +118,11 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
|
||||||
|
type: 'array',
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ import {
|
|||||||
sliders2Vertical,
|
sliders2Vertical,
|
||||||
sortAlphaDown,
|
sortAlphaDown,
|
||||||
sortAlphaUpAlt,
|
sortAlphaUpAlt,
|
||||||
|
stack,
|
||||||
stars,
|
stars,
|
||||||
tag,
|
tag,
|
||||||
tagFill,
|
tagFill,
|
||||||
@@ -344,6 +345,7 @@ const icons = {
|
|||||||
sliders2Vertical,
|
sliders2Vertical,
|
||||||
sortAlphaDown,
|
sortAlphaDown,
|
||||||
sortAlphaUpAlt,
|
sortAlphaUpAlt,
|
||||||
|
stack,
|
||||||
stars,
|
stars,
|
||||||
tagFill,
|
tagFill,
|
||||||
tag,
|
tag,
|
||||||
|
|||||||
Reference in New Issue
Block a user