Compare commits

...

6 Commits

Author SHA1 Message Date
Crowdin Bot
18709f5fe8 New Crowdin translations by GitHub Action 2025-03-09 12:09:34 +00:00
shamoon
955ff32dcd Fix: fix improved close behavior with production minified javascript 2025-03-08 19:17:08 -08:00
shamoon
b746b6f2d6 Chore: prefix codecov flags 2025-03-07 17:08:36 -08:00
shamoon
b4b0f802e1
Fix: correct all results with whoosh queries (#9331) 2025-03-07 13:31:23 -08:00
Trenton H
5f16d5f5f1
Chore: Adds an option for funding through GitHub sponsors (#9328) 2025-03-07 17:39:23 +00:00
shamoon
8db04398c7
Chore: ensure codecov upload gets attempted (#9308)
---------

Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2025-03-07 09:29:55 -08:00
89 changed files with 89564 additions and 59432 deletions

View File

@ -1,18 +1,18 @@
codecov: codecov:
require_ci_to_pass: true require_ci_to_pass: true
# https://docs.codecov.com/docs/flags#recommended-automatic-flag-management # https://docs.codecov.com/docs/components
# Require each flag to have 1 upload before notification component_management:
flag_management: individual_components:
individual_flags: - component_id: backend
- name: backend
paths: paths:
- src/ - src/**
- name: frontend - component_id: frontend
paths: paths:
- src-ui/ - src-ui/**
# https://docs.codecov.com/docs/pull-request-comments # https://docs.codecov.com/docs/pull-request-comments
# codecov will only comment if coverage changes # codecov will only comment if coverage changes
comment: comment:
layout: "header, diff, components, flags, files"
require_changes: true require_changes: true
# https://docs.codecov.com/docs/javascript-bundle-analysis # https://docs.codecov.com/docs/javascript-bundle-analysis
require_bundle_changes: true require_bundle_changes: true

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [shamoon, stumpylog]

View File

@ -162,16 +162,20 @@ jobs:
--frozen \ --frozen \
pytest pytest
- -
name: Upload coverage name: Upload backend test results to Codecov
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }} if: always()
uses: actions/upload-artifact@v4 uses: codecov/test-results-action@v1
with: with:
name: backend-coverage-report token: ${{ secrets.CODECOV_TOKEN }}
path: | flags: backend-python-${{ matrix.python-version }}
coverage.xml files: junit.xml
junit.xml -
retention-days: 7 name: Upload backend coverage to Codecov
if-no-files-found: error uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }}
files: coverage.xml
- -
name: Stop containers name: Stop containers
if: always() if: always()
@ -179,7 +183,7 @@ jobs:
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
install-frontend-depedendencies: install-frontend-dependencies:
name: "Install Frontend Dependencies" name: "Install Frontend Dependencies"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
@ -214,7 +218,7 @@ jobs:
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})" name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- install-frontend-depedendencies - install-frontend-dependencies
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -245,111 +249,33 @@ jobs:
run: cd src-ui && npm run lint run: cd src-ui && npm run lint
- -
name: Run Jest unit tests name: Run Jest unit tests
env:
JEST_JUNIT_OUTPUT_FILE: junit-report-${{ matrix.shard-index }}.xml
run: cd src-ui && npm run test -- --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && npm run test -- --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
-
name: Upload Jest coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: jest-coverage-report-${{ matrix.shard-index }}
path: |
src-ui/coverage/coverage-final.json
src-ui/coverage/lcov.info
src-ui/coverage/clover.xml
retention-days: 7
if-no-files-found: error
- -
name: Run Playwright e2e tests name: Run Playwright e2e tests
run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && npx playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
- -
name: Upload Playwright test results name: Upload frontend test results to Codecov
uses: codecov/test-results-action@v1
if: always() if: always()
uses: actions/upload-artifact@v4
with: with:
name: playwright-report-${{ matrix.shard-index }} token: ${{ secrets.CODECOV_TOKEN }}
path: src-ui/playwright-report flags: frontend-node-${{ matrix.node-version }}
retention-days: 7 directory: src-ui/
-
name: Upload frontend test results
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-report-${{ matrix.shard-index }}
path: src-ui/junit-report-${{ matrix.shard-index }}.xml
retention-days: 7
tests-coverage-upload:
name: "Upload to Codecov"
runs-on: ubuntu-24.04
needs:
- tests-backend
- tests-frontend
steps:
-
uses: actions/checkout@v4
-
name: Download frontend jest coverage
uses: actions/download-artifact@v4
with:
path: src-ui/coverage/
pattern: jest-coverage-report-*
-
name: Download frontend playwright coverage
uses: actions/download-artifact@v4
with:
path: src-ui/coverage/
pattern: playwright-report-*
merge-multiple: true
-
name: Download frontend test results
uses: actions/download-artifact@v4
with:
path: src-ui/junit/
pattern: junit-report-*
merge-multiple: true
- -
name: Upload frontend coverage to Codecov name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
# not required for public repos, but intermittently fails otherwise
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/ directory: src-ui/coverage/
# dont include backend coverage files here
files: '!coverage.xml' frontend-bundle-analysis:
- name: "Frontend Bundle Analysis"
name: Upload frontend test results to Codecov runs-on: ubuntu-24.04
if: ${{ !cancelled() }} needs:
uses: codecov/test-results-action@v1 - tests-frontend
with: steps:
token: ${{ secrets.CODECOV_TOKEN }} - uses: actions/checkout@v4
flags: frontend
directory: src-ui/junit/
-
name: Download backend coverage
uses: actions/download-artifact@v4
with:
name: backend-coverage-report
path: src/
-
name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
# not required for public repos, but intermittently fails otherwise
token: ${{ secrets.CODECOV_TOKEN }}
# future expansion
flags: backend
directory: src/
-
name: Upload backend test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend
directory: src/
- -
name: Use Node.js 20 name: Use Node.js 20
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@ -36,7 +36,13 @@ export const routes: Routes = [
component: AppFrameComponent, component: AppFrameComponent,
canDeactivate: [DirtyDocGuard], canDeactivate: [DirtyDocGuard],
children: [ children: [
{ path: 'dashboard', component: DashboardComponent }, {
path: 'dashboard',
component: DashboardComponent,
data: {
componentName: 'AppFrameComponent',
},
},
{ {
path: 'documents', path: 'documents',
component: DocumentListComponent, component: DocumentListComponent,
@ -47,6 +53,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Document, type: PermissionType.Document,
}, },
componentName: 'DocumentListComponent',
}, },
}, },
{ {
@ -59,6 +66,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.SavedView, type: PermissionType.SavedView,
}, },
componentName: 'DocumentListComponent',
}, },
}, },
{ {
@ -70,6 +78,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Document, type: PermissionType.Document,
}, },
componentName: 'DocumentDetailComponent',
}, },
}, },
{ {
@ -81,6 +90,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Document, type: PermissionType.Document,
}, },
componentName: 'DocumentDetailComponent',
}, },
}, },
{ {
@ -92,6 +102,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Document, type: PermissionType.Document,
}, },
componentName: 'DocumentAsnComponent',
}, },
}, },
{ {
@ -103,6 +114,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Tag, type: PermissionType.Tag,
}, },
componentName: 'TagListComponent',
}, },
}, },
{ {
@ -114,6 +126,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.DocumentType, type: PermissionType.DocumentType,
}, },
componentName: 'DocumentTypeListComponent',
}, },
}, },
{ {
@ -125,6 +138,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Correspondent, type: PermissionType.Correspondent,
}, },
componentName: 'CorrespondentListComponent',
}, },
}, },
{ {
@ -136,6 +150,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.StoragePath, type: PermissionType.StoragePath,
}, },
componentName: 'StoragePathListComponent',
}, },
}, },
{ {
@ -144,6 +159,7 @@ export const routes: Routes = [
canActivate: [PermissionsGuard], canActivate: [PermissionsGuard],
data: { data: {
requireAdmin: true, requireAdmin: true,
componentName: 'LogsComponent',
}, },
}, },
{ {
@ -155,6 +171,7 @@ export const routes: Routes = [
action: PermissionAction.Delete, action: PermissionAction.Delete,
type: PermissionType.Document, type: PermissionType.Document,
}, },
componentName: 'TrashComponent',
}, },
}, },
// redirect old paths // redirect old paths
@ -180,6 +197,7 @@ export const routes: Routes = [
action: PermissionAction.Change, action: PermissionAction.Change,
type: PermissionType.UISettings, type: PermissionType.UISettings,
}, },
componentName: 'SettingsComponent',
}, },
}, },
{ {
@ -192,6 +210,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.UISettings, type: PermissionType.UISettings,
}, },
componentName: 'SettingsComponent',
}, },
}, },
{ {
@ -203,6 +222,7 @@ export const routes: Routes = [
action: PermissionAction.Change, action: PermissionAction.Change,
type: PermissionType.AppConfig, type: PermissionType.AppConfig,
}, },
componentName: 'ConfigComponent',
}, },
}, },
{ {
@ -214,6 +234,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.PaperlessTask, type: PermissionType.PaperlessTask,
}, },
componentName: 'TasksComponent',
}, },
}, },
{ {
@ -225,6 +246,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.CustomField, type: PermissionType.CustomField,
}, },
componentName: 'CustomFieldsComponent',
}, },
}, },
{ {
@ -236,6 +258,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Workflow, type: PermissionType.Workflow,
}, },
componentName: 'WorkflowsComponent',
}, },
}, },
{ {
@ -247,6 +270,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.MailAccount, type: PermissionType.MailAccount,
}, },
componentName: 'MailComponent',
}, },
}, },
{ {
@ -258,6 +282,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.User, type: PermissionType.User,
}, },
componentName: 'UsersAndGroupsComponent',
}, },
}, },
{ {
@ -269,6 +294,7 @@ export const routes: Routes = [
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.SavedView, type: PermissionType.SavedView,
}, },
componentName: 'SavedViewsComponent',
}, },
}, },
], ],

View File

@ -29,7 +29,7 @@ describe('ComponentRouterService', () => {
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url', url: 'test-url',
component: { name: 'TestComponent' }, data: { componentName: 'TestComponent' },
} as any) } as any)
) )
@ -41,13 +41,13 @@ describe('ComponentRouterService', () => {
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url-1', url: 'test-url-1',
component: { name: 'TestComponent' }, data: { componentName: 'TestComponent' },
} as any) } as any)
) )
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url-2', url: 'test-url-2',
component: { name: 'TestComponent' }, data: { componentName: 'TestComponent' },
} as any) } as any)
) )
@ -59,13 +59,13 @@ describe('ComponentRouterService', () => {
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url-1', url: 'test-url-1',
component: { name: 'TestComponent1' }, data: { componentName: 'TestComponent1' },
} as any) } as any)
) )
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url-2', url: 'test-url-2',
component: { name: 'TestComponent2' }, data: { componentName: 'TestComponent2' },
} as any) } as any)
) )
@ -76,13 +76,13 @@ describe('ComponentRouterService', () => {
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url-1', url: 'test-url-1',
component: { name: 'TestComponent' }, data: { componentName: 'TestComponent' },
} as any) } as any)
) )
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url-2', url: 'test-url-2',
component: { name: 'TestComponent' }, data: { componentName: 'TestComponent' },
} as any) } as any)
) )
@ -93,7 +93,7 @@ describe('ComponentRouterService', () => {
eventsSubject.next( eventsSubject.next(
new ActivationStart({ new ActivationStart({
url: 'test-url', url: 'test-url',
component: { name: 'TestComponent' }, data: { componentName: 'TestComponent' },
} as any) } as any)
) )

View File

@ -17,11 +17,11 @@ export class ComponentRouterService {
.subscribe((event: ActivationStart) => { .subscribe((event: ActivationStart) => {
if ( if (
this.componentHistory[this.componentHistory.length - 1] !== this.componentHistory[this.componentHistory.length - 1] !==
event.snapshot.component.name && event.snapshot.data.componentName &&
!EXCLUDE_COMPONENTS.includes(event.snapshot.component.name) !EXCLUDE_COMPONENTS.includes(event.snapshot.data.componentName)
) { ) {
this.history.push(event.snapshot.url.toString()) this.history.push(event.snapshot.url.toString())
this.componentHistory.push(event.snapshot.component.name) this.componentHistory.push(event.snapshot.data.componentName)
} else { } else {
// Update the URL of the current component in case the same component was loaded via a different URL // Update the URL of the current component in case the same component was loaded via a different URL
this.history[this.history.length - 1] = event.snapshot.url.toString() this.history[this.history.length - 1] = event.snapshot.url.toString()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from documents.index import DelayedQuery
from documents.permissions import PaperlessObjectPermissions from documents.permissions import PaperlessObjectPermissions
from paperless.filters import GroupFilterSet from paperless.filters import GroupFilterSet
from paperless.filters import UserFilterSet from paperless.filters import UserFilterSet
@ -66,17 +67,17 @@ class StandardPagination(PageNumberPagination):
) )
def get_all_result_ids(self): def get_all_result_ids(self):
ids = [] query = self.page.paginator.object_list
if hasattr(self.page.paginator.object_list, "saved_results"): if isinstance(query, DelayedQuery):
results_page = self.page.paginator.object_list.saved_results[0] try:
if results_page is not None: ids = [
for i in range(len(results_page.results.docs())): query.searcher.ixreader.stored_fields(
try: doc_num,
fields = results_page.results.fields(i) )["id"]
if "id" in fields: for doc_num in query.saved_results.get(0).results.docs()
ids.append(fields["id"]) ]
except Exception: except Exception:
pass pass
else: else:
ids = self.page.paginator.object_list.values_list("pk", flat=True) ids = self.page.paginator.object_list.values_list("pk", flat=True)
return ids return ids