mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-07 23:42:46 -06:00
Compare commits
3 Commits
feature-do
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ec574fe79 | ||
|
|
5c3d02e6d4 | ||
|
|
1d89ec402b |
7
.github/workflows/ci-backend.yml
vendored
7
.github/workflows/ci-backend.yml
vendored
@@ -128,6 +128,11 @@ jobs:
|
|||||||
- name: List installed Python dependencies
|
- name: List installed Python dependencies
|
||||||
run: |
|
run: |
|
||||||
uv pip list
|
uv pip list
|
||||||
|
- name: Check typing (pyrefly)
|
||||||
|
run: |
|
||||||
|
uv run pyrefly \
|
||||||
|
check \
|
||||||
|
src/
|
||||||
- name: Cache Mypy
|
- name: Cache Mypy
|
||||||
uses: actions/cache@v5.0.3
|
uses: actions/cache@v5.0.3
|
||||||
with:
|
with:
|
||||||
@@ -137,7 +142,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
|
||||||
${{ runner.os }}-mypy-
|
${{ runner.os }}-mypy-
|
||||||
- name: Check typing
|
- name: Check typing (mypy)
|
||||||
run: |
|
run: |
|
||||||
uv run mypy \
|
uv run mypy \
|
||||||
--show-error-codes \
|
--show-error-codes \
|
||||||
|
|||||||
@@ -2441,9 +2441,6 @@ src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "Non
|
|||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "mode" [union-attr]
|
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "mode" [union-attr]
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "output_type" [union-attr]
|
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "output_type" [union-attr]
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "pages" [union-attr]
|
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "pages" [union-attr]
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "rotate_pages" [union-attr]
|
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "rotate_pages_threshold" [union-attr]
|
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
||||||
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
src/paperless_tesseract/tests/test_parser_custom_settings.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "save" [union-attr]
|
||||||
@@ -2471,4 +2468,3 @@ src/paperless_tika/parsers.py:0: error: Function is missing a type annotation fo
|
|||||||
src/paperless_tika/parsers.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "None") [assignment]
|
src/paperless_tika/parsers.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "None") [assignment]
|
||||||
src/paperless_tika/signals.py:0: error: Function is missing a type annotation [no-untyped-def]
|
src/paperless_tika/signals.py:0: error: Function is missing a type annotation [no-untyped-def]
|
||||||
src/paperless_tika/signals.py:0: error: Function is missing a type annotation [no-untyped-def]
|
src/paperless_tika/signals.py:0: error: Function is missing a type annotation [no-untyped-def]
|
||||||
src/paperless_tika/tests/test_live_tika.py:0: error: Unsupported right operand type for in ("None") [operator]
|
|
||||||
|
|||||||
17368
.pyrefly-baseline.json
Normal file
17368
.pyrefly-baseline.json
Normal file
File diff suppressed because one or more lines are too long
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.9.29-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.10.0-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ typing = [
|
|||||||
"microsoft-python-type-stubs @ git+https://github.com/microsoft/python-type-stubs.git",
|
"microsoft-python-type-stubs @ git+https://github.com/microsoft/python-type-stubs.git",
|
||||||
"mypy",
|
"mypy",
|
||||||
"mypy-baseline",
|
"mypy-baseline",
|
||||||
|
"pyrefly",
|
||||||
"types-bleach",
|
"types-bleach",
|
||||||
"types-channels",
|
"types-channels",
|
||||||
"types-colorama",
|
"types-colorama",
|
||||||
@@ -346,6 +347,11 @@ disallow_untyped_defs = true
|
|||||||
warn_redundant_casts = true
|
warn_redundant_casts = true
|
||||||
warn_unused_ignores = true
|
warn_unused_ignores = true
|
||||||
|
|
||||||
|
[tool.pyrefly]
|
||||||
|
search-path = [ "src" ]
|
||||||
|
baseline = ".pyrefly-baseline.json"
|
||||||
|
python-platform = "linux"
|
||||||
|
|
||||||
[tool.django-stubs]
|
[tool.django-stubs]
|
||||||
django_settings_module = "paperless.settings"
|
django_settings_module = "paperless.settings"
|
||||||
|
|
||||||
|
|||||||
@@ -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('.sidebar-slim-toggler').click()
|
await page.locator('#sidebarMenu').getByRole('button').click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
|
||||||
).toBeHidden()
|
).toBeHidden()
|
||||||
await page.locator('.sidebar-slim-toggler').click()
|
await page.locator('#sidebarMenu').getByRole('button').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: 'Attributes' })
|
page.getByRole('link', { name: 'Correspondents' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/correspondents')
|
await page.goto('/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,10 +44,8 @@ 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(
|
await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
|
||||||
page.getByRole('link', { name: 'Attributes' })
|
await page.goto('/tags')
|
||||||
).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
|
||||||
)
|
)
|
||||||
@@ -57,9 +55,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: 'Attributes' })
|
page.getByRole('link', { name: 'Document Types' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/documenttypes')
|
await page.goto('/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
|
||||||
)
|
)
|
||||||
@@ -69,9 +67,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: 'Attributes' })
|
page.getByRole('link', { name: 'Storage Paths' })
|
||||||
).not.toBeAttached()
|
).not.toBeAttached()
|
||||||
await page.goto('/attributes/storagepaths')
|
await page.goto('/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,9 +11,13 @@ 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 { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component'
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
|
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||||
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { 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'
|
||||||
@@ -101,77 +105,53 @@ export const routes: Routes = [
|
|||||||
componentName: 'DocumentAsnComponent',
|
componentName: 'DocumentAsnComponent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'attributes',
|
|
||||||
component: DocumentAttributesComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermissionAny: [
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.Tag },
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.Correspondent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
|
||||||
],
|
|
||||||
componentName: 'DocumentAttributesComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'attributes/:section',
|
|
||||||
component: DocumentAttributesComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
requiredPermissionAny: [
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.Tag },
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.Correspondent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: PermissionAction.View,
|
|
||||||
type: PermissionType.DocumentType,
|
|
||||||
},
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.StoragePath },
|
|
||||||
{ action: PermissionAction.View, type: PermissionType.CustomField },
|
|
||||||
],
|
|
||||||
componentName: 'DocumentAttributesComponent',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'documentproperties',
|
|
||||||
redirectTo: '/attributes',
|
|
||||||
pathMatch: 'full',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'documentproperties/:section',
|
|
||||||
redirectTo: '/attributes/:section',
|
|
||||||
pathMatch: 'full',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'tags',
|
path: 'tags',
|
||||||
redirectTo: '/attributes/tags',
|
component: TagListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Tag,
|
||||||
|
},
|
||||||
|
componentName: 'TagListComponent',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'correspondents',
|
|
||||||
redirectTo: '/attributes/correspondents',
|
|
||||||
pathMatch: 'full',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'documenttypes',
|
path: 'documenttypes',
|
||||||
redirectTo: '/attributes/documenttypes',
|
component: DocumentTypeListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.DocumentType,
|
||||||
|
},
|
||||||
|
componentName: 'DocumentTypeListComponent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'correspondents',
|
||||||
|
component: CorrespondentListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Correspondent,
|
||||||
|
},
|
||||||
|
componentName: 'CorrespondentListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'storagepaths',
|
path: 'storagepaths',
|
||||||
redirectTo: '/attributes/storagepaths',
|
component: StoragePathListComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.StoragePath,
|
||||||
|
},
|
||||||
|
componentName: 'StoragePathListComponent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logs',
|
path: 'logs',
|
||||||
@@ -259,8 +239,15 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'customfields',
|
path: 'customfields',
|
||||||
redirectTo: '/attributes/customfields',
|
component: CustomFieldsComponent,
|
||||||
pathMatch: 'full',
|
canActivate: [PermissionsGuard],
|
||||||
|
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`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.`,
|
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
|
||||||
route: '/attributes/tags',
|
route: '/tags',
|
||||||
backdropConfig: {
|
backdropConfig: {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -175,60 +175,44 @@
|
|||||||
<span i18n>Manage</span>
|
<span i18n>Manage</span>
|
||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2">
|
<ul class="nav flex-column mb-2">
|
||||||
@if (canManageAttributes) {
|
<li class="nav-item app-link"
|
||||||
<li class="nav-item app-link" tourAnchor="tour.tags">
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
||||||
<div class="d-flex align-items-center attributes-row">
|
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<a class="nav-link flex-fill" 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="stack"></i-bs><span> <ng-container i18n>Attributes</ng-container></span>
|
|
||||||
</a>
|
|
||||||
@if (!slimSidebarEnabled) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
|
||||||
(click)="toggleAttributesSections($event)"
|
|
||||||
[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>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
|
|
||||||
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
|
|
||||||
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
<i-bs class="me-1" name="person"></i-bs><span> <ng-container i18n>Correspondents</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
|
||||||
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
|
tourAnchor="tour.tags">
|
||||||
<i-bs class="me-1" name="hash"></i-bs><span> <ng-container i18n>Document types</ng-container></span>
|
<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>
|
</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 py-1" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage paths</ng-container></span>
|
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>
|
</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 py-1" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
|
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||||
<i-bs class="me-1" name="ui-radios"></i-bs><span> <ng-container i18n>Custom fields</ng-container></span>
|
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>
|
||||||
</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,15 +177,6 @@ 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,10 +28,7 @@ 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 {
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
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'
|
||||||
@@ -261,7 +258,7 @@ describe('AppFrameComponent', () => {
|
|||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
component.toggleSlimSidebar()
|
component.toggleSlimSidebar()
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.match(`${environment.apiBaseUrl}ui_settings/`)[0]
|
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||||
.flush('error', {
|
.flush('error', {
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: 'error',
|
statusText: 'error',
|
||||||
@@ -376,36 +373,4 @@ 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 { CollapsibleSection, SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { 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,20 +141,11 @@ 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}`}`
|
||||||
}
|
}
|
||||||
@@ -176,31 +167,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -220,31 +186,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } 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,7 +7,6 @@ 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'
|
||||||
@@ -15,16 +14,21 @@ 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 { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.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.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
providers: [{ provide: CustomDatePipe }],
|
providers: [{ provide: CustomDatePipe }],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
@@ -33,6 +37,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<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,7 +110,10 @@ 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')
|
||||||
|
|
||||||
component.editField()
|
const createButton = fixture.debugElement
|
||||||
|
.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,10 +7,6 @@ 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,
|
||||||
@@ -25,12 +21,18 @@ 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,
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// .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);
|
|
||||||
// }
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
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 { 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 } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } 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,21 +7,25 @@ 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 { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.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.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
||||||
@@ -1,3 +1,50 @@
|
|||||||
|
<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,10 +16,6 @@ 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,
|
||||||
@@ -44,6 +40,10 @@ 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 } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } 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,21 +7,25 @@ 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 { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.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.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
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 } from '@angular/common'
|
import { NgClass, NgTemplateOutlet, TitleCasePipe } 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,21 +7,25 @@ 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 { ManagementListComponent } from '../management-list.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.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.component.html',
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
styleUrls: ['./../management-list.component.scss'],
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
TitleCasePipe,
|
||||||
IfPermissionsDirective,
|
IfPermissionsDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -31,6 +35,7 @@ import { ManagementListComponent } from '../management-list.component'
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TagListComponent extends ManagementListComponent<Tag> {
|
export class TagListComponent extends ManagementListComponent<Tag> {
|
||||||
@@ -19,10 +19,6 @@ 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 = {
|
||||||
@@ -55,8 +51,6 @@ 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',
|
||||||
@@ -118,11 +112,6 @@ 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,52 +96,4 @@ 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,20 +20,12 @@ 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,7 +126,6 @@ import {
|
|||||||
sliders2Vertical,
|
sliders2Vertical,
|
||||||
sortAlphaDown,
|
sortAlphaDown,
|
||||||
sortAlphaUpAlt,
|
sortAlphaUpAlt,
|
||||||
stack,
|
|
||||||
stars,
|
stars,
|
||||||
tag,
|
tag,
|
||||||
tagFill,
|
tagFill,
|
||||||
@@ -345,7 +344,6 @@ const icons = {
|
|||||||
sliders2Vertical,
|
sliders2Vertical,
|
||||||
sortAlphaDown,
|
sortAlphaDown,
|
||||||
sortAlphaUpAlt,
|
sortAlphaUpAlt,
|
||||||
stack,
|
|
||||||
stars,
|
stars,
|
||||||
tagFill,
|
tagFill,
|
||||||
tag,
|
tag,
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ class TestParserSettingsFromDb(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
"""
|
"""
|
||||||
with override_settings(OCR_ROTATE_PAGES=False, OCR_ROTATE_PAGES_THRESHOLD=30.0):
|
with override_settings(OCR_ROTATE_PAGES=False, OCR_ROTATE_PAGES_THRESHOLD=30.0):
|
||||||
instance = ApplicationConfiguration.objects.all().first()
|
instance = ApplicationConfiguration.objects.all().first()
|
||||||
|
assert instance is not None
|
||||||
instance.rotate_pages = True
|
instance.rotate_pages = True
|
||||||
instance.rotate_pages_threshold = 15.0
|
instance.rotate_pages_threshold = 15.0
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class TestTikaParserAgainstServer:
|
|||||||
[sample_doc_file, "application/msword"],
|
[sample_doc_file, "application/msword"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert tika_parser.text is not None
|
||||||
assert (
|
assert (
|
||||||
"This is a test document, saved in the older .doc format"
|
"This is a test document, saved in the older .doc format"
|
||||||
in tika_parser.text
|
in tika_parser.text
|
||||||
|
|||||||
31
uv.lock
generated
31
uv.lock
generated
@@ -3098,7 +3098,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.16.0"
|
version = "2.17.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -3110,9 +3110,9 @@ dependencies = [
|
|||||||
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3272,6 +3272,7 @@ typing = [
|
|||||||
{ name = "microsoft-python-type-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "microsoft-python-type-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "mypy-baseline", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "mypy-baseline", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
{ name = "pyrefly", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "types-bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "types-bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "types-channels", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "types-channels", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "types-colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "types-colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -3413,6 +3414,7 @@ typing = [
|
|||||||
{ name = "microsoft-python-type-stubs", git = "https://github.com/microsoft/python-type-stubs.git" },
|
{ name = "microsoft-python-type-stubs", git = "https://github.com/microsoft/python-type-stubs.git" },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
{ name = "mypy-baseline" },
|
{ name = "mypy-baseline" },
|
||||||
|
{ name = "pyrefly" },
|
||||||
{ name = "types-bleach" },
|
{ name = "types-bleach" },
|
||||||
{ name = "types-channels" },
|
{ name = "types-channels" },
|
||||||
{ name = "types-colorama" },
|
{ name = "types-colorama" },
|
||||||
@@ -4065,6 +4067,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrefly"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/bd/b8065b801b4058954577afa3f78bc1dda5f119f7ea353570ba9029db5109/pyrefly-0.51.0.tar.gz", hash = "sha256:99467db60f148bb6965c45cdc3e769d94b704100e9d57b6455cc6796e5a9e7b1", size = 4918889, upload-time = "2026-02-02T15:32:58.45Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/c1/0aa9b4cf5180f481e9f07a8fbfe9c3bc6044ec97612373fdd4f9f6aa49a4/pyrefly-0.51.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4013f914d3b523a9b1afc25a620a011406f7745ad5cfc5781ec95235bc9cd583", size = 11900057, upload-time = "2026-02-02T15:32:34.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/07/6a576ec997845bc8e7d89afebe12bc6386092446330194789d120f6a73f7/pyrefly-0.51.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4a6eeffd5649d393bf457b7c1253f89b33295d475b1cae0f9a21377986708804", size = 11480421, upload-time = "2026-02-02T15:32:37.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/0e/1b4675289a29b72818c812d7456031a7cab98532826d207d39465f75712c/pyrefly-0.51.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beace17854735136134848e5a0e8678b6862ee1144eaeb27f1bb70ff1f8fd9ca", size = 32511878, upload-time = "2026-02-02T15:32:40.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/4e/d564711718e4158339397123085da6afcad1c62222efa483cb7db5dab58b/pyrefly-0.51.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40055df65c184d825081e7177b99d277c8a1cb29c6e41a54ff40828d355aa467", size = 34797013, upload-time = "2026-02-02T15:32:43.687Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/db/961162ec2bb74a0cd5d0ef988f71695581449b3c6fce76ede9a984cdc8d1/pyrefly-0.51.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65689401e35b7d01a1394cdb1bafd46e2f49369b0f9891a333bce3568f100ce2", size = 35915591, upload-time = "2026-02-02T15:32:47.64Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.2"
|
||||||
@@ -5395,6 +5410,10 @@ dependencies = [
|
|||||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" },
|
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:4db72a4d257c45c3502f11764ee41460a87312fdc3dff47a8957812efe961725" },
|
||||||
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0826ac8e409551e12b2360ac18b4161a838cbd111933e694752f351191331d09" },
|
||||||
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:7fbbf409143a4fe0812a40c0b46a436030a7e1d14fe8c5234dfbe44df47f617e" },
|
||||||
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:b39cafff7229699f9d6e172cac74d85fd71b568268e439e08d9c540e54732a3e" },
|
||||||
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2d16abfce6c92584ceeb00c3b2665d5798424dd9ed235ea69b72e045cd53ae97" },
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2d16abfce6c92584ceeb00c3b2665d5798424dd9ed235ea69b72e045cd53ae97" },
|
||||||
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:4584ab167995c0479f6821e3dceaf199c8166c811d3adbba5d8eedbbfa6764fd" },
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:4584ab167995c0479f6821e3dceaf199c8166c811d3adbba5d8eedbbfa6764fd" },
|
||||||
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:45a1c5057629444aeb1c452c18298fa7f30f2f7aeadd4dc41f9d340980294407" },
|
{ url = "https://download.pytorch.org/whl/cpu/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:45a1c5057629444aeb1c452c18298fa7f30f2f7aeadd4dc41f9d340980294407" },
|
||||||
@@ -5619,11 +5638,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-dateparser"
|
name = "types-dateparser"
|
||||||
version = "1.2.2.20250809"
|
version = "1.3.0.20260206"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/63/54/2d2b77d1beba5bdb7faeabc7d7f0b9b2f8e428f79f45a144ad7ab87d1a29/types_dateparser-1.2.2.20250809.tar.gz", hash = "sha256:a898f5527e6c34d213bc5d85254b8246d8b1e76239ed9243711198add0c8a29c", size = 15804, upload-time = "2025-08-09T03:15:11.298Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f5/e0/72045089c4503d22f21f53f04898eb0cf48ee7fad3d80187e32069f9593b/types_dateparser-1.3.0.20260206.tar.gz", hash = "sha256:8d2ee287aa33f0b26fe8cc48cd8e28f958ada839b2db5a4e3f54371dfe77a696", size = 16516, upload-time = "2026-02-06T04:03:39.022Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/5a/a5cf930804f639f5f1c58434613a1bbc1bd4641e29aec07444f316b41dff/types_dateparser-1.2.2.20250809-py3-none-any.whl", hash = "sha256:f12ae46abc3085e60e16fbe55730c5acbce980cbe3b176b17b08b4cef85850ef", size = 22140, upload-time = "2025-08-09T03:15:10.234Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/31/2d8b6c693f015349885092173e604af2e480c3fd0a16cdb5ca97024238b0/types_dateparser-1.3.0.20260206-py3-none-any.whl", hash = "sha256:d6e5d33101b46d9cc14866f105806c80c9b8826492c75e9b323c8fc45ceb1390", size = 22963, upload-time = "2026-02-06T04:03:37.711Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user