diff --git a/src-ui/src/app/components/explorer/explorer.component.html b/src-ui/src/app/components/explorer/explorer.component.html
index feb9be68d..0d5db1567 100644
--- a/src-ui/src/app/components/explorer/explorer.component.html
+++ b/src-ui/src/app/components/explorer/explorer.component.html
@@ -370,7 +370,7 @@
|
-
-
-
-
- {{ d.notes.length }}
-
- |
params.has('id')), // only on saved view e.g. /view/id
- switchMap((params) => {
- return this.savedViewService
- .getCached(+params.get('id'))
- .pipe(map((view) => ({ view })))
- })
- )
- .pipe(takeUntil(this.unsubscribeNotifier))
- .subscribe(({ view }) => {
- if (!view) {
- this.router.navigate(['404'])
- return
- }
- this.unmodifiedSavedView = view
- this.list.activateSavedViewWithQueryParams(
- view,
- convertToParamMap(this.route.snapshot.queryParams)
- )
- // this.list.reload()
- this.unmodifiedFilterRules = view.filter_rules
- })
-
this.route.queryParamMap
- .pipe(
- filter(() => !this.route.snapshot.paramMap.has('id')), // only when not on /view/id
- takeUntil(this.unsubscribeNotifier)
- )
+ .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((queryParams) => {
- if (queryParams.has('view')) {
- // loading a saved view on /documents
- this.loadViewConfig(parseInt(queryParams.get('view')))
- } else {
- this.list.activateSavedView(null)
- this.list.loadFromQueryParams(queryParams)
- this.unmodifiedFilterRules = []
- }
+ this.list.loadFromQueryParams(queryParams)
+ this.unmodifiedFilterRules = []
})
}
@@ -187,19 +164,10 @@ export class ExplorerComponent
this.unsubscribeNotifier.complete()
}
- loadViewConfig(viewID: number) {
- this.savedViewService
- .getCached(viewID)
- .pipe(first())
- .subscribe((view) => {
- this.unmodifiedSavedView = view
- this.list.activateSavedView(view)
- this.list.reload()
- })
- }
-
- openDocumentDetail(document: PaperlessDocument) {
- this.router.navigate(['documents', document.id])
+ openDocumentDetail(storagePath: PaperlessStoragePath) {
+ this.router.navigate(['explorer'], {
+ queryParams: { spid: storagePath.id },
+ })
}
toggleSelected(document: PaperlessDocument, event: MouseEvent): void {
diff --git a/src-ui/src/app/services/rest/custom-storage-path.service.ts b/src-ui/src/app/services/rest/custom-storage-path.service.ts
index 28cd1e23f..fc7343842 100644
--- a/src-ui/src/app/services/rest/custom-storage-path.service.ts
+++ b/src-ui/src/app/services/rest/custom-storage-path.service.ts
@@ -1,10 +1,7 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
-import { Observable, map } from 'rxjs'
+import { Observable, filter, map, switchMap, tap } from 'rxjs'
import { FilterRule } from 'src/app/data/filter-rule'
-import { PaperlessDocument } from 'src/app/data/paperless-document'
-import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'
-import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { Results } from 'src/app/data/results'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
@@ -26,8 +23,6 @@ interface SelectionData {
providedIn: 'root',
})
export class CustomStoragePathService extends AbstractPaperlessService {
- private _searchQuery: string
-
constructor(http: HttpClient) {
super(http, 'storage_paths')
}
@@ -38,16 +33,40 @@ export class CustomStoragePathService extends AbstractPaperlessService> {
- return this.list(
- page,
- pageSize,
- sortField,
- sortReverse,
- Object.assign(extraParams, queryParamsFromFilterRules(filterRules))
- ).pipe(
+ const params = Object.assign(
+ extraParams,
+ queryParamsFromFilterRules(filterRules)
+ )
+ if (parentStoragePathId !== null && parentStoragePathId !== undefined) {
+ return this.get(parentStoragePathId).pipe(
+ switchMap((storagePath) => {
+ params.path__istartswith = storagePath.path
+ return this.list(page, pageSize, sortField, sortReverse, params).pipe(
+ map((results) => {
+ results.results = results.results.filter((s) => {
+ const isNotParent = s.id !== parentStoragePathId
+ const isDirectChild =
+ s.path
+ .replace(storagePath.path, '')
+ .split('/')
+ .filter((s) => !!s).length === 1
+ return isNotParent && isDirectChild
+ })
+ return results
+ })
+ )
+ })
+ )
+ }
+
+ return this.list(page, pageSize, sortField, sortReverse, params).pipe(
map((results) => {
+ results.results = results.results.filter(
+ (s) => s.path.split('/').length === 1
+ )
return results
})
)
@@ -58,79 +77,4 @@ export class CustomStoragePathService extends AbstractPaperlessService response.results.map((doc) => doc.id)))
}
-
- getPreviewUrl(id: number, original: boolean = false): string {
- let url = this.getResourceUrl(id, 'preview')
- if (this._searchQuery) url += `#search="${this._searchQuery}"`
- if (original) {
- url += '?original=true'
- }
- return url
- }
-
- getThumbUrl(id: number): string {
- return this.getResourceUrl(id, 'thumb')
- }
-
- getDownloadUrl(id: number, original: boolean = false): string {
- let url = this.getResourceUrl(id, 'download')
- if (original) {
- url += '?original=true'
- }
- return url
- }
-
- update(o: PaperlessDocument): Observable {
- // we want to only set created_date
- o.created = undefined
- return super.update(o)
- }
-
- uploadDocument(formData) {
- return this.http.post(
- this.getResourceUrl(null, 'post_document'),
- formData,
- { reportProgress: true, observe: 'events' }
- )
- }
-
- getMetadata(id: number): Observable {
- return this.http.get(
- this.getResourceUrl(id, 'metadata')
- )
- }
-
- bulkEdit(ids: number[], method: string, args: any) {
- return this.http.post(this.getResourceUrl(null, 'bulk_edit'), {
- documents: ids,
- method: method,
- parameters: args,
- })
- }
-
- getSuggestions(id: number): Observable {
- return this.http.get(
- this.getResourceUrl(id, 'suggestions')
- )
- }
-
- bulkDownload(
- ids: number[],
- content = 'both',
- useFilenameFormatting: boolean = false
- ) {
- return this.http.post(
- this.getResourceUrl(null, 'bulk_download'),
- {
- documents: ids,
- content: content,
- follow_formatting: useFilenameFormatting,
- },
- { responseType: 'blob' }
- )
- }
-
- public set searchQuery(query: string) {
- this._searchQuery = query
- }
}
diff --git a/src-ui/src/app/services/storage-path-list-view.service.ts b/src-ui/src/app/services/storage-path-list-view.service.ts
index 2fc2dec14..53dda6dff 100644
--- a/src-ui/src/app/services/storage-path-list-view.service.ts
+++ b/src-ui/src/app/services/storage-path-list-view.service.ts
@@ -57,6 +57,8 @@ export interface ListViewState {
* Contains the IDs of all selected documents.
*/
selected?: Set
+
+ storagePathId?: number | null
}
/**
@@ -108,6 +110,7 @@ export class StoragePathListViewService {
sortReverse: true,
filterRules: [],
selected: new Set(),
+ storagePathId: null,
}
}
@@ -121,56 +124,22 @@ export class StoragePathListViewService {
return this.listViewStates.get(this._activeSavedViewId)
}
- activateSavedView(view: PaperlessSavedView) {
- this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
- if (view) {
- this._activeSavedViewId = view.id
- this.loadSavedView(view)
- } else {
- this._activeSavedViewId = null
- }
- }
-
- activateSavedViewWithQueryParams(
- view: PaperlessSavedView,
- queryParams: ParamMap
- ) {
- const viewState = paramsToViewState(queryParams)
- this.activateSavedView(view)
- this.currentPage = viewState.currentPage
- }
-
- loadSavedView(view: PaperlessSavedView, closeCurrentView: boolean = false) {
- if (closeCurrentView) {
- this._activeSavedViewId = null
- }
-
- this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
- this.activeListViewState.sortField = view.sort_field
- this.activeListViewState.sortReverse = view.sort_reverse
- if (this._activeSavedViewId) {
- this.activeListViewState.title = view.name
- }
-
- this.reduceSelectionToFilter()
-
- if (!this.router.routerState.snapshot.url.includes('/view/')) {
- this.router.navigate(['view', view.id])
- }
- }
-
loadFromQueryParams(queryParams: ParamMap) {
- const paramsEmpty: boolean = queryParams.keys.length == 0
- let newState: ListViewState = this.listViewStates.get(
- this._activeSavedViewId
- )
- if (!paramsEmpty) newState = paramsToViewState(queryParams)
+ const isParamsEmpty: boolean = queryParams.keys.length == 0
+ let newState: ListViewState & { storagePathId?: number } =
+ this.listViewStates.get(this._activeSavedViewId)
+ if (!isParamsEmpty) {
+ newState = paramsToViewState(queryParams)
+ if (queryParams.has('spid')) {
+ newState.storagePathId = parseInt(queryParams.get('spid'))
+ }
+ }
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
// only reload if things have changed
if (
!this.initialized ||
- paramsEmpty ||
+ isParamsEmpty ||
this.activeListViewState.sortField !== newState.sortField ||
this.activeListViewState.sortReverse !== newState.sortReverse ||
this.activeListViewState.currentPage !== newState.currentPage ||
@@ -183,7 +152,8 @@ export class StoragePathListViewService {
this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage
- this.reload(null, paramsEmpty) // update the params if there arent any
+ this.activeListViewState.storagePathId = newState.storagePathId
+ this.reload(null, isParamsEmpty) // update the params if there arent any
}
}
@@ -198,11 +168,12 @@ export class StoragePathListViewService {
activeListViewState.sortField,
activeListViewState.sortReverse,
activeListViewState.filterRules,
- { truncate_content: true }
+ { truncate_content: true },
+ activeListViewState.storagePathId
)
.subscribe({
next: (result) => {
- console.log('list filtered result:', result)
+ console.log('result:', result)
this.initialized = true
this.isReloading = false
activeListViewState.collectionSize = result.count
diff --git a/src/documents/filters.py b/src/documents/filters.py
index 271b91108..fe2414776 100644
--- a/src/documents/filters.py
+++ b/src/documents/filters.py
@@ -1,159 +1,159 @@
-from django.db.models import Q
-from django_filters.rest_framework import BooleanFilter
-from django_filters.rest_framework import Filter
-from django_filters.rest_framework import FilterSet
-from rest_framework_guardian.filters import ObjectPermissionsFilter
-
-from .models import Correspondent
-from .models import Document
-from .models import DocumentType
-from .models import Log
-from .models import StoragePath
-from .models import Tag
-
-
-CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
-ID_KWARGS = ["in", "exact"]
-INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
-DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"]
-
-
-class CorrespondentFilterSet(FilterSet):
- class Meta:
- model = Correspondent
- fields = {"name": CHAR_KWARGS}
-
-
-class TagFilterSet(FilterSet):
- class Meta:
- model = Tag
- fields = {"name": CHAR_KWARGS}
-
-
-class DocumentTypeFilterSet(FilterSet):
- class Meta:
- model = DocumentType
- fields = {"name": CHAR_KWARGS}
-
-
-class ObjectFilter(Filter):
- def __init__(self, exclude=False, in_list=False, field_name=""):
- super().__init__()
- self.exclude = exclude
- self.in_list = in_list
- self.field_name = field_name
-
- def filter(self, qs, value):
- if not value:
- return qs
-
- try:
- object_ids = [int(x) for x in value.split(",")]
- except ValueError:
- return qs
-
- if self.in_list:
- qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
- else:
- for obj_id in object_ids:
- if self.exclude:
- qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
- else:
- qs = qs.filter(**{f"{self.field_name}__id": obj_id})
-
- return qs
-
-
-class InboxFilter(Filter):
- def filter(self, qs, value):
- if value == "true":
- return qs.filter(tags__is_inbox_tag=True)
- elif value == "false":
- return qs.exclude(tags__is_inbox_tag=True)
- else:
- return qs
-
-
-class TitleContentFilter(Filter):
- def filter(self, qs, value):
- if value:
- return qs.filter(Q(title__icontains=value) | Q(content__icontains=value))
- else:
- return qs
-
-
-class DocumentFilterSet(FilterSet):
-
- is_tagged = BooleanFilter(
- label="Is tagged",
- field_name="tags",
- lookup_expr="isnull",
- exclude=True,
- )
-
- tags__id__all = ObjectFilter(field_name="tags")
-
- tags__id__none = ObjectFilter(field_name="tags", exclude=True)
-
- tags__id__in = ObjectFilter(field_name="tags", in_list=True)
-
- correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
-
- document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
-
- storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
-
- is_in_inbox = InboxFilter()
-
- title_content = TitleContentFilter()
-
- class Meta:
- model = Document
- fields = {
- "title": CHAR_KWARGS,
- "content": CHAR_KWARGS,
- "archive_serial_number": INT_KWARGS,
- "created": DATE_KWARGS,
- "added": DATE_KWARGS,
- "modified": DATE_KWARGS,
- "correspondent": ["isnull"],
- "correspondent__id": ID_KWARGS,
- "correspondent__name": CHAR_KWARGS,
- "tags__id": ID_KWARGS,
- "tags__name": CHAR_KWARGS,
- "document_type": ["isnull"],
- "document_type__id": ID_KWARGS,
- "document_type__name": CHAR_KWARGS,
- "storage_path": ["isnull"],
- "storage_path__id": ID_KWARGS,
- "storage_path__name": CHAR_KWARGS,
- }
-
-
-class LogFilterSet(FilterSet):
- class Meta:
- model = Log
- fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS}
-
-
-class StoragePathFilterSet(FilterSet):
- class Meta:
- model = StoragePath
- fields = {
- "name": CHAR_KWARGS,
- "path": CHAR_KWARGS,
- }
-
-
-class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
- """
- A filter backend that limits results to those where the requesting user
- has read object level permissions, owns the objects, or objects without
- an owner (for backwards compat)
- """
-
- def filter_queryset(self, request, queryset, view):
- objects_with_perms = super().filter_queryset(request, queryset, view)
- objects_owned = queryset.filter(owner=request.user)
- objects_unowned = queryset.filter(owner__isnull=True)
- return objects_with_perms | objects_owned | objects_unowned
+from django.db.models import Q
+from django_filters.rest_framework import BooleanFilter
+from django_filters.rest_framework import Filter
+from django_filters.rest_framework import FilterSet
+from rest_framework_guardian.filters import ObjectPermissionsFilter
+
+from .models import Correspondent
+from .models import Document
+from .models import DocumentType
+from .models import Log
+from .models import StoragePath
+from .models import Tag
+
+
+CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
+ID_KWARGS = ["in", "exact"]
+INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
+DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"]
+
+
+class CorrespondentFilterSet(FilterSet):
+ class Meta:
+ model = Correspondent
+ fields = {"name": CHAR_KWARGS}
+
+
+class TagFilterSet(FilterSet):
+ class Meta:
+ model = Tag
+ fields = {"name": CHAR_KWARGS}
+
+
+class DocumentTypeFilterSet(FilterSet):
+ class Meta:
+ model = DocumentType
+ fields = {"name": CHAR_KWARGS}
+
+
+class ObjectFilter(Filter):
+ def __init__(self, exclude=False, in_list=False, field_name=""):
+ super().__init__()
+ self.exclude = exclude
+ self.in_list = in_list
+ self.field_name = field_name
+
+ def filter(self, qs, value):
+ if not value:
+ return qs
+
+ try:
+ object_ids = [int(x) for x in value.split(",")]
+ except ValueError:
+ return qs
+
+ if self.in_list:
+ qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
+ else:
+ for obj_id in object_ids:
+ if self.exclude:
+ qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
+ else:
+ qs = qs.filter(**{f"{self.field_name}__id": obj_id})
+
+ return qs
+
+
+class InboxFilter(Filter):
+ def filter(self, qs, value):
+ if value == "true":
+ return qs.filter(tags__is_inbox_tag=True)
+ elif value == "false":
+ return qs.exclude(tags__is_inbox_tag=True)
+ else:
+ return qs
+
+
+class TitleContentFilter(Filter):
+ def filter(self, qs, value):
+ if value:
+ return qs.filter(Q(title__icontains=value) | Q(content__icontains=value))
+ else:
+ return qs
+
+
+class DocumentFilterSet(FilterSet):
+
+ is_tagged = BooleanFilter(
+ label="Is tagged",
+ field_name="tags",
+ lookup_expr="isnull",
+ exclude=True,
+ )
+
+ tags__id__all = ObjectFilter(field_name="tags")
+
+ tags__id__none = ObjectFilter(field_name="tags", exclude=True)
+
+ tags__id__in = ObjectFilter(field_name="tags", in_list=True)
+
+ correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
+
+ document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
+
+ storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
+
+ is_in_inbox = InboxFilter()
+
+ title_content = TitleContentFilter()
+
+ class Meta:
+ model = Document
+ fields = {
+ "title": CHAR_KWARGS,
+ "content": CHAR_KWARGS,
+ "archive_serial_number": INT_KWARGS,
+ "created": DATE_KWARGS,
+ "added": DATE_KWARGS,
+ "modified": DATE_KWARGS,
+ "correspondent": ["isnull"],
+ "correspondent__id": ID_KWARGS,
+ "correspondent__name": CHAR_KWARGS,
+ "tags__id": ID_KWARGS,
+ "tags__name": CHAR_KWARGS,
+ "document_type": ["isnull"],
+ "document_type__id": ID_KWARGS,
+ "document_type__name": CHAR_KWARGS,
+ "storage_path": ["isnull"],
+ "storage_path__id": ID_KWARGS,
+ "storage_path__name": CHAR_KWARGS,
+ }
+
+
+class LogFilterSet(FilterSet):
+ class Meta:
+ model = Log
+ fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS}
+
+
+class StoragePathFilterSet(FilterSet):
+ class Meta:
+ model = StoragePath
+ fields = {
+ "name": CHAR_KWARGS,
+ "path": CHAR_KWARGS,
+ }
+
+
+class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
+ """
+ A filter backend that limits results to those where the requesting user
+ has read object level permissions, owns the objects, or objects without
+ an owner (for backwards compat)
+ """
+
+ def filter_queryset(self, request, queryset, view):
+ objects_with_perms = super().filter_queryset(request, queryset, view)
+ objects_owned = queryset.filter(owner=request.user)
+ objects_unowned = queryset.filter(owner__isnull=True)
+ return objects_with_perms | objects_owned | objects_unowned
|