mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-02 07:57:15 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
912c6eb52e | ||
|
|
73ef14f37a |
+140
-140
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,8 @@ 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())
|
||||||
@@ -200,8 +202,7 @@ export class TrashComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDaysRemaining(document: Document): number {
|
getDaysRemaining(document: Document): number {
|
||||||
this.settingsService.trackChanges()
|
const delay = this.emptyTrashDelaySetting()
|
||||||
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,6 +193,23 @@ 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,6 +98,29 @@ 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() {
|
||||||
@@ -191,33 +214,23 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get versionString(): string {
|
get versionString(): string {
|
||||||
this.settingsService.trackChanges()
|
return `${environment.appTitle} v${this.versionSetting()}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
||||||
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get appTitle(): string {
|
get appTitle(): string {
|
||||||
this.settingsService.trackChanges()
|
return this.appTitleSetting() || environment.appTitle
|
||||||
return (
|
|
||||||
this.settingsService.get(SETTINGS_KEYS.APP_TITLE) || environment.appTitle
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get customAppTitle(): string {
|
get customAppTitle(): string {
|
||||||
this.settingsService.trackChanges()
|
return this.appTitleSetting()
|
||||||
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasCustomBranding(): boolean {
|
get hasCustomBranding(): boolean {
|
||||||
this.settingsService.trackChanges()
|
return !!(this.appTitleSetting()?.length || this.appLogoSetting()?.length)
|
||||||
return !!(
|
|
||||||
this.settingsService.get(SETTINGS_KEYS.APP_TITLE)?.length ||
|
|
||||||
this.settingsService.get(SETTINGS_KEYS.APP_LOGO)?.length
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get customAppLogo(): string {
|
get customAppLogo(): string {
|
||||||
this.settingsService.trackChanges()
|
const logo = this.appLogoSetting()
|
||||||
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
|
||||||
@@ -262,8 +275,7 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
this.settingsService.trackChanges()
|
return this.slimSidebarSetting()
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set slimSidebarEnabled(enabled: boolean) {
|
set slimSidebarEnabled(enabled: boolean) {
|
||||||
@@ -286,10 +298,9 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get attributesSectionsCollapsed(): boolean {
|
get attributesSectionsCollapsed(): boolean {
|
||||||
this.settingsService.trackChanges()
|
return this.attributesSectionsCollapsedSetting()?.includes(
|
||||||
return this.settingsService
|
CollapsibleSection.ATTRIBUTES
|
||||||
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
|
)
|
||||||
?.includes(CollapsibleSection.ATTRIBUTES)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set attributesSectionsCollapsed(collapsed: boolean) {
|
set attributesSectionsCollapsed(collapsed: boolean) {
|
||||||
@@ -312,8 +323,7 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
this.settingsService.trackChanges()
|
return this.aiEnabledSetting()
|
||||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:resize')
|
@HostListener('window:resize')
|
||||||
@@ -480,9 +490,8 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get showSidebarCounts(): boolean {
|
get showSidebarCounts(): boolean {
|
||||||
this.settingsService.trackChanges()
|
|
||||||
return (
|
return (
|
||||||
this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT) &&
|
this.sidebarViewsShowCountSetting() &&
|
||||||
!this.settingsService.organizingSidebarSavedViews()
|
!this.settingsService.organizingSidebarSavedViews()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ 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)
|
||||||
@@ -97,11 +101,7 @@ export class GlobalSearchComponent implements OnInit {
|
|||||||
@ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>
|
@ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>
|
||||||
|
|
||||||
get useAdvancedForFullSearch(): boolean {
|
get useAdvancedForFullSearch(): boolean {
|
||||||
this.settingsService.trackChanges()
|
return this.searchFullTypeSetting() === GlobalSearchType.ADVANCED
|
||||||
return (
|
|
||||||
this.settingsService.get(SETTINGS_KEYS.SEARCH_FULL_TYPE) ===
|
|
||||||
GlobalSearchType.ADVANCED
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
+19
-13
@@ -196,6 +196,16 @@ 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')
|
||||||
@@ -218,7 +228,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', () => {
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
setActionSettings()
|
||||||
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)
|
||||||
@@ -242,7 +252,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Email, remote OCR and AI all disabled
|
// Email, remote OCR and AI all disabled
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(false)
|
setActionSettings({ email: false, remoteOcr: false, ai: false })
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
expect(component.actionTypeOptions).toEqual(
|
expect(component.actionTypeOptions).toEqual(
|
||||||
WORKFLOW_ACTION_OPTIONS.filter(
|
WORKFLOW_ACTION_OPTIONS.filter(
|
||||||
@@ -255,7 +265,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should offer remote OCR only for consumption workflows', () => {
|
it('should offer remote OCR only for consumption workflows', () => {
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
setActionSettings()
|
||||||
|
|
||||||
// A consumption trigger makes the action reachable
|
// A consumption trigger makes the action reachable
|
||||||
component.object = {
|
component.object = {
|
||||||
@@ -285,7 +295,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', () => {
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
setActionSettings()
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
|
|
||||||
// Nothing for the action to apply to yet
|
// Nothing for the action to apply to yet
|
||||||
@@ -311,7 +321,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', () => {
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
setActionSettings()
|
||||||
|
|
||||||
// Otherwise changing the trigger would silently blank the selection
|
// Otherwise changing the trigger would silently blank the selection
|
||||||
component.object = {
|
component.object = {
|
||||||
@@ -329,9 +339,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not offer remote OCR when no engine is configured', () => {
|
it('should not offer remote OCR when no engine is configured', () => {
|
||||||
jest
|
setActionSettings({ remoteOcr: false })
|
||||||
.spyOn(settingsService, 'get')
|
|
||||||
.mockImplementation((key) => key !== SETTINGS_KEYS.REMOTE_OCR_CONFIGURED)
|
|
||||||
|
|
||||||
component.object = {
|
component.object = {
|
||||||
name: 'Workflow 1',
|
name: 'Workflow 1',
|
||||||
@@ -348,7 +356,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', () => {
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
setActionSettings()
|
||||||
|
|
||||||
// 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
|
||||||
@@ -382,7 +390,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', () => {
|
||||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
setActionSettings()
|
||||||
|
|
||||||
// Otherwise changing the trigger would silently blank the selection
|
// Otherwise changing the trigger would silently blank the selection
|
||||||
component.object = {
|
component.object = {
|
||||||
@@ -400,9 +408,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should not offer apply AI suggestions when AI is disabled', () => {
|
it('should not offer apply AI suggestions when AI is disabled', () => {
|
||||||
jest
|
setActionSettings({ ai: false })
|
||||||
.spyOn(settingsService, 'get')
|
|
||||||
.mockImplementation((key) => key !== SETTINGS_KEYS.AI_ENABLED)
|
|
||||||
|
|
||||||
component.object = {
|
component.object = {
|
||||||
name: 'Workflow 1',
|
name: 'Workflow 1',
|
||||||
|
|||||||
+10
-4
@@ -537,6 +537,13 @@ 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
|
||||||
|
|
||||||
@@ -589,7 +596,7 @@ export class WorkflowEditDialogComponent
|
|||||||
private getAllowedActionTypes() {
|
private getAllowedActionTypes() {
|
||||||
let allowed = WORKFLOW_ACTION_OPTIONS
|
let allowed = WORKFLOW_ACTION_OPTIONS
|
||||||
|
|
||||||
if (!this.settingsService.get(SETTINGS_KEYS.EMAIL_ENABLED)) {
|
if (!this.emailEnabledSetting()) {
|
||||||
allowed = allowed.filter((a) => a.id !== WorkflowActionType.Email)
|
allowed = allowed.filter((a) => a.id !== WorkflowActionType.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +604,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.settingsService.get(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED) &&
|
this.remoteOcrConfiguredSetting() &&
|
||||||
(formWorkflow?.triggers?.some(
|
(formWorkflow?.triggers?.some(
|
||||||
(trigger) => trigger.type === WorkflowTriggerType.Consumption
|
(trigger) => trigger.type === WorkflowTriggerType.Consumption
|
||||||
) ||
|
) ||
|
||||||
@@ -612,7 +619,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.settingsService.get(SETTINGS_KEYS.AI_ENABLED) &&
|
this.aiEnabledSetting() &&
|
||||||
(!formWorkflow?.triggers?.length ||
|
(!formWorkflow?.triggers?.length ||
|
||||||
formWorkflow.triggers.some(
|
formWorkflow.triggers.some(
|
||||||
(trigger) => trigger.type !== WorkflowTriggerType.Consumption
|
(trigger) => trigger.type !== WorkflowTriggerType.Consumption
|
||||||
@@ -1362,7 +1369,6 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -839,7 +839,9 @@ 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 = []
|
||||||
@@ -855,7 +857,9 @@ 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)
|
||||||
})
|
})
|
||||||
@@ -865,7 +869,9 @@ 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)
|
||||||
})
|
})
|
||||||
@@ -966,7 +972,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(
|
||||||
|
|||||||
+125
-104
@@ -64,43 +64,56 @@ 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 _documentCounts: SelectionDataItem[] = []
|
private readonly _logicalOperator = signal(LogicalOperator.And)
|
||||||
|
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 = counts
|
this._documentCounts.set(counts)
|
||||||
if (this.documentCountSortingEnabled) {
|
if (this.documentCountSortingEnabled) {
|
||||||
this.sortItems()
|
this._items.set(this.sortItems(this.items))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = Array.from(items)
|
this._items.set(this.withNullItem(this.sortItems(Array.from(items))))
|
||||||
this.sortItems()
|
|
||||||
this.setNullItem()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setNullItem() {
|
private withNullItem(items: MatchingModel[]): MatchingModel[] {
|
||||||
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
|
||||||
if (this._items[0]?.id === null) {
|
return items[0]?.id === null ? items.slice(1) : items
|
||||||
this._items.shift()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = {
|
const nullItem = {
|
||||||
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
|
||||||
@@ -108,22 +121,17 @@ export class FilterableDropdownSelectionModel {
|
|||||||
: NEGATIVE_NULL_FILTER_VALUE,
|
: NEGATIVE_NULL_FILTER_VALUE,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return items[0]?.id === null || items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
||||||
this._items[0]?.id === null ||
|
? [nullItem, ...items.slice(1)]
|
||||||
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
|
: [nullItem, ...items]
|
||||||
) {
|
|
||||||
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() {
|
private sortItems(items: MatchingModel[]): MatchingModel[] {
|
||||||
this._items.sort((a, b) => {
|
const sorted = [...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 &&
|
||||||
@@ -154,13 +162,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)
|
||||||
) {
|
) {
|
||||||
@@ -170,14 +178,10 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this._documentCounts.length) {
|
return this._documentCounts().length
|
||||||
this.promoteBranchesWithDocumentCounts()
|
? this.promoteBranchesWithDocumentCounts(sorted)
|
||||||
|
: sorted
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private selectionStates = new Map<number, ToggleableItemState>()
|
|
||||||
|
|
||||||
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
|
||||||
|
|
||||||
getSelectedItems() {
|
getSelectedItems() {
|
||||||
return this.items.filter(
|
return this.items.filter(
|
||||||
@@ -194,30 +198,33 @@ 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) {
|
||||||
this.temporarySelectionStates.delete(id)
|
states.delete(id)
|
||||||
} else {
|
} else {
|
||||||
this.temporarySelectionStates.set(id, state)
|
states.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) {
|
||||||
let state = this.temporarySelectionStates.get(id)
|
const states = new Map(this.temporarySelectionStates)
|
||||||
|
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) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
|
states.set(id, ToggleableItemState.Selected)
|
||||||
|
|
||||||
if (this.singleSelect) {
|
if (this.singleSelect) {
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
for (let key of states.keys()) {
|
||||||
if (key != id) {
|
if (key != id) {
|
||||||
this.temporarySelectionStates.delete(key)
|
states.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,25 +240,26 @@ export class FilterableDropdownSelectionModel {
|
|||||||
) {
|
) {
|
||||||
newState = ToggleableItemState.NotSelected
|
newState = ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
this.temporarySelectionStates.set(id, newState)
|
states.set(id, newState)
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
state == ToggleableItemState.Selected ||
|
state == ToggleableItemState.Selected ||
|
||||||
state == ToggleableItemState.Excluded
|
state == ToggleableItemState.Excluded
|
||||||
) {
|
) {
|
||||||
this.temporarySelectionStates.delete(id)
|
states.delete(id)
|
||||||
this.clearDescendantSelections(id)
|
this.clearDescendantSelections(states, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
for (let key of states.keys()) {
|
||||||
if (key) {
|
if (key) {
|
||||||
this.temporarySelectionStates.delete(key)
|
states.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.temporarySelectionStates.delete(null)
|
states.delete(null)
|
||||||
}
|
}
|
||||||
|
this._temporarySelectionStates.set(states)
|
||||||
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
@@ -259,20 +267,21 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exclude(id: number, fireEvent: boolean = true) {
|
exclude(id: number, fireEvent: boolean = true) {
|
||||||
let state = this.temporarySelectionStates.get(id)
|
const states = new Map(this.temporarySelectionStates)
|
||||||
|
let state = states.get(id)
|
||||||
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
if (id && (state == null || state != ToggleableItemState.Excluded)) {
|
||||||
this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
|
const operator = this.manyToOne ? LogicalOperator.And : LogicalOperator.Or
|
||||||
? LogicalOperator.And
|
this.temporaryLogicalOperator.set(operator)
|
||||||
: LogicalOperator.Or
|
this._logicalOperator.set(operator)
|
||||||
|
|
||||||
if (this.manyToOne || this.singleSelect) {
|
if (this.manyToOne || this.singleSelect) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
states.set(id, ToggleableItemState.Excluded)
|
||||||
this.clearDescendantSelections(id)
|
this.clearDescendantSelections(states, id)
|
||||||
|
|
||||||
if (this.singleSelect) {
|
if (this.singleSelect) {
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
for (let key of states.keys()) {
|
||||||
if (key != id) {
|
if (key != id) {
|
||||||
this.temporarySelectionStates.delete(key)
|
states.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,17 +296,18 @@ export class FilterableDropdownSelectionModel {
|
|||||||
) {
|
) {
|
||||||
newState = ToggleableItemState.NotSelected
|
newState = ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
this.temporarySelectionStates.set(id, newState)
|
states.set(id, newState)
|
||||||
if (newState == ToggleableItemState.Excluded) {
|
if (newState == ToggleableItemState.Excluded) {
|
||||||
this.clearDescendantSelections(id)
|
this.clearDescendantSelections(states, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!id || state == ToggleableItemState.Excluded) {
|
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||||
this.temporarySelectionStates.delete(id)
|
states.delete(id)
|
||||||
if (id) {
|
if (id) {
|
||||||
this.clearDescendantSelections(id)
|
this.clearDescendantSelections(states, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._temporarySelectionStates.set(states)
|
||||||
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
@@ -308,9 +318,12 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearDescendantSelections(id: number) {
|
private clearDescendantSelections(
|
||||||
|
states: Map<number, ToggleableItemState>,
|
||||||
|
id: number
|
||||||
|
) {
|
||||||
for (const descendantID of this.getDescendantIDs(id)) {
|
for (const descendantID of this.getDescendantIDs(id)) {
|
||||||
this.temporarySelectionStates.delete(descendantID)
|
states.delete(descendantID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +333,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' &&
|
||||||
@@ -336,12 +349,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 = operator
|
this.temporaryLogicalOperator.set(operator)
|
||||||
this.setNullItem()
|
this._items.set(this.withNullItem(this.items))
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOperator() {
|
toggleOperator() {
|
||||||
@@ -349,12 +362,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 = intersection
|
this.temporaryIntersection.set(intersection)
|
||||||
this.setNullItem()
|
this._items.set(this.withNullItem(this.items))
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleIntersection() {
|
toggleIntersection() {
|
||||||
@@ -364,18 +377,20 @@ export class FilterableDropdownSelectionModel {
|
|||||||
? ToggleableItemState.Selected
|
? ToggleableItemState.Selected
|
||||||
: ToggleableItemState.Excluded
|
: ToggleableItemState.Excluded
|
||||||
|
|
||||||
this.temporarySelectionStates.forEach((state, key) => {
|
const states = new Map(this.temporarySelectionStates)
|
||||||
|
states.forEach((state, key) => {
|
||||||
if (key === null && this.intersection === Intersection.Exclude) {
|
if (key === null && this.intersection === Intersection.Exclude) {
|
||||||
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
|
states.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
|
||||||
) {
|
) {
|
||||||
this.temporarySelectionStates.set(null, newState)
|
states.set(null, newState)
|
||||||
} else {
|
} else {
|
||||||
this.temporarySelectionStates.set(key, newState)
|
states.set(key, newState)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this._temporarySelectionStates.set(states)
|
||||||
|
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
@@ -395,10 +410,12 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear(fireEvent = true) {
|
clear(fireEvent = true) {
|
||||||
this.temporarySelectionStates.clear()
|
this._temporarySelectionStates.set(new Map())
|
||||||
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
|
this.temporaryLogicalOperator.set(LogicalOperator.And)
|
||||||
this.temporaryIntersection = this._intersection = Intersection.Include
|
this._logicalOperator.set(LogicalOperator.And)
|
||||||
this.setNullItem()
|
this.temporaryIntersection.set(Intersection.Include)
|
||||||
|
this._intersection.set(Intersection.Include)
|
||||||
|
this._items.set(this.withNullItem(this.items))
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
@@ -419,9 +436,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
|
||||||
@@ -438,23 +455,29 @@ 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(
|
||||||
const parentById = this.buildParentById()
|
items: MatchingModel[]
|
||||||
|
): MatchingModel[] {
|
||||||
|
const parentById = this.buildParentById(items)
|
||||||
const findRootId = this.createRootFinder(parentById)
|
const findRootId = this.createRootFinder(parentById)
|
||||||
const getRootDocCount = this.createRootDocCounter()
|
const getRootDocCount = this.createRootDocCounter(items)
|
||||||
const summaries = this.buildBranchSummaries(findRootId, getRootDocCount)
|
const summaries = this.buildBranchSummaries(
|
||||||
|
items,
|
||||||
|
findRootId,
|
||||||
|
getRootDocCount
|
||||||
|
)
|
||||||
const orderedBranches = this.orderBranchesByPriority(summaries)
|
const orderedBranches = this.orderBranchesByPriority(summaries)
|
||||||
|
|
||||||
this._items = orderedBranches.flatMap((summary) => summary.items)
|
return orderedBranches.flatMap((summary) => summary.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildParentById(): Map<number, number | null> {
|
private buildParentById(items: MatchingModel[]): Map<number, number | null> {
|
||||||
const parentById = new Map<number, number | null>()
|
const parentById = new Map<number, number | null>()
|
||||||
|
|
||||||
for (const item of this._items) {
|
for (const item of 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(
|
||||||
@@ -492,7 +515,9 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return findRootId
|
return findRootId
|
||||||
}
|
}
|
||||||
|
|
||||||
private createRootDocCounter(): (rootId: number) => number {
|
private createRootDocCounter(
|
||||||
|
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 => {
|
||||||
@@ -507,7 +532,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return explicit
|
return explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootItem = this._items.find((i) => i.id === rootId)
|
const rootItem = 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']
|
||||||
@@ -519,12 +544,13 @@ 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 this._items.entries()) {
|
for (const [index, item] of items.entries()) {
|
||||||
const { key, special, rootId } = this.describeBranchItem(
|
const { key, special, rootId } = this.describeBranchItem(
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
@@ -616,28 +642,23 @@ export class FilterableDropdownSelectionModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(map: Map<number, ToggleableItemState>) {
|
init(map: Map<number, ToggleableItemState>) {
|
||||||
this.temporarySelectionStates = map
|
this._temporarySelectionStates.set(new Map(map))
|
||||||
this.apply()
|
this.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
apply() {
|
apply() {
|
||||||
this.selectionStates.clear()
|
this._selectionStates.set(new Map(this.temporarySelectionStates))
|
||||||
this.temporarySelectionStates.forEach((value, key) => {
|
this._logicalOperator.set(this.temporaryLogicalOperator())
|
||||||
this.selectionStates.set(key, value)
|
this._intersection.set(this.temporaryIntersection())
|
||||||
})
|
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.clear()
|
this._selectionStates.set(new Map())
|
||||||
|
this._temporarySelectionStates.set(new Map())
|
||||||
} else {
|
} else {
|
||||||
this.selectionStates.forEach((value, key) => {
|
this._temporarySelectionStates.set(new Map(this.selectionStates))
|
||||||
this.temporarySelectionStates.set(key, value)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -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,7 +65,8 @@
|
|||||||
<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()"
|
||||||
@@ -78,10 +79,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)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled">
|
<input type="checkbox" class="form-check-input" id="hideUnowned" [ngModel]="selectionModel.hideUnowned()" (ngModelChange)="selectionModel.hideUnowned.set($event)" (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>
|
||||||
|
|||||||
+39
-30
@@ -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 = true
|
component.selectionModel.hideUnowned.set(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 = OwnerFilterType.SELF
|
component.selectionModel.ownerFilter.set(OwnerFilterType.SELF)
|
||||||
component.selectionModel.userID = 1
|
component.selectionModel.userID.set(1)
|
||||||
expect(component.ownerFilterLabel).toEqual('Owned by user1')
|
expect(component.ownerFilterLabel).toEqual('Owned by user1')
|
||||||
|
|
||||||
component.selectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
component.selectionModel.ownerFilter.set(OwnerFilterType.NOT_SELF)
|
||||||
component.selectionModel.excludeUsers = [1]
|
component.selectionModel.excludeUsers.set([1])
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual('Not owned by user1')
|
expect(component.ownerExclusionFilterLabel).toEqual('Not owned by user1')
|
||||||
|
|
||||||
component.selectionModel.ownerFilter = OwnerFilterType.SHARED_BY_ME
|
component.selectionModel.ownerFilter.set(OwnerFilterType.SHARED_BY_ME)
|
||||||
component.selectionModel.userID = 1
|
component.selectionModel.userID.set(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 = OwnerFilterType.SELF
|
component.selectionModel.ownerFilter.set(OwnerFilterType.SELF)
|
||||||
component.selectionModel.userID = 99
|
component.selectionModel.userID.set(99)
|
||||||
expect(component.ownerFilterLabel).toEqual('Owned by another user')
|
expect(component.ownerFilterLabel).toEqual('Owned by another user')
|
||||||
|
|
||||||
component.selectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
component.selectionModel.ownerFilter.set(OwnerFilterType.NOT_SELF)
|
||||||
component.selectionModel.excludeUsers = [99]
|
component.selectionModel.excludeUsers.set([99])
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual(
|
expect(component.ownerExclusionFilterLabel).toEqual(
|
||||||
'Not owned by another user'
|
'Not owned by another user'
|
||||||
)
|
)
|
||||||
|
|
||||||
component.selectionModel.excludeUsers = [98, 99]
|
component.selectionModel.excludeUsers.set([98, 99])
|
||||||
expect(component.ownerExclusionFilterLabel).toEqual(
|
expect(component.ownerExclusionFilterLabel).toEqual(
|
||||||
'Not owned by selected users'
|
'Not owned by selected users'
|
||||||
)
|
)
|
||||||
|
|
||||||
component.selectionModel.ownerFilter = OwnerFilterType.SHARED_BY_ME
|
component.selectionModel.ownerFilter.set(OwnerFilterType.SHARED_BY_ME)
|
||||||
component.selectionModel.userID = 99
|
component.selectionModel.userID.set(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 = currentUserID
|
component.selectionModel.userID.set(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 = [currentUserID]
|
component.selectionModel.excludeUsers.set([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 = OwnerFilterType.NONE
|
component.selectionModel.ownerFilter.set(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,32 +148,41 @@ 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 = OwnerFilterType.NONE
|
component.selectionModel.ownerFilter.set(OwnerFilterType.NONE)
|
||||||
|
|
||||||
// this would normally be done by select component
|
// this would normally be done by select component
|
||||||
component.selectionModel.includeUsers = [12]
|
component.selectionModel.includeUsers.set([12])
|
||||||
component.onUserSelect()
|
component.onUserSelect()
|
||||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.OTHERS)
|
expect(component.selectionModel.ownerFilter()).toEqual(
|
||||||
|
OwnerFilterType.OTHERS
|
||||||
|
)
|
||||||
|
|
||||||
// this would normally be done by select component
|
// this would normally be done by select component
|
||||||
component.selectionModel.includeUsers = null
|
component.selectionModel.includeUsers.set(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', () => {
|
||||||
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
const emitted = () => ({
|
||||||
|
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(ownerFilterSetResult).toEqual({
|
expect(emitted()).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -182,7 +191,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.NOT_SELF)
|
component.setFilter(OwnerFilterType.NOT_SELF)
|
||||||
expect(ownerFilterSetResult).toEqual({
|
expect(emitted()).toEqual({
|
||||||
excludeUsers: [currentUserID],
|
excludeUsers: [currentUserID],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -191,7 +200,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.NONE)
|
component.setFilter(OwnerFilterType.NONE)
|
||||||
expect(ownerFilterSetResult).toEqual({
|
expect(emitted()).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -200,7 +209,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.SHARED_BY_ME)
|
component.setFilter(OwnerFilterType.SHARED_BY_ME)
|
||||||
expect(ownerFilterSetResult).toEqual({
|
expect(emitted()).toEqual({
|
||||||
excludeUsers: [],
|
excludeUsers: [],
|
||||||
hideUnowned: false,
|
hideUnowned: false,
|
||||||
includeUsers: [],
|
includeUsers: [],
|
||||||
@@ -209,7 +218,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
component.setFilter(OwnerFilterType.UNOWNED)
|
component.setFilter(OwnerFilterType.UNOWNED)
|
||||||
expect(ownerFilterSetResult).toEqual({
|
expect(emitted()).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 {
|
||||||
ownerFilter: OwnerFilterType
|
readonly ownerFilter = signal(OwnerFilterType.NONE)
|
||||||
hideUnowned: boolean
|
readonly hideUnowned = signal(false)
|
||||||
userID: number
|
readonly userID = signal<number>(null)
|
||||||
includeUsers: number[]
|
readonly includeUsers = signal<number[]>([])
|
||||||
excludeUsers: number[]
|
readonly excludeUsers = signal<number[]>([])
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.ownerFilter = OwnerFilterType.NONE
|
this.ownerFilter.set(OwnerFilterType.NONE)
|
||||||
this.userID = null
|
this.userID.set(null)
|
||||||
this.hideUnowned = false
|
this.hideUnowned.set(false)
|
||||||
this.includeUsers = []
|
this.includeUsers.set([])
|
||||||
this.excludeUsers = []
|
this.excludeUsers.set([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,33 +84,31 @@ 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)
|
||||||
) {
|
) {
|
||||||
@@ -130,13 +128,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`
|
||||||
@@ -169,34 +167,36 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFilter(type: OwnerFilterType) {
|
setFilter(type: OwnerFilterType) {
|
||||||
this.selectionModel.ownerFilter = type
|
this.selectionModel.ownerFilter.set(type)
|
||||||
if (this.selectionModel.ownerFilter === OwnerFilterType.SELF) {
|
if (this.selectionModel.ownerFilter() === OwnerFilterType.SELF) {
|
||||||
this.selectionModel.includeUsers = []
|
this.selectionModel.includeUsers.set([])
|
||||||
this.selectionModel.excludeUsers = []
|
this.selectionModel.excludeUsers.set([])
|
||||||
this.selectionModel.userID = this.settingsService.currentUser().id
|
this.selectionModel.userID.set(this.settingsService.currentUser().id)
|
||||||
this.selectionModel.hideUnowned = false
|
this.selectionModel.hideUnowned.set(false)
|
||||||
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
|
} else if (this.selectionModel.ownerFilter() === OwnerFilterType.NOT_SELF) {
|
||||||
this.selectionModel.userID = null
|
this.selectionModel.userID.set(null)
|
||||||
this.selectionModel.includeUsers = []
|
this.selectionModel.includeUsers.set([])
|
||||||
this.selectionModel.excludeUsers = [this.settingsService.currentUser().id]
|
this.selectionModel.excludeUsers.set([
|
||||||
this.selectionModel.hideUnowned = false
|
this.settingsService.currentUser().id,
|
||||||
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NONE) {
|
])
|
||||||
this.selectionModel.userID = null
|
this.selectionModel.hideUnowned.set(false)
|
||||||
this.selectionModel.includeUsers = []
|
} else if (this.selectionModel.ownerFilter() === OwnerFilterType.NONE) {
|
||||||
this.selectionModel.excludeUsers = []
|
this.selectionModel.userID.set(null)
|
||||||
this.selectionModel.hideUnowned = false
|
this.selectionModel.includeUsers.set([])
|
||||||
|
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 = this.settingsService.currentUser()?.id
|
this.selectionModel.userID.set(this.settingsService.currentUser()?.id)
|
||||||
this.selectionModel.includeUsers = []
|
this.selectionModel.includeUsers.set([])
|
||||||
this.selectionModel.excludeUsers = []
|
this.selectionModel.excludeUsers.set([])
|
||||||
this.selectionModel.hideUnowned = false
|
this.selectionModel.hideUnowned.set(false)
|
||||||
} else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
|
} else if (this.selectionModel.ownerFilter() === OwnerFilterType.UNOWNED) {
|
||||||
this.selectionModel.userID = null
|
this.selectionModel.userID.set(null)
|
||||||
this.selectionModel.includeUsers = []
|
this.selectionModel.includeUsers.set([])
|
||||||
this.selectionModel.excludeUsers = []
|
this.selectionModel.excludeUsers.set([])
|
||||||
this.selectionModel.hideUnowned = false
|
this.selectionModel.hideUnowned.set(false)
|
||||||
}
|
}
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
@@ -206,11 +206,11 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUserSelect() {
|
onUserSelect() {
|
||||||
if (this.selectionModel.includeUsers?.length) {
|
this.selectionModel.ownerFilter.set(
|
||||||
this.selectionModel.ownerFilter = OwnerFilterType.OTHERS
|
this.selectionModel.includeUsers()?.length
|
||||||
} else {
|
? OwnerFilterType.OTHERS
|
||||||
this.selectionModel.ownerFilter = OwnerFilterType.NONE
|
: OwnerFilterType.NONE
|
||||||
}
|
)
|
||||||
this.onChange()
|
this.onChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1209,24 +1209,53 @@ 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', () => {
|
it('should display built-in pdf viewer if not disabled', async () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.document().archived_file_name = 'file.pdf'
|
component.document.update((document) => ({
|
||||||
|
...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()
|
||||||
fixture.detectChanges()
|
await fixture.whenStable()
|
||||||
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().archived_file_name = 'file.pdf'
|
component.document.update((document) => ({
|
||||||
|
...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 }))
|
||||||
@@ -1685,7 +1714,10 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
it('should change preview element by render type', () => {
|
it('should change preview element by render type', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.document().archived_file_name = 'file.pdf'
|
component.document.update((document) => ({
|
||||||
|
...document,
|
||||||
|
archived_file_name: 'file.pdf',
|
||||||
|
}))
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.PDF
|
component.ContentRenderType.PDF
|
||||||
@@ -1694,8 +1726,11 @@ describe('DocumentDetailComponent', () => {
|
|||||||
fixture.debugElement.query(By.css('pdf-viewer-container'))
|
fixture.debugElement.query(By.css('pdf-viewer-container'))
|
||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
|
|
||||||
component.document().archived_file_name = undefined
|
component.document.update((document) => ({
|
||||||
component.document().mime_type = 'text/plain'
|
...document,
|
||||||
|
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
|
||||||
@@ -1704,7 +1739,10 @@ describe('DocumentDetailComponent', () => {
|
|||||||
fixture.debugElement.query(By.css('div.preview-sticky'))
|
fixture.debugElement.query(By.css('div.preview-sticky'))
|
||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
|
|
||||||
component.document().mime_type = 'image/jpeg'
|
component.document.update((document) => ({
|
||||||
|
...document,
|
||||||
|
mime_type: 'image/jpeg',
|
||||||
|
}))
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.Image
|
component.ContentRenderType.Image
|
||||||
@@ -1712,9 +1750,12 @@ 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().mime_type =
|
component.document.update((document) => ({
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'),
|
...document,
|
||||||
fixture.detectChanges())
|
mime_type:
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
}))
|
||||||
|
fixture.detectChanges()
|
||||||
expect(component.archiveContentRenderType).toEqual(
|
expect(component.archiveContentRenderType).toEqual(
|
||||||
component.ContentRenderType.Other
|
component.ContentRenderType.Other
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -227,6 +227,19 @@ 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
|
||||||
@@ -333,8 +346,7 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get useNativePdfViewer(): boolean {
|
get useNativePdfViewer(): boolean {
|
||||||
this.settings.trackChanges()
|
return this.useNativePdfViewerSetting()
|
||||||
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMobile(): boolean {
|
get isMobile(): boolean {
|
||||||
@@ -342,12 +354,10 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get aiEnabled(): boolean {
|
get aiEnabled(): boolean {
|
||||||
this.settings.trackChanges()
|
return this.aiEnabledSetting()
|
||||||
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
|
||||||
@@ -359,22 +369,17 @@ 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 {
|
||||||
this.settings.trackChanges()
|
return this.showThumbnailOverlaySetting()
|
||||||
return this.settings.get(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isFieldHidden(fieldId: DocumentDetailFieldID): boolean {
|
isFieldHidden(fieldId: DocumentDetailFieldID): boolean {
|
||||||
this.settings.trackChanges()
|
return this.hiddenFieldsSetting().includes(fieldId)
|
||||||
return this.settings
|
|
||||||
.get(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
|
|
||||||
.includes(fieldId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRenderType(mimeType: string): ContentRenderType {
|
private getRenderType(mimeType: string): ContentRenderType {
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ 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
|
||||||
@@ -574,8 +576,7 @@ export class DocumentListComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get notesEnabled(): boolean {
|
get notesEnabled(): boolean {
|
||||||
this.settingsService.trackChanges()
|
return this.notesEnabledSetting()
|
||||||
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetFilters() {
|
resetFilters() {
|
||||||
|
|||||||
+86
-20
@@ -621,6 +621,43 @@ 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 = [
|
||||||
@@ -1078,7 +1115,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 = [
|
||||||
@@ -1087,15 +1124,38 @@ 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 = [
|
||||||
@@ -1104,14 +1164,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 = [
|
||||||
@@ -1120,14 +1180,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 = [
|
||||||
@@ -1136,10 +1196,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', () => {
|
||||||
@@ -1149,14 +1209,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', () => {
|
||||||
@@ -1166,7 +1226,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '2',
|
value: '2',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
expect(component.permissionsSelectionModel.userID).toEqual(2)
|
expect(component.permissionsSelectionModel.userID()).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
// GET filterRules
|
// GET filterRules
|
||||||
@@ -1932,7 +1992,10 @@ describe('FilterEditorComponent', () => {
|
|||||||
value: '1',
|
value: '1',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
component.permissionsSelectionModel.excludeUsers.push(2)
|
component.permissionsSelectionModel.excludeUsers.update((users) => [
|
||||||
|
...users,
|
||||||
|
2,
|
||||||
|
])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
@@ -1982,8 +2045,11 @@ 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 = OwnerFilterType.OTHERS
|
component.permissionsSelectionModel.ownerFilter.set(OwnerFilterType.OTHERS)
|
||||||
component.permissionsSelectionModel.includeUsers.push(3)
|
component.permissionsSelectionModel.includeUsers.update((users) => [
|
||||||
|
...users,
|
||||||
|
3,
|
||||||
|
])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
@@ -2003,7 +2069,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 = true
|
component.permissionsSelectionModel.hideUnowned.set(true)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.filterRules).toEqual([
|
expect(component.filterRules).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -735,38 +735,50 @@ export class FilterEditorComponent
|
|||||||
this._textFilter = rule.value
|
this._textFilter = rule.value
|
||||||
break
|
break
|
||||||
case FILTER_OWNER:
|
case FILTER_OWNER:
|
||||||
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.SELF
|
this.permissionsSelectionModel.ownerFilter.set(OwnerFilterType.SELF)
|
||||||
this.permissionsSelectionModel.hideUnowned = false
|
this.permissionsSelectionModel.hideUnowned.set(false)
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.userID = parseInt(rule.value, 10)
|
this.permissionsSelectionModel.userID.set(
|
||||||
|
Number.parseInt(rule.value, 10)
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case FILTER_OWNER_ANY:
|
case FILTER_OWNER_ANY:
|
||||||
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS
|
this.permissionsSelectionModel.ownerFilter.set(OwnerFilterType.OTHERS)
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.includeUsers.push(
|
this.permissionsSelectionModel.includeUsers.update((users) => [
|
||||||
parseInt(rule.value, 10)
|
...users,
|
||||||
)
|
Number.parseInt(rule.value, 10),
|
||||||
|
])
|
||||||
break
|
break
|
||||||
case FILTER_OWNER_DOES_NOT_INCLUDE:
|
case FILTER_OWNER_DOES_NOT_INCLUDE:
|
||||||
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.NOT_SELF
|
this.permissionsSelectionModel.ownerFilter.set(
|
||||||
if (rule.value)
|
OwnerFilterType.NOT_SELF
|
||||||
this.permissionsSelectionModel.excludeUsers.push(
|
|
||||||
parseInt(rule.value, 10)
|
|
||||||
)
|
)
|
||||||
|
if (rule.value)
|
||||||
|
this.permissionsSelectionModel.excludeUsers.update((users) => [
|
||||||
|
...users,
|
||||||
|
Number.parseInt(rule.value, 10),
|
||||||
|
])
|
||||||
break
|
break
|
||||||
case FILTER_SHARED_BY_USER:
|
case FILTER_SHARED_BY_USER:
|
||||||
this.permissionsSelectionModel.ownerFilter =
|
this.permissionsSelectionModel.ownerFilter.set(
|
||||||
OwnerFilterType.SHARED_BY_ME
|
OwnerFilterType.SHARED_BY_ME
|
||||||
|
)
|
||||||
if (rule.value)
|
if (rule.value)
|
||||||
this.permissionsSelectionModel.userID = parseInt(rule.value, 10)
|
this.permissionsSelectionModel.userID.set(
|
||||||
|
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 = false
|
this.permissionsSelectionModel.hideUnowned.set(false)
|
||||||
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.UNOWNED
|
this.permissionsSelectionModel.ownerFilter.set(
|
||||||
|
OwnerFilterType.UNOWNED
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
this.permissionsSelectionModel.hideUnowned =
|
this.permissionsSelectionModel.hideUnowned.set(
|
||||||
rule.value === 'false' || rule.value === '0'
|
rule.value === 'false' || rule.value === '0'
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1074,34 +1086,35 @@ 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 == OwnerFilterType.SHARED_BY_ME
|
this.permissionsSelectionModel.ownerFilter() ==
|
||||||
|
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,
|
||||||
@@ -1109,7 +1122,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,6 +210,48 @@ 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,6 +2,8 @@ import { HttpClient } from '@angular/common/http'
|
|||||||
import {
|
import {
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
|
Signal,
|
||||||
|
computed,
|
||||||
inject,
|
inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
LOCALE_ID,
|
LOCALE_ID,
|
||||||
@@ -297,6 +299,7 @@ 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()
|
||||||
@@ -326,10 +329,6 @@ 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
|
||||||
@@ -339,6 +338,7 @@ 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,6 +594,18 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user