diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html index 2dd453978..2b3494376 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html @@ -24,6 +24,11 @@ +
+
+ +
+ diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts index 4be31cb29..75e972384 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts @@ -2,9 +2,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { provideHttpClientTesting } from '@angular/common/http/testing' import { ElementRef } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' +import { By } from '@angular/platform-browser' import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { of, Subject, throwError } from 'rxjs' +import { PermissionsFormComponent } from 'src/app/components/common/input/permissions/permissions-form/permissions-form.component' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { Document } from 'src/app/data/document' import { @@ -14,10 +16,15 @@ import { OcrZoneTestResult, OcrZoneTransform, } from 'src/app/data/ocr-template' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { User } from 'src/app/data/user' +import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentService } from 'src/app/services/rest/document.service' import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service' +import { UserService } from 'src/app/services/rest/user.service' +import { SettingsService } from 'src/app/services/settings.service' import { OcrTemplateEditorComponent } from './ocr-template-editor.component' const customFields: CustomField[] = [ @@ -76,6 +83,10 @@ describe('OcrTemplateEditorComponent', () => { jest .spyOn(TestBed.inject(DocumentTypeService), 'listAll') .mockReturnValue(of({ count: 0, all: [], results: [] })) + jest + .spyOn(TestBed.inject(UserService), 'listAll') + .mockReturnValue(of({ count: 0, all: [], results: [] })) + TestBed.inject(PermissionsService).initialize([], { id: 1 } as User) documentService = TestBed.inject(DocumentService) jest .spyOn(documentService, 'get') @@ -147,6 +158,92 @@ describe('OcrTemplateEditorComponent', () => { expect(component.template.document_type).toEqual(2) }) + it('should initialize new template permissions from saved defaults', () => { + const defaults = { + [SETTINGS_KEYS.DEFAULT_PERMS_OWNER]: 1, + [SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS]: [2], + [SETTINGS_KEYS.DEFAULT_PERMS_VIEW_GROUPS]: [3], + [SETTINGS_KEYS.DEFAULT_PERMS_EDIT_USERS]: [], + [SETTINGS_KEYS.DEFAULT_PERMS_EDIT_GROUPS]: [4], + } + jest + .spyOn(TestBed.inject(SettingsService), 'get') + .mockImplementation((key) => defaults[key]) + fixture.detectChanges() + expect(component.permissionsForm).toEqual({ + owner: 1, + set_permissions: { + view: { users: [2], groups: [3] }, + change: { users: [], groups: [4] }, + }, + }) + expect( + fixture.debugElement.query(By.directive(PermissionsFormComponent)) + ).toBeTruthy() + expect(component.isDirty).toBe(false) + const create = jest + .spyOn(templateService, 'create') + .mockReturnValue(of({ ...component.template, id: 3 })) + component.save() + expect(create).toHaveBeenCalledWith( + expect.objectContaining(component.permissionsForm) + ) + }) + + it('should copy existing permissions and track changes without mutating the list item', () => { + component.object = { + ...component.template, + id: 3, + owner: 1, + permissions: { + view: { users: [2], groups: [] }, + change: { users: [], groups: [3] }, + }, + } + fixture.detectChanges() + expect(component.permissionsForm.owner).toBe(1) + expect(component.isDirty).toBe(false) + component.permissionsForm.set_permissions.view.users.push(4) + expect(component.isDirty).toBe(true) + expect(component.object.permissions.view.users).toEqual([2]) + const update = jest + .spyOn(templateService, 'update') + .mockReturnValue(of(component.template)) + component.save() + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 1, + set_permissions: { + view: { users: [2, 4], groups: [] }, + change: { users: [], groups: [3] }, + }, + }) + ) + }) + + it('should hide sharing controls and omit permission changes for a shared editor', () => { + component.object = { + ...component.template, + id: 3, + owner: 2, + user_can_change: true, + } + fixture.detectChanges() + expect( + fixture.debugElement.query(By.directive(PermissionsFormComponent)) + ).toBeNull() + component.permissionsForm = { + owner: 1, + set_permissions: { view: { users: [1] } }, + } + const update = jest + .spyOn(templateService, 'update') + .mockReturnValue(of(component.template)) + component.save() + expect(update.mock.calls[0][0].owner).toBe(2) + expect(update.mock.calls[0][0]).not.toHaveProperty('set_permissions') + }) + it('should keep only the last picked sample document', () => { component.setSampleDocument([5, 6]) expect(component.sampleDocumentIds).toEqual([6]) diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts index 657eb8a0c..e5c5099b3 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts @@ -25,6 +25,10 @@ import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-d import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { DocumentLinkComponent } from 'src/app/components/common/input/document-link/document-link.component' import { NumberComponent } from 'src/app/components/common/input/number/number.component' +import { + PermissionsFormComponent, + PermissionsFormObject, +} from 'src/app/components/common/input/permissions/permissions-form/permissions-form.component' import { SelectComponent } from 'src/app/components/common/input/select/select.component' import { SwitchComponent } from 'src/app/components/common/input/switch/switch.component' import { TextComponent } from 'src/app/components/common/input/text/text.component' @@ -38,14 +42,20 @@ import { OcrTemplate, OcrTemplateZone, OcrZoneTarget, - OcrZoneTransform, OcrZoneTestResult, + OcrZoneTransform, ZoneTestRequest, } from 'src/app/data/ocr-template' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { User } from 'src/app/data/user' +import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' +import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentService } from 'src/app/services/rest/document.service' import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service' +import { UserService } from 'src/app/services/rest/user.service' +import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { OcrTemplateEditorZoneListComponent } from './ocr-template-editor-zone-list/ocr-template-editor-zone-list.component' import { @@ -135,6 +145,8 @@ const RESIZE_CURSOR: Record = { NgbNavModule, NgxBootstrapIconsModule, OcrTemplateEditorZoneListComponent, + PermissionsFormComponent, + IfOwnerDirective, ], templateUrl: './ocr-template-editor.component.html', styleUrls: ['./ocr-template-editor.component.scss'], @@ -147,6 +159,9 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { private readonly documentService = inject(DocumentService) private readonly toastService = inject(ToastService) private readonly modalService = inject(NgbModal) + private readonly userService = inject(UserService) + private readonly settingsService = inject(SettingsService) + private readonly permissionsService = inject(PermissionsService) private readonly destroy$ = new Subject() private readonly cancelZoneTest$ = new Subject() @@ -195,6 +210,8 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { .map((field) => ({ id: field.id, name: field.name })), ]) public readonly documentTypes = signal([]) + public readonly users = signal([]) + permissionsForm: PermissionsFormObject = {} public readonly OcrZoneTransform = OcrZoneTransform public readonly saving = signal(false) @@ -271,6 +288,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { ...this.template, combine_formats: Object.fromEntries(formats), sample_document: this.previewDocId ?? null, + permissions_form: this.permissionsForm, }) } @@ -281,6 +299,10 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { } ngOnInit() { + this.userService + .listAll() + .pipe(takeUntil(this.destroy$)) + .subscribe((r) => this.users.set(r.results)) this.customFieldsService .listAll() .pipe(takeUntil(this.destroy$)) @@ -305,6 +327,42 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { this.setSampleDocument([this.template.sample_document]) } } + const permissions = this.object?.permissions + this.permissionsForm = this.object + ? { + owner: this.object.owner, + set_permissions: { + view: { + users: [...(permissions?.view.users ?? [])], + groups: [...(permissions?.view.groups ?? [])], + }, + change: { + users: [...(permissions?.change.users ?? [])], + groups: [...(permissions?.change.groups ?? [])], + }, + }, + } + : { + owner: this.settingsService.get(SETTINGS_KEYS.DEFAULT_PERMS_OWNER), + set_permissions: { + view: { + users: this.settingsService.get( + SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS + ), + groups: this.settingsService.get( + SETTINGS_KEYS.DEFAULT_PERMS_VIEW_GROUPS + ), + }, + change: { + users: this.settingsService.get( + SETTINGS_KEYS.DEFAULT_PERMS_EDIT_USERS + ), + groups: this.settingsService.get( + SETTINGS_KEYS.DEFAULT_PERMS_EDIT_GROUPS + ), + }, + }, + } this.initialState = this.editorState() } @@ -741,9 +799,12 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { this.saving.set(true) this.pruneCombineFormats() this.template.sample_document = this.previewDocId + const payload = this.permissionsService.currentUserOwnsObject(this.object) + ? { ...this.template, ...this.permissionsForm } + : this.template const obs = this.template.id - ? this.templateService.update(this.template) - : this.templateService.create(this.template) + ? this.templateService.update(payload) + : this.templateService.create(payload) obs.pipe(takeUntil(this.destroy$)).subscribe({ next: (saved) => { diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html index a78e77a32..834af215b 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-templates.component.html @@ -31,12 +31,12 @@ @for (template of templates(); track template.id) {
  • -
    +
    {{getDocumentTypeName(template)}}
    {{template.zones.length}}
    - + @@ -50,17 +50,17 @@
    - - + +