Merge pull request #11 from tienthienhd/feature/frontend-warehouses
Feature/frontend warehouses
This commit is contained in:
commit
0511fb09c9
@ -9,6 +9,7 @@ import { DocumentTypeListComponent } from './components/manage/document-type-lis
|
|||||||
import { LogsComponent } from './components/admin/logs/logs.component'
|
import { LogsComponent } from './components/admin/logs/logs.component'
|
||||||
import { SettingsComponent } from './components/admin/settings/settings.component'
|
import { SettingsComponent } from './components/admin/settings/settings.component'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
|
import { WarehouseListComponent } from './components/manage/warehouse-list/warehouse-list.component'
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||||
@ -103,6 +104,17 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'warehouses',
|
||||||
|
component: WarehouseListComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
requiredPermission: {
|
||||||
|
action: PermissionAction.View,
|
||||||
|
type: PermissionType.Warehouse,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'documenttypes',
|
path: 'documenttypes',
|
||||||
component: DocumentTypeListComponent,
|
component: DocumentTypeListComponent,
|
||||||
|
@ -12,6 +12,7 @@ import { DocumentListComponent } from './components/document-list/document-list.
|
|||||||
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
|
||||||
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
import { DashboardComponent } from './components/dashboard/dashboard.component'
|
||||||
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
|
||||||
|
import { WarehouseListComponent } from './components/manage/warehouse-list/warehouse-list.component'
|
||||||
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
|
||||||
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
|
||||||
import { LogsComponent } from './components/admin/logs/logs.component'
|
import { LogsComponent } from './components/admin/logs/logs.component'
|
||||||
@ -22,6 +23,7 @@ import { NotFoundComponent } from './components/not-found/not-found.component'
|
|||||||
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
|
||||||
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
|
import { WarehouseEditDialogComponent } from './components/common/edit-dialog/warehouse-edit-dialog/warehouse-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { TagComponent } from './components/common/tag/tag.component'
|
import { TagComponent } from './components/common/tag/tag.component'
|
||||||
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
||||||
@ -388,6 +390,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DocumentTypeListComponent,
|
DocumentTypeListComponent,
|
||||||
CorrespondentListComponent,
|
CorrespondentListComponent,
|
||||||
StoragePathListComponent,
|
StoragePathListComponent,
|
||||||
|
WarehouseListComponent,
|
||||||
LogsComponent,
|
LogsComponent,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
@ -396,6 +399,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
TagEditDialogComponent,
|
TagEditDialogComponent,
|
||||||
DocumentTypeEditDialogComponent,
|
DocumentTypeEditDialogComponent,
|
||||||
StoragePathEditDialogComponent,
|
StoragePathEditDialogComponent,
|
||||||
|
WarehouseEditDialogComponent,
|
||||||
TagComponent,
|
TagComponent,
|
||||||
ClearableBadgeComponent,
|
ClearableBadgeComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
|
@ -203,6 +203,13 @@
|
|||||||
<i-bs class="me-1" name="folder"></i-bs><span> <ng-container i18n>Storage Paths</ng-container></span>
|
<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.Warehouse }">
|
||||||
|
<a class="nav-link" routerLink="warehouses" routerLinkActive="active" (click)="closeMenu()"
|
||||||
|
ngbPopover="Warehouses" 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>Warehouses</ng-container></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
|
||||||
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
|
||||||
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||||
|
<pngx-input-text i18n-title title="Type" formControlName="type" [error]="error?.type" autocomplete="off"></pngx-input-text>
|
||||||
|
<pngx-input-text i18n-title title="Parent Warehouse" formControlName="parent_warehouse" [error]="error?.parent_warehouse" autocomplete="off"></pngx-input-text>
|
||||||
|
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||||
|
@if (patternRequired) {
|
||||||
|
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||||
|
}
|
||||||
|
@if (patternRequired) {
|
||||||
|
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div *pngxIfOwner="object">
|
||||||
|
<pngx-permissions-form [users]="users" accordion="true" formControlName="permissions_form"></pngx-permissions-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,64 @@
|
|||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { CheckComponent } from '../../input/check/check.component'
|
||||||
|
import { ColorComponent } from '../../input/color/color.component'
|
||||||
|
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||||
|
import { SelectComponent } from '../../input/select/select.component'
|
||||||
|
import { TextComponent } from '../../input/text/text.component'
|
||||||
|
import { EditDialogMode } from '../edit-dialog.component'
|
||||||
|
import { WarehouseEditDialogComponent } from './warehouse-edit-dialog.component'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
|
describe('WarehouseEditDialogComponent', () => {
|
||||||
|
let component: WarehouseEditDialogComponent
|
||||||
|
let settingsService: SettingsService
|
||||||
|
let fixture: ComponentFixture<WarehouseEditDialogComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
WarehouseEditDialogComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
IfOwnerDirective,
|
||||||
|
SelectComponent,
|
||||||
|
TextComponent,
|
||||||
|
PermissionsFormComponent,
|
||||||
|
CheckComponent,
|
||||||
|
],
|
||||||
|
providers: [NgbActiveModal, SettingsService],
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgSelectModule,
|
||||||
|
NgbModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WarehouseEditDialogComponent)
|
||||||
|
settingsService = TestBed.inject(SettingsService)
|
||||||
|
settingsService.currentUser = { id: 99, username: 'user99' }
|
||||||
|
component = fixture.componentInstance
|
||||||
|
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support create and edit modes', () => {
|
||||||
|
component.dialogMode = EditDialogMode.CREATE
|
||||||
|
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||||
|
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(createTitleSpy).toHaveBeenCalled()
|
||||||
|
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||||
|
component.dialogMode = EditDialogMode.EDIT
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(editTitleSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-warehouse-edit-dialog',
|
||||||
|
templateUrl: './warehouse-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./warehouse-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class WarehouseEditDialogComponent extends EditDialogComponent<Warehouse> {
|
||||||
|
constructor(
|
||||||
|
service: WarehouseService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
userService: UserService,
|
||||||
|
settingsService: SettingsService
|
||||||
|
) {
|
||||||
|
super(service, activeModal, userService, settingsService)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new warehouse`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit warehouse`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(''),
|
||||||
|
type: new FormControl(''),
|
||||||
|
parent_warehouse: new FormControl(''),
|
||||||
|
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||||
|
match: new FormControl(''),
|
||||||
|
is_insensitive: new FormControl(true),
|
||||||
|
permissions_form: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
@if (tag === undefined) {
|
||||||
|
@if (!clickable) {
|
||||||
|
<span class="badge private" i18n>Private</span>
|
||||||
|
}
|
||||||
|
@if (clickable) {
|
||||||
|
<a [title]="linkTitle" class="badge private" i18n>Private</a>
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
opacity: .5;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { WarehouseComponent } from './warehouse.component'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
|
|
||||||
|
const warehouse: Warehouse = {
|
||||||
|
id: 1,
|
||||||
|
type: 'Warehouse',
|
||||||
|
name: 'Warehouse1',
|
||||||
|
parent_warehouse: null,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WarehouseComponent', () => {
|
||||||
|
let component: WarehouseComponent
|
||||||
|
let fixture: ComponentFixture<WarehouseComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [WarehouseComponent],
|
||||||
|
providers: [],
|
||||||
|
imports: [],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WarehouseComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('should handle private warehouses', () => {
|
||||||
|
expect(
|
||||||
|
fixture.debugElement.query(By.css('span')).nativeElement.textContent
|
||||||
|
).toEqual('Private')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support clickable option', () => {
|
||||||
|
component.warehouse = warehouse
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.debugElement.query(By.css('a.badge'))).toBeNull()
|
||||||
|
component.clickable = true
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.debugElement.query(By.css('a.badge'))).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-warehouse',
|
||||||
|
templateUrl: './warehouse.component.html',
|
||||||
|
styleUrls: ['./warehouse.component.scss'],
|
||||||
|
})
|
||||||
|
export class WarehouseComponent {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
warehouse: Warehouse
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
linkTitle: string = ''
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
clickable: boolean = false
|
||||||
|
}
|
@ -84,6 +84,14 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Warehouse }">
|
||||||
|
@if (statistics?.warehouse_count > 0) {
|
||||||
|
<a class="list-group-item d-flex justify-content-between align-items-center" routerLink="/warehouses/">
|
||||||
|
<ng-container i18n>Warehouses</ng-container>:
|
||||||
|
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.warehouse_count | number}}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</pngx-widget-frame>
|
</pngx-widget-frame>
|
||||||
|
@ -18,6 +18,7 @@ export interface Statistics {
|
|||||||
correspondent_count?: number
|
correspondent_count?: number
|
||||||
document_type_count?: number
|
document_type_count?: number
|
||||||
storage_path_count?: number
|
storage_path_count?: number
|
||||||
|
warehouse_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DocumentFileType {
|
interface DocumentFileType {
|
||||||
|
@ -111,6 +111,8 @@
|
|||||||
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
|
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
|
||||||
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||||
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
|
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
|
||||||
|
<pngx-input-select [items]="warehouses" i18n-title title="Warehouse" formControlName="warehouses" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
|
||||||
|
(createNew)="createWarehouse($event)" [suggestions]="suggestions?.warehouses" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Warehouse }"></pngx-input-select>
|
||||||
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
|
<pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
|
||||||
@for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
|
@for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
|
||||||
<div [formGroup]="customFieldFormFields.controls[i]">
|
<div [formGroup]="customFieldFormFields.controls[i]">
|
||||||
|
@ -29,6 +29,7 @@ import {
|
|||||||
FILTER_CORRESPONDENT,
|
FILTER_CORRESPONDENT,
|
||||||
FILTER_DOCUMENT_TYPE,
|
FILTER_DOCUMENT_TYPE,
|
||||||
FILTER_STORAGE_PATH,
|
FILTER_STORAGE_PATH,
|
||||||
|
FILTER_WAREHOUSE,
|
||||||
FILTER_HAS_TAGS_ALL,
|
FILTER_HAS_TAGS_ALL,
|
||||||
FILTER_CREATED_AFTER,
|
FILTER_CREATED_AFTER,
|
||||||
FILTER_CREATED_BEFORE,
|
FILTER_CREATED_BEFORE,
|
||||||
@ -37,6 +38,7 @@ import { Correspondent } from 'src/app/data/correspondent'
|
|||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
@ -52,6 +54,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
|||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.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'
|
||||||
@ -59,6 +62,7 @@ import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.
|
|||||||
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { WarehouseEditDialogComponent } from '../common/edit-dialog/warehouse-edit-dialog/warehouse-edit-dialog.component'
|
||||||
import { DateComponent } from '../common/input/date/date.component'
|
import { DateComponent } from '../common/input/date/date.component'
|
||||||
import { NumberComponent } from '../common/input/number/number.component'
|
import { NumberComponent } from '../common/input/number/number.component'
|
||||||
import { PermissionsFormComponent } from '../common/input/permissions/permissions-form/permissions-form.component'
|
import { PermissionsFormComponent } from '../common/input/permissions/permissions-form/permissions-form.component'
|
||||||
@ -165,6 +169,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
DocumentTypeEditDialogComponent,
|
DocumentTypeEditDialogComponent,
|
||||||
CorrespondentEditDialogComponent,
|
CorrespondentEditDialogComponent,
|
||||||
StoragePathEditDialogComponent,
|
StoragePathEditDialogComponent,
|
||||||
|
WarehouseEditDialogComponent,
|
||||||
IfOwnerDirective,
|
IfOwnerDirective,
|
||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
SafeHtmlPipe,
|
SafeHtmlPipe,
|
||||||
@ -220,6 +225,20 @@ describe('DocumentDetailComponent', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: WarehouseService,
|
||||||
|
useValue: {
|
||||||
|
listAll: () =>
|
||||||
|
of({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 41,
|
||||||
|
name: 'Warehouse41',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@ -366,6 +385,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(component.correspondents).toBeUndefined()
|
expect(component.correspondents).toBeUndefined()
|
||||||
expect(component.documentTypes).toBeUndefined()
|
expect(component.documentTypes).toBeUndefined()
|
||||||
expect(component.storagePaths).toBeUndefined()
|
expect(component.storagePaths).toBeUndefined()
|
||||||
|
expect(component.warehouses).toBeUndefined()
|
||||||
expect(component.users).toBeUndefined()
|
expect(component.users).toBeUndefined()
|
||||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||||
httpTestingController.expectNone(
|
httpTestingController.expectNone(
|
||||||
@ -377,6 +397,9 @@ describe('DocumentDetailComponent', () => {
|
|||||||
httpTestingController.expectNone(
|
httpTestingController.expectNone(
|
||||||
`${environment.apiBaseUrl}documents/storage_paths/`
|
`${environment.apiBaseUrl}documents/storage_paths/`
|
||||||
)
|
)
|
||||||
|
httpTestingController.expectNone(
|
||||||
|
`${environment.apiBaseUrl}documents/warehouses/`
|
||||||
|
)
|
||||||
currentUserCan = true
|
currentUserCan = true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -419,6 +442,20 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(component.documentForm.get('storage_path').value).toEqual(12)
|
expect(component.documentForm.get('storage_path').value).toEqual(12)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support creating warehouse', () => {
|
||||||
|
initNormally()
|
||||||
|
let openModal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||||
|
const modalSpy = jest.spyOn(modalService, 'open')
|
||||||
|
component.createWarehouse('NewWarehouse12')
|
||||||
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
|
openModal.componentInstance.succeeded.next({
|
||||||
|
id: 12,
|
||||||
|
name: 'NewWarehouse12',
|
||||||
|
})
|
||||||
|
expect(component.documentForm.get('warehouse').value).toEqual(12)
|
||||||
|
})
|
||||||
|
|
||||||
it('should allow dischard changes', () => {
|
it('should allow dischard changes', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.title = 'Foo Bar'
|
component.title = 'Foo Bar'
|
||||||
@ -819,6 +856,24 @@ describe('DocumentDetailComponent', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support quick filtering by warehouse', () => {
|
||||||
|
initNormally()
|
||||||
|
const object = {
|
||||||
|
id: 22,
|
||||||
|
name: 'Warehouse22',
|
||||||
|
type: 'Warehouse',
|
||||||
|
parent_warehouse: 22,
|
||||||
|
} as Warehouse
|
||||||
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
|
component.filterDocuments([object])
|
||||||
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_WAREHOUSE,
|
||||||
|
value: object.id.toString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('should support quick filtering by all tags', () => {
|
it('should support quick filtering by all tags', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
const object1 = {
|
const object1 = {
|
||||||
|
@ -44,10 +44,14 @@ import {
|
|||||||
FILTER_FULLTEXT_MORELIKE,
|
FILTER_FULLTEXT_MORELIKE,
|
||||||
FILTER_HAS_TAGS_ALL,
|
FILTER_HAS_TAGS_ALL,
|
||||||
FILTER_STORAGE_PATH,
|
FILTER_STORAGE_PATH,
|
||||||
|
FILTER_WAREHOUSE,
|
||||||
} from 'src/app/data/filter-rule-type'
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
|
import { WarehouseEditDialogComponent } from '../common/edit-dialog/warehouse-edit-dialog/warehouse-edit-dialog.component'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
@ -134,6 +138,8 @@ export class DocumentDetailComponent
|
|||||||
correspondents: Correspondent[]
|
correspondents: Correspondent[]
|
||||||
documentTypes: DocumentType[]
|
documentTypes: DocumentType[]
|
||||||
storagePaths: StoragePath[]
|
storagePaths: StoragePath[]
|
||||||
|
warehouses: Warehouse[]
|
||||||
|
|
||||||
|
|
||||||
documentForm: FormGroup = new FormGroup({
|
documentForm: FormGroup = new FormGroup({
|
||||||
title: new FormControl(''),
|
title: new FormControl(''),
|
||||||
@ -142,6 +148,7 @@ export class DocumentDetailComponent
|
|||||||
correspondent: new FormControl(),
|
correspondent: new FormControl(),
|
||||||
document_type: new FormControl(),
|
document_type: new FormControl(),
|
||||||
storage_path: new FormControl(),
|
storage_path: new FormControl(),
|
||||||
|
warehouses: new FormControl(),
|
||||||
archive_serial_number: new FormControl(),
|
archive_serial_number: new FormControl(),
|
||||||
tags: new FormControl([]),
|
tags: new FormControl([]),
|
||||||
permissions_form: new FormControl(null),
|
permissions_form: new FormControl(null),
|
||||||
@ -197,6 +204,7 @@ export class DocumentDetailComponent
|
|||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private settings: SettingsService,
|
private settings: SettingsService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
|
private warehouseService: WarehouseService,
|
||||||
private permissionsService: PermissionsService,
|
private permissionsService: PermissionsService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private customFieldsService: CustomFieldsService,
|
private customFieldsService: CustomFieldsService,
|
||||||
@ -285,6 +293,17 @@ export class DocumentDetailComponent
|
|||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result) => (this.storagePaths = result.results))
|
.subscribe((result) => (this.storagePaths = result.results))
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Warehouse
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.warehouseService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((result) => (this.warehouses = result.results))
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.permissionsService.currentUserCan(
|
this.permissionsService.currentUserCan(
|
||||||
PermissionAction.View,
|
PermissionAction.View,
|
||||||
@ -408,6 +427,7 @@ export class DocumentDetailComponent
|
|||||||
correspondent: doc.correspondent,
|
correspondent: doc.correspondent,
|
||||||
document_type: doc.document_type,
|
document_type: doc.document_type,
|
||||||
storage_path: doc.storage_path,
|
storage_path: doc.storage_path,
|
||||||
|
warehouses: doc.warehouses,
|
||||||
archive_serial_number: doc.archive_serial_number,
|
archive_serial_number: doc.archive_serial_number,
|
||||||
tags: [...doc.tags],
|
tags: [...doc.tags],
|
||||||
permissions_form: {
|
permissions_form: {
|
||||||
@ -602,6 +622,27 @@ export class DocumentDetailComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createWarehouse(newName: string) {
|
||||||
|
var modal = this.modalService.open(WarehouseEditDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||||
|
if (newName) modal.componentInstance.object = { name: newName }
|
||||||
|
modal.componentInstance.succeeded
|
||||||
|
.pipe(
|
||||||
|
switchMap((newWarehouse) => {
|
||||||
|
return this.warehouseService
|
||||||
|
.listAll()
|
||||||
|
.pipe(map((warehouses) => ({ newWarehouse, warehouses })))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(({ newWarehouse, warehouses }) => {
|
||||||
|
this.warehouses = warehouses.results
|
||||||
|
this.documentForm.get('warehouses').setValue(newWarehouse.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.get(this.documentId)
|
.get(this.documentId)
|
||||||
@ -968,6 +1009,12 @@ export class DocumentDetailComponent
|
|||||||
rule_type: FILTER_STORAGE_PATH,
|
rule_type: FILTER_STORAGE_PATH,
|
||||||
value: (i as StoragePath).id.toString(),
|
value: (i as StoragePath).id.toString(),
|
||||||
}
|
}
|
||||||
|
} else if (i.hasOwnProperty('type')) {
|
||||||
|
// Warehouse
|
||||||
|
return {
|
||||||
|
rule_type: FILTER_WAREHOUSE,
|
||||||
|
value: (i as Warehouse).id.toString(),
|
||||||
|
}
|
||||||
} else if (i.hasOwnProperty('is_inbox_tag')) {
|
} else if (i.hasOwnProperty('is_inbox_tag')) {
|
||||||
// Tag
|
// Tag
|
||||||
return {
|
return {
|
||||||
|
@ -74,6 +74,22 @@
|
|||||||
(apply)="setStoragePaths($event)">
|
(apply)="setStoragePaths($event)">
|
||||||
</pngx-filterable-dropdown>
|
</pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Warehouse)) {
|
||||||
|
<pngx-filterable-dropdown title="Warehouses" icon="warehouse-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter warehouses" i18n-filterPlaceholder
|
||||||
|
[items]="warehouses"
|
||||||
|
[disabled]="!userCanEditAll"
|
||||||
|
[editing]="true"
|
||||||
|
[manyToOne]="true"
|
||||||
|
[applyOnClose]="applyOnClose"
|
||||||
|
[createRef]="createWarehouse.bind(this)"
|
||||||
|
(opened)="openWarehousesDropdown()"
|
||||||
|
[(selectionModel)]="warehouseSelectionModel"
|
||||||
|
[documentCounts]="warehouseDocumentCounts"
|
||||||
|
(apply)="setWarehouses($event)">
|
||||||
|
</pngx-filterable-dropdown>
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||||
<div class="btn-toolbar">
|
<div class="btn-toolbar">
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
DocumentService,
|
DocumentService,
|
||||||
} from 'src/app/services/rest/document.service'
|
} from 'src/app/services/rest/document.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
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'
|
||||||
@ -49,9 +50,11 @@ import { Tag } from 'src/app/data/tag'
|
|||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { WarehouseEditDialogComponent } from '../../common/edit-dialog/warehouse-edit-dialog/warehouse-edit-dialog.component'
|
||||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||||
@ -82,6 +85,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
let correspondentsService: CorrespondentService
|
let correspondentsService: CorrespondentService
|
||||||
let documentTypeService: DocumentTypeService
|
let documentTypeService: DocumentTypeService
|
||||||
let storagePathService: StoragePathService
|
let storagePathService: StoragePathService
|
||||||
|
let warehouseService: WarehouseService
|
||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -148,6 +152,18 @@ describe('BulkEditorComponent', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: WarehouseService,
|
||||||
|
useValue: {
|
||||||
|
listAll: () =>
|
||||||
|
of({
|
||||||
|
results: [
|
||||||
|
{ id: 88, name: 'warehouse88' },
|
||||||
|
{ id: 77, name: 'warehouse77' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
FilterPipe,
|
FilterPipe,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
{
|
{
|
||||||
@ -189,6 +205,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
correspondentsService = TestBed.inject(CorrespondentService)
|
correspondentsService = TestBed.inject(CorrespondentService)
|
||||||
documentTypeService = TestBed.inject(DocumentTypeService)
|
documentTypeService = TestBed.inject(DocumentTypeService)
|
||||||
storagePathService = TestBed.inject(StoragePathService)
|
storagePathService = TestBed.inject(StoragePathService)
|
||||||
|
warehouseService = TestBed.inject(WarehouseService)
|
||||||
httpTestingController = TestBed.inject(HttpTestingController)
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
|
|
||||||
fixture = TestBed.createComponent(BulkEditorComponent)
|
fixture = TestBed.createComponent(BulkEditorComponent)
|
||||||
@ -262,6 +279,22 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
|
expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should apply selection data to warehouse menu', () => {
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(
|
||||||
|
component.warehousesSelectionModel.getSelectedItems()
|
||||||
|
).toHaveLength(0)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 5, 7]))
|
||||||
|
jest
|
||||||
|
.spyOn(documentService, 'getSelectionData')
|
||||||
|
.mockReturnValue(of(selectionData))
|
||||||
|
component.openWarehouseDropdown()
|
||||||
|
expect(component.warehousesSelectionModel.selectionSize()).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should execute modify tags bulk operation', () => {
|
it('should execute modify tags bulk operation', () => {
|
||||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
jest
|
jest
|
||||||
@ -679,6 +712,105 @@ describe('BulkEditorComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should execute modify warehouse bulk operation', () => {
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = false
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setWarehouses({
|
||||||
|
itemsToAdd: [{ id: 101 }],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
|
let req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
|
)
|
||||||
|
req.flush(true)
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
documents: [3, 4],
|
||||||
|
method: 'set_warehouse',
|
||||||
|
parameters: { warehouse: 101 },
|
||||||
|
})
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
) // list reload
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
|
) // listAllFilteredIds
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute modify warehouse bulk operation with confirmation dialog if enabled', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = true
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setWarehouses({
|
||||||
|
itemsToAdd: [{ id: 101 }],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
modal.componentInstance.confirm()
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||||
|
.flush(true)
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
) // list reload
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
|
) // listAllFilteredIds
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set modal dialog text accordingly for warehouse edit confirmation', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = true
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.setWarehouses({
|
||||||
|
itemsToAdd: [],
|
||||||
|
itemsToRemove: [{ id: 101, name: 'Warehouse 101' }],
|
||||||
|
})
|
||||||
|
expect(modal.componentInstance.message).toEqual(
|
||||||
|
'This operation will remove the warehouse from 2 selected document(s).'
|
||||||
|
)
|
||||||
|
modal.close()
|
||||||
|
component.setWarehouses({
|
||||||
|
itemsToAdd: [{ id: 101, name: 'Warehouse 101' }],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
|
expect(modal.componentInstance.message).toEqual(
|
||||||
|
'This operation will assign the storage path "Warehouse 101" to 2 selected document(s).'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
it('should only execute bulk operations when changes are detected', () => {
|
it('should only execute bulk operations when changes are detected', () => {
|
||||||
component.setTags({
|
component.setTags({
|
||||||
itemsToAdd: [],
|
itemsToAdd: [],
|
||||||
@ -696,6 +828,10 @@ describe('BulkEditorComponent', () => {
|
|||||||
itemsToAdd: [],
|
itemsToAdd: [],
|
||||||
itemsToRemove: [],
|
itemsToRemove: [],
|
||||||
})
|
})
|
||||||
|
component.setWarehouses({
|
||||||
|
itemsToAdd: [],
|
||||||
|
itemsToRemove: [],
|
||||||
|
})
|
||||||
httpTestingController.expectNone(
|
httpTestingController.expectNone(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
@ -988,6 +1124,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect(component.correspondents).toBeUndefined()
|
expect(component.correspondents).toBeUndefined()
|
||||||
expect(component.documentTypes).toBeUndefined()
|
expect(component.documentTypes).toBeUndefined()
|
||||||
expect(component.storagePaths).toBeUndefined()
|
expect(component.storagePaths).toBeUndefined()
|
||||||
|
expect(component.warehouses).toBeUndefined()
|
||||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||||
httpTestingController.expectNone(
|
httpTestingController.expectNone(
|
||||||
`${environment.apiBaseUrl}documents/correspondents/`
|
`${environment.apiBaseUrl}documents/correspondents/`
|
||||||
@ -998,6 +1135,9 @@ describe('BulkEditorComponent', () => {
|
|||||||
httpTestingController.expectNone(
|
httpTestingController.expectNone(
|
||||||
`${environment.apiBaseUrl}documents/storage_paths/`
|
`${environment.apiBaseUrl}documents/storage_paths/`
|
||||||
)
|
)
|
||||||
|
httpTestingController.expectNone(
|
||||||
|
`${environment.apiBaseUrl}documents/warehouses/`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support create new tag', () => {
|
it('should support create new tag', () => {
|
||||||
@ -1175,4 +1315,48 @@ describe('BulkEditorComponent', () => {
|
|||||||
)
|
)
|
||||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support create new warehouse', () => {
|
||||||
|
const name = 'New Warehouse'
|
||||||
|
const newWarehouse = { id: 101, name: 'New Warehouse' }
|
||||||
|
const warehouses: Results<Warehouse> = {
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'Warehouse 1' },
|
||||||
|
{ id: 2, name: 'Warehouse 2' },
|
||||||
|
],
|
||||||
|
count: 2,
|
||||||
|
all: [1, 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalInstance = {
|
||||||
|
componentInstance: {
|
||||||
|
dialogMode: EditDialogMode.CREATE,
|
||||||
|
object: { name },
|
||||||
|
succeeded: of(newWarehouse),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const warehousesListAllSpy = jest.spyOn(warehouseService, 'listAll')
|
||||||
|
warehousesListAllSpy.mockReturnValue(of(warehouses))
|
||||||
|
|
||||||
|
const warehousesSelectionModelToggleSpy = jest.spyOn(
|
||||||
|
component.warehousesSelectionModel,
|
||||||
|
'toggle'
|
||||||
|
)
|
||||||
|
|
||||||
|
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||||
|
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||||
|
|
||||||
|
component.createWarehouse(name)
|
||||||
|
|
||||||
|
expect(modalServiceOpenSpy).toHaveBeenCalledWith(
|
||||||
|
WarehouseEditDialogComponent,
|
||||||
|
{ backdrop: 'static' }
|
||||||
|
)
|
||||||
|
expect(warehousesListAllSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(warehousesSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||||
|
newWarehouse.id
|
||||||
|
)
|
||||||
|
expect(component.warehouses).toEqual(warehouses.results)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -24,6 +24,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||||
@ -39,6 +41,7 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
|||||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { WarehouseEditDialogComponent } from '../../common/edit-dialog/warehouse-edit-dialog/warehouse-edit-dialog.component'
|
||||||
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
|
||||||
|
|
||||||
@ -55,15 +58,19 @@ export class BulkEditorComponent
|
|||||||
correspondents: Correspondent[]
|
correspondents: Correspondent[]
|
||||||
documentTypes: DocumentType[]
|
documentTypes: DocumentType[]
|
||||||
storagePaths: StoragePath[]
|
storagePaths: StoragePath[]
|
||||||
|
warehouses: Warehouse[]
|
||||||
|
|
||||||
|
|
||||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
warehousesSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
correspondentDocumentCounts: SelectionDataItem[]
|
correspondentDocumentCounts: SelectionDataItem[]
|
||||||
documentTypeDocumentCounts: SelectionDataItem[]
|
documentTypeDocumentCounts: SelectionDataItem[]
|
||||||
storagePathDocumentCounts: SelectionDataItem[]
|
storagePathDocumentCounts: SelectionDataItem[]
|
||||||
|
warehouseDocumentCounts: SelectionDataItem[]
|
||||||
awaitingDownload: boolean
|
awaitingDownload: boolean
|
||||||
|
|
||||||
unsubscribeNotifier: Subject<any> = new Subject()
|
unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
@ -85,6 +92,7 @@ export class BulkEditorComponent
|
|||||||
private settings: SettingsService,
|
private settings: SettingsService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
|
private warehouseService: WarehouseService,
|
||||||
private permissionService: PermissionsService
|
private permissionService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@ -166,6 +174,17 @@ export class BulkEditorComponent
|
|||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((result) => (this.storagePaths = result.results))
|
.subscribe((result) => (this.storagePaths = result.results))
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.permissionService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Warehouse
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.warehouseService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.warehouses = result.results))
|
||||||
|
}
|
||||||
|
|
||||||
this.downloadForm
|
this.downloadForm
|
||||||
.get('downloadFileTypeArchive')
|
.get('downloadFileTypeArchive')
|
||||||
@ -297,6 +316,19 @@ export class BulkEditorComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openWarehouseDropdown() {
|
||||||
|
this.documentService
|
||||||
|
.getSelectionData(Array.from(this.list.selected))
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((s) => {
|
||||||
|
this.warehouseDocumentCounts = s.selected_warehouses
|
||||||
|
this.applySelectionData(
|
||||||
|
s.selected_warehouses,
|
||||||
|
this.warehousesSelectionModel
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private _localizeList(items: MatchingModel[]) {
|
private _localizeList(items: MatchingModel[]) {
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
return ''
|
return ''
|
||||||
@ -495,6 +527,44 @@ export class BulkEditorComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setWarehouses(changedDocumentPaths: ChangedItems) {
|
||||||
|
if (
|
||||||
|
changedDocumentPaths.itemsToAdd.length == 0 &&
|
||||||
|
changedDocumentPaths.itemsToRemove.length == 0
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
let warehouse =
|
||||||
|
changedDocumentPaths.itemsToAdd.length > 0
|
||||||
|
? changedDocumentPaths.itemsToAdd[0]
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (this.showConfirmationDialogs) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.title = $localize`Confirm warehouse assignment`
|
||||||
|
if (warehouse) {
|
||||||
|
modal.componentInstance.message = $localize`This operation will assign the warehouse "${warehouse.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
|
} else {
|
||||||
|
modal.componentInstance.message = $localize`This operation will remove the warehouse from ${this.list.selected.size} selected document(s).`
|
||||||
|
}
|
||||||
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.executeBulkOperation(modal, 'set_warehouse', {
|
||||||
|
warehouse: warehouse ? warehouse.id : null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.executeBulkOperation(null, 'set_warehouse', {
|
||||||
|
warehouse: warehouse ? warehouse.id : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createTag(name: string) {
|
createTag(name: string) {
|
||||||
let modal = this.modalService.open(TagEditDialogComponent, {
|
let modal = this.modalService.open(TagEditDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
@ -581,6 +651,27 @@ export class BulkEditorComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createWarehouse(name: string) {
|
||||||
|
let modal = this.modalService.open(WarehouseEditDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||||
|
modal.componentInstance.object = { name }
|
||||||
|
modal.componentInstance.succeeded
|
||||||
|
.pipe(
|
||||||
|
switchMap((newWarehouse) => {
|
||||||
|
return this.warehouseService
|
||||||
|
.listAll()
|
||||||
|
.pipe(map((warehouses) => ({ newWarehouse, warehouses })))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(({ newWarehouse, warehouses }) => {
|
||||||
|
this.warehouses = warehouses.results
|
||||||
|
this.warehousesSelectionModel.toggle(newWarehouse.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
applyDelete() {
|
applyDelete() {
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
|
@ -83,6 +83,12 @@
|
|||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@if (document.warehouses) {
|
||||||
|
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by warehouse" i18n-title
|
||||||
|
(click)="clickWarehouse.emit(document.warehouses);$event.stopPropagation()">
|
||||||
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.warehouses$ | async)?.name}}</small>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
@if (document.archive_serial_number | isNumber) {
|
@if (document.archive_serial_number | isNumber) {
|
||||||
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
||||||
|
@ -53,6 +53,9 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
|||||||
@Output()
|
@Output()
|
||||||
clickStoragePath = new EventEmitter<number>()
|
clickStoragePath = new EventEmitter<number>()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
clickWarehouse = new EventEmitter<number>()
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
clickMoreLike = new EventEmitter()
|
clickMoreLike = new EventEmitter()
|
||||||
|
|
||||||
|
@ -54,6 +54,13 @@
|
|||||||
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@if (document.warehouses) {
|
||||||
|
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle warehouse filter" i18n-title
|
||||||
|
(click)="clickWarehouse.emit(document.warehouses);$event.stopPropagation()">
|
||||||
|
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
|
||||||
|
<small>{{(document.warehouses$ | async)?.name ?? privateName}}</small>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||||
<ng-template #dateTooltip>
|
<ng-template #dateTooltip>
|
||||||
<div class="d-flex flex-column text-light">
|
<div class="d-flex flex-column text-light">
|
||||||
|
@ -50,6 +50,9 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
|||||||
@Output()
|
@Output()
|
||||||
clickStoragePath = new EventEmitter<number>()
|
clickStoragePath = new EventEmitter<number>()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
clickWarehouse = new EventEmitter<number>()
|
||||||
|
|
||||||
moreTags: number = null
|
moreTags: number = null
|
||||||
|
|
||||||
@ViewChild('popover') popover: NgbPopover
|
@ViewChild('popover') popover: NgbPopover
|
||||||
|
@ -192,6 +192,15 @@
|
|||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Storage path</th>
|
i18n>Storage path</th>
|
||||||
}
|
}
|
||||||
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Warehouse)) {
|
||||||
|
<th class="d-none d-xl-table-cell"
|
||||||
|
pngxSortable="warehouse__name"
|
||||||
|
title="Sort by warehouse" i18n-title
|
||||||
|
[currentSortField]="list.sortField"
|
||||||
|
[currentSortReverse]="list.sortReverse"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
i18n>Warehouse</th>
|
||||||
|
}
|
||||||
<th
|
<th
|
||||||
pngxSortable="created"
|
pngxSortable="created"
|
||||||
title="Sort by created date" i18n-title
|
title="Sort by created date" i18n-title
|
||||||
@ -260,6 +269,13 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Warehouse)) {
|
||||||
|
<td class="d-none d-xl-table-cell">
|
||||||
|
@if (d.warehouses) {
|
||||||
|
<a (click)="clickWarehouse(d.warehouses);$event.stopPropagation()" title="Filter by warehouse" i18n-title>{{(d.warehouses$ | async)?.name}}</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
<td>
|
<td>
|
||||||
{{d.created_date | customDate}}
|
{{d.created_date | customDate}}
|
||||||
</td>
|
</td>
|
||||||
|
@ -588,6 +588,7 @@ describe('DocumentListComponent', () => {
|
|||||||
component.clickCorrespondent(2)
|
component.clickCorrespondent(2)
|
||||||
component.clickDocumentType(3)
|
component.clickDocumentType(3)
|
||||||
component.clickStoragePath(4)
|
component.clickStoragePath(4)
|
||||||
|
component.clickWarehouse(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support quick filter on document more like', () => {
|
it('should support quick filter on document more like', () => {
|
||||||
|
@ -291,6 +291,11 @@ export class DocumentListComponent
|
|||||||
this.filterEditor.toggleStoragePath(storagePathID)
|
this.filterEditor.toggleStoragePath(storagePathID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickWarehouse(warehouseID: number) {
|
||||||
|
this.list.selectNone()
|
||||||
|
this.filterEditor.toggleWarehouse(warehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
clickMoreLike(documentID: number) {
|
clickMoreLike(documentID: number) {
|
||||||
this.list.quickFilter([
|
this.list.quickFilter([
|
||||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
||||||
|
@ -70,6 +70,16 @@
|
|||||||
[documentCounts]="storagePathDocumentCounts"
|
[documentCounts]="storagePathDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
|
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Warehouse)) {
|
||||||
|
<pngx-filterable-dropdown class="flex-fill" title="Warehouse" icon="folder-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter warehouses" i18n-filterPlaceholder
|
||||||
|
[items]="warehouses"
|
||||||
|
[(selectionModel)]="warehouseSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
(opened)="onWarehouseDropdownOpen()"
|
||||||
|
[documentCounts]="warehouseDocumentCounts"
|
||||||
|
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<pngx-date-dropdown
|
<pngx-date-dropdown
|
||||||
|
@ -45,6 +45,9 @@ import {
|
|||||||
FILTER_STORAGE_PATH,
|
FILTER_STORAGE_PATH,
|
||||||
FILTER_HAS_STORAGE_PATH_ANY,
|
FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
FILTER_WAREHOUSE,
|
||||||
|
FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
FILTER_OWNER,
|
FILTER_OWNER,
|
||||||
FILTER_OWNER_ANY,
|
FILTER_OWNER_ANY,
|
||||||
FILTER_OWNER_DOES_NOT_INCLUDE,
|
FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||||
@ -56,6 +59,7 @@ import { Correspondent } from 'src/app/data/correspondent'
|
|||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { StoragePath } from 'src/app/data/storage-path'
|
import { StoragePath } from 'src/app/data/storage-path'
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
@ -65,6 +69,7 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
|||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
@ -131,6 +136,17 @@ const storage_paths: StoragePath[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const warehouses: Warehouse[] = [
|
||||||
|
{
|
||||||
|
id: 42,
|
||||||
|
name: 'Warehouse32',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 43,
|
||||||
|
name: 'Warehouse33',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const users: User[] = [
|
const users: User[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -187,6 +203,12 @@ describe('FilterEditorComponent', () => {
|
|||||||
listAll: () => of({ results: storage_paths }),
|
listAll: () => of({ results: storage_paths }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: WarehouseService,
|
||||||
|
useValue: {
|
||||||
|
listAll: () => of({ results: warehouses }),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: UserService,
|
provide: UserService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@ -806,6 +828,89 @@ describe('FilterEditorComponent', () => {
|
|||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
it('should ingest filter rules for has warehouse', fakeAsync(() => {
|
||||||
|
expect(component.warehouseSelectionModel.getSelectedItems()).toHaveLength(
|
||||||
|
0
|
||||||
|
)
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_WAREHOUSE,
|
||||||
|
value: '42',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
expect(component.warehouseSelectionModel.logicalOperator).toEqual(
|
||||||
|
LogicalOperator.Or
|
||||||
|
)
|
||||||
|
expect(component.warehouseSelectionModel.intersection).toEqual(
|
||||||
|
Intersection.Include
|
||||||
|
)
|
||||||
|
expect(component.warehouseSelectionModel.getSelectedItems()).toEqual([
|
||||||
|
warehouses[0],
|
||||||
|
])
|
||||||
|
component.toggleWarehouse(42) // coverage
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('should ingest filter rules for has any of warehouse', fakeAsync(() => {
|
||||||
|
expect(component.warehouseSelectionModel.getSelectedItems()).toHaveLength(
|
||||||
|
0
|
||||||
|
)
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
value: '42',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
value: '43',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
expect(component.warehouseSelectionModel.logicalOperator).toEqual(
|
||||||
|
LogicalOperator.Or
|
||||||
|
)
|
||||||
|
expect(component.warehouseSelectionModel.intersection).toEqual(
|
||||||
|
Intersection.Include
|
||||||
|
)
|
||||||
|
expect(component.warehouseSelectionModel.getSelectedItems()).toEqual(
|
||||||
|
warehouses
|
||||||
|
)
|
||||||
|
// coverage
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('should ingest filter rules for does not have any of warehouses', fakeAsync(() => {
|
||||||
|
expect(component.warehouseSelectionModel.getExcludedItems()).toHaveLength(
|
||||||
|
0
|
||||||
|
)
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
value: '42',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
value: '43',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
expect(component.warehouseSelectionModel.intersection).toEqual(
|
||||||
|
Intersection.Exclude
|
||||||
|
)
|
||||||
|
expect(component.warehouseSelectionModel.getExcludedItems()).toEqual(
|
||||||
|
warehouses
|
||||||
|
)
|
||||||
|
// coverage
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
it('should ingest filter rules for owner', fakeAsync(() => {
|
it('should ingest filter rules for owner', fakeAsync(() => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
@ -1317,6 +1422,63 @@ describe('FilterEditorComponent', () => {
|
|||||||
])
|
])
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
it('should convert user input to correct filter rules on warehouse selections', fakeAsync(() => {
|
||||||
|
const warehouseFilterableDropdown = fixture.debugElement.queryAll(
|
||||||
|
By.directive(FilterableDropdownComponent)
|
||||||
|
)[4] // Warehouse dropdown
|
||||||
|
warehouseFilterableDropdown.triggerEventHandler('opened')
|
||||||
|
const warehouseButtons = warehouseFilterableDropdown.queryAll(
|
||||||
|
By.directive(ToggleableDropdownButtonComponent)
|
||||||
|
)
|
||||||
|
warehouseButtons[1].triggerEventHandler('toggle')
|
||||||
|
warehouseButtons[2].triggerEventHandler('toggle')
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.filterRules).toEqual([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
value: warehouses[0].id.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
value: warehouses[1].id.toString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const toggleIntersectionButtons = warehouseFilterableDropdown.queryAll(
|
||||||
|
By.css('input[type=radio]')
|
||||||
|
)
|
||||||
|
toggleIntersectionButtons[1].nativeElement.checked = true
|
||||||
|
toggleIntersectionButtons[1].triggerEventHandler('change')
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.filterRules).toEqual([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
value: warehouses[0].id.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
value: warehouses[1].id.toString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('should convert user input to correct filter rules on warehouse select not assigned', fakeAsync(() => {
|
||||||
|
const warehousesFilterableDropdown = fixture.debugElement.queryAll(
|
||||||
|
By.directive(FilterableDropdownComponent)
|
||||||
|
)[4]
|
||||||
|
warehousesFilterableDropdown.triggerEventHandler('opened')
|
||||||
|
const notAssignedButton = warehousesFilterableDropdown.queryAll(
|
||||||
|
By.directive(ToggleableDropdownButtonComponent)
|
||||||
|
)[0]
|
||||||
|
notAssignedButton.triggerEventHandler('toggle')
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.filterRules).toEqual([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_WAREHOUSE,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
|
||||||
it('should convert user input to correct filter rules on date created after', fakeAsync(() => {
|
it('should convert user input to correct filter rules on date created after', fakeAsync(() => {
|
||||||
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
const dateCreatedDropdown = fixture.debugElement.queryAll(
|
||||||
By.directive(DateDropdownComponent)
|
By.directive(DateDropdownComponent)
|
||||||
|
@ -11,10 +11,12 @@ import {
|
|||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
import { Subject, Subscription } from 'rxjs'
|
import { Subject, Subscription } from 'rxjs'
|
||||||
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
|
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { FilterRule } from 'src/app/data/filter-rule'
|
import { FilterRule } from 'src/app/data/filter-rule'
|
||||||
import { filterRulesDiffer } from 'src/app/utils/filter-rules'
|
import { filterRulesDiffer } from 'src/app/utils/filter-rules'
|
||||||
@ -35,15 +37,18 @@ import {
|
|||||||
FILTER_TITLE,
|
FILTER_TITLE,
|
||||||
FILTER_TITLE_CONTENT,
|
FILTER_TITLE_CONTENT,
|
||||||
FILTER_HAS_STORAGE_PATH_ANY,
|
FILTER_HAS_STORAGE_PATH_ANY,
|
||||||
|
FILTER_HAS_WAREHOUSE_ANY,
|
||||||
FILTER_ASN_ISNULL,
|
FILTER_ASN_ISNULL,
|
||||||
FILTER_ASN_GT,
|
FILTER_ASN_GT,
|
||||||
FILTER_ASN_LT,
|
FILTER_ASN_LT,
|
||||||
FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
FILTER_DOES_NOT_HAVE_CORRESPONDENT,
|
||||||
FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
|
||||||
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
FILTER_DOES_NOT_HAVE_STORAGE_PATH,
|
||||||
|
FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
FILTER_DOCUMENT_TYPE,
|
FILTER_DOCUMENT_TYPE,
|
||||||
FILTER_CORRESPONDENT,
|
FILTER_CORRESPONDENT,
|
||||||
FILTER_STORAGE_PATH,
|
FILTER_STORAGE_PATH,
|
||||||
|
FILTER_WAREHOUSE,
|
||||||
FILTER_OWNER,
|
FILTER_OWNER,
|
||||||
FILTER_OWNER_DOES_NOT_INCLUDE,
|
FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||||
FILTER_OWNER_ISNULL,
|
FILTER_OWNER_ISNULL,
|
||||||
@ -189,6 +194,16 @@ export class FilterEditorComponent
|
|||||||
return $localize`Without document type`
|
return $localize`Without document type`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case FILTER_WAREHOUSE:
|
||||||
|
case FILTER_HAS_WAREHOUSE_ANY:
|
||||||
|
if (rule.value) {
|
||||||
|
return $localize`Warehouse: ${this.warehouses.find(
|
||||||
|
(w) => w.id == +rule.value
|
||||||
|
)?.name}`
|
||||||
|
} else {
|
||||||
|
return $localize`Without warehouse`
|
||||||
|
}
|
||||||
|
|
||||||
case FILTER_STORAGE_PATH:
|
case FILTER_STORAGE_PATH:
|
||||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||||
if (rule.value) {
|
if (rule.value) {
|
||||||
@ -231,6 +246,7 @@ export class FilterEditorComponent
|
|||||||
constructor(
|
constructor(
|
||||||
private documentTypeService: DocumentTypeService,
|
private documentTypeService: DocumentTypeService,
|
||||||
private tagService: TagService,
|
private tagService: TagService,
|
||||||
|
private warehouseService: WarehouseService,
|
||||||
private correspondentService: CorrespondentService,
|
private correspondentService: CorrespondentService,
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
@ -246,11 +262,13 @@ export class FilterEditorComponent
|
|||||||
correspondents: Correspondent[] = []
|
correspondents: Correspondent[] = []
|
||||||
documentTypes: DocumentType[] = []
|
documentTypes: DocumentType[] = []
|
||||||
storagePaths: StoragePath[] = []
|
storagePaths: StoragePath[] = []
|
||||||
|
warehouses: Warehouse[] = []
|
||||||
|
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
correspondentDocumentCounts: SelectionDataItem[]
|
correspondentDocumentCounts: SelectionDataItem[]
|
||||||
documentTypeDocumentCounts: SelectionDataItem[]
|
documentTypeDocumentCounts: SelectionDataItem[]
|
||||||
storagePathDocumentCounts: SelectionDataItem[]
|
storagePathDocumentCounts: SelectionDataItem[]
|
||||||
|
warehouseDocumentCounts: SelectionDataItem[]
|
||||||
|
|
||||||
_textFilter = ''
|
_textFilter = ''
|
||||||
_moreLikeId: number
|
_moreLikeId: number
|
||||||
@ -288,6 +306,8 @@ export class FilterEditorComponent
|
|||||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
warehouseSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
|
|
||||||
|
|
||||||
dateCreatedBefore: string
|
dateCreatedBefore: string
|
||||||
dateCreatedAfter: string
|
dateCreatedAfter: string
|
||||||
@ -320,6 +340,7 @@ export class FilterEditorComponent
|
|||||||
|
|
||||||
this.documentTypeSelectionModel.clear(false)
|
this.documentTypeSelectionModel.clear(false)
|
||||||
this.storagePathSelectionModel.clear(false)
|
this.storagePathSelectionModel.clear(false)
|
||||||
|
this.warehouseSelectionModel.clear(false)
|
||||||
this.tagSelectionModel.clear(false)
|
this.tagSelectionModel.clear(false)
|
||||||
this.correspondentSelectionModel.clear(false)
|
this.correspondentSelectionModel.clear(false)
|
||||||
this._textFilter = null
|
this._textFilter = null
|
||||||
@ -470,6 +491,24 @@ export class FilterEditorComponent
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case FILTER_WAREHOUSE:
|
||||||
|
case FILTER_HAS_WAREHOUSE_ANY:
|
||||||
|
this.warehouseSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
|
this.warehouseSelectionModel.intersection = Intersection.Include
|
||||||
|
this.warehouseSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Selected,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FILTER_DOES_NOT_HAVE_WAREHOUSE:
|
||||||
|
this.warehouseSelectionModel.intersection = Intersection.Exclude
|
||||||
|
this.warehouseSelectionModel.set(
|
||||||
|
rule.value ? +rule.value : null,
|
||||||
|
ToggleableItemState.Excluded,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
break
|
||||||
case FILTER_STORAGE_PATH:
|
case FILTER_STORAGE_PATH:
|
||||||
case FILTER_HAS_STORAGE_PATH_ANY:
|
case FILTER_HAS_STORAGE_PATH_ANY:
|
||||||
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
|
||||||
@ -683,6 +722,26 @@ export class FilterEditorComponent
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.warehouseSelectionModel.isNoneSelected()) {
|
||||||
|
filterRules.push({ rule_type: FILTER_WAREHOUSE, value: null })
|
||||||
|
} else {
|
||||||
|
this.warehouseSelectionModel
|
||||||
|
.getSelectedItems()
|
||||||
|
.forEach((warehouse) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
value: warehouse.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.warehouseSelectionModel
|
||||||
|
.getExcludedItems()
|
||||||
|
.forEach((warehouse) => {
|
||||||
|
filterRules.push({
|
||||||
|
rule_type: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
value: warehouse.id?.toString(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
if (this.storagePathSelectionModel.isNoneSelected()) {
|
if (this.storagePathSelectionModel.isNoneSelected()) {
|
||||||
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
|
||||||
} else {
|
} else {
|
||||||
@ -845,6 +904,8 @@ export class FilterEditorComponent
|
|||||||
selectionData?.selected_correspondents ?? null
|
selectionData?.selected_correspondents ?? null
|
||||||
this.storagePathDocumentCounts =
|
this.storagePathDocumentCounts =
|
||||||
selectionData?.selected_storage_paths ?? null
|
selectionData?.selected_storage_paths ?? null
|
||||||
|
this.warehouseDocumentCounts =
|
||||||
|
selectionData?.selected_warehouses ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesModified: boolean = false
|
rulesModified: boolean = false
|
||||||
@ -895,6 +956,16 @@ export class FilterEditorComponent
|
|||||||
.listAll()
|
.listAll()
|
||||||
.subscribe((result) => (this.documentTypes = result.results))
|
.subscribe((result) => (this.documentTypes = result.results))
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Warehouse
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.warehouseService
|
||||||
|
.listAll()
|
||||||
|
.subscribe((result) => (this.warehouses = result.results))
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.permissionsService.currentUserCan(
|
this.permissionsService.currentUserCan(
|
||||||
PermissionAction.View,
|
PermissionAction.View,
|
||||||
@ -941,6 +1012,10 @@ export class FilterEditorComponent
|
|||||||
this.documentTypeSelectionModel.toggle(documentTypeId)
|
this.documentTypeSelectionModel.toggle(documentTypeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleWarehouse(warehouseId: number) {
|
||||||
|
this.warehouseSelectionModel.toggle(warehouseId)
|
||||||
|
}
|
||||||
|
|
||||||
toggleStoragePath(storagePathID: number) {
|
toggleStoragePath(storagePathID: number) {
|
||||||
this.storagePathSelectionModel.toggle(storagePathID)
|
this.storagePathSelectionModel.toggle(storagePathID)
|
||||||
}
|
}
|
||||||
@ -957,6 +1032,10 @@ export class FilterEditorComponent
|
|||||||
this.documentTypeSelectionModel.apply()
|
this.documentTypeSelectionModel.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onWarehouseDropdownOpen() {
|
||||||
|
this.warehouseSelectionModel.apply()
|
||||||
|
}
|
||||||
|
|
||||||
onStoragePathDropdownOpen() {
|
onStoragePathDropdownOpen() {
|
||||||
this.storagePathSelectionModel.apply()
|
this.storagePathSelectionModel.apply()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
import { DatePipe } from '@angular/common'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { of } from 'rxjs'
|
||||||
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
|
import { WarehouseListComponent } from './warehouse-list.component'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
|
describe('WarehouseListComponent', () => {
|
||||||
|
let component: WarehouseListComponent
|
||||||
|
let fixture: ComponentFixture<WarehouseListComponent>
|
||||||
|
let warehouseService: WarehouseService
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
WarehouseListComponent,
|
||||||
|
SortableDirective,
|
||||||
|
PageHeaderComponent,
|
||||||
|
IfPermissionsDirective,
|
||||||
|
SafeHtmlPipe,
|
||||||
|
],
|
||||||
|
providers: [DatePipe],
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
NgbPaginationModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
warehouseService = TestBed.inject(WarehouseService)
|
||||||
|
jest.spyOn(warehouseService, 'listFiltered').mockReturnValue(
|
||||||
|
of({
|
||||||
|
count: 3,
|
||||||
|
all: [1, 2, 3],
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Warehouse1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Warehouse2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Warehouse3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
fixture = TestBed.createComponent(WarehouseListComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests are included in management-list.component.spec.ts
|
||||||
|
|
||||||
|
it('should use correct delete message', () => {
|
||||||
|
expect(component.getDeleteMessage({ id: 1, name: 'Warehouse1' })).toEqual(
|
||||||
|
'Do you really want to delete the warehouse "Warehouse1"?'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
|
import {
|
||||||
|
PermissionsService,
|
||||||
|
PermissionType,
|
||||||
|
} from 'src/app/services/permissions.service'
|
||||||
|
import { WarehouseService } from 'src/app/services/rest/warehouse.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { WarehouseEditDialogComponent } from '../../common/edit-dialog/warehouse-edit-dialog/warehouse-edit-dialog.component'
|
||||||
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-warehouse-list',
|
||||||
|
templateUrl: './../management-list/management-list.component.html',
|
||||||
|
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||||
|
})
|
||||||
|
export class WarehouseListComponent extends ManagementListComponent<Warehouse> {
|
||||||
|
constructor(
|
||||||
|
warehouseService: WarehouseService,
|
||||||
|
modalService: NgbModal,
|
||||||
|
toastService: ToastService,
|
||||||
|
documentListViewService: DocumentListViewService,
|
||||||
|
permissionsService: PermissionsService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
warehouseService,
|
||||||
|
modalService,
|
||||||
|
WarehouseEditDialogComponent,
|
||||||
|
toastService,
|
||||||
|
documentListViewService,
|
||||||
|
permissionsService,
|
||||||
|
FILTER_HAS_TAGS_ALL,
|
||||||
|
$localize`warehouse`,
|
||||||
|
$localize`warehouses`,
|
||||||
|
PermissionType.Warehouse,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
name: $localize`Type`,
|
||||||
|
rendersHtml: true,
|
||||||
|
valueFn: (w: Warehouse) => {
|
||||||
|
return w.type
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeleteMessage(object: Warehouse) {
|
||||||
|
return $localize`Do you really want to delete the warehouse "${object.name}"?`
|
||||||
|
}
|
||||||
|
}
|
@ -7,5 +7,7 @@ export interface DocumentSuggestions {
|
|||||||
|
|
||||||
storage_paths?: number[]
|
storage_paths?: number[]
|
||||||
|
|
||||||
|
warehouses?: number[]
|
||||||
|
|
||||||
dates?: string[] // ISO-formatted date string e.g. 2022-11-03
|
dates?: string[] // ISO-formatted date string e.g. 2022-11-03
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { Tag } from './tag'
|
|||||||
import { DocumentType } from './document-type'
|
import { DocumentType } from './document-type'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { StoragePath } from './storage-path'
|
import { StoragePath } from './storage-path'
|
||||||
|
import { Warehouse } from './warehouse'
|
||||||
import { ObjectWithPermissions } from './object-with-permissions'
|
import { ObjectWithPermissions } from './object-with-permissions'
|
||||||
import { DocumentNote } from './document-note'
|
import { DocumentNote } from './document-note'
|
||||||
import { CustomFieldInstance } from './custom-field-instance'
|
import { CustomFieldInstance } from './custom-field-instance'
|
||||||
@ -28,6 +29,10 @@ export interface Document extends ObjectWithPermissions {
|
|||||||
|
|
||||||
storage_path?: number
|
storage_path?: number
|
||||||
|
|
||||||
|
warehouses$?: Observable<Warehouse>
|
||||||
|
|
||||||
|
warehouses?: number
|
||||||
|
|
||||||
title?: string
|
title?: string
|
||||||
|
|
||||||
content?: string
|
content?: string
|
||||||
|
@ -49,6 +49,10 @@ export const FILTER_SHARED_BY_USER = 37
|
|||||||
|
|
||||||
export const FILTER_CUSTOM_FIELDS = 36
|
export const FILTER_CUSTOM_FIELDS = 36
|
||||||
|
|
||||||
|
export const FILTER_WAREHOUSE = 50
|
||||||
|
export const FILTER_HAS_WAREHOUSE_ANY = 51
|
||||||
|
export const FILTER_DOES_NOT_HAVE_WAREHOUSE = 52
|
||||||
|
|
||||||
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||||
{
|
{
|
||||||
id: FILTER_TITLE,
|
id: FILTER_TITLE,
|
||||||
@ -108,6 +112,25 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
|||||||
datatype: 'storage_path',
|
datatype: 'storage_path',
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_WAREHOUSE,
|
||||||
|
filtervar: 'warehouses__id',
|
||||||
|
isnull_filtervar: 'warehouses__isnull',
|
||||||
|
datatype: 'warehouse',
|
||||||
|
multi: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_HAS_WAREHOUSE_ANY,
|
||||||
|
filtervar: 'warehouses__id__in',
|
||||||
|
datatype: 'warehouse',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: FILTER_DOES_NOT_HAVE_WAREHOUSE,
|
||||||
|
filtervar: 'warehouses__id__none',
|
||||||
|
datatype: 'warehouse',
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: FILTER_DOCUMENT_TYPE,
|
id: FILTER_DOCUMENT_TYPE,
|
||||||
filtervar: 'document_type__id',
|
filtervar: 'document_type__id',
|
||||||
|
8
src-ui/src/app/data/warehouse.ts
Normal file
8
src-ui/src/app/data/warehouse.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { MatchingModel } from './matching-model'
|
||||||
|
|
||||||
|
export interface Warehouse extends MatchingModel {
|
||||||
|
|
||||||
|
type?: string
|
||||||
|
|
||||||
|
parent_warehouse?: number
|
||||||
|
}
|
@ -17,6 +17,8 @@ export interface WorkflowAction extends ObjectWithId {
|
|||||||
|
|
||||||
assign_storage_path?: number // StoragePath.id
|
assign_storage_path?: number // StoragePath.id
|
||||||
|
|
||||||
|
assign_warehouses?: number // Warehouse.id
|
||||||
|
|
||||||
assign_owner?: number // User.id
|
assign_owner?: number // User.id
|
||||||
|
|
||||||
assign_view_users?: number[] // [User.id]
|
assign_view_users?: number[] // [User.id]
|
||||||
@ -45,6 +47,10 @@ export interface WorkflowAction extends ObjectWithId {
|
|||||||
|
|
||||||
remove_all_storage_paths?: boolean
|
remove_all_storage_paths?: boolean
|
||||||
|
|
||||||
|
remove_warehouses?: number[] // [Warehouse.id]
|
||||||
|
|
||||||
|
remove_all_warehouses?: boolean
|
||||||
|
|
||||||
remove_owners?: number[] // [User.id]
|
remove_owners?: number[] // [User.id]
|
||||||
|
|
||||||
remove_all_owners?: boolean
|
remove_all_owners?: boolean
|
||||||
|
@ -264,6 +264,10 @@ describe('PermissionsService', () => {
|
|||||||
'change_applicationconfiguration',
|
'change_applicationconfiguration',
|
||||||
'delete_applicationconfiguration',
|
'delete_applicationconfiguration',
|
||||||
'view_applicationconfiguration',
|
'view_applicationconfiguration',
|
||||||
|
'change_warehouse',
|
||||||
|
'view_warehouse',
|
||||||
|
'add_warehouse',
|
||||||
|
'delete_warehouse',
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
|
@ -12,6 +12,7 @@ export enum PermissionAction {
|
|||||||
export enum PermissionType {
|
export enum PermissionType {
|
||||||
Document = '%s_document',
|
Document = '%s_document',
|
||||||
Tag = '%s_tag',
|
Tag = '%s_tag',
|
||||||
|
Warehouse = '%s_warehouse',
|
||||||
Correspondent = '%s_correspondent',
|
Correspondent = '%s_correspondent',
|
||||||
DocumentType = '%s_documenttype',
|
DocumentType = '%s_documenttype',
|
||||||
StoragePath = '%s_storagepath',
|
StoragePath = '%s_storagepath',
|
||||||
|
@ -13,6 +13,8 @@ import { TagService } from './tag.service'
|
|||||||
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
||||||
import { queryParamsFromFilterRules } from '../../utils/query-params'
|
import { queryParamsFromFilterRules } from '../../utils/query-params'
|
||||||
import { StoragePathService } from './storage-path.service'
|
import { StoragePathService } from './storage-path.service'
|
||||||
|
import { WarehouseService } from './warehouse.service'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PermissionAction,
|
PermissionAction,
|
||||||
PermissionType,
|
PermissionType,
|
||||||
@ -26,6 +28,7 @@ export const DOCUMENT_SORT_FIELDS = [
|
|||||||
{ field: 'correspondent__name', name: $localize`Correspondent` },
|
{ field: 'correspondent__name', name: $localize`Correspondent` },
|
||||||
{ field: 'title', name: $localize`Title` },
|
{ field: 'title', name: $localize`Title` },
|
||||||
{ field: 'document_type__name', name: $localize`Document type` },
|
{ field: 'document_type__name', name: $localize`Document type` },
|
||||||
|
{ field: 'warehouses__name', name: $localize`Warehouse` },
|
||||||
{ field: 'created', name: $localize`Created` },
|
{ field: 'created', name: $localize`Created` },
|
||||||
{ field: 'added', name: $localize`Added` },
|
{ field: 'added', name: $localize`Added` },
|
||||||
{ field: 'modified', name: $localize`Modified` },
|
{ field: 'modified', name: $localize`Modified` },
|
||||||
@ -51,6 +54,8 @@ export interface SelectionData {
|
|||||||
selected_correspondents: SelectionDataItem[]
|
selected_correspondents: SelectionDataItem[]
|
||||||
selected_tags: SelectionDataItem[]
|
selected_tags: SelectionDataItem[]
|
||||||
selected_document_types: SelectionDataItem[]
|
selected_document_types: SelectionDataItem[]
|
||||||
|
selected_warehouses: SelectionDataItem[]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -65,6 +70,7 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
|||||||
private documentTypeService: DocumentTypeService,
|
private documentTypeService: DocumentTypeService,
|
||||||
private tagService: TagService,
|
private tagService: TagService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
|
private warehouseService: WarehouseService,
|
||||||
private permissionsService: PermissionsService,
|
private permissionsService: PermissionsService,
|
||||||
private settingsService: SettingsService
|
private settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
@ -116,6 +122,15 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
|||||||
) {
|
) {
|
||||||
doc.storage_path$ = this.storagePathService.getCached(doc.storage_path)
|
doc.storage_path$ = this.storagePathService.getCached(doc.storage_path)
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
doc.warehouses &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Warehouse
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
doc.warehouses$ = this.warehouseService.getCached(doc.warehouses)
|
||||||
|
}
|
||||||
return doc
|
return doc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
src-ui/src/app/services/rest/warehouse.service.spec.ts
Normal file
4
src-ui/src/app/services/rest/warehouse.service.spec.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { WarehouseService } from './warehouse.service'
|
||||||
|
import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
|
||||||
|
|
||||||
|
commonAbstractNameFilterPaperlessServiceTests('warehouses', WarehouseService)
|
13
src-ui/src/app/services/rest/warehouse.service.ts
Normal file
13
src-ui/src/app/services/rest/warehouse.service.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Warehouse } from 'src/app/data/warehouse'
|
||||||
|
import { AbstractNameFilterService } from './abstract-name-filter-service'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class WarehouseService extends AbstractNameFilterService<Warehouse> {
|
||||||
|
constructor(http: HttpClient) {
|
||||||
|
super(http, 'warehouses')
|
||||||
|
}
|
||||||
|
}
|
@ -192,6 +192,8 @@ class DocumentFilterSet(FilterSet):
|
|||||||
|
|
||||||
storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
|
storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
|
||||||
|
|
||||||
|
warehouses__id__none = ObjectFilter(field_name="warehouses", exclude=True)
|
||||||
|
|
||||||
is_in_inbox = InboxFilter()
|
is_in_inbox = InboxFilter()
|
||||||
|
|
||||||
title_content = TitleContentFilter()
|
title_content = TitleContentFilter()
|
||||||
@ -225,6 +227,9 @@ class DocumentFilterSet(FilterSet):
|
|||||||
"storage_path": ["isnull"],
|
"storage_path": ["isnull"],
|
||||||
"storage_path__id": ID_KWARGS,
|
"storage_path__id": ID_KWARGS,
|
||||||
"storage_path__name": CHAR_KWARGS,
|
"storage_path__name": CHAR_KWARGS,
|
||||||
|
"warehouses": ["isnull"],
|
||||||
|
"warehouses__id": ID_KWARGS,
|
||||||
|
"warehouses__name": CHAR_KWARGS,
|
||||||
"owner": ["isnull"],
|
"owner": ["isnull"],
|
||||||
"owner__id": ID_KWARGS,
|
"owner__id": ID_KWARGS,
|
||||||
"custom_fields": ["icontains"],
|
"custom_fields": ["icontains"],
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-20 09:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '1047_warehouse_document_warehouses'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='parent_warehouse',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subwarehouses', to='documents.warehouse'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-20 09:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '1048_alter_warehouse_parent_warehouse'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='parent_warehouse',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subwarehouse', to='documents.warehouse'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-20 10:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '1049_alter_warehouse_parent_warehouse'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='parent_warehouse',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='documents.warehouse'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-21 07:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '1050_alter_warehouse_parent_warehouse'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='warehouse',
|
||||||
|
options={'ordering': ('name',), 'verbose_name': 'warehouse', 'verbose_name_plural': 'warehouses'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='is_insensitive',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is insensitive'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='match',
|
||||||
|
field=models.CharField(blank=True, max_length=256, verbose_name='match'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='matching_algorithm',
|
||||||
|
field=models.PositiveIntegerField(choices=[(0, 'None'), (1, 'Any word'), (2, 'All words'), (3, 'Exact match'), (4, 'Regular expression'), (5, 'Fuzzy word'), (6, 'Automatic')], default=1, verbose_name='matching algorithm'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='warehouse',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='warehouse',
|
||||||
|
constraint=models.UniqueConstraint(fields=('name', 'owner'), name='documents_warehouse_unique_name_owner'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='warehouse',
|
||||||
|
constraint=models.UniqueConstraint(condition=models.Q(('owner__isnull', True)), fields=('name',), name='documents_warehouse_name_uniq'),
|
||||||
|
),
|
||||||
|
]
|
@ -129,7 +129,7 @@ class StoragePath(MatchingModel):
|
|||||||
verbose_name = _("storage path")
|
verbose_name = _("storage path")
|
||||||
verbose_name_plural = _("storage paths")
|
verbose_name_plural = _("storage paths")
|
||||||
|
|
||||||
class Warehouse(ModelWithOwner):
|
class Warehouse(MatchingModel):
|
||||||
|
|
||||||
WAREHOUSE = "Warehouse"
|
WAREHOUSE = "Warehouse"
|
||||||
SHELF = "Shelf"
|
SHELF = "Shelf"
|
||||||
@ -140,13 +140,12 @@ class Warehouse(ModelWithOwner):
|
|||||||
(BOXCASE, _("Boxcase")),
|
(BOXCASE, _("Boxcase")),
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=256, unique=True)
|
|
||||||
type = models.CharField(max_length=20, null=True, blank=True,
|
type = models.CharField(max_length=20, null=True, blank=True,
|
||||||
choices=TYPE_WAREHOUSE,
|
choices=TYPE_WAREHOUSE,
|
||||||
default=WAREHOUSE,)
|
default=WAREHOUSE,)
|
||||||
parent_warehouse = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name="parent_warehouses" )
|
parent_warehouse = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True )
|
||||||
|
|
||||||
class Meta:
|
class Meta(MatchingModel.Meta):
|
||||||
verbose_name = _("warehouse")
|
verbose_name = _("warehouse")
|
||||||
verbose_name_plural = _("warehouses")
|
verbose_name_plural = _("warehouses")
|
||||||
|
|
||||||
|
@ -1756,6 +1756,10 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WarehouseSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
class WarehouseSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
||||||
|
document_count = serializers.SerializerMethodField()
|
||||||
|
def get_document_count(self,obj):
|
||||||
|
document = Document.objects.filter(warehouses=obj).count()
|
||||||
|
return document
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Warehouse
|
model = Warehouse
|
||||||
@ -1763,10 +1767,17 @@ class WarehouseSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
|
|
||||||
|
document_count = self.get_document_count(instance)
|
||||||
|
data['document_count'] = document_count
|
||||||
|
|
||||||
if instance.parent_warehouse:
|
if instance.parent_warehouse:
|
||||||
data['parent_warehouse'] = WarehouseSerializer(instance.parent_warehouse).data
|
parent_serializer = self.__class__(instance.parent_warehouse)
|
||||||
|
data['parent_warehouse'] = parent_serializer.data
|
||||||
|
data['parent_warehouse']['document_count'] = document_count
|
||||||
else:
|
else:
|
||||||
data['parent_warehouse'] = None
|
data['parent_warehouse'] = None
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ class DocumentViewSet(
|
|||||||
ObjectOwnedOrGrantedPermissionsFilter,
|
ObjectOwnedOrGrantedPermissionsFilter,
|
||||||
)
|
)
|
||||||
filterset_class = DocumentFilterSet
|
filterset_class = DocumentFilterSet
|
||||||
search_fields = ("title", "correspondent__name", "content")
|
search_fields = ("title", "correspondent__name", "content", "warehouses")
|
||||||
ordering_fields = (
|
ordering_fields = (
|
||||||
"id",
|
"id",
|
||||||
"title",
|
"title",
|
||||||
@ -1519,9 +1519,18 @@ class BulkEditObjectsView(PassUserMixin):
|
|||||||
"Error performing bulk permissions edit, check logs for more detail.",
|
"Error performing bulk permissions edit, check logs for more detail.",
|
||||||
)
|
)
|
||||||
|
|
||||||
elif operation == "delete":
|
elif operation == "delete" and object_type == "warehouses":
|
||||||
|
|
||||||
|
documents = Document.objects.filter(warehouses__in=object_ids)
|
||||||
|
documents.delete()
|
||||||
objs.delete()
|
objs.delete()
|
||||||
|
|
||||||
|
elif operation == "delete":
|
||||||
|
|
||||||
|
objs.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Response({"result": "OK"})
|
return Response({"result": "OK"})
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user