mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-02 07:57:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a201439a6d |
+1
-1
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.12.5-python3.14-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.12.8-python3.14-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -2088,12 +2088,6 @@ password. All of these options come from their similarly-named [Django settings]
|
|||||||
|
|
||||||
Defaults to "always".
|
Defaults to "always".
|
||||||
|
|
||||||
#### [`PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=<bool>`](#PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS) {#PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS}
|
|
||||||
|
|
||||||
: If set to false, Paperless blocks remote OCR endpoint URLs that resolve to non-public addresses (e.g., localhost, etc).
|
|
||||||
|
|
||||||
Defaults to True.
|
|
||||||
|
|
||||||
## AI {#ai}
|
## AI {#ai}
|
||||||
|
|
||||||
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
||||||
|
|||||||
+140
-140
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,6 @@ export class TrashComponent
|
|||||||
private modalService = inject(NgbModal)
|
private modalService = inject(NgbModal)
|
||||||
private settingsService = inject(SettingsService)
|
private settingsService = inject(SettingsService)
|
||||||
private router = inject(Router)
|
private router = inject(Router)
|
||||||
private readonly emptyTrashDelaySetting =
|
|
||||||
this.settingsService.getSignal<number>(SETTINGS_KEYS.EMPTY_TRASH_DELAY)
|
|
||||||
|
|
||||||
readonly documentsInTrash = signal<Document[]>([])
|
readonly documentsInTrash = signal<Document[]>([])
|
||||||
readonly selectedDocuments = signal<Set<number>>(new Set())
|
readonly selectedDocuments = signal<Set<number>>(new Set())
|
||||||
@@ -202,7 +200,8 @@ export class TrashComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDaysRemaining(document: Document): number {
|
getDaysRemaining(document: Document): number {
|
||||||
const delay = this.emptyTrashDelaySetting()
|
this.settingsService.trackChanges()
|
||||||
|
const delay = this.settingsService.get(SETTINGS_KEYS.EMPTY_TRASH_DELAY)
|
||||||
const diff = new Date().getTime() - new Date(document.deleted_at).getTime()
|
const diff = new Date().getTime() - new Date(document.deleted_at).getTime()
|
||||||
const days = Math.ceil(diff / (1000 * 3600 * 24))
|
const days = Math.ceil(diff / (1000 * 3600 * 24))
|
||||||
return delay - days
|
return delay - days
|
||||||
|
|||||||
@@ -193,23 +193,6 @@ describe('AppFrameComponent', () => {
|
|||||||
expect(savedViewSpy).toHaveBeenCalled()
|
expect(savedViewSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update reinitialized signal-backed settings without manual change detection', async () => {
|
|
||||||
settingsService.initializeSettings().subscribe()
|
|
||||||
httpTestingController
|
|
||||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
|
||||||
.flush({
|
|
||||||
settings: { app_title: 'Reactive title' },
|
|
||||||
user: {},
|
|
||||||
permissions: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
await fixture.whenStable()
|
|
||||||
|
|
||||||
expect(
|
|
||||||
fixture.nativeElement.querySelector('.brand-title').textContent
|
|
||||||
).toBe('Reactive title')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check for update if enabled', () => {
|
it('should check for update if enabled', () => {
|
||||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||||
updateCheckSpy.mockImplementation(() => {
|
updateCheckSpy.mockImplementation(() => {
|
||||||
|
|||||||
@@ -98,29 +98,6 @@ export class AppFrameComponent
|
|||||||
readonly isMenuCollapsed = signal(true)
|
readonly isMenuCollapsed = signal(true)
|
||||||
readonly slimSidebarAnimating = signal(false)
|
readonly slimSidebarAnimating = signal(false)
|
||||||
readonly mobileSearchHidden = signal(false)
|
readonly mobileSearchHidden = signal(false)
|
||||||
private readonly versionSetting = this.settingsService.getSignal<string>(
|
|
||||||
SETTINGS_KEYS.VERSION
|
|
||||||
)
|
|
||||||
private readonly appTitleSetting = this.settingsService.getSignal<string>(
|
|
||||||
SETTINGS_KEYS.APP_TITLE
|
|
||||||
)
|
|
||||||
private readonly appLogoSetting = this.settingsService.getSignal<string>(
|
|
||||||
SETTINGS_KEYS.APP_LOGO
|
|
||||||
)
|
|
||||||
private readonly slimSidebarSetting = this.settingsService.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.SLIM_SIDEBAR
|
|
||||||
)
|
|
||||||
private readonly attributesSectionsCollapsedSetting =
|
|
||||||
this.settingsService.getSignal<CollapsibleSection[]>(
|
|
||||||
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED
|
|
||||||
)
|
|
||||||
private readonly aiEnabledSetting = this.settingsService.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.AI_ENABLED
|
|
||||||
)
|
|
||||||
private readonly sidebarViewsShowCountSetting =
|
|
||||||
this.settingsService.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT
|
|
||||||
)
|
|
||||||
private lastScrollY: number = 0
|
private lastScrollY: number = 0
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -214,23 +191,33 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get versionString(): string {
|
get versionString(): string {
|
||||||
return `${environment.appTitle} v${this.versionSetting()}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
this.settingsService.trackChanges()
|
||||||
|
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
||||||
}
|
}
|
||||||
|
|
||||||
get appTitle(): string {
|
get appTitle(): string {
|
||||||
return this.appTitleSetting() || environment.appTitle
|
this.settingsService.trackChanges()
|
||||||
|
return (
|
||||||
|
this.settingsService.get(SETTINGS_KEYS.APP_TITLE) || environment.appTitle
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get customAppTitle(): string {
|
get customAppTitle(): string {
|
||||||
return this.appTitleSetting()
|
this.settingsService.trackChanges()
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasCustomBranding(): boolean {
|
get hasCustomBranding(): boolean {
|
||||||
return !!(this.appTitleSetting()?.length || this.appLogoSetting()?.length)
|
this.settingsService.trackChanges()
|
||||||
|
return !!(
|
||||||
|
this.settingsService.get(SETTINGS_KEYS.APP_TITLE)?.length ||
|
||||||
|
this.settingsService.get(SETTINGS_KEYS.APP_LOGO)?.length
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get customAppLogo(): string {
|
get customAppLogo(): string {
|
||||||
const logo = this.appLogoSetting()
|
this.settingsService.trackChanges()
|
||||||
|
const logo = this.settingsService.get(SETTINGS_KEYS.APP_LOGO)
|
||||||
return logo?.length
|
return logo?.length
|
||||||
? environment.apiBaseUrl.replace(/\/api\/$/, logo)
|
? environment.apiBaseUrl.replace(/\/api\/$/, logo)
|
||||||
: null
|
: null
|
||||||
@@ -275,7 +262,8 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.slimSidebarSetting()
|
this.settingsService.trackChanges()
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
|
|
||||||
set slimSidebarEnabled(enabled: boolean) {
|
set slimSidebarEnabled(enabled: boolean) {
|
||||||
@@ -298,9 +286,10 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get attributesSectionsCollapsed(): boolean {
|
get attributesSectionsCollapsed(): boolean {
|
||||||
return this.attributesSectionsCollapsedSetting()?.includes(
|
this.settingsService.trackChanges()
|
||||||
CollapsibleSection.ATTRIBUTES
|
return this.settingsService
|
||||||
)
|
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
|
||||||
|
?.includes(CollapsibleSection.ATTRIBUTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
set attributesSectionsCollapsed(collapsed: boolean) {
|
set attributesSectionsCollapsed(collapsed: boolean) {
|
||||||
@@ -323,7 +312,8 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
return this.aiEnabledSetting()
|
this.settingsService.trackChanges()
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:resize')
|
@HostListener('window:resize')
|
||||||
@@ -490,8 +480,9 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get showSidebarCounts(): boolean {
|
get showSidebarCounts(): boolean {
|
||||||
|
this.settingsService.trackChanges()
|
||||||
return (
|
return (
|
||||||
this.sidebarViewsShowCountSetting() &&
|
this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT) &&
|
||||||
!this.settingsService.organizingSidebarSavedViews()
|
!this.settingsService.organizingSidebarSavedViews()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ export class GlobalSearchComponent implements OnInit {
|
|||||||
private hotkeyService = inject(HotKeyService)
|
private hotkeyService = inject(HotKeyService)
|
||||||
private settingsService = inject(SettingsService)
|
private settingsService = inject(SettingsService)
|
||||||
private locationStrategy = inject(LocationStrategy)
|
private locationStrategy = inject(LocationStrategy)
|
||||||
private readonly searchFullTypeSetting =
|
|
||||||
this.settingsService.getSignal<GlobalSearchType>(
|
|
||||||
SETTINGS_KEYS.SEARCH_FULL_TYPE
|
|
||||||
)
|
|
||||||
|
|
||||||
public DataType = DataType
|
public DataType = DataType
|
||||||
readonly query = signal<string>(null)
|
readonly query = signal<string>(null)
|
||||||
@@ -101,7 +97,11 @@ export class GlobalSearchComponent implements OnInit {
|
|||||||
@ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>
|
@ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>
|
||||||
|
|
||||||
get useAdvancedForFullSearch(): boolean {
|
get useAdvancedForFullSearch(): boolean {
|
||||||
return this.searchFullTypeSetting() === GlobalSearchType.ADVANCED
|
this.settingsService.trackChanges()
|
||||||
|
return (
|
||||||
|
this.settingsService.get(SETTINGS_KEYS.SEARCH_FULL_TYPE) ===
|
||||||
|
GlobalSearchType.ADVANCED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
+13
-19
@@ -196,16 +196,6 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
|
|
||||||
function setActionSettings({
|
|
||||||
email = true,
|
|
||||||
remoteOcr = true,
|
|
||||||
ai = true,
|
|
||||||
} = {}) {
|
|
||||||
settingsService.set(SETTINGS_KEYS.EMAIL_ENABLED, email)
|
|
||||||
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, remoteOcr)
|
|
||||||
settingsService.set(SETTINGS_KEYS.AI_ENABLED, ai)
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should support create and edit modes, support adding triggers and actions on new workflow', () => {
|
it('should support create and edit modes, support adding triggers and actions on new workflow', () => {
|
||||||
component.dialogMode.set(EditDialogMode.CREATE)
|
component.dialogMode.set(EditDialogMode.CREATE)
|
||||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||||
@@ -228,7 +218,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return source options, type options, type name, schedule date field options', () => {
|
it('should return source options, type options, type name, schedule date field options', () => {
|
||||||
setActionSettings()
|
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
|
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
|
||||||
expect(component.triggerTypeOptions).toEqual(WORKFLOW_TYPE_OPTIONS)
|
expect(component.triggerTypeOptions).toEqual(WORKFLOW_TYPE_OPTIONS)
|
||||||
@@ -252,7 +242,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Email, remote OCR and AI all disabled
|
// Email, remote OCR and AI all disabled
|
||||||
setActionSettings({ email: false, remoteOcr: false, ai: false })
|
jest.spyOn(settingsService, 'get').mockReturnValue(false)
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
expect(component.actionTypeOptions).toEqual(
|
expect(component.actionTypeOptions).toEqual(
|
||||||
WORKFLOW_ACTION_OPTIONS.filter(
|
WORKFLOW_ACTION_OPTIONS.filter(
|
||||||
@@ -265,7 +255,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should offer remote OCR only for consumption workflows', () => {
|
it('should offer remote OCR only for consumption workflows', () => {
|
||||||
setActionSettings()
|
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||||
|
|
||||||
// A consumption trigger makes the action reachable
|
// A consumption trigger makes the action reachable
|
||||||
component.object = {
|
component.object = {
|
||||||
@@ -295,7 +285,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should offer remote OCR on a trigger added to a new workflow', () => {
|
it('should offer remote OCR on a trigger added to a new workflow', () => {
|
||||||
setActionSettings()
|
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
|
|
||||||
// Nothing for the action to apply to yet
|
// Nothing for the action to apply to yet
|
||||||
@@ -321,7 +311,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should keep remote OCR listed when an action already uses it', () => {
|
it('should keep remote OCR listed when an action already uses it', () => {
|
||||||
setActionSettings()
|
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||||
|
|
||||||
// Otherwise changing the trigger would silently blank the selection
|
// Otherwise changing the trigger would silently blank the selection
|
||||||
component.object = {
|
component.object = {
|
||||||
@@ -339,7 +329,9 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not offer remote OCR when no engine is configured', () => {
|
it('should not offer remote OCR when no engine is configured', () => {
|
||||||
setActionSettings({ remoteOcr: false })
|
jest
|
||||||
|
.spyOn(settingsService, 'get')
|
||||||
|
.mockImplementation((key) => key !== SETTINGS_KEYS.REMOTE_OCR_CONFIGURED)
|
||||||
|
|
||||||
component.object = {
|
component.object = {
|
||||||
name: 'Workflow 1',
|
name: 'Workflow 1',
|
||||||
@@ -356,7 +348,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should offer apply AI suggestions unless every trigger is consumption', () => {
|
it('should offer apply AI suggestions unless every trigger is consumption', () => {
|
||||||
setActionSettings()
|
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||||
|
|
||||||
// Consumption runs before the document has been parsed, so there would be
|
// Consumption runs before the document has been parsed, so there would be
|
||||||
// no content to make suggestions from
|
// no content to make suggestions from
|
||||||
@@ -390,7 +382,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should keep apply AI suggestions listed when an action already uses it', () => {
|
it('should keep apply AI suggestions listed when an action already uses it', () => {
|
||||||
setActionSettings()
|
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||||
|
|
||||||
// Otherwise changing the trigger would silently blank the selection
|
// Otherwise changing the trigger would silently blank the selection
|
||||||
component.object = {
|
component.object = {
|
||||||
@@ -408,7 +400,9 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not offer apply AI suggestions when AI is disabled', () => {
|
it('should not offer apply AI suggestions when AI is disabled', () => {
|
||||||
setActionSettings({ ai: false })
|
jest
|
||||||
|
.spyOn(settingsService, 'get')
|
||||||
|
.mockImplementation((key) => key !== SETTINGS_KEYS.AI_ENABLED)
|
||||||
|
|
||||||
component.object = {
|
component.object = {
|
||||||
name: 'Workflow 1',
|
name: 'Workflow 1',
|
||||||
|
|||||||
+4
-10
@@ -537,13 +537,6 @@ export class WorkflowEditDialogComponent
|
|||||||
readonly dateCustomFields = computed(() =>
|
readonly dateCustomFields = computed(() =>
|
||||||
this.customFields()?.filter((f) => f.data_type === CustomFieldDataType.Date)
|
this.customFields()?.filter((f) => f.data_type === CustomFieldDataType.Date)
|
||||||
)
|
)
|
||||||
private readonly emailEnabledSetting =
|
|
||||||
this.settingsService.getSignal<boolean>(SETTINGS_KEYS.EMAIL_ENABLED)
|
|
||||||
private readonly remoteOcrConfiguredSetting =
|
|
||||||
this.settingsService.getSignal<boolean>(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED)
|
|
||||||
private readonly aiEnabledSetting = this.settingsService.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.AI_ENABLED
|
|
||||||
)
|
|
||||||
|
|
||||||
expandedItem: number = null
|
expandedItem: number = null
|
||||||
|
|
||||||
@@ -596,7 +589,7 @@ export class WorkflowEditDialogComponent
|
|||||||
private getAllowedActionTypes() {
|
private getAllowedActionTypes() {
|
||||||
let allowed = WORKFLOW_ACTION_OPTIONS
|
let allowed = WORKFLOW_ACTION_OPTIONS
|
||||||
|
|
||||||
if (!this.emailEnabledSetting()) {
|
if (!this.settingsService.get(SETTINGS_KEYS.EMAIL_ENABLED)) {
|
||||||
allowed = allowed.filter((a) => a.id !== WorkflowActionType.Email)
|
allowed = allowed.filter((a) => a.id !== WorkflowActionType.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,7 +597,7 @@ export class WorkflowEditDialogComponent
|
|||||||
// offered for workflows that run at consumption.
|
// offered for workflows that run at consumption.
|
||||||
const formWorkflow: Workflow = this.objectForm?.value
|
const formWorkflow: Workflow = this.objectForm?.value
|
||||||
const remoteOcrUsable =
|
const remoteOcrUsable =
|
||||||
this.remoteOcrConfiguredSetting() &&
|
this.settingsService.get(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED) &&
|
||||||
(formWorkflow?.triggers?.some(
|
(formWorkflow?.triggers?.some(
|
||||||
(trigger) => trigger.type === WorkflowTriggerType.Consumption
|
(trigger) => trigger.type === WorkflowTriggerType.Consumption
|
||||||
) ||
|
) ||
|
||||||
@@ -619,7 +612,7 @@ export class WorkflowEditDialogComponent
|
|||||||
// once every trigger is consumption, so it stays offered on a workflow
|
// once every trigger is consumption, so it stays offered on a workflow
|
||||||
// that has no triggers yet.
|
// that has no triggers yet.
|
||||||
const aiSuggestionsUsable =
|
const aiSuggestionsUsable =
|
||||||
this.aiEnabledSetting() &&
|
this.settingsService.get(SETTINGS_KEYS.AI_ENABLED) &&
|
||||||
(!formWorkflow?.triggers?.length ||
|
(!formWorkflow?.triggers?.length ||
|
||||||
formWorkflow.triggers.some(
|
formWorkflow.triggers.some(
|
||||||
(trigger) => trigger.type !== WorkflowTriggerType.Consumption
|
(trigger) => trigger.type !== WorkflowTriggerType.Consumption
|
||||||
@@ -1369,6 +1362,7 @@ export class WorkflowEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get actionTypeOptions() {
|
get actionTypeOptions() {
|
||||||
|
this.settingsService.trackChanges()
|
||||||
// Computed on read rather than cached
|
// Computed on read rather than cached
|
||||||
return this.getAllowedActionTypes()
|
return this.getAllowedActionTypes()
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-10
@@ -839,9 +839,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
selectionModel.items = [memoRoot]
|
selectionModel.items = [memoRoot]
|
||||||
selectionModel.documentCounts = [{ id: memoRoot.id, document_count: 9 }]
|
selectionModel.documentCounts = [{ id: memoRoot.id, document_count: 9 }]
|
||||||
|
|
||||||
const getRootDocCount = (selectionModel as any).createRootDocCounter(
|
const getRootDocCount = (selectionModel as any).createRootDocCounter()
|
||||||
selectionModel.items
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(getRootDocCount(memoRoot.id)).toEqual(9)
|
expect(getRootDocCount(memoRoot.id)).toEqual(9)
|
||||||
selectionModel.documentCounts = []
|
selectionModel.documentCounts = []
|
||||||
@@ -857,9 +855,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
selectionModel.items = [rootWithoutSelection]
|
selectionModel.items = [rootWithoutSelection]
|
||||||
selectionModel.documentCounts = []
|
selectionModel.documentCounts = []
|
||||||
|
|
||||||
const getRootDocCount = (selectionModel as any).createRootDocCounter(
|
const getRootDocCount = (selectionModel as any).createRootDocCounter()
|
||||||
selectionModel.items
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(getRootDocCount(rootWithoutSelection.id)).toEqual(4)
|
expect(getRootDocCount(rootWithoutSelection.id)).toEqual(4)
|
||||||
})
|
})
|
||||||
@@ -869,9 +865,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
selectionModel.items = [rootWithoutCounts]
|
selectionModel.items = [rootWithoutCounts]
|
||||||
selectionModel.documentCounts = []
|
selectionModel.documentCounts = []
|
||||||
|
|
||||||
const getRootDocCount = (selectionModel as any).createRootDocCounter(
|
const getRootDocCount = (selectionModel as any).createRootDocCounter()
|
||||||
selectionModel.items
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(getRootDocCount(rootWithoutCounts.id)).toEqual(0)
|
expect(getRootDocCount(rootWithoutCounts.id)).toEqual(0)
|
||||||
})
|
})
|
||||||
@@ -972,7 +966,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
component.selectionModel['temporarySelectionStates'].set(id, state)
|
component.selectionModel['temporarySelectionStates'].set(id, state)
|
||||||
const changedSpy = jest.spyOn(component.selectionModel.changed, 'next')
|
const changedSpy = jest.spyOn(component.selectionModel.changed, 'next')
|
||||||
component.selectionModel.exclude(id)
|
component.selectionModel.exclude(id)
|
||||||
expect(component.selectionModel.temporaryLogicalOperator()).toBe(
|
expect(component.selectionModel.temporaryLogicalOperator).toBe(
|
||||||
LogicalOperator.And
|
LogicalOperator.And
|
||||||
)
|
)
|
||||||
expect(component.selectionModel['temporarySelectionStates'].get(id)).toBe(
|
expect(component.selectionModel['temporarySelectionStates'].get(id)).toBe(
|
||||||
|
|||||||
+104
-125
@@ -64,56 +64,43 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
manyToOne = false
|
manyToOne = false
|
||||||
singleSelect = false
|
singleSelect = false
|
||||||
|
private _logicalOperator: LogicalOperator = LogicalOperator.And
|
||||||
|
temporaryLogicalOperator: LogicalOperator = this._logicalOperator
|
||||||
|
private _intersection: Intersection = Intersection.Include
|
||||||
|
temporaryIntersection: Intersection = this._intersection
|
||||||
|
|
||||||
private readonly _logicalOperator = signal(LogicalOperator.And)
|
private _documentCounts: SelectionDataItem[] = []
|
||||||
readonly temporaryLogicalOperator = signal(LogicalOperator.And)
|
|
||||||
private readonly _intersection = signal(Intersection.Include)
|
|
||||||
readonly temporaryIntersection = signal(Intersection.Include)
|
|
||||||
private readonly _documentCounts = signal<SelectionDataItem[]>([])
|
|
||||||
private readonly _items = signal<MatchingModel[]>([])
|
|
||||||
private readonly _selectionStates = signal(
|
|
||||||
new Map<number, ToggleableItemState>()
|
|
||||||
)
|
|
||||||
private readonly _temporarySelectionStates = signal(
|
|
||||||
new Map<number, ToggleableItemState>()
|
|
||||||
)
|
|
||||||
|
|
||||||
public documentCountSortingEnabled = false
|
public documentCountSortingEnabled = false
|
||||||
|
|
||||||
private get selectionStates(): ReadonlyMap<number, ToggleableItemState> {
|
|
||||||
return this._selectionStates()
|
|
||||||
}
|
|
||||||
|
|
||||||
private get temporarySelectionStates(): ReadonlyMap<
|
|
||||||
number,
|
|
||||||
ToggleableItemState
|
|
||||||
> {
|
|
||||||
return this._temporarySelectionStates()
|
|
||||||
}
|
|
||||||
|
|
||||||
public set documentCounts(counts: SelectionDataItem[]) {
|
public set documentCounts(counts: SelectionDataItem[]) {
|
||||||
this._documentCounts.set(counts)
|
this._documentCounts = counts
|
||||||
if (this.documentCountSortingEnabled) {
|
if (this.documentCountSortingEnabled) {
|
||||||
this._items.set(this.sortItems(this.items))
|
this.sortItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _items: MatchingModel[] = []
|
||||||
get items(): MatchingModel[] {
|
get items(): MatchingModel[] {
|
||||||
return this._items()
|
return this._items
|
||||||
}
|
}
|
||||||
|
|
||||||
set items(items: MatchingModel[]) {
|
set items(items: MatchingModel[]) {
|
||||||
if (items) {
|
if (items) {
|
||||||
this._items.set(this.withNullItem(this.sortItems(Array.from(items))))
|
this._items = Array.from(items)
|
||||||
|
this.sortItems()
|
||||||
|
this.setNullItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private withNullItem(items: MatchingModel[]): MatchingModel[] {
|
private setNullItem() {
|
||||||
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
||||||
return items[0]?.id === null ? items.slice(1) : items
|
if (this._items[0]?.id === null) {
|
||||||
|
this._items.shift()
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nullItem = {
|
const item = {
|
||||||
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||||
id:
|
id:
|
||||||
this.manyToOne || this.intersection === Intersection.Include
|
this.manyToOne || this.intersection === Intersection.Include
|
||||||
@@ -121,17 +108,22 @@ export class FilterableDropdownSelectionModel {
|
|||||||
: NEGATIVE_NULL_FILTER_VALUE,
|
: NEGATIVE_NULL_FILTER_VALUE,
|
||||||
}
|
}
|
||||||
|
|
||||||
return items[0]?.id === null || items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
if (
|
||||||
? [nullItem, ...items.slice(1)]
|
this._items[0]?.id === null ||
|
||||||
: [nullItem, ...items]
|
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
||||||
|
) {
|
||||||
|
this._items[0] = item
|
||||||
|
} else if (this._items) {
|
||||||
|
this._items.unshift(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(manyToOne: boolean = false) {
|
constructor(manyToOne: boolean = false) {
|
||||||
this.manyToOne = manyToOne
|
this.manyToOne = manyToOne
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortItems(items: MatchingModel[]): MatchingModel[] {
|
private sortItems() {
|
||||||
const sorted = [...items].sort((a, b) => {
|
this._items.sort((a, b) => {
|
||||||
if (
|
if (
|
||||||
(a.id == null && b.id != null) ||
|
(a.id == null && b.id != null) ||
|
||||||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
|
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
|
||||||
@@ -162,13 +154,13 @@ export class FilterableDropdownSelectionModel {
|
|||||||
) {
|
) {
|
||||||
return -1
|
return -1
|
||||||
} else if (
|
} else if (
|
||||||
this._documentCounts().length &&
|
this._documentCounts.length &&
|
||||||
this.getDocumentCount(b.id) === 0 &&
|
this.getDocumentCount(b.id) === 0 &&
|
||||||
this.getDocumentCount(a.id) > this.getDocumentCount(b.id)
|
this.getDocumentCount(a.id) > this.getDocumentCount(b.id)
|
||||||
) {
|
) {
|
||||||
return -1
|
return -1
|
||||||
} else if (
|
} else if (
|
||||||
this._documentCounts().length &&
|
this._documentCounts.length &&
|
||||||
this.getDocumentCount(a.id) === 0 &&
|
this.getDocumentCount(a.id) === 0 &&
|
||||||
this.getDocumentCount(a.id) < this.getDocumentCount(b.id)
|
this.getDocumentCount(a.id) < this.getDocumentCount(b.id)
|
||||||
) {
|
) {
|
||||||
@@ -178,11 +170,15 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._documentCounts().length
|
if (this._documentCounts.length) {
|
||||||
? this.promoteBranchesWithDocumentCounts(sorted)
|
this.promoteBranchesWithDocumentCounts()
|
||||||
: sorted
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private selectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
|
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
getSelectedItems() {
|
getSelectedItems() {
|
||||||
return this.items.filter(
|
return this.items.filter(
|
||||||
(i) =>
|
(i) =>
|
||||||
@@ -198,33 +194,30 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
set(id: number, state: ToggleableItemState, fireEvent = true) {
|
||||||
const states = new Map(this.temporarySelectionStates)
|
|
||||||
if (state == ToggleableItemState.NotSelected) {
|
if (state == ToggleableItemState.NotSelected) {
|
||||||
states.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
} else {
|
} else {
|
||||||
states.set(id, state)
|
this.temporarySelectionStates.set(id, state)
|
||||||
}
|
}
|
||||||
this._temporarySelectionStates.set(states)
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(id: number, fireEvent = true) {
|
toggle(id: number, fireEvent = true) {
|
||||||
const states = new Map(this.temporarySelectionStates)
|
let state = this.temporarySelectionStates.get(id)
|
||||||
let state = states.get(id)
|
|
||||||
if (
|
if (
|
||||||
state == undefined ||
|
state == undefined ||
|
||||||
(state != ToggleableItemState.Selected &&
|
(state != ToggleableItemState.Selected &&
|
||||||
state != ToggleableItemState.Excluded)
|
state != ToggleableItemState.Excluded)
|
||||||
) {
|
) {
|
||||||
if (this.manyToOne || this.singleSelect) {
|
if (this.manyToOne || this.singleSelect) {
|
||||||
states.set(id, ToggleableItemState.Selected)
|
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
||||||
|
|
||||||
if (this.singleSelect) {
|
if (this.singleSelect) {
|
||||||
for (let key of states.keys()) {
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
if (key != id) {
|
if (key != id) {
|
||||||
states.delete(key)
|
this.temporarySelectionStates.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,26 +233,25 @@ export class FilterableDropdownSelectionModel {
|
|||||||
) {
|
) {
|
||||||
newState = ToggleableItemState.NotSelected
|
newState = ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
states.set(id, newState)
|
this.temporarySelectionStates.set(id, newState)
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
state == ToggleableItemState.Selected ||
|
state == ToggleableItemState.Selected ||
|
||||||
state == ToggleableItemState.Excluded
|
state == ToggleableItemState.Excluded
|
||||||
) {
|
) {
|
||||||
states.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
this.clearDescendantSelections(states, id)
|
this.clearDescendantSelections(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
for (let key of states.keys()) {
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
if (key) {
|
if (key) {
|
||||||
states.delete(key)
|
this.temporarySelectionStates.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
states.delete(null)
|
this.temporarySelectionStates.delete(null)
|
||||||
}
|
}
|
||||||
this._temporarySelectionStates.set(states)
|
|
||||||
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
@@ -267,21 +259,20 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exclude(id: number, fireEvent: boolean = true) {
|
exclude(id: number, fireEvent: boolean = true) {
|
||||||
const states = new Map(this.temporarySelectionStates)
|
let state = this.temporarySelectionStates.get(id)
|
||||||
let state = states.get(id)
|
|
||||||
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
||||||
const operator = this.manyToOne ? LogicalOperator.And : LogicalOperator.Or
|
this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
|
||||||
this.temporaryLogicalOperator.set(operator)
|
? LogicalOperator.And
|
||||||
this._logicalOperator.set(operator)
|
: LogicalOperator.Or
|
||||||
|
|
||||||
if (this.manyToOne || this.singleSelect) {
|
if (this.manyToOne || this.singleSelect) {
|
||||||
states.set(id, ToggleableItemState.Excluded)
|
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||||
this.clearDescendantSelections(states, id)
|
this.clearDescendantSelections(id)
|
||||||
|
|
||||||
if (this.singleSelect) {
|
if (this.singleSelect) {
|
||||||
for (let key of states.keys()) {
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
if (key != id) {
|
if (key != id) {
|
||||||
states.delete(key)
|
this.temporarySelectionStates.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,18 +287,17 @@ export class FilterableDropdownSelectionModel {
|
|||||||
) {
|
) {
|
||||||
newState = ToggleableItemState.NotSelected
|
newState = ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
states.set(id, newState)
|
this.temporarySelectionStates.set(id, newState)
|
||||||
if (newState == ToggleableItemState.Excluded) {
|
if (newState == ToggleableItemState.Excluded) {
|
||||||
this.clearDescendantSelections(states, id)
|
this.clearDescendantSelections(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!id || state == ToggleableItemState.Excluded) {
|
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||||
states.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
if (id) {
|
if (id) {
|
||||||
this.clearDescendantSelections(states, id)
|
this.clearDescendantSelections(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._temporarySelectionStates.set(states)
|
|
||||||
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
@@ -318,12 +308,9 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearDescendantSelections(
|
private clearDescendantSelections(id: number) {
|
||||||
states: Map<number, ToggleableItemState>,
|
|
||||||
id: number
|
|
||||||
) {
|
|
||||||
for (const descendantID of this.getDescendantIDs(id)) {
|
for (const descendantID of this.getDescendantIDs(id)) {
|
||||||
states.delete(descendantID)
|
this.temporarySelectionStates.delete(descendantID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +320,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const parentID = queue.shift()
|
const parentID = queue.shift()
|
||||||
for (const item of this.items) {
|
for (const item of this._items) {
|
||||||
if (
|
if (
|
||||||
typeof item?.id === 'number' &&
|
typeof item?.id === 'number' &&
|
||||||
typeof (item as any)['parent'] === 'number' &&
|
typeof (item as any)['parent'] === 'number' &&
|
||||||
@@ -349,12 +336,12 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get logicalOperator(): LogicalOperator {
|
get logicalOperator(): LogicalOperator {
|
||||||
return this.temporaryLogicalOperator()
|
return this.temporaryLogicalOperator
|
||||||
}
|
}
|
||||||
|
|
||||||
set logicalOperator(operator: LogicalOperator) {
|
set logicalOperator(operator: LogicalOperator) {
|
||||||
this.temporaryLogicalOperator.set(operator)
|
this.temporaryLogicalOperator = operator
|
||||||
this._items.set(this.withNullItem(this.items))
|
this.setNullItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOperator() {
|
toggleOperator() {
|
||||||
@@ -362,12 +349,12 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get intersection(): Intersection {
|
get intersection(): Intersection {
|
||||||
return this.temporaryIntersection()
|
return this.temporaryIntersection
|
||||||
}
|
}
|
||||||
|
|
||||||
set intersection(intersection: Intersection) {
|
set intersection(intersection: Intersection) {
|
||||||
this.temporaryIntersection.set(intersection)
|
this.temporaryIntersection = intersection
|
||||||
this._items.set(this.withNullItem(this.items))
|
this.setNullItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleIntersection() {
|
toggleIntersection() {
|
||||||
@@ -377,20 +364,18 @@ export class FilterableDropdownSelectionModel {
|
|||||||
? ToggleableItemState.Selected
|
? ToggleableItemState.Selected
|
||||||
: ToggleableItemState.Excluded
|
: ToggleableItemState.Excluded
|
||||||
|
|
||||||
const states = new Map(this.temporarySelectionStates)
|
this.temporarySelectionStates.forEach((state, key) => {
|
||||||
states.forEach((state, key) => {
|
|
||||||
if (key === null && this.intersection === Intersection.Exclude) {
|
if (key === null && this.intersection === Intersection.Exclude) {
|
||||||
states.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
||||||
} else if (
|
} else if (
|
||||||
key === NEGATIVE_NULL_FILTER_VALUE &&
|
key === NEGATIVE_NULL_FILTER_VALUE &&
|
||||||
this.intersection === Intersection.Include
|
this.intersection === Intersection.Include
|
||||||
) {
|
) {
|
||||||
states.set(null, newState)
|
this.temporarySelectionStates.set(null, newState)
|
||||||
} else {
|
} else {
|
||||||
states.set(key, newState)
|
this.temporarySelectionStates.set(key, newState)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this._temporarySelectionStates.set(states)
|
|
||||||
|
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
@@ -410,12 +395,10 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear(fireEvent = true) {
|
clear(fireEvent = true) {
|
||||||
this._temporarySelectionStates.set(new Map())
|
this.temporarySelectionStates.clear()
|
||||||
this.temporaryLogicalOperator.set(LogicalOperator.And)
|
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
||||||
this._logicalOperator.set(LogicalOperator.And)
|
this.temporaryIntersection = this._intersection = Intersection.Include
|
||||||
this.temporaryIntersection.set(Intersection.Include)
|
this.setNullItem()
|
||||||
this._intersection.set(Intersection.Include)
|
|
||||||
this._items.set(this.withNullItem(this.items))
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
@@ -436,9 +419,9 @@ export class FilterableDropdownSelectionModel {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
} else if (this.temporaryLogicalOperator() !== this._logicalOperator()) {
|
} else if (this.temporaryLogicalOperator !== this._logicalOperator) {
|
||||||
return true
|
return true
|
||||||
} else if (this.temporaryIntersection() !== this._intersection()) {
|
} else if (this.temporaryIntersection !== this._intersection) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@@ -455,29 +438,23 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDocumentCount(id: number) {
|
getDocumentCount(id: number) {
|
||||||
return this._documentCounts().find((c) => c.id === id)?.document_count
|
return this._documentCounts.find((c) => c.id === id)?.document_count
|
||||||
}
|
}
|
||||||
|
|
||||||
private promoteBranchesWithDocumentCounts(
|
private promoteBranchesWithDocumentCounts() {
|
||||||
items: MatchingModel[]
|
const parentById = this.buildParentById()
|
||||||
): MatchingModel[] {
|
|
||||||
const parentById = this.buildParentById(items)
|
|
||||||
const findRootId = this.createRootFinder(parentById)
|
const findRootId = this.createRootFinder(parentById)
|
||||||
const getRootDocCount = this.createRootDocCounter(items)
|
const getRootDocCount = this.createRootDocCounter()
|
||||||
const summaries = this.buildBranchSummaries(
|
const summaries = this.buildBranchSummaries(findRootId, getRootDocCount)
|
||||||
items,
|
|
||||||
findRootId,
|
|
||||||
getRootDocCount
|
|
||||||
)
|
|
||||||
const orderedBranches = this.orderBranchesByPriority(summaries)
|
const orderedBranches = this.orderBranchesByPriority(summaries)
|
||||||
|
|
||||||
return orderedBranches.flatMap((summary) => summary.items)
|
this._items = orderedBranches.flatMap((summary) => summary.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildParentById(items: MatchingModel[]): Map<number, number | null> {
|
private buildParentById(): Map<number, number | null> {
|
||||||
const parentById = new Map<number, number | null>()
|
const parentById = new Map<number, number | null>()
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of this._items) {
|
||||||
if (typeof item?.id === 'number') {
|
if (typeof item?.id === 'number') {
|
||||||
const parentValue = (item as any)['parent']
|
const parentValue = (item as any)['parent']
|
||||||
parentById.set(
|
parentById.set(
|
||||||
@@ -515,9 +492,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return findRootId
|
return findRootId
|
||||||
}
|
}
|
||||||
|
|
||||||
private createRootDocCounter(
|
private createRootDocCounter(): (rootId: number) => number {
|
||||||
items: MatchingModel[]
|
|
||||||
): (rootId: number) => number {
|
|
||||||
const docCountMemo = new Map<number, number>()
|
const docCountMemo = new Map<number, number>()
|
||||||
|
|
||||||
return (rootId: number): number => {
|
return (rootId: number): number => {
|
||||||
@@ -532,7 +507,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return explicit
|
return explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootItem = items.find((i) => i.id === rootId)
|
const rootItem = this._items.find((i) => i.id === rootId)
|
||||||
const fallback =
|
const fallback =
|
||||||
typeof (rootItem as any)?.['document_count'] === 'number'
|
typeof (rootItem as any)?.['document_count'] === 'number'
|
||||||
? (rootItem as any)['document_count']
|
? (rootItem as any)['document_count']
|
||||||
@@ -544,13 +519,12 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildBranchSummaries(
|
private buildBranchSummaries(
|
||||||
items: MatchingModel[],
|
|
||||||
findRootId: (id: number) => number,
|
findRootId: (id: number) => number,
|
||||||
getRootDocCount: (rootId: number) => number
|
getRootDocCount: (rootId: number) => number
|
||||||
): Map<string, BranchSummary> {
|
): Map<string, BranchSummary> {
|
||||||
const summaries = new Map<string, BranchSummary>()
|
const summaries = new Map<string, BranchSummary>()
|
||||||
|
|
||||||
for (const [index, item] of items.entries()) {
|
for (const [index, item] of this._items.entries()) {
|
||||||
const { key, special, rootId } = this.describeBranchItem(
|
const { key, special, rootId } = this.describeBranchItem(
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
@@ -642,23 +616,28 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(map: Map<number, ToggleableItemState>) {
|
init(map: Map<number, ToggleableItemState>) {
|
||||||
this._temporarySelectionStates.set(new Map(map))
|
this.temporarySelectionStates = map
|
||||||
this.apply()
|
this.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
apply() {
|
apply() {
|
||||||
this._selectionStates.set(new Map(this.temporarySelectionStates))
|
this.selectionStates.clear()
|
||||||
this._logicalOperator.set(this.temporaryLogicalOperator())
|
this.temporarySelectionStates.forEach((value, key) => {
|
||||||
this._intersection.set(this.temporaryIntersection())
|
this.selectionStates.set(key, value)
|
||||||
this._items.set(this.sortItems(this.items))
|
})
|
||||||
|
this._logicalOperator = this.temporaryLogicalOperator
|
||||||
|
this._intersection = this.temporaryIntersection
|
||||||
|
this.sortItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(complete: boolean = false) {
|
reset(complete: boolean = false) {
|
||||||
|
this.temporarySelectionStates.clear()
|
||||||
if (complete) {
|
if (complete) {
|
||||||
this._selectionStates.set(new Map())
|
this.selectionStates.clear()
|
||||||
this._temporarySelectionStates.set(new Map())
|
|
||||||
} else {
|
} else {
|
||||||
this._temporarySelectionStates.set(new Map(this.selectionStates))
|
this.selectionStates.forEach((value, key) => {
|
||||||
|
this.temporarySelectionStates.set(key, value)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -7,7 +7,7 @@
|
|||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.NONE) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.NONE) {
|
||||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.SELF) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.SELF) {
|
||||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.NOT_SELF) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
|
||||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.SHARED_BY_ME) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME) {
|
||||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.UNOWNED) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
|
||||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
|
<button *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
|
||||||
<div class="selected-icon me-1">
|
<div class="selected-icon me-1">
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.OTHERS) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.OTHERS) {
|
||||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -65,8 +65,7 @@
|
|||||||
<ng-select
|
<ng-select
|
||||||
name="user"
|
name="user"
|
||||||
class="user-select small"
|
class="user-select small"
|
||||||
[ngModel]="selectionModel.includeUsers()"
|
[(ngModel)]="selectionModel.includeUsers"
|
||||||
(ngModelChange)="selectionModel.includeUsers.set($event)"
|
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
[items]="users()"
|
[items]="users()"
|
||||||
@@ -79,10 +78,10 @@
|
|||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@if (selectionModel.ownerFilter() === OwnerFilterType.NONE || selectionModel.ownerFilter() === OwnerFilterType.NOT_SELF) {
|
@if (selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
|
||||||
<div class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
|
<div class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
|
||||||
<div class="form-check form-switch w-100">
|
<div class="form-check form-switch w-100">
|
||||||
<input type="checkbox" class="form-check-input" id="hideUnowned" [ngModel]="selectionModel.hideUnowned()" (ngModelChange)="selectionModel.hideUnowned.set($event)" (change)="onChange()" [disabled]="disabled">
|
<input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled">
|
||||||
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
|
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+30
-39
@@ -90,56 +90,56 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
component.setFilter(OwnerFilterType.OTHERS)
|
component.setFilter(OwnerFilterType.OTHERS)
|
||||||
expect(component.isActive).toBeTruthy()
|
expect(component.isActive).toBeTruthy()
|
||||||
component.setFilter(OwnerFilterType.NONE)
|
component.setFilter(OwnerFilterType.NONE)
|
||||||
component.selectionModel.hideUnowned.set(true)
|
component.selectionModel.hideUnowned = true
|
||||||
expect(component.isActive).toBeTruthy()
|
expect(component.isActive).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should describe concrete user filters honestly', () => {
|
it('should describe concrete user filters honestly', () => {
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.SELF)
|
component.selectionModel.ownerFilter = OwnerFilterType.SELF
|
||||||
component.selectionModel.userID.set(1)
|
component.selectionModel.userID = 1
|
||||||
expect(component.ownerFilterLabel).toEqual('Owned by user1')
|
expect(component.ownerFilterLabel).toEqual('Owned by user1')
|
||||||
|
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.NOT_SELF)
|
component.selectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
||||||
component.selectionModel.excludeUsers.set([1])
|
component.selectionModel.excludeUsers = [1]
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual('Not owned by user1')
|
expect(component.ownerExclusionFilterLabel).toEqual('Not owned by user1')
|
||||||
|
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.SHARED_BY_ME)
|
component.selectionModel.ownerFilter = OwnerFilterType.SHARED_BY_ME
|
||||||
component.selectionModel.userID.set(1)
|
component.selectionModel.userID = 1
|
||||||
expect(component.sharedByFilterLabel).toEqual('Shared by user1')
|
expect(component.sharedByFilterLabel).toEqual('Shared by user1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should describe concrete filters when usernames are unavailable', () => {
|
it('should describe concrete filters when usernames are unavailable', () => {
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.SELF)
|
component.selectionModel.ownerFilter = OwnerFilterType.SELF
|
||||||
component.selectionModel.userID.set(99)
|
component.selectionModel.userID = 99
|
||||||
expect(component.ownerFilterLabel).toEqual('Owned by another user')
|
expect(component.ownerFilterLabel).toEqual('Owned by another user')
|
||||||
|
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.NOT_SELF)
|
component.selectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
||||||
component.selectionModel.excludeUsers.set([99])
|
component.selectionModel.excludeUsers = [99]
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual(
|
expect(component.ownerExclusionFilterLabel).toEqual(
|
||||||
'Not owned by another user'
|
'Not owned by another user'
|
||||||
)
|
)
|
||||||
|
|
||||||
component.selectionModel.excludeUsers.set([98, 99])
|
component.selectionModel.excludeUsers = [98, 99]
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual(
|
expect(component.ownerExclusionFilterLabel).toEqual(
|
||||||
'Not owned by selected users'
|
'Not owned by selected users'
|
||||||
)
|
)
|
||||||
|
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.SHARED_BY_ME)
|
component.selectionModel.ownerFilter = OwnerFilterType.SHARED_BY_ME
|
||||||
component.selectionModel.userID.set(99)
|
component.selectionModel.userID = 99
|
||||||
expect(component.sharedByFilterLabel).toEqual('Shared by another user')
|
expect(component.sharedByFilterLabel).toEqual('Shared by another user')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should retain relative labels for filters bound to the current user', () => {
|
it('should retain relative labels for filters bound to the current user', () => {
|
||||||
component.selectionModel.userID.set(currentUserID)
|
component.selectionModel.userID = currentUserID
|
||||||
expect(component.ownerFilterLabel).toEqual('My documents')
|
expect(component.ownerFilterLabel).toEqual('My documents')
|
||||||
expect(component.sharedByFilterLabel).toEqual('Shared by me')
|
expect(component.sharedByFilterLabel).toEqual('Shared by me')
|
||||||
|
|
||||||
component.selectionModel.excludeUsers.set([currentUserID])
|
component.selectionModel.excludeUsers = [currentUserID]
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual('Shared with me')
|
expect(component.ownerExclusionFilterLabel).toEqual('Shared with me')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should retain relative labels for inactive filter choices', () => {
|
it('should retain relative labels for inactive filter choices', () => {
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.NONE)
|
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||||
|
|
||||||
expect(component.ownerFilterLabel).toEqual('My documents')
|
expect(component.ownerFilterLabel).toEqual('My documents')
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual('Shared with me')
|
expect(component.ownerExclusionFilterLabel).toEqual('Shared with me')
|
||||||
@@ -148,41 +148,32 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
|
|
||||||
it('should support reset', () => {
|
it('should support reset', () => {
|
||||||
component.setFilter(OwnerFilterType.OTHERS)
|
component.setFilter(OwnerFilterType.OTHERS)
|
||||||
expect(component.selectionModel.ownerFilter()).not.toEqual(
|
expect(component.selectionModel.ownerFilter).not.toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
)
|
)
|
||||||
component.reset()
|
component.reset()
|
||||||
expect(component.selectionModel.ownerFilter()).toEqual(OwnerFilterType.NONE)
|
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should toggle owner filter type when users selected', () => {
|
it('should toggle owner filter type when users selected', () => {
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.NONE)
|
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||||
|
|
||||||
// this would normally be done by select component
|
// this would normally be done by select component
|
||||||
component.selectionModel.includeUsers.set([12])
|
component.selectionModel.includeUsers = [12]
|
||||||
component.onUserSelect()
|
component.onUserSelect()
|
||||||
expect(component.selectionModel.ownerFilter()).toEqual(
|
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.OTHERS)
|
||||||
OwnerFilterType.OTHERS
|
|
||||||
)
|
|
||||||
|
|
||||||
// this would normally be done by select component
|
// this would normally be done by select component
|
||||||
component.selectionModel.includeUsers.set(null)
|
component.selectionModel.includeUsers = null
|
||||||
component.onUserSelect()
|
component.onUserSelect()
|
||||||
|
|
||||||
expect(component.selectionModel.ownerFilter()).toEqual(OwnerFilterType.NONE)
|
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
|
||||||
})
|
})
|
||||||
it('should emit a selection model depending on the type of owner filter set', () => {
|
it('should emit a selection model depending on the type of owner filter set', () => {
|
||||||
const emitted = () => ({
|
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||||
excludeUsers: ownerFilterSetResult.excludeUsers(),
|
|
||||||
hideUnowned: ownerFilterSetResult.hideUnowned(),
|
|
||||||
includeUsers: ownerFilterSetResult.includeUsers(),
|
|
||||||
ownerFilter: ownerFilterSetResult.ownerFilter(),
|
|
||||||
userID: ownerFilterSetResult.userID(),
|
|
||||||
})
|
|
||||||
component.selectionModel.ownerFilter.set(OwnerFilterType.NONE)
|
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.SELF)
|
component.setFilter(OwnerFilterType.SELF)
|
||||||
expect(emitted()).toEqual({
|
expect(ownerFilterSetResult).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -191,7 +182,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.NOT_SELF)
|
component.setFilter(OwnerFilterType.NOT_SELF)
|
||||||
expect(emitted()).toEqual({
|
expect(ownerFilterSetResult).toEqual({
|
||||||
excludeUsers: [currentUserID],
|
excludeUsers: [currentUserID],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -200,7 +191,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.NONE)
|
component.setFilter(OwnerFilterType.NONE)
|
||||||
expect(emitted()).toEqual({
|
expect(ownerFilterSetResult).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -209,7 +200,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.SHARED_BY_ME)
|
component.setFilter(OwnerFilterType.SHARED_BY_ME)
|
||||||
expect(emitted()).toEqual({
|
expect(ownerFilterSetResult).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -218,7 +209,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.UNOWNED)
|
component.setFilter(OwnerFilterType.UNOWNED)
|
||||||
expect(emitted()).toEqual({
|
expect(ownerFilterSetResult).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
|
|||||||
+53
-53
@@ -25,18 +25,18 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
|
|||||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||||
|
|
||||||
export class PermissionsSelectionModel {
|
export class PermissionsSelectionModel {
|
||||||
readonly ownerFilter = signal(OwnerFilterType.NONE)
|
ownerFilter: OwnerFilterType
|
||||||
readonly hideUnowned = signal(false)
|
hideUnowned: boolean
|
||||||
readonly userID = signal<number>(null)
|
userID: number
|
||||||
readonly includeUsers = signal<number[]>([])
|
includeUsers: number[]
|
||||||
readonly excludeUsers = signal<number[]>([])
|
excludeUsers: number[]
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.ownerFilter.set(OwnerFilterType.NONE)
|
this.ownerFilter = OwnerFilterType.NONE
|
||||||
this.userID.set(null)
|
this.userID = null
|
||||||
this.hideUnowned.set(false)
|
this.hideUnowned = false
|
||||||
this.includeUsers.set([])
|
this.includeUsers = []
|
||||||
this.excludeUsers.set([])
|
this.excludeUsers = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,31 +84,33 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
|
|
||||||
readonly users = signal<User[]>([])
|
readonly users = signal<User[]>([])
|
||||||
|
|
||||||
|
hideUnowned: boolean
|
||||||
|
|
||||||
get isActive(): boolean {
|
get isActive(): boolean {
|
||||||
return (
|
return (
|
||||||
this.selectionModel.ownerFilter() !== OwnerFilterType.NONE ||
|
this.selectionModel.ownerFilter !== OwnerFilterType.NONE ||
|
||||||
this.selectionModel.hideUnowned()
|
this.selectionModel.hideUnowned
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get ownerFilterLabel(): string {
|
get ownerFilterLabel(): string {
|
||||||
if (
|
if (
|
||||||
this.selectionModel?.ownerFilter() !== OwnerFilterType.SELF ||
|
this.selectionModel?.ownerFilter !== OwnerFilterType.SELF ||
|
||||||
this.selectionModel?.userID() === this.settingsService.currentUser()?.id
|
this.selectionModel?.userID === this.settingsService.currentUser()?.id
|
||||||
) {
|
) {
|
||||||
return $localize`My documents`
|
return $localize`My documents`
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = this.getUsername(this.selectionModel?.userID())
|
const username = this.getUsername(this.selectionModel?.userID)
|
||||||
return username
|
return username
|
||||||
? $localize`Owned by ${username}`
|
? $localize`Owned by ${username}`
|
||||||
: $localize`Owned by another user`
|
: $localize`Owned by another user`
|
||||||
}
|
}
|
||||||
|
|
||||||
get ownerExclusionFilterLabel(): string {
|
get ownerExclusionFilterLabel(): string {
|
||||||
const excludedUsers = this.selectionModel?.excludeUsers() ?? []
|
const excludedUsers = this.selectionModel?.excludeUsers ?? []
|
||||||
if (
|
if (
|
||||||
this.selectionModel?.ownerFilter() !== OwnerFilterType.NOT_SELF ||
|
this.selectionModel?.ownerFilter !== OwnerFilterType.NOT_SELF ||
|
||||||
(excludedUsers.length === 1 &&
|
(excludedUsers.length === 1 &&
|
||||||
excludedUsers[0] === this.settingsService.currentUser()?.id)
|
excludedUsers[0] === this.settingsService.currentUser()?.id)
|
||||||
) {
|
) {
|
||||||
@@ -128,13 +130,13 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
|
|
||||||
get sharedByFilterLabel(): string {
|
get sharedByFilterLabel(): string {
|
||||||
if (
|
if (
|
||||||
this.selectionModel?.ownerFilter() !== OwnerFilterType.SHARED_BY_ME ||
|
this.selectionModel?.ownerFilter !== OwnerFilterType.SHARED_BY_ME ||
|
||||||
this.selectionModel?.userID() === this.settingsService.currentUser()?.id
|
this.selectionModel?.userID === this.settingsService.currentUser()?.id
|
||||||
) {
|
) {
|
||||||
return $localize`Shared by me`
|
return $localize`Shared by me`
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = this.getUsername(this.selectionModel?.userID())
|
const username = this.getUsername(this.selectionModel?.userID)
|
||||||
return username
|
return username
|
||||||
? $localize`Shared by ${username}`
|
? $localize`Shared by ${username}`
|
||||||
: $localize`Shared by another user`
|
: $localize`Shared by another user`
|
||||||
@@ -167,36 +169,34 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFilter(type: OwnerFilterType) {
|
setFilter(type: OwnerFilterType) {
|
||||||
this.selectionModel.ownerFilter.set(type)
|
this.selectionModel.ownerFilter = type
|
||||||
if (this.selectionModel.ownerFilter() === OwnerFilterType.SELF) {
|
if (this.selectionModel.ownerFilter === OwnerFilterType.SELF) {
|
||||||
this.selectionModel.includeUsers.set([])
|
this.selectionModel.includeUsers = []
|
||||||
this.selectionModel.excludeUsers.set([])
|
this.selectionModel.excludeUsers = []
|
||||||
this.selectionModel.userID.set(this.settingsService.currentUser().id)
|
this.selectionModel.userID = this.settingsService.currentUser().id
|
||||||
this.selectionModel.hideUnowned.set(false)
|
this.selectionModel.hideUnowned = false
|
||||||
} else if (this.selectionModel.ownerFilter() === OwnerFilterType.NOT_SELF) {
|
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
|
||||||
this.selectionModel.userID.set(null)
|
this.selectionModel.userID = null
|
||||||
this.selectionModel.includeUsers.set([])
|
this.selectionModel.includeUsers = []
|
||||||
this.selectionModel.excludeUsers.set([
|
this.selectionModel.excludeUsers = [this.settingsService.currentUser().id]
|
||||||
this.settingsService.currentUser().id,
|
this.selectionModel.hideUnowned = false
|
||||||
])
|
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NONE) {
|
||||||
this.selectionModel.hideUnowned.set(false)
|
this.selectionModel.userID = null
|
||||||
} else if (this.selectionModel.ownerFilter() === OwnerFilterType.NONE) {
|
this.selectionModel.includeUsers = []
|
||||||
this.selectionModel.userID.set(null)
|
this.selectionModel.excludeUsers = []
|
||||||
this.selectionModel.includeUsers.set([])
|
this.selectionModel.hideUnowned = false
|
||||||
this.selectionModel.excludeUsers.set([])
|
|
||||||
this.selectionModel.hideUnowned.set(false)
|
|
||||||
} else if (
|
} else if (
|
||||||
this.selectionModel.ownerFilter() === OwnerFilterType.SHARED_BY_ME
|
this.selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME
|
||||||
) {
|
) {
|
||||||
this.selectionModel.userID.set(this.settingsService.currentUser()?.id)
|
this.selectionModel.userID = this.settingsService.currentUser()?.id
|
||||||
this.selectionModel.includeUsers.set([])
|
this.selectionModel.includeUsers = []
|
||||||
this.selectionModel.excludeUsers.set([])
|
this.selectionModel.excludeUsers = []
|
||||||
this.selectionModel.hideUnowned.set(false)
|
this.selectionModel.hideUnowned = false
|
||||||
} else if (this.selectionModel.ownerFilter() === OwnerFilterType.UNOWNED) {
|
} else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
|
||||||
this.selectionModel.userID.set(null)
|
this.selectionModel.userID = null
|
||||||
this.selectionModel.includeUsers.set([])
|
this.selectionModel.includeUsers = []
|
||||||
this.selectionModel.excludeUsers.set([])
|
this.selectionModel.excludeUsers = []
|
||||||
this.selectionModel.hideUnowned.set(false)
|
this.selectionModel.hideUnowned = false
|
||||||
}
|
}
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
@@ -206,11 +206,11 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUserSelect() {
|
onUserSelect() {
|
||||||
this.selectionModel.ownerFilter.set(
|
if (this.selectionModel.includeUsers?.length) {
|
||||||
this.selectionModel.includeUsers()?.length
|
this.selectionModel.ownerFilter = OwnerFilterType.OTHERS
|
||||||
? OwnerFilterType.OTHERS
|
} else {
|
||||||
: OwnerFilterType.NONE
|
this.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||||
)
|
}
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1209,53 +1209,24 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(fixture.debugElement.queryAll(By.css('textarea.rtl'))).not.toBeNull()
|
expect(fixture.debugElement.queryAll(By.css('textarea.rtl'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display built-in pdf viewer if not disabled', async () => {
|
it('should display built-in pdf viewer if not disabled', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.document.update((document) => ({
|
component.document().archived_file_name = 'file.pdf'
|
||||||
...document,
|
|
||||||
archived_file_name: 'file.pdf',
|
|
||||||
}))
|
|
||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
||||||
expect(component.useNativePdfViewer).toBeFalsy()
|
expect(component.useNativePdfViewer).toBeFalsy()
|
||||||
await fixture.whenStable()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
|
expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display native pdf viewer if enabled', () => {
|
it('should display native pdf viewer if enabled', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.document.update((document) => ({
|
component.document().archived_file_name = 'file.pdf'
|
||||||
...document,
|
|
||||||
archived_file_name: 'file.pdf',
|
|
||||||
}))
|
|
||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
|
||||||
expect(component.useNativePdfViewer).toBeTruthy()
|
expect(component.useNativePdfViewer).toBeTruthy()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
|
expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reflect signal-backed document detail display settings', () => {
|
|
||||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL, false)
|
|
||||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, [
|
|
||||||
component.DocumentDetailFieldID.Correspondent,
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(component.showThumbnailOverlay).toBeFalsy()
|
|
||||||
expect(
|
|
||||||
component.isFieldHidden(component.DocumentDetailFieldID.Correspondent)
|
|
||||||
).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
component.isFieldHidden(component.DocumentDetailFieldID.DocumentType)
|
|
||||||
).toBeFalsy()
|
|
||||||
|
|
||||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL, true)
|
|
||||||
settingsService.set(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, [])
|
|
||||||
|
|
||||||
expect(component.showThumbnailOverlay).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
component.isFieldHidden(component.DocumentDetailFieldID.Correspondent)
|
|
||||||
).toBeFalsy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should attempt to retrieve metadata', () => {
|
it('should attempt to retrieve metadata', () => {
|
||||||
const metadataSpy = jest.spyOn(documentService, 'getMetadata')
|
const metadataSpy = jest.spyOn(documentService, 'getMetadata')
|
||||||
metadataSpy.mockReturnValue(of({ has_archive_version: true }))
|
metadataSpy.mockReturnValue(of({ has_archive_version: true }))
|
||||||
@@ -1714,10 +1685,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
it('should change preview element by render type', () => {
|
it('should change preview element by render type', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.document.update((document) => ({
|
component.document().archived_file_name = 'file.pdf'
|
||||||
...document,
|
|
||||||
archived_file_name: 'file.pdf',
|
|
||||||
}))
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.PDF
|
component.ContentRenderType.PDF
|
||||||
@@ -1726,11 +1694,8 @@ describe('DocumentDetailComponent', () => {
|
|||||||
fixture.debugElement.query(By.css('pdf-viewer-container'))
|
fixture.debugElement.query(By.css('pdf-viewer-container'))
|
||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
|
|
||||||
component.document.update((document) => ({
|
component.document().archived_file_name = undefined
|
||||||
...document,
|
component.document().mime_type = 'text/plain'
|
||||||
archived_file_name: undefined,
|
|
||||||
mime_type: 'text/plain',
|
|
||||||
}))
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.Text
|
component.ContentRenderType.Text
|
||||||
@@ -1739,10 +1704,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
fixture.debugElement.query(By.css('div.preview-sticky'))
|
fixture.debugElement.query(By.css('div.preview-sticky'))
|
||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
|
|
||||||
component.document.update((document) => ({
|
component.document().mime_type = 'image/jpeg'
|
||||||
...document,
|
|
||||||
mime_type: 'image/jpeg',
|
|
||||||
}))
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.Image
|
component.ContentRenderType.Image
|
||||||
@@ -1750,12 +1712,9 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(
|
expect(
|
||||||
fixture.debugElement.query(By.css('.preview-sticky img'))
|
fixture.debugElement.query(By.css('.preview-sticky img'))
|
||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
component.document.update((document) => ({
|
;((component.document().mime_type =
|
||||||
...document,
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'),
|
||||||
mime_type:
|
fixture.detectChanges())
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
||||||
}))
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.Other
|
component.ContentRenderType.Other
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -227,19 +227,6 @@ export class DocumentDetailComponent
|
|||||||
private deviceDetectorService = inject(DeviceDetectorService)
|
private deviceDetectorService = inject(DeviceDetectorService)
|
||||||
private savedViewService = inject(SavedViewService)
|
private savedViewService = inject(SavedViewService)
|
||||||
private readonly websocketStatusService = inject(WebsocketStatusService)
|
private readonly websocketStatusService = inject(WebsocketStatusService)
|
||||||
private readonly useNativePdfViewerSetting = this.settings.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
|
|
||||||
)
|
|
||||||
private readonly aiEnabledSetting = this.settings.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.AI_ENABLED
|
|
||||||
)
|
|
||||||
private readonly showThumbnailOverlaySetting =
|
|
||||||
this.settings.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL
|
|
||||||
)
|
|
||||||
private readonly hiddenFieldsSetting = this.settings.getSignal<
|
|
||||||
DocumentDetailFieldID[]
|
|
||||||
>(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
|
|
||||||
|
|
||||||
@ViewChild('inputTitle')
|
@ViewChild('inputTitle')
|
||||||
titleInput: TextComponent
|
titleInput: TextComponent
|
||||||
@@ -346,7 +333,8 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get useNativePdfViewer(): boolean {
|
get useNativePdfViewer(): boolean {
|
||||||
return this.useNativePdfViewerSetting()
|
this.settings.trackChanges()
|
||||||
|
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMobile(): boolean {
|
get isMobile(): boolean {
|
||||||
@@ -354,10 +342,12 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
return this.aiEnabledSetting()
|
this.settings.trackChanges()
|
||||||
|
return this.settings.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
get archiveContentRenderType(): ContentRenderType {
|
get archiveContentRenderType(): ContentRenderType {
|
||||||
|
this.settings.trackChanges()
|
||||||
const hasArchiveVersion =
|
const hasArchiveVersion =
|
||||||
this.metadata()?.has_archive_version ??
|
this.metadata()?.has_archive_version ??
|
||||||
!!this.document()?.archived_file_name
|
!!this.document()?.archived_file_name
|
||||||
@@ -369,17 +359,22 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get originalContentRenderType(): ContentRenderType {
|
get originalContentRenderType(): ContentRenderType {
|
||||||
|
this.settings.trackChanges()
|
||||||
return this.getRenderType(
|
return this.getRenderType(
|
||||||
this.metadata()?.original_mime_type || this.document()?.mime_type
|
this.metadata()?.original_mime_type || this.document()?.mime_type
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get showThumbnailOverlay(): boolean {
|
get showThumbnailOverlay(): boolean {
|
||||||
return this.showThumbnailOverlaySetting()
|
this.settings.trackChanges()
|
||||||
|
return this.settings.get(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL)
|
||||||
}
|
}
|
||||||
|
|
||||||
isFieldHidden(fieldId: DocumentDetailFieldID): boolean {
|
isFieldHidden(fieldId: DocumentDetailFieldID): boolean {
|
||||||
return this.hiddenFieldsSetting().includes(fieldId)
|
this.settings.trackChanges()
|
||||||
|
return this.settings
|
||||||
|
.get(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
|
||||||
|
.includes(fieldId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRenderType(mimeType: string): ContentRenderType {
|
private getRenderType(mimeType: string): ContentRenderType {
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ export class DocumentListComponent
|
|||||||
settingsService = inject(SettingsService)
|
settingsService = inject(SettingsService)
|
||||||
private hotKeyService = inject(HotKeyService)
|
private hotKeyService = inject(HotKeyService)
|
||||||
permissionService = inject(PermissionsService)
|
permissionService = inject(PermissionsService)
|
||||||
private readonly notesEnabledSetting =
|
|
||||||
this.settingsService.getSignal<boolean>(SETTINGS_KEYS.NOTES_ENABLED)
|
|
||||||
|
|
||||||
DisplayField = DisplayField
|
DisplayField = DisplayField
|
||||||
DisplayMode = DisplayMode
|
DisplayMode = DisplayMode
|
||||||
@@ -576,7 +574,8 @@ export class DocumentListComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get notesEnabled(): boolean {
|
get notesEnabled(): boolean {
|
||||||
return this.notesEnabledSetting()
|
this.settingsService.trackChanges()
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
|
|||||||
+20
-86
@@ -621,43 +621,6 @@ describe('FilterEditorComponent', () => {
|
|||||||
component.toggleTag(2) // coverage
|
component.toggleTag(2) // coverage
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reflect ingested tag filter rules in the dropdown toggle', () => {
|
|
||||||
const dropdown = fixture.debugElement.query(
|
|
||||||
By.css('pngx-filterable-dropdown')
|
|
||||||
)
|
|
||||||
const toggle = dropdown.nativeElement.querySelector('#dropdown_tags')
|
|
||||||
expect(toggle.classList.contains('btn-primary')).toBeFalsy()
|
|
||||||
expect(
|
|
||||||
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
|
||||||
).toBeNull()
|
|
||||||
|
|
||||||
// switching to a view with a tag filter
|
|
||||||
component.filterRules = [
|
|
||||||
{
|
|
||||||
rule_type: FILTER_HAS_TAGS_ALL,
|
|
||||||
value: '2',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(toggle.classList.contains('btn-primary')).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
|
||||||
).not.toBeNull()
|
|
||||||
|
|
||||||
// and back to a view without one
|
|
||||||
component.filterRules = [
|
|
||||||
{
|
|
||||||
rule_type: FILTER_HAS_CORRESPONDENT_ANY,
|
|
||||||
value: '12',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(toggle.classList.contains('btn-primary')).toBeFalsy()
|
|
||||||
expect(
|
|
||||||
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
|
||||||
).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should ingest filter rules for has any tags', () => {
|
it('should ingest filter rules for has any tags', () => {
|
||||||
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
|
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
@@ -1115,7 +1078,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner', () => {
|
it('should ingest filter rules for owner', () => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
)
|
)
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
@@ -1124,38 +1087,15 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '100',
|
value: '100',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.SELF
|
OwnerFilterType.SELF
|
||||||
)
|
)
|
||||||
expect(component.permissionsSelectionModel.hideUnowned()).toBeFalsy()
|
expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
|
||||||
expect(component.permissionsSelectionModel.userID()).toEqual(100)
|
expect(component.permissionsSelectionModel.userID).toEqual(100)
|
||||||
})
|
|
||||||
|
|
||||||
it('should reflect ingested owner filter rules in the dropdown toggle', () => {
|
|
||||||
const dropdown = fixture.debugElement.query(
|
|
||||||
By.css('pngx-permissions-filter-dropdown')
|
|
||||||
)
|
|
||||||
const toggle = dropdown.nativeElement.querySelector('button')
|
|
||||||
expect(toggle.classList.contains('btn-primary')).toBeFalsy()
|
|
||||||
|
|
||||||
// switching to a view with an owner filter
|
|
||||||
component.filterRules = [
|
|
||||||
{
|
|
||||||
rule_type: FILTER_OWNER,
|
|
||||||
value: '100',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(toggle.classList.contains('btn-primary')).toBeTruthy()
|
|
||||||
|
|
||||||
// and back to a view without one
|
|
||||||
component.filterRules = []
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(toggle.classList.contains('btn-primary')).toBeFalsy()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner is others', () => {
|
it('should ingest filter rules for owner is others', () => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
)
|
)
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
@@ -1164,14 +1104,14 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '50',
|
value: '50',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.OTHERS
|
OwnerFilterType.OTHERS
|
||||||
)
|
)
|
||||||
expect(component.permissionsSelectionModel.includeUsers()).toContain(50)
|
expect(component.permissionsSelectionModel.includeUsers).toContain(50)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner does not include others', () => {
|
it('should ingest filter rules for owner does not include others', () => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
)
|
)
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
@@ -1180,14 +1120,14 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '50',
|
value: '50',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NOT_SELF
|
OwnerFilterType.NOT_SELF
|
||||||
)
|
)
|
||||||
expect(component.permissionsSelectionModel.excludeUsers()).toContain(50)
|
expect(component.permissionsSelectionModel.excludeUsers).toContain(50)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner is null', () => {
|
it('should ingest filter rules for owner is null', () => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
)
|
)
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
@@ -1196,10 +1136,10 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: 'true',
|
value: 'true',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.ownerFilter()).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.UNOWNED
|
OwnerFilterType.UNOWNED
|
||||||
)
|
)
|
||||||
expect(component.permissionsSelectionModel.hideUnowned()).toBeFalsy()
|
expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner is not null', () => {
|
it('should ingest filter rules for owner is not null', () => {
|
||||||
@@ -1209,14 +1149,14 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: 'false',
|
value: 'false',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.hideUnowned()).toBeTruthy()
|
expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
{
|
{
|
||||||
rule_type: FILTER_OWNER_ISNULL,
|
rule_type: FILTER_OWNER_ISNULL,
|
||||||
value: '0',
|
value: '0',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.hideUnowned()).toBeTruthy()
|
expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for shared by me', () => {
|
it('should ingest filter rules for shared by me', () => {
|
||||||
@@ -1226,7 +1166,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '2',
|
value: '2',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.userID()).toEqual(2)
|
expect(component.permissionsSelectionModel.userID).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
// GET filterRules
|
// GET filterRules
|
||||||
@@ -1992,10 +1932,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '1',
|
value: '1',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
component.permissionsSelectionModel.excludeUsers.update((users) => [
|
component.permissionsSelectionModel.excludeUsers.push(2)
|
||||||
...users,
|
|
||||||
2,
|
|
||||||
])
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
@@ -2045,11 +1982,8 @@ describe('FilterEditorComponent', () => {
|
|||||||
// TODO: mock input in code
|
// TODO: mock input in code
|
||||||
// userSelect.query(By.css('input')).nativeElement.value = '3'
|
// userSelect.query(By.css('input')).nativeElement.value = '3'
|
||||||
// userSelect.triggerEventHandler('change')
|
// userSelect.triggerEventHandler('change')
|
||||||
component.permissionsSelectionModel.ownerFilter.set(OwnerFilterType.OTHERS)
|
component.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS
|
||||||
component.permissionsSelectionModel.includeUsers.update((users) => [
|
component.permissionsSelectionModel.includeUsers.push(3)
|
||||||
...users,
|
|
||||||
3,
|
|
||||||
])
|
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
@@ -2069,7 +2003,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
ownerToggle.nativeElement.checked = true
|
ownerToggle.nativeElement.checked = true
|
||||||
// ownerToggle.triggerEventHandler('change')
|
// ownerToggle.triggerEventHandler('change')
|
||||||
// TODO: ngModel isn't doing this here
|
// TODO: ngModel isn't doing this here
|
||||||
component.permissionsSelectionModel.hideUnowned.set(true)
|
component.permissionsSelectionModel.hideUnowned = true
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -735,50 +735,38 @@ export class FilterEditorComponent
|
|||||||
this._textFilter = rule.value
|
this._textFilter = rule.value
|
||||||
break
|
break
|
||||||
case FILTER_OWNER:
|
case FILTER_OWNER:
|
||||||
this.permissionsSelectionModel.ownerFilter.set(OwnerFilterType.SELF)
|
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.SELF
|
||||||
this.permissionsSelectionModel.hideUnowned.set(false)
|
this.permissionsSelectionModel.hideUnowned = false
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.userID.set(
|
this.permissionsSelectionModel.userID = parseInt(rule.value, 10)
|
||||||
Number.parseInt(rule.value, 10)
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
case FILTER_OWNER_ANY:
|
case FILTER_OWNER_ANY:
|
||||||
this.permissionsSelectionModel.ownerFilter.set(OwnerFilterType.OTHERS)
|
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.includeUsers.update((users) => [
|
this.permissionsSelectionModel.includeUsers.push(
|
||||||
...users,
|
parseInt(rule.value, 10)
|
||||||
Number.parseInt(rule.value, 10),
|
)
|
||||||
])
|
|
||||||
break
|
break
|
||||||
case FILTER_OWNER_DOES_NOT_INCLUDE:
|
case FILTER_OWNER_DOES_NOT_INCLUDE:
|
||||||
this.permissionsSelectionModel.ownerFilter.set(
|
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
||||||
OwnerFilterType.NOT_SELF
|
|
||||||
)
|
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.excludeUsers.update((users) => [
|
this.permissionsSelectionModel.excludeUsers.push(
|
||||||
...users,
|
parseInt(rule.value, 10)
|
||||||
Number.parseInt(rule.value, 10),
|
)
|
||||||
])
|
|
||||||
break
|
break
|
||||||
case FILTER_SHARED_BY_USER:
|
case FILTER_SHARED_BY_USER:
|
||||||
this.permissionsSelectionModel.ownerFilter.set(
|
this.permissionsSelectionModel.ownerFilter =
|
||||||
OwnerFilterType.SHARED_BY_ME
|
OwnerFilterType.SHARED_BY_ME
|
||||||
)
|
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.userID.set(
|
this.permissionsSelectionModel.userID = parseInt(rule.value, 10)
|
||||||
Number.parseInt(rule.value, 10)
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
case FILTER_OWNER_ISNULL:
|
case FILTER_OWNER_ISNULL:
|
||||||
if (rule.value === 'true' || rule.value === '1') {
|
if (rule.value === 'true' || rule.value === '1') {
|
||||||
this.permissionsSelectionModel.hideUnowned.set(false)
|
this.permissionsSelectionModel.hideUnowned = false
|
||||||
this.permissionsSelectionModel.ownerFilter.set(
|
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.UNOWNED
|
||||||
OwnerFilterType.UNOWNED
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
this.permissionsSelectionModel.hideUnowned.set(
|
this.permissionsSelectionModel.hideUnowned =
|
||||||
rule.value === 'false' || rule.value === '0'
|
rule.value === 'false' || rule.value === '0'
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1086,35 +1074,34 @@ export class FilterEditorComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.permissionsSelectionModel.ownerFilter() == OwnerFilterType.SELF) {
|
if (this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SELF) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_OWNER,
|
rule_type: FILTER_OWNER,
|
||||||
value: this.permissionsSelectionModel.userID().toString(),
|
value: this.permissionsSelectionModel.userID.toString(),
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
this.permissionsSelectionModel.ownerFilter() == OwnerFilterType.NOT_SELF
|
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.NOT_SELF
|
||||||
) {
|
) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
|
rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
|
||||||
value: this.permissionsSelectionModel.excludeUsers()?.join(','),
|
value: this.permissionsSelectionModel.excludeUsers?.join(','),
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
this.permissionsSelectionModel.ownerFilter() == OwnerFilterType.OTHERS
|
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.OTHERS
|
||||||
) {
|
) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_OWNER_ANY,
|
rule_type: FILTER_OWNER_ANY,
|
||||||
value: this.permissionsSelectionModel.includeUsers()?.join(','),
|
value: this.permissionsSelectionModel.includeUsers?.join(','),
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
this.permissionsSelectionModel.ownerFilter() ==
|
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SHARED_BY_ME
|
||||||
OwnerFilterType.SHARED_BY_ME
|
|
||||||
) {
|
) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_SHARED_BY_USER,
|
rule_type: FILTER_SHARED_BY_USER,
|
||||||
value: this.permissionsSelectionModel.userID().toString(),
|
value: this.permissionsSelectionModel.userID.toString(),
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
this.permissionsSelectionModel.ownerFilter() == OwnerFilterType.UNOWNED
|
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.UNOWNED
|
||||||
) {
|
) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_OWNER_ISNULL,
|
rule_type: FILTER_OWNER_ISNULL,
|
||||||
@@ -1122,7 +1109,7 @@ export class FilterEditorComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.permissionsSelectionModel.hideUnowned()) {
|
if (this.permissionsSelectionModel.hideUnowned) {
|
||||||
filterRules.push({
|
filterRules.push({
|
||||||
rule_type: FILTER_OWNER_ISNULL,
|
rule_type: FILTER_OWNER_ISNULL,
|
||||||
value: 'false',
|
value: 'false',
|
||||||
|
|||||||
@@ -210,48 +210,6 @@ describe('SettingsService', () => {
|
|||||||
expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('#000000')
|
expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('#000000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('provides stable signals that update when settings change', () => {
|
|
||||||
const req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}ui_settings/`
|
|
||||||
)
|
|
||||||
req.flush(ui_settings)
|
|
||||||
|
|
||||||
const notesEnabled = settingsService.getSignal<boolean>(
|
|
||||||
SETTINGS_KEYS.NOTES_ENABLED
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(notesEnabled()).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
settingsService.getSignal<boolean>(SETTINGS_KEYS.NOTES_ENABLED)
|
|
||||||
).toBe(notesEnabled)
|
|
||||||
|
|
||||||
settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false)
|
|
||||||
|
|
||||||
expect(notesEnabled()).toBeFalsy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates setting signals when settings are reinitialized', () => {
|
|
||||||
let req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}ui_settings/`
|
|
||||||
)
|
|
||||||
req.flush(ui_settings)
|
|
||||||
const appTitle = settingsService.getSignal<string>(SETTINGS_KEYS.APP_TITLE)
|
|
||||||
|
|
||||||
settingsService.initializeSettings().subscribe()
|
|
||||||
req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}ui_settings/`
|
|
||||||
)
|
|
||||||
req.flush({
|
|
||||||
...ui_settings,
|
|
||||||
settings: {
|
|
||||||
...ui_settings.settings,
|
|
||||||
app_title: 'Updated title',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(appTitle()).toBe('Updated title')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sets django cookie for languages', () => {
|
it('sets django cookie for languages', () => {
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { HttpClient } from '@angular/common/http'
|
|||||||
import {
|
import {
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Signal,
|
|
||||||
computed,
|
|
||||||
inject,
|
inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
LOCALE_ID,
|
LOCALE_ID,
|
||||||
@@ -299,7 +297,6 @@ export class SettingsService {
|
|||||||
|
|
||||||
private settings: Record<string, any> = {}
|
private settings: Record<string, any> = {}
|
||||||
private readonly settingsVersion = signal(0)
|
private readonly settingsVersion = signal(0)
|
||||||
private readonly settingSignals = new Map<string, Signal<unknown>>()
|
|
||||||
readonly currentUser = signal<User>(undefined)
|
readonly currentUser = signal<User>(undefined)
|
||||||
|
|
||||||
public settingsSaved: EventEmitter<any> = new EventEmitter()
|
public settingsSaved: EventEmitter<any> = new EventEmitter()
|
||||||
@@ -329,6 +326,10 @@ export class SettingsService {
|
|||||||
return !UNSAFE_OBJECT_KEYS.has(key)
|
return !UNSAFE_OBJECT_KEYS.has(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public trackChanges(): void {
|
||||||
|
this.settingsVersion()
|
||||||
|
}
|
||||||
|
|
||||||
private assignSafeSettings(source: Record<string, any>) {
|
private assignSafeSettings(source: Record<string, any>) {
|
||||||
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
||||||
return
|
return
|
||||||
@@ -338,7 +339,6 @@ export class SettingsService {
|
|||||||
if (!this.isSafeObjectKey(key)) continue
|
if (!this.isSafeObjectKey(key)) continue
|
||||||
this.settings[key] = source[key]
|
this.settings[key] = source[key]
|
||||||
}
|
}
|
||||||
this.settingsVersion.update((version) => version + 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is called by the app initializer in app.module
|
// this is called by the app initializer in app.module
|
||||||
@@ -594,18 +594,6 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSignal<T = any>(key: string): Signal<T> {
|
|
||||||
let settingSignal = this.settingSignals.get(key)
|
|
||||||
if (!settingSignal) {
|
|
||||||
settingSignal = computed(() => {
|
|
||||||
this.settingsVersion()
|
|
||||||
return this.get(key)
|
|
||||||
})
|
|
||||||
this.settingSignals.set(key, settingSignal)
|
|
||||||
}
|
|
||||||
return settingSignal as Signal<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key: string, value: any) {
|
set(key: string, value: any) {
|
||||||
// parse key:key:key into nested object
|
// parse key:key:key into nested object
|
||||||
let settingObj = this.settings
|
let settingObj = this.settings
|
||||||
|
|||||||
@@ -1063,79 +1063,3 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn("non-public address", str(response.data).lower())
|
self.assertIn("non-public address", str(response.data).lower())
|
||||||
|
|
||||||
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=False)
|
|
||||||
def test_update_remote_ocr_endpoint_blocks_internal_endpoint_when_disallowed(
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- Internal remote OCR endpoints are disallowed
|
|
||||||
WHEN:
|
|
||||||
- The config is updated with a remote OCR endpoint resolving internally
|
|
||||||
THEN:
|
|
||||||
- The request is rejected
|
|
||||||
"""
|
|
||||||
response = self.client.patch(
|
|
||||||
f"{self.ENDPOINT}1/",
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"remote_ocr_endpoint": "http://127.0.0.1:5000",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
||||||
self.assertIn("non-public address", str(response.data).lower())
|
|
||||||
|
|
||||||
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=True)
|
|
||||||
def test_update_remote_ocr_endpoint_allows_internal_endpoint_by_default(
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- Internal remote OCR endpoints are allowed (the default)
|
|
||||||
WHEN:
|
|
||||||
- The config is updated with a remote OCR endpoint resolving internally
|
|
||||||
THEN:
|
|
||||||
- The request is accepted, preserving existing self-hosted deployments
|
|
||||||
"""
|
|
||||||
response = self.client.patch(
|
|
||||||
f"{self.ENDPOINT}1/",
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"remote_ocr_endpoint": "http://127.0.0.1:5000",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(
|
|
||||||
response.data["remote_ocr_endpoint"],
|
|
||||||
"http://127.0.0.1:5000",
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS=False)
|
|
||||||
def test_update_remote_ocr_endpoint_empty_value_skips_validation(
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- Internal remote OCR endpoints are disallowed
|
|
||||||
WHEN:
|
|
||||||
- The config is updated with an empty remote OCR endpoint
|
|
||||||
THEN:
|
|
||||||
- The request is accepted; clearing the field never needs
|
|
||||||
outbound URL validation
|
|
||||||
"""
|
|
||||||
response = self.client.patch(
|
|
||||||
f"{self.ENDPOINT}1/",
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"remote_ocr_endpoint": "",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.data["remote_ocr_endpoint"], "")
|
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ if TYPE_CHECKING:
|
|||||||
import datetime
|
import datetime
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
|
||||||
from azure.core.pipeline import PipelineRequest
|
|
||||||
|
|
||||||
from paperless.parsers import MetadataEntry
|
from paperless.parsers import MetadataEntry
|
||||||
from paperless.parsers import ParserContext
|
from paperless.parsers import ParserContext
|
||||||
|
|
||||||
@@ -438,45 +436,9 @@ class RemoteDocumentParser:
|
|||||||
from azure.ai.documentintelligence.models import DocumentContentFormat
|
from azure.ai.documentintelligence.models import DocumentContentFormat
|
||||||
from azure.core.credentials import AzureKeyCredential
|
from azure.core.credentials import AzureKeyCredential
|
||||||
|
|
||||||
from paperless.network import validate_outbound_http_url
|
|
||||||
|
|
||||||
allow_internal = settings.REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_outbound_http_url(config.endpoint, allow_internal=allow_internal)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ParseError(f"Invalid remote OCR endpoint: {e}") from e
|
|
||||||
|
|
||||||
def _revalidate_request_host(request: PipelineRequest) -> None:
|
|
||||||
"""Re-validates the destination host of every request sent.
|
|
||||||
|
|
||||||
The check above only covers the moment the client is built. A
|
|
||||||
single analysis involves several requests spread over the
|
|
||||||
polling loop below, and any one of them can be redirected.
|
|
||||||
Wiring this through ``raw_request_hook`` (Azure's built-in
|
|
||||||
CustomHookPolicy) rather than a custom policy means it runs
|
|
||||||
*after* RedirectPolicy in the pipeline, so it sees - and
|
|
||||||
re-checks - every actual outbound URL, including redirect
|
|
||||||
targets, not just the original request.
|
|
||||||
"""
|
|
||||||
validate_outbound_http_url(
|
|
||||||
request.http_request.url,
|
|
||||||
allow_internal=allow_internal,
|
|
||||||
)
|
|
||||||
|
|
||||||
client = DocumentIntelligenceClient(
|
client = DocumentIntelligenceClient(
|
||||||
endpoint=config.endpoint,
|
endpoint=config.endpoint,
|
||||||
credential=AzureKeyCredential(config.api_key),
|
credential=AzureKeyCredential(config.api_key),
|
||||||
raw_request_hook=_revalidate_request_host,
|
|
||||||
# AzureKeyCredential is sent as Ocp-Apim-Subscription-Key, which
|
|
||||||
# Azure's default SensitiveHeaderCleanupPolicy does not strip on
|
|
||||||
# a cross-domain redirect (only Authorization and
|
|
||||||
# x-ms-authorization-auxiliary are, by default).
|
|
||||||
blocked_redirect_headers=[
|
|
||||||
"Authorization",
|
|
||||||
"x-ms-authorization-auxiliary",
|
|
||||||
"Ocp-Apim-Subscription-Key",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -305,22 +305,6 @@ class ApplicationConfigurationSerializer(
|
|||||||
|
|
||||||
validate_llm_embedding_endpoint = validate_llm_endpoint
|
validate_llm_embedding_endpoint = validate_llm_endpoint
|
||||||
|
|
||||||
def validate_remote_ocr_endpoint(self, value: str | None) -> str | None:
|
|
||||||
if not value:
|
|
||||||
return value
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_outbound_http_url(
|
|
||||||
value,
|
|
||||||
allow_internal=settings.REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS,
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
f"Invalid remote OCR endpoint: {e.args[0]}, see logs for details",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ApplicationConfiguration
|
model = ApplicationConfiguration
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -1208,10 +1208,6 @@ REMOTE_OCR_MODE = get_choice_from_env(
|
|||||||
{"always", "workflow_only"},
|
{"always", "workflow_only"},
|
||||||
default="always",
|
default="always",
|
||||||
)
|
)
|
||||||
REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS = get_bool_from_env(
|
|
||||||
"PAPERLESS_REMOTE_OCR_ALLOW_INTERNAL_ENDPOINTS",
|
|
||||||
"true",
|
|
||||||
)
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# AI Settings #
|
# AI Settings #
|
||||||
|
|||||||
Reference in New Issue
Block a user