Compare commits

..

18 Commits

Author SHA1 Message Date
Trenton H
367f47b2fd Enables testing against 3.13 for fun 2025-10-01 08:50:33 -07:00
Trenton H
80fabb0b56 Fixes extra dependency on psycopg-pool which should only have been in the Postgres group 2025-10-01 08:33:43 -07:00
guleyupo
af1c235af5 Documentation: fix typo (#10974) 2025-10-01 06:11:45 +00:00
dependabot[bot]
92ee906701 docker-compose(deps): Bump library/postgres in /docker/compose (#10965)
Bumps library/postgres from 17 to 18.

---
updated-dependencies:
- dependency-name: library/postgres
  dependency-version: '18'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 20:25:02 +00:00
shamoon
d6710de486 Chore: refactor for clarity 2025-09-30 12:16:22 -07:00
GitHub Actions
f71b13b82a Auto translate strings 2025-09-30 16:51:15 +00:00
Antoine Mérino
3df43d828a Performance: Cache django-guardian permissions when counting documents (#10657)
Fixes N+1 queries in tag, correspondent, storage path, custom field,
and document type list views.
Reduces SQL queries from 160 to 9.
2025-09-30 09:48:44 -07:00
Trenton H
643e2b4a8e Chore(deps): Bulk upgrade backend dependencies (#10971)
* Stop this dependabot nonsense with a bulk upgrade and lock

* Downgrades fido2 as allauth MFA doesn't support it yet
2025-09-30 08:51:06 -07:00
dependabot[bot]
6fa896df39 Chore(deps): Bump the major-versions group with 2 updates (#10960)
Bumps the major-versions group with 2 updates: [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) and [types-setuptools](https://github.com/typeshed-internal/stub_uploader).


Updates `pytest-rerunfailures` from 15.1 to 16.0.1
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/15.1...16.0.1)

Updates `types-setuptools` from 75.8.2.20250301 to 80.9.0.20250822
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  dependency-version: 16.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-versions
- dependency-name: types-setuptools
  dependency-version: 80.9.0.20250822
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-versions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 07:09:22 -07:00
dependabot[bot]
6aeb5a5503 Chore(deps): Bump types-colorama from 0.4.15.20240311 to 0.4.15.20250801 (#10961)
Bumps [types-colorama](https://github.com/typeshed-internal/stub_uploader) from 0.4.15.20240311 to 0.4.15.20250801.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-colorama
  dependency-version: 0.4.15.20250801
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 15:24:36 -07:00
dependabot[bot]
86dbeb3a27 Chore(deps): Bump django-guardian from 3.1.3 to 3.2.0 (#10909)
Bumps [django-guardian](https://github.com/django-guardian/django-guardian) from 3.1.3 to 3.2.0.
- [Release notes](https://github.com/django-guardian/django-guardian/releases)
- [Commits](https://github.com/django-guardian/django-guardian/compare/3.1.3...3.2.0)

---
updated-dependencies:
- dependency-name: django-guardian
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 11:54:56 -07:00
dependabot[bot]
e97217f267 Chore(deps): Bump django-soft-delete from 1.0.19 to 1.0.21 (#10908)
Bumps [django-soft-delete](https://github.com/san4ezy/django_softdelete) from 1.0.19 to 1.0.21.
- [Changelog](https://github.com/san4ezy/django_softdelete/blob/master/CHANGELOG.md)
- [Commits](https://github.com/san4ezy/django_softdelete/commits)

---
updated-dependencies:
- dependency-name: django-soft-delete
  dependency-version: 1.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 09:38:39 -07:00
dependabot[bot]
05d5d7e796 Chore(deps): Bump whitenoise from 6.10.0 to 6.11.0 (#10910)
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 6.10.0 to 6.11.0.
- [Changelog](https://github.com/evansd/whitenoise/blob/main/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/6.10.0...6.11.0)

---
updated-dependencies:
- dependency-name: whitenoise
  dependency-version: 6.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 08:38:35 -07:00
GitHub Actions
e8957de4a7 Auto translate strings 2025-09-26 20:49:13 +00:00
shamoon
1717517e70 Tweakhancement: reorganize some list & bulk editing buttons (#10944) 2025-09-26 13:47:24 -07:00
dependabot[bot]
af544177d4 Chore(deps): Bump django-cors-headers from 4.8.0 to 4.9.0 (#10907)
Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 4.8.0 to 4.9.0.
- [Changelog](https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/adamchainz/django-cors-headers/compare/4.8.0...4.9.0)

---
updated-dependencies:
- dependency-name: django-cors-headers
  dependency-version: 4.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 12:21:18 -07:00
dependabot[bot]
766af6a48a docker(deps): bump astral-sh/uv (#10906)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.8.17-python3.12-bookworm-slim to 0.8.19-python3.12-bookworm-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.8.17...0.8.19)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.8.19-python3.12-bookworm-slim
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 18:42:28 +00:00
shamoon
e985051890 Chore: remove Codecov token from CI workflow (#10941) 2025-09-26 10:01:31 -07:00
19 changed files with 1909 additions and 1885 deletions

View File

@@ -127,7 +127,7 @@ jobs:
- pre-commit - pre-commit
strategy: strategy:
matrix: matrix:
python-version: ['3.10', '3.11', '3.12'] python-version: ['3.10', '3.11', '3.12', '3.13']
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
@@ -183,13 +183,11 @@ jobs:
if: always() if: always()
uses: codecov/test-results-action@v1 uses: codecov/test-results-action@v1
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: junit.xml files: junit.xml
- name: Upload backend coverage to Codecov - name: Upload backend coverage to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: coverage.xml files: coverage.xml
- name: Stop containers - name: Stop containers
@@ -265,13 +263,11 @@ jobs:
uses: codecov/test-results-action@v1 uses: codecov/test-results-action@v1
if: always() if: always()
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/ directory: src-ui/
- name: Upload frontend coverage to Codecov - name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/ directory: src-ui/coverage/
tests-frontend-e2e: tests-frontend-e2e:
@@ -322,455 +318,6 @@ jobs:
run: cd src-ui && pnpm exec playwright install run: cd src-ui && pnpm exec playwright install
- name: Run Playwright e2e tests - name: Run Playwright e2e tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
codecov-comment:
name: "Codecov PR Comment"
runs-on: ubuntu-24.04
needs:
- tests-backend
- tests-frontend
- tests-frontend-e2e
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
steps:
- name: Gather pull request context
id: pr
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
if (!pr) {
core.info('No associated pull request. Skipping.');
core.setOutput('shouldRun', 'false');
return;
}
core.setOutput('shouldRun', 'true');
core.setOutput('prNumber', pr.number.toString());
core.setOutput('headSha', pr.head.sha);
- name: Fetch Codecov coverage
id: coverage
if: steps.pr.outputs.shouldRun == 'true'
uses: actions/github-script@v7
env:
COMMIT_SHA: ${{ steps.pr.outputs.headSha }}
PR_NUMBER: ${{ steps.pr.outputs.prNumber }}
with:
script: |
const commitSha = process.env.COMMIT_SHA;
const prNumber = process.env.PR_NUMBER;
const owner = context.repo.owner;
const repo = context.repo.repo;
const service = 'gh';
const baseUrl = `https://api.codecov.io/api/v2/${service}/${owner}/repos/${repo}`;
const commitUrl = `${baseUrl}/commits/${commitSha}`;
const maxAttempts = 20;
const waitMs = 15000;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let data;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
core.info(`Fetching Codecov report (attempt ${attempt}/${maxAttempts})`);
let response;
try {
response = await fetch(commitUrl, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
} catch (error) {
core.warning(`Codecov fetch failed: ${error}. Waiting before retrying.`);
await sleep(waitMs);
continue;
}
if (response.status === 404) {
core.info('Report not ready yet (404). Waiting before retrying.');
await sleep(waitMs);
continue;
}
if ([429, 500, 502, 503, 504].includes(response.status)) {
const text = await response.text().catch(() => '');
core.info(`Codecov API transient error ${response.status}: ${text}. Waiting before retrying.`);
await sleep(waitMs);
continue;
}
if (!response.ok) {
const text = await response.text().catch(() => '');
core.warning(`Codecov API returned ${response.status}: ${text}. Skipping comment.`);
core.setOutput('shouldComment', 'false');
return;
}
data = await response.json().catch((error) => {
core.warning(`Failed to parse Codecov response: ${error}.`);
return undefined;
});
if (data && Object.keys(data).length > 0) {
break;
}
core.info('Report payload empty. Waiting before retrying.');
await sleep(waitMs);
}
if (!data && prNumber) {
core.info('Attempting to retrieve coverage from PR endpoint.');
const prUrl = `${baseUrl}/pulls/${prNumber}`;
let prResponse;
try {
prResponse = await fetch(prUrl, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
} catch (error) {
core.warning(`Codecov PR fetch failed: ${error}.`);
}
if (prResponse) {
if ([429, 500, 502, 503, 504].includes(prResponse.status)) {
const text = await prResponse.text().catch(() => '');
core.info(`Codecov PR endpoint transient error ${prResponse.status}: ${text}.`);
} else if (!prResponse.ok) {
const text = await prResponse.text().catch(() => '');
core.warning(`Codecov PR endpoint returned ${prResponse.status}: ${text}.`);
} else {
const prData = await prResponse.json().catch((error) => {
core.warning(`Failed to parse Codecov PR response: ${error}.`);
return undefined;
});
if (prData?.latest_report) {
data = { report: prData.latest_report };
} else if (prData?.head_totals) {
const headTotals = prData.head_totals;
const baseTotals = prData.base_totals;
let compareTotals;
if (baseTotals && headTotals) {
const headCoverage = Number(headTotals.coverage);
const baseCoverage = Number(baseTotals.coverage);
if (Number.isFinite(headCoverage) && Number.isFinite(baseCoverage)) {
compareTotals = {
base_coverage: baseCoverage,
coverage_change: headCoverage - baseCoverage,
};
}
}
data = {
report: {
totals: headTotals,
compare: compareTotals ? { totals: compareTotals } : undefined,
totals_by_flag: [],
},
head_totals: headTotals,
base_totals: baseTotals,
};
} else {
data = prData;
}
}
}
}
if (!data) {
core.warning('Unable to retrieve Codecov report after multiple attempts.');
core.setOutput('shouldComment', 'false');
return;
}
const toNumber = (value) => {
if (value === null || value === undefined || value === '') {
return undefined;
}
const num = Number(value);
return Number.isFinite(num) ? num : undefined;
};
const reportData = data.report || data;
const totals = reportData.totals ?? data.head_totals ?? data.totals;
if (!totals) {
core.warning('Codecov response does not contain coverage totals.');
core.setOutput('shouldComment', 'false');
return;
}
let compareTotals = reportData.compare?.totals ?? data.compare?.totals;
if (!compareTotals && data.base_totals) {
const baseCoverageValue = toNumber(data.base_totals.coverage);
if (baseCoverageValue !== undefined) {
const headCoverageValue = toNumber((data.head_totals ?? {}).coverage);
compareTotals = {
base_coverage: baseCoverageValue,
coverage_change:
headCoverageValue !== undefined ? headCoverageValue - baseCoverageValue : undefined,
};
}
}
const coverage = toNumber(totals.coverage);
const baseCoverage = toNumber(compareTotals?.base_coverage ?? compareTotals?.base);
let delta = toNumber(
compareTotals?.coverage_change ??
compareTotals?.coverage_diff ??
totals.delta ??
totals.diff ??
totals.change,
);
if (delta === undefined && coverage !== undefined && baseCoverage !== undefined) {
delta = coverage - baseCoverage;
}
const formatPercent = (value) => {
if (value === undefined) return '—';
return `${value.toFixed(2)}%`;
};
const formatDelta = (value) => {
if (value === undefined) return '—';
const sign = value >= 0 ? '+' : '';
return `${sign}${value.toFixed(2)}%`;
};
const shortSha = commitSha.slice(0, 7);
const reportBaseUrl = `https://app.codecov.io/gh/${owner}/${repo}`;
const commitReportUrl = `${reportBaseUrl}/commit/${commitSha}?src=pr&el=comment`;
const prReportUrl = prNumber
? `${reportBaseUrl}/pull/${prNumber}?src=pr&el=comment`
: commitReportUrl;
const findBaseCommitSha = () =>
data?.report?.compare?.base_commitid ??
data?.report?.compare?.base?.commitid ??
data?.report?.base_commitid ??
data?.compare?.base_commitid ??
data?.compare?.base?.commitid ??
data?.base_commitid ??
data?.base?.commitid;
const baseCommitSha = findBaseCommitSha();
const baseCommitUrl = baseCommitSha
? `${reportBaseUrl}/commit/${baseCommitSha}?src=pr&el=comment`
: undefined;
const baseShortSha = baseCommitSha ? baseCommitSha.slice(0, 7) : undefined;
const lines = ['<!-- codecov-coverage-comment -->'];
lines.push(`## [Codecov](${prReportUrl}) Report`);
lines.push('');
if (coverage !== undefined) {
lines.push(`:white_check_mark: Project coverage for \`${shortSha}\` is ${formatPercent(coverage)}.`);
} else {
lines.push(':warning: Coverage for the head commit is unavailable.');
}
if (baseCoverage !== undefined) {
const changeEmoji = delta === undefined ? ':grey_question:' : delta >= 0 ? ':white_check_mark:' : ':small_red_triangle_down:';
const baseCoverageText = `Base${baseShortSha ? ` \`${baseShortSha}\`` : ''} ${formatPercent(baseCoverage)}`;
const baseLink = baseCommitUrl ? `[${baseCoverageText}](${baseCommitUrl})` : baseCoverageText;
const changeText =
delta !== undefined
? `${baseLink} (${formatDelta(delta)})`
: `${baseLink} (change unknown)`;
lines.push(`${changeEmoji} ${changeText}.`);
}
lines.push(`:clipboard: [View full report on Codecov](${commitReportUrl}).`);
const normalizeTotals = (value) => {
if (!value) return undefined;
if (value.totals && typeof value.totals === 'object') return value.totals;
return value;
};
const headTotals = normalizeTotals(totals) ?? {};
const baseTotals =
normalizeTotals(data.base_totals) ??
normalizeTotals(reportData.base_totals) ??
normalizeTotals(reportData.compare?.base_totals) ??
normalizeTotals(reportData.compare?.base);
const formatInteger = (value) => {
if (value === undefined) return '—';
return value.toLocaleString('en-US');
};
const formatIntegerDelta = (value) => {
if (value === undefined) return '—';
const sign = value >= 0 ? '+' : '';
return `${sign}${value.toLocaleString('en-US')}`;
};
const getInteger = (value) => {
const num = toNumber(value);
return Number.isFinite(num) ? Math.round(num) : undefined;
};
const metrics = [];
metrics.push({
label: 'Coverage',
base: baseCoverage,
head: coverage,
diff: delta,
format: formatPercent,
formatDiff: formatDelta,
});
const pushIntegerMetric = (label, headValueRaw, baseValueRaw) => {
const headValue = getInteger(headValueRaw);
const baseValue = getInteger(baseValueRaw);
if (headValue === undefined && baseValue === undefined) {
return;
}
const diff = headValue !== undefined && baseValue !== undefined ? headValue - baseValue : undefined;
metrics.push({
label,
base: baseValue,
head: headValue,
diff,
format: formatInteger,
formatDiff: formatIntegerDelta,
});
};
pushIntegerMetric('Files', headTotals.files, baseTotals?.files);
pushIntegerMetric('Lines', headTotals.lines, baseTotals?.lines);
pushIntegerMetric('Branches', headTotals.branches, baseTotals?.branches);
pushIntegerMetric('Hits', headTotals.hits, baseTotals?.hits);
pushIntegerMetric('Misses', headTotals.misses, baseTotals?.misses);
const hasMetricData = metrics.some((metric) => metric.base !== undefined || metric.head !== undefined);
if (hasMetricData) {
lines.push('');
lines.push('<details><summary>Coverage summary</summary>');
lines.push('');
lines.push('| Metric | Base | Head | Δ |');
lines.push('| --- | --- | --- | --- |');
for (const metric of metrics) {
const baseValue = metric.base !== undefined ? metric.format(metric.base) : '—';
const headValue = metric.head !== undefined ? metric.format(metric.head) : '—';
const diffValue = metric.diff !== undefined ? metric.formatDiff(metric.diff) : '—';
lines.push(`| ${metric.label} | ${baseValue} | ${headValue} | ${diffValue} |`);
}
lines.push('');
lines.push('</details>');
}
const normalizeEntries = (raw) => {
if (!raw) return [];
if (Array.isArray(raw)) return raw;
if (typeof raw === 'object') {
return Object.entries(raw).map(([name, totals]) => ({ name, ...(typeof totals === 'object' ? totals : { coverage: totals }) }));
}
return [];
};
const buildTableRows = (entries) => {
const rows = [];
for (const entry of entries) {
const label = entry.flag ?? entry.name ?? entry.component ?? entry.id;
const entryTotals = entry.totals ?? entry;
const entryCoverage = toNumber(entryTotals?.coverage);
if (!label || entryCoverage === undefined) {
continue;
}
const entryDelta = toNumber(
entryTotals?.coverage_change ??
entryTotals?.coverage_diff ??
entryTotals?.delta ??
entryTotals?.diff ??
entryTotals?.change,
);
const coverageText = entryCoverage !== undefined ? `\`${formatPercent(entryCoverage)}\`` : '—';
const deltaText = entryDelta !== undefined ? `\`${formatDelta(entryDelta)}\`` : '—';
rows.push(`| ${label} | ${coverageText} | ${deltaText} |`);
}
return rows;
};
const componentEntries = normalizeEntries(reportData.components ?? data.components);
const flagEntries = normalizeEntries(reportData.totals_by_flag ?? data.totals_by_flag);
if (componentEntries.length) {
const componentsLink = prNumber
? `${reportBaseUrl}/pull/${prNumber}/components?src=pr&el=components`
: `${commitReportUrl}`;
const componentRows = buildTableRows(componentEntries);
if (componentRows.length) {
lines.push('');
lines.push(`[Components report](${componentsLink})`);
lines.push('');
lines.push('| Component | Coverage | Δ |');
lines.push('| --- | --- | --- |');
lines.push(...componentRows);
}
}
if (flagEntries.length) {
const flagsLink = prNumber
? `${reportBaseUrl}/pull/${prNumber}/flags?src=pr&el=flags`
: `${commitReportUrl}`;
const flagRows = buildTableRows(flagEntries);
if (flagRows.length) {
lines.push('');
lines.push(`[Flags report](${flagsLink})`);
lines.push('');
lines.push('| Flag | Coverage | Δ |');
lines.push('| --- | --- | --- |');
lines.push(...flagRows);
}
}
const commentBody = lines.join('\n');
const shouldComment = coverage !== undefined;
core.setOutput('shouldComment', shouldComment ? 'true' : 'false');
if (shouldComment) {
core.setOutput('commentBody', commentBody);
}
- name: Upsert coverage comment
if: steps.pr.outputs.shouldRun == 'true' && steps.coverage.outputs.shouldComment == 'true'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr.outputs.prNumber }}
COMMENT_BODY: ${{ steps.coverage.outputs.commentBody }}
with:
script: |
const prNumber = Number(process.env.PR_NUMBER);
const body = process.env.COMMENT_BODY;
const marker = '<!-- codecov-coverage-comment -->';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find((comment) => comment.body?.includes(marker));
if (existing) {
core.info(`Updating existing coverage comment (id: ${existing.id}).`);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
core.info('Creating new coverage comment.');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
frontend-bundle-analysis: frontend-bundle-analysis:
name: "Frontend Bundle Analysis" name: "Frontend Bundle Analysis"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04

View File

@@ -49,7 +49,7 @@ repos:
- 'prettier-plugin-organize-imports@4.1.0' - 'prettier-plugin-organize-imports@4.1.0'
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.0 rev: v0.13.2
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
@@ -59,7 +59,7 @@ repos:
- id: pyproject-fmt - id: pyproject-fmt
# Dockerfile hooks # Dockerfile hooks
- repo: https://github.com/AleksaC/hadolint-py - repo: https://github.com/AleksaC/hadolint-py
rev: v2.12.1b3 rev: v2.14.0
hooks: hooks:
- id: hadolint - id: hadolint
# Shell script hooks # Shell script hooks

View File

@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.8.17-python3.12-bookworm-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.8.22-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6

View File

@@ -32,7 +32,7 @@ services:
volumes: volumes:
- redisdata:/data - redisdata:/data
db: db:
image: docker.io/library/postgres:17 image: docker.io/library/postgres:18
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data

View File

@@ -35,7 +35,7 @@ services:
volumes: volumes:
- redisdata:/data - redisdata:/data
db: db:
image: docker.io/library/postgres:17 image: docker.io/library/postgres:18
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data

View File

@@ -31,7 +31,7 @@ services:
volumes: volumes:
- redisdata:/data - redisdata:/data
db: db:
image: docker.io/library/postgres:17 image: docker.io/library/postgres:18
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data

View File

@@ -637,7 +637,7 @@ When you first delete a document it is moved to the 'trash' until either it is e
You can set how long documents remain in the trash before being automatically deleted with [`PAPERLESS_EMPTY_TRASH_DELAY`](configuration.md#PAPERLESS_EMPTY_TRASH_DELAY), which defaults You can set how long documents remain in the trash before being automatically deleted with [`PAPERLESS_EMPTY_TRASH_DELAY`](configuration.md#PAPERLESS_EMPTY_TRASH_DELAY), which defaults
to 30 days. Until the file is actually deleted (e.g. the trash is emptied), all files and database content remains intact and can be restored at any point up until that time. to 30 days. Until the file is actually deleted (e.g. the trash is emptied), all files and database content remains intact and can be restored at any point up until that time.
Additionally you may configure a directory where deleted files are moved to when they the trash is emptied with [`PAPERLESS_EMPTY_TRASH_DIR`](configuration.md#PAPERLESS_EMPTY_TRASH_DIR). Additionally you may configure a directory where deleted files are moved to when the trash is emptied with [`PAPERLESS_EMPTY_TRASH_DIR`](configuration.md#PAPERLESS_EMPTY_TRASH_DIR).
Note that the empty trash directory only stores the original file, the archive file and all database information is permanently removed once a document is fully deleted. Note that the empty trash directory only stores the original file, the archive file and all database information is permanently removed once a document is fully deleted.
## Best practices {#basic-searching} ## Best practices {#basic-searching}

View File

@@ -30,10 +30,10 @@ dependencies = [
"django-cachalot~=2.8.0", "django-cachalot~=2.8.0",
"django-celery-results~=2.6.0", "django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0", "django-compression-middleware~=0.5.0",
"django-cors-headers~=4.8.0", "django-cors-headers~=4.9.0",
"django-extensions~=4.1", "django-extensions~=4.1",
"django-filter~=25.1", "django-filter~=25.1",
"django-guardian~=3.1.2", "django-guardian~=3.2.0",
"django-multiselectfield~=1.0.1", "django-multiselectfield~=1.0.1",
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
"django-treenode>=0.23.2", "django-treenode>=0.23.2",
@@ -54,7 +54,6 @@ dependencies = [
"ocrmypdf~=16.11.0", "ocrmypdf~=16.11.0",
"pathvalidate~=3.3.1", "pathvalidate~=3.3.1",
"pdf2image~=1.17.0", "pdf2image~=1.17.0",
"psycopg-pool",
"python-dateutil~=2.9.0", "python-dateutil~=2.9.0",
"python-dotenv~=1.1.0", "python-dotenv~=1.1.0",
"python-gnupg~=0.5.4", "python-gnupg~=0.5.4",

View File

@@ -174,7 +174,7 @@ test('bulk edit', async ({ page }) => {
await expect(page.locator('pngx-document-list')).toHaveText( await expect(page.locator('pngx-document-list')).toHaveText(
/Selected 61 of 61 documents/i /Selected 61 of 61 documents/i
) )
await page.getByRole('button', { name: 'Cancel' }).click() await page.getByRole('button', { name: 'None' }).click()
await page.locator('pngx-document-card-small').nth(1).click() await page.locator('pngx-document-card-small').nth(1).click()
await page.locator('pngx-document-card-small').nth(2).click() await page.locator('pngx-document-card-small').nth(2).click()

View File

@@ -324,7 +324,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">192</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -743,7 +743,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">134</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -1167,7 +1167,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">217</context> <context context-type="linenumber">242</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -1209,7 +1209,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">78</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1494,10 +1494,6 @@
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">182</context> <context context-type="linenumber">182</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
<context context-type="linenumber">81</context> <context context-type="linenumber">81</context>
@@ -1604,6 +1600,10 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">8</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">153</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">4</context>
@@ -1755,7 +1755,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">244</context> <context context-type="linenumber">269</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -1808,7 +1808,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">103</context> <context context-type="linenumber">87</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -2109,7 +2109,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">157</context> <context context-type="linenumber">140</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@@ -2769,11 +2769,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">5</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">199</context> <context context-type="linenumber">224</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -3001,7 +3001,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">129</context> <context context-type="linenumber">112</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
@@ -3448,8 +3448,8 @@
<context context-type="linenumber">27</context> <context context-type="linenumber">27</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1496549861742963591" datatype="html"> <trans-unit id="1496549861742963591" datatype="html">
@@ -3529,7 +3529,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">278</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -6356,7 +6356,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">298</context> <context context-type="linenumber">323</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="78870852467682010" datatype="html"> <trans-unit id="78870852467682010" datatype="html">
@@ -6371,7 +6371,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">338</context> <context context-type="linenumber">363</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="157572966557284263" datatype="html"> <trans-unit id="157572966557284263" datatype="html">
@@ -6386,7 +6386,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">345</context> <context context-type="linenumber">370</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="883965278435032344" datatype="html"> <trans-unit id="883965278435032344" datatype="html">
@@ -6404,7 +6404,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">366</context> <context context-type="linenumber">391</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3542042671420335679" datatype="html"> <trans-unit id="3542042671420335679" datatype="html">
@@ -6415,7 +6415,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">366</context> <context context-type="linenumber">391</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="872092479747931526" datatype="html"> <trans-unit id="872092479747931526" datatype="html">
@@ -6585,8 +6585,8 @@
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">27</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2266163016683537825" datatype="html"> <trans-unit id="2266163016683537825" datatype="html">
@@ -6625,7 +6625,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">107</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7049887240439736400" datatype="html"> <trans-unit id="7049887240439736400" datatype="html">
@@ -6686,7 +6686,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">196</context> <context context-type="linenumber">221</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@@ -6723,11 +6723,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">35</context> <context context-type="linenumber">19</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">186</context> <context context-type="linenumber">211</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -6750,11 +6750,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">33</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">226</context> <context context-type="linenumber">251</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -6777,11 +6777,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">47</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">235</context> <context context-type="linenumber">260</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7188,25 +7188,18 @@
<context context-type="linenumber">10</context> <context context-type="linenumber">10</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6857598786757174736" datatype="html">
<source>Select:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="6299008920007331381" datatype="html"> <trans-unit id="6299008920007331381" datatype="html">
<source>Edit:</source> <source>Edit:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">3</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7001227209911602786" datatype="html"> <trans-unit id="7001227209911602786" datatype="html">
<source>Filter tags</source> <source>Filter tags</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">22</context> <context context-type="linenumber">6</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7217,7 +7210,7 @@
<source>Filter correspondents</source> <source>Filter correspondents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">36</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7228,7 +7221,7 @@
<source>Filter document types</source> <source>Filter document types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">34</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7239,7 +7232,7 @@
<source>Filter storage paths</source> <source>Filter storage paths</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">64</context> <context context-type="linenumber">48</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7250,7 +7243,7 @@
<source>Custom fields</source> <source>Custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">77</context> <context context-type="linenumber">61</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7265,56 +7258,56 @@
<source>Filter custom fields</source> <source>Filter custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">78</context> <context context-type="linenumber">62</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5139192806922838657" datatype="html"> <trans-unit id="5139192806922838657" datatype="html">
<source>Set values</source> <source>Set values</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">86</context> <context context-type="linenumber">70</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1050269006235116171" datatype="html"> <trans-unit id="1050269006235116171" datatype="html">
<source>Rotate</source> <source>Rotate</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">110</context> <context context-type="linenumber">94</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3206542606001340679" datatype="html"> <trans-unit id="3206542606001340679" datatype="html">
<source>Merge</source> <source>Merge</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">113</context> <context context-type="linenumber">97</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1015374532025907183" datatype="html"> <trans-unit id="1015374532025907183" datatype="html">
<source>Include:</source> <source>Include:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">135</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1537670659786159738" datatype="html"> <trans-unit id="1537670659786159738" datatype="html">
<source>Archived files</source> <source>Archived files</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">139</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2520291319362448498" datatype="html"> <trans-unit id="2520291319362448498" datatype="html">
<source>Original files</source> <source>Original files</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">143</context> <context context-type="linenumber">126</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8009862506882713059" datatype="html"> <trans-unit id="8009862506882713059" datatype="html">
<source>Use formatted filename</source> <source>Use formatted filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">131</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1215215387232313677" datatype="html"> <trans-unit id="1215215387232313677" datatype="html">
@@ -7614,7 +7607,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">314</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="106713086593101376" datatype="html"> <trans-unit id="106713086593101376" datatype="html">
@@ -7738,7 +7731,7 @@
<source>Select</source> <source>Select</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">5</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
@@ -7749,36 +7742,51 @@
<source>Select none</source> <source>Select none</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">11</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1512866475468373520" datatype="html"> <trans-unit id="1512866475468373520" datatype="html">
<source>Select page</source> <source>Select page</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">12</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">313</context> <context context-type="linenumber">315</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1494518490116523821" datatype="html"> <trans-unit id="1494518490116523821" datatype="html">
<source>Select all</source> <source>Select all</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">306</context> <context context-type="linenumber">308</context>
</context-group>
</trans-unit>
<trans-unit id="6252070156626006029" datatype="html">
<source>None</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">120</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
<context context-type="linenumber">45</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8461842260159597706" datatype="html"> <trans-unit id="8461842260159597706" datatype="html">
<source>Show</source> <source>Show</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">37</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
@@ -7789,63 +7797,63 @@
<source>Sort</source> <source>Sort</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">68</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2123659921722214537" datatype="html"> <trans-unit id="2123659921722214537" datatype="html">
<source>Views</source> <source>Views</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">74</context> <context context-type="linenumber">94</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1233494216161906927" datatype="html"> <trans-unit id="1233494216161906927" datatype="html">
<source>Save &quot;<x id="INTERPOLATION" equiv-text="{{list.activeSavedViewTitle}}"/>&quot;</source> <source>Save &quot;<x id="INTERPOLATION" equiv-text="{{list.activeSavedViewTitle}}"/>&quot;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">93</context> <context context-type="linenumber">113</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2276119452079372898" datatype="html"> <trans-unit id="2276119452079372898" datatype="html">
<source>Save as...</source> <source>Save as...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">96</context> <context context-type="linenumber">116</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1450797155766668235" datatype="html"> <trans-unit id="1450797155766668235" datatype="html">
<source>All saved views</source> <source>All saved views</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">117</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8786996283897742947" datatype="html"> <trans-unit id="8786996283897742947" datatype="html">
<source>{VAR_PLURAL, plural, =1 {Selected <x id="INTERPOLATION"/> of one document} other {Selected <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> documents}}</source> <source>{VAR_PLURAL, plural, =1 {Selected <x id="INTERPOLATION"/> of one document} other {Selected <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> documents}}</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">117</context> <context context-type="linenumber">137</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6600548268163632449" datatype="html"> <trans-unit id="6600548268163632449" datatype="html">
<source>{VAR_PLURAL, plural, =1 {One document} other {<x id="INTERPOLATION"/> documents}}</source> <source>{VAR_PLURAL, plural, =1 {One document} other {<x id="INTERPOLATION"/> documents}}</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">121</context> <context context-type="linenumber">141</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2243770355958919528" datatype="html"> <trans-unit id="2243770355958919528" datatype="html">
<source>(filtered)</source> <source>(filtered)</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">123</context> <context context-type="linenumber">143</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6849725902312323996" datatype="html"> <trans-unit id="6849725902312323996" datatype="html">
<source>Reset filters</source> <source>Reset filters</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">128</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -7856,21 +7864,21 @@
<source>Error while loading documents</source> <source>Error while loading documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">144</context> <context context-type="linenumber">169</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="494022736054110363" datatype="html"> <trans-unit id="494022736054110363" datatype="html">
<source>Sort by ASN</source> <source>Sort by ASN</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">173</context> <context context-type="linenumber">198</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7517688192215738656" datatype="html"> <trans-unit id="7517688192215738656" datatype="html">
<source>ASN</source> <source>ASN</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">177</context> <context context-type="linenumber">202</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@@ -7889,28 +7897,28 @@
<source>Sort by correspondent</source> <source>Sort by correspondent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">182</context> <context context-type="linenumber">207</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2066713941761361709" datatype="html"> <trans-unit id="2066713941761361709" datatype="html">
<source>Sort by title</source> <source>Sort by title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">191</context> <context context-type="linenumber">216</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6232673011753681091" datatype="html"> <trans-unit id="6232673011753681091" datatype="html">
<source>Sort by owner</source> <source>Sort by owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">204</context> <context context-type="linenumber">229</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3715596725146409911" datatype="html"> <trans-unit id="3715596725146409911" datatype="html">
<source>Owner</source> <source>Owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">208</context> <context context-type="linenumber">233</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -7925,49 +7933,49 @@
<source>Sort by notes</source> <source>Sort by notes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">213</context> <context context-type="linenumber">238</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5499001829734502606" datatype="html"> <trans-unit id="5499001829734502606" datatype="html">
<source>Sort by document type</source> <source>Sort by document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">222</context> <context context-type="linenumber">247</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6213829731736042759" datatype="html"> <trans-unit id="6213829731736042759" datatype="html">
<source>Sort by storage path</source> <source>Sort by storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">231</context> <context context-type="linenumber">256</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3406167410329973166" datatype="html"> <trans-unit id="3406167410329973166" datatype="html">
<source>Sort by created date</source> <source>Sort by created date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">240</context> <context context-type="linenumber">265</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3769035778779263084" datatype="html"> <trans-unit id="3769035778779263084" datatype="html">
<source>Sort by added date</source> <source>Sort by added date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">249</context> <context context-type="linenumber">274</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4874754501044009042" datatype="html"> <trans-unit id="4874754501044009042" datatype="html">
<source>Sort by number of pages</source> <source>Sort by number of pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">258</context> <context context-type="linenumber">283</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3817498941817715969" datatype="html"> <trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source> <source>Pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">262</context> <context context-type="linenumber">287</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -7986,77 +7994,77 @@
<source> Shared </source> <source> Shared </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">265,267</context> <context context-type="linenumber">290,292</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5083658411133224968" datatype="html"> <trans-unit id="5083658411133224968" datatype="html">
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source> <source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">272,273</context> <context context-type="linenumber">297,298</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2179847500064178686" datatype="html"> <trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source> <source>Edit document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">306</context> <context context-type="linenumber">331</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3420321797707163677" datatype="html"> <trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source> <source>Preview document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">307</context> <context context-type="linenumber">332</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4512084577073831437" datatype="html"> <trans-unit id="4512084577073831437" datatype="html">
<source>Reset filters / selection</source> <source>Reset filters / selection</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">294</context> <context context-type="linenumber">296</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4135055128446167640" datatype="html"> <trans-unit id="4135055128446167640" datatype="html">
<source>Open first [selected] document</source> <source>Open first [selected] document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">322</context> <context context-type="linenumber">324</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3629960544875360046" datatype="html"> <trans-unit id="3629960544875360046" datatype="html">
<source>Previous page</source> <source>Previous page</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">338</context> <context context-type="linenumber">340</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3337301694210287595" datatype="html"> <trans-unit id="3337301694210287595" datatype="html">
<source>Next page</source> <source>Next page</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">350</context> <context context-type="linenumber">352</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2155249406916744630" datatype="html"> <trans-unit id="2155249406916744630" datatype="html">
<source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source> <source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">383</context> <context context-type="linenumber">385</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4646273665293421938" datatype="html"> <trans-unit id="4646273665293421938" datatype="html">
<source>Failed to save view &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot;.</source> <source>Failed to save view &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot;.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">389</context> <context context-type="linenumber">391</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6837554170707123455" datatype="html"> <trans-unit id="6837554170707123455" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source> <source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">435</context> <context context-type="linenumber">437</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="739880801667335279" datatype="html"> <trans-unit id="739880801667335279" datatype="html">
@@ -8861,17 +8869,6 @@
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6252070156626006029" datatype="html">
<source>None</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">120</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
<context context-type="linenumber">45</context>
</context-group>
</trans-unit>
<trans-unit id="211408744872436427" datatype="html"> <trans-unit id="211408744872436427" datatype="html">
<source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source> <source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location"> <context-group purpose="location">

View File

@@ -1,161 +1,144 @@
<div class="d-flex flex-wrap gap-4"> <div class="d-flex flex-wrap gap-4">
<div class="d-flex align-items-center" role="group" aria-label="Select"> <div class="d-flex flex-wrap align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()"> <label class="me-2" i18n>Edit:</label>
<i-bs name="slash-circle"></i-bs>&nbsp;<ng-container i18n>Cancel</ng-container> @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createTag.bind(this)"
(opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel"
[documentCounts]="tagDocumentCounts"
(apply)="setTags($event)"
shortcutKey="t">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCorrespondent.bind(this)"
(opened)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel"
[documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)"
shortcutKey="y">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createDocumentType.bind(this)"
(opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)"
shortcutKey="u">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createStoragePath.bind(this)"
(opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)"
shortcutKey="i">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCustomField.bind(this)"
(opened)="openCustomFieldsDropdown()"
[(selectionModel)]="customFieldsSelectionModel"
[documentCounts]="customFieldDocumentCounts"
extraButtonTitle="Set values"
i18n-extraButtonTitle
(extraButton)="setCustomFieldValues($event)"
(apply)="setCustomFields($event)">
</pngx-filterable-dropdown>
}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button> </button>
</div> </div>
<div class="d-flex align-items-center gap-2" role="group" aria-label="Select"> </div>
<label class="me-2" i18n>Select:</label> <div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-group"> <div class="btn-toolbar">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()"> <div ngbDropdown>
<i-bs name="file-earmark-check"></i-bs>&nbsp;<ng-container i18n>Page</ng-container> <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" [disabled]="!userCanEdit && !userCanAdd" ngbDropdownToggle>
<i-bs name="three-dots"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="reprocessSelected()" [disabled]="!userCanEditAll && !userCanEditAll">
<i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Reprocess</ng-container>
</button>
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll && !userCanEditAll">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanAdd || list.selected.size < 2">
<i-bs name="journals"></i-bs>&nbsp;<ng-container i18n>Merge</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
<i-bs name="check-all"></i-bs>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
</div> </div>
<div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> </div>
<label class="me-2" i18n>Edit:</label> </div>
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) { <div class="btn-group btn-group-sm">
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title <button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
filterPlaceholder="Filter tags" i18n-filterPlaceholder @if (!awaitingDownload) {
[disabled]="!userCanEditAll || disabled" <i-bs name="arrow-down"></i-bs>
[editing]="true" }
[applyOnClose]="applyOnClose" @if (awaitingDownload) {
[createRef]="createTag.bind(this)" <div class="spinner-border spinner-border-sm" role="status">
(opened)="openTagsDropdown()" <span class="visually-hidden">Preparing download...</span>
[(selectionModel)]="tagSelectionModel" </div>
[documentCounts]="tagDocumentCounts" }
(apply)="setTags($event)" <div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
shortcutKey="t"> </button>
</pngx-filterable-dropdown> <div ngbDropdown class="me-2 d-flex btn-group" role="group">
} <button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title <form [formGroup]="downloadForm" class="px-3 py-1">
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder <p class="mb-1" i18n>Include:</p>
[disabled]="!userCanEditAll || disabled" <div class="form-group ps-3 mb-2">
[editing]="true" <div class="form-check">
[applyOnClose]="applyOnClose" <input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
[createRef]="createCorrespondent.bind(this)" <label class="form-check-label" for="downloadFileType_archive" i18n>Archived files</label>
(opened)="openCorrespondentDropdown()" </div>
[(selectionModel)]="correspondentSelectionModel" <div class="form-check">
[documentCounts]="correspondentDocumentCounts" <input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
(apply)="setCorrespondents($event)" <label class="form-check-label" for="downloadFileType_originals" i18n>Original files</label>
shortcutKey="y"> </div>
</pngx-filterable-dropdown> </div>
} <div class="form-check">
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { <input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title <label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
filterPlaceholder="Filter document types" i18n-filterPlaceholder </div>
[disabled]="!userCanEditAll || disabled" </form>
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createDocumentType.bind(this)"
(opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)"
shortcutKey="u">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[disabled]="!userCanEditAll || disabled"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createStoragePath.bind(this)"
(opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)"
shortcutKey="i">
</pngx-filterable-dropdown>
}
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
[disabled]="!userCanEditAll"
[editing]="true"
[applyOnClose]="applyOnClose"
[createRef]="createCustomField.bind(this)"
(opened)="openCustomFieldsDropdown()"
[(selectionModel)]="customFieldsSelectionModel"
[documentCounts]="customFieldDocumentCounts"
extraButtonTitle="Set values"
i18n-extraButtonTitle
(extraButton)="setCustomFieldValues($event)"
(apply)="setCustomFields($event)">
</pngx-filterable-dropdown>
}
</div> </div>
<div class="d-flex align-items-center gap-2 ms-auto"> </div>
<div class="btn-toolbar"> </div>
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll"> <div class="btn-group btn-group-sm">
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div> <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
</button> <i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
<div ngbDropdown> </div>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" [disabled]="!userCanEdit && !userCanAdd" ngbDropdownToggle> </div>
<i-bs name="three-dots"></i-bs> </div>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="reprocessSelected()" [disabled]="!userCanEditAll && !userCanEditAll">
<i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Reprocess</ng-container>
</button>
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll && !userCanEditAll">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanAdd || list.selected.size < 2">
<i-bs name="journals"></i-bs>&nbsp;<ng-container i18n>Merge</ng-container>
</button>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
@if (!awaitingDownload) {
<i-bs name="arrow-down"></i-bs>
}
@if (awaitingDownload) {
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span>
</div>
}
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Download</ng-container></div>
</button>
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<form [formGroup]="downloadForm" class="px-3 py-1">
<p class="mb-1" i18n>Include:</p>
<div class="form-group ps-3 mb-2">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
<label class="form-check-label" for="downloadFileType_archive" i18n>Archived files</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
<label class="form-check-label" for="downloadFileType_originals" i18n>Original files</label>
</div>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
<label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
</div>
</form>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>

View File

@@ -5,3 +5,7 @@
.dropdown-menu{ .dropdown-menu{
--bs-dropdown-min-width: 12rem; --bs-dropdown-min-width: 12rem;
} }
.btn-group .btn {
white-space: nowrap;
}

View File

@@ -1,16 +1,36 @@
<pngx-page-header [title]="getTitle()"> <pngx-page-header [title]="getTitle()">
<div ngbDropdown class="btn-group flex-fill d-sm-none">
<div ngbDropdown class="btn-group flex-fill"> <button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<i-bs name="text-indent-left"></i-bs> <i-bs name="text-indent-left"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Select</ng-container></div> <div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Select</ng-container></div>
@if (list.selected.size > 0) {
<pngx-clearable-badge [selected]="list.selected.size > 0" [number]="list.selected.size" (cleared)="list.selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
}
</button> </button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> <div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
<button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button> <button ngbDropdownItem (click)="list.selectNone()" i18n>Select none</button>
<button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button> <button ngbDropdownItem (click)="list.selectPage()" i18n>Select page</button>
<button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button> <button ngbDropdownItem (click)="list.selectAll()" i18n>Select all</button>
</div> </div>
</div> </div>
<div class="d-none d-sm-flex flex-fill me-3">
<div class="input-group input-group-sm">
<span class="input-group-text border-0">Select:</span>
</div>
<div class="btn-group btn-group-sm flex-nowrap">
@if (list.selected.size > 0) {
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
<i-bs name="slash-circle"></i-bs>&nbsp;<ng-container i18n>None</ng-container>
</button>
}
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<i-bs name="file-earmark-check"></i-bs>&nbsp;<ng-container i18n>Page</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
<i-bs name="check-all"></i-bs>&nbsp;<ng-container i18n>All</ng-container>
</button>
</div>
</div>
<div ngbDropdown class="btn-group flex-fill"> <div ngbDropdown class="btn-group flex-fill">
<button class="btn btn-sm btn-outline-primary" id="dropdownDisplayFields" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary" id="dropdownDisplayFields" ngbDropdownToggle>
<i-bs name="card-heading"></i-bs> <i-bs name="card-heading"></i-bs>
@@ -126,8 +146,13 @@
@if (!list.isReloading && isFiltered) { @if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()"> <button class="btn btn-link py-0" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small> <i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button> </button>
} }
@if (!list.isReloading && list.selected.size > 0) {
<button class="btn btn-link py-0" (click)="list.selectNone()">
<i-bs width="1em" height="1em" name="slash-circle" class="me-1"></i-bs><small i18n>Clear selection</small>
</button>
}
</div> </div>
@if (list.collectionSize) { @if (list.collectionSize) {
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" <ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"

View File

@@ -56,6 +56,7 @@ import {
filterRulesDiffer, filterRulesDiffer,
isFullTextFilterRule, isFullTextFilterRule,
} from 'src/app/utils/filter-rules' } from 'src/app/utils/filter-rules'
import { ClearableBadgeComponent } from '../common/clearable-badge/clearable-badge.component'
import { CustomFieldDisplayComponent } from '../common/custom-field-display/custom-field-display.component' import { CustomFieldDisplayComponent } from '../common/custom-field-display/custom-field-display.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component' import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { PreviewPopupComponent } from '../common/preview-popup/preview-popup.component' import { PreviewPopupComponent } from '../common/preview-popup/preview-popup.component'
@@ -72,6 +73,7 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
templateUrl: './document-list.component.html', templateUrl: './document-list.component.html',
styleUrls: ['./document-list.component.scss'], styleUrls: ['./document-list.component.scss'],
imports: [ imports: [
ClearableBadgeComponent,
CustomFieldDisplayComponent, CustomFieldDisplayComponent,
PageHeaderComponent, PageHeaderComponent,
BulkEditorComponent, BulkEditorComponent,

View File

@@ -6,6 +6,7 @@ import re
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Literal
import magic import magic
from celery import states from celery import states
@@ -252,6 +253,35 @@ class OwnedObjectSerializer(
except KeyError: except KeyError:
pass pass
def _get_perms(self, obj, codename: str, target: Literal["users", "groups"]):
"""
Get the given permissions from context or from django-guardian.
:param codename: The permission codename, e.g. 'view' or 'change'
:param target: 'users' or 'groups'
"""
key = f"{target}_{codename}_perms"
cached = self.context.get(key, {}).get(obj.pk)
if cached is not None:
return list(cached)
# Permission not found in the context, get it from guardian
if target == "users":
return list(
get_users_with_perms(
obj,
only_with_perms_in=[f"{codename}_{obj.__class__.__name__.lower()}"],
with_group_users=False,
).values_list("id", flat=True),
)
else: # groups
return list(
get_groups_with_only_permission(
obj,
codename=f"{codename}_{obj.__class__.__name__.lower()}",
).values_list("id", flat=True),
)
@extend_schema_field( @extend_schema_field(
field={ field={
"type": "object", "type": "object",
@@ -286,31 +316,14 @@ class OwnedObjectSerializer(
}, },
) )
def get_permissions(self, obj) -> dict: def get_permissions(self, obj) -> dict:
view_codename = f"view_{obj.__class__.__name__.lower()}"
change_codename = f"change_{obj.__class__.__name__.lower()}"
return { return {
"view": { "view": {
"users": get_users_with_perms( "users": self._get_perms(obj, "view", "users"),
obj, "groups": self._get_perms(obj, "view", "groups"),
only_with_perms_in=[view_codename],
with_group_users=False,
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=view_codename,
).values_list("id", flat=True),
}, },
"change": { "change": {
"users": get_users_with_perms( "users": self._get_perms(obj, "change", "users"),
obj, "groups": self._get_perms(obj, "change", "groups"),
only_with_perms_in=[change_codename],
with_group_users=False,
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=change_codename,
).values_list("id", flat=True),
}, },
} }

View File

@@ -1,17 +1,23 @@
import json
import tempfile import tempfile
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import connection
from django.test import TestCase from django.test import TestCase
from django.test import override_settings from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.utils import timezone from django.utils import timezone
from guardian.shortcuts import assign_perm
from rest_framework import status from rest_framework import status
from documents.models import Document from documents.models import Document
from documents.models import ShareLink from documents.models import ShareLink
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
from paperless.models import ApplicationConfiguration from paperless.models import ApplicationConfiguration
@@ -154,3 +160,113 @@ class TestViews(DirectoriesMixin, TestCase):
response.render() response.render()
self.assertEqual(response.request["PATH_INFO"], "/accounts/login/") self.assertEqual(response.request["PATH_INFO"], "/accounts/login/")
self.assertContains(response, b"Share link has expired") self.assertContains(response, b"Share link has expired")
def test_list_with_full_permissions(self):
"""
GIVEN:
- Tags with different permissions
WHEN:
- Request to get tag list with full permissions is made
THEN:
- Tag list is returned with the right permission information
"""
user2 = User.objects.create(username="user2")
user3 = User.objects.create(username="user3")
group1 = Group.objects.create(name="group1")
group2 = Group.objects.create(name="group2")
group3 = Group.objects.create(name="group3")
t1 = Tag.objects.create(name="invoice", pk=1)
assign_perm("view_tag", self.user, t1)
assign_perm("view_tag", user2, t1)
assign_perm("view_tag", user3, t1)
assign_perm("view_tag", group1, t1)
assign_perm("view_tag", group2, t1)
assign_perm("view_tag", group3, t1)
assign_perm("change_tag", self.user, t1)
assign_perm("change_tag", user2, t1)
assign_perm("change_tag", group1, t1)
assign_perm("change_tag", group2, t1)
Tag.objects.create(name="bank statement", pk=2)
d1 = Document.objects.create(
title="Invoice 1",
content="This is the invoice of a very expensive item",
checksum="A",
)
d1.tags.add(t1)
d2 = Document.objects.create(
title="Invoice 2",
content="Internet invoice, I should pay it to continue contributing",
checksum="B",
)
d2.tags.add(t1)
view_permissions = Permission.objects.filter(
codename__contains="view_tag",
)
self.user.user_permissions.add(*view_permissions)
self.user.save()
self.client.force_login(self.user)
response = self.client.get("/api/tags/?page=1&full_perms=true")
results = json.loads(response.content)["results"]
for tag in results:
if tag["name"] == "invoice":
assert tag["permissions"] == {
"view": {
"users": [self.user.pk, user2.pk, user3.pk],
"groups": [group1.pk, group2.pk, group3.pk],
},
"change": {
"users": [self.user.pk, user2.pk],
"groups": [group1.pk, group2.pk],
},
}
elif tag["name"] == "bank statement":
assert tag["permissions"] == {
"view": {"users": [], "groups": []},
"change": {"users": [], "groups": []},
}
else:
assert False, f"Unexpected tag found: {tag['name']}"
def test_list_no_n_plus_1_queries(self):
"""
GIVEN:
- Tags with different permissions
WHEN:
- Request to get tag list with full permissions is made
THEN:
- Permissions are not queried in database tag by tag,
i.e. there are no N+1 queries
"""
view_permissions = Permission.objects.filter(
codename__contains="view_tag",
)
self.user.user_permissions.add(*view_permissions)
self.user.save()
self.client.force_login(self.user)
# Start by a small list, and count the number of SQL queries
for i in range(2):
Tag.objects.create(name=f"tag_{i}")
with CaptureQueriesContext(connection) as ctx_small:
response_small = self.client.get("/api/tags/?full_perms=true")
assert response_small.status_code == 200
num_queries_small = len(ctx_small.captured_queries)
# Complete the list, and count the number of SQL queries again
for i in range(2, 50):
Tag.objects.create(name=f"tag_{i}")
with CaptureQueriesContext(connection) as ctx_large:
response_large = self.client.get("/api/tags/?full_perms=true")
assert response_large.status_code == 200
num_queries_large = len(ctx_large.captured_queries)
# A few additional queries are allowed, but not a linear explosion
assert num_queries_large <= num_queries_small + 5, (
f"Possible N+1 queries detected: {num_queries_small} queries for 2 tags, "
f"but {num_queries_large} queries for 50 tags"
)

View File

@@ -5,9 +5,11 @@ import platform
import re import re
import tempfile import tempfile
import zipfile import zipfile
from collections import defaultdict
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from time import mktime from time import mktime
from typing import Literal
from unicodedata import normalize from unicodedata import normalize
from urllib.parse import quote from urllib.parse import quote
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -19,6 +21,7 @@ from celery import states
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db import connections from django.db import connections
from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
@@ -56,6 +59,8 @@ from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import extend_schema_view from drf_spectacular.utils import extend_schema_view
from drf_spectacular.utils import inline_serializer from drf_spectacular.utils import inline_serializer
from guardian.utils import get_group_obj_perms_model
from guardian.utils import get_user_obj_perms_model
from langdetect import detect from langdetect import detect
from packaging import version as packaging_version from packaging import version as packaging_version
from redis import Redis from redis import Redis
@@ -254,7 +259,104 @@ class PassUserMixin(GenericAPIView):
return super().get_serializer(*args, **kwargs) return super().get_serializer(*args, **kwargs)
class PermissionsAwareDocumentCountMixin(PassUserMixin): class BulkPermissionMixin:
"""
Prefetch Django-Guardian permissions for a list before serialization, to avoid N+1 queries.
"""
def _get_object_perms(
self,
objects: list,
perm_codenames: list[str],
actor: Literal["users", "groups"],
) -> dict[int, dict[str, list[int]]]:
"""
Collect object-level permissions for either users or groups.
"""
model = self.queryset.model
obj_perm_model = (
get_user_obj_perms_model(model)
if actor == "users"
else get_group_obj_perms_model(model)
)
id_field = "user_id" if actor == "users" else "group_id"
ctype = ContentType.objects.get_for_model(model)
object_pks = [obj.pk for obj in objects]
perms_qs = obj_perm_model.objects.filter(
content_type=ctype,
object_pk__in=object_pks,
permission__codename__in=perm_codenames,
).values_list("object_pk", id_field, "permission__codename")
perms: dict[int, dict[str, list[int]]] = defaultdict(lambda: defaultdict(list))
for object_pk, actor_id, codename in perms_qs:
perms[int(object_pk)][codename].append(actor_id)
# Ensure that all objects have all codenames, even if empty
for pk in object_pks:
for codename in perm_codenames:
perms[pk][codename]
return perms
def get_serializer_context(self):
"""
Get all permissions of the current list of objects at once and pass them to the serializer.
This avoid fetching permissions object by object in database.
"""
context = super().get_serializer_context()
try:
full_perms = get_boolean(
str(self.request.query_params.get("full_perms", "false")),
)
except ValueError:
full_perms = False
if not full_perms:
return context
# Check which objects are being paginated
page = getattr(self, "paginator", None)
if page and hasattr(page, "page"):
queryset = page.page.object_list
elif hasattr(self, "page"):
queryset = self.page
else:
queryset = self.filter_queryset(self.get_queryset())
model_name = self.queryset.model.__name__.lower()
permission_name_view = f"view_{model_name}"
permission_name_change = f"change_{model_name}"
user_perms = self._get_object_perms(
objects=queryset,
perm_codenames=[permission_name_view, permission_name_change],
actor="users",
)
group_perms = self._get_object_perms(
objects=queryset,
perm_codenames=[permission_name_view, permission_name_change],
actor="groups",
)
context["users_view_perms"] = {
pk: user_perms[pk][permission_name_view] for pk in user_perms
}
context["users_change_perms"] = {
pk: user_perms[pk][permission_name_change] for pk in user_perms
}
context["groups_view_perms"] = {
pk: group_perms[pk][permission_name_view] for pk in group_perms
}
context["groups_change_perms"] = {
pk: group_perms[pk][permission_name_change] for pk in group_perms
}
return context
class PermissionsAwareDocumentCountMixin(BulkPermissionMixin, PassUserMixin):
""" """
Mixin to add document count to queryset, permissions-aware if needed Mixin to add document count to queryset, permissions-aware if needed
""" """

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-22 18:20+0000\n" "POT-Creation-Date: 2025-09-30 16:50+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -1191,44 +1191,44 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:140 #: documents/serialisers.py:141
#, python-format #, python-format
msgid "Invalid regular expression: %(error)s" msgid "Invalid regular expression: %(error)s"
msgstr "" msgstr ""
#: documents/serialisers.py:594 #: documents/serialisers.py:607
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:623 #: documents/serialisers.py:636
msgid "Invalid parent tag." msgid "Invalid parent tag."
msgstr "" msgstr ""
#: documents/serialisers.py:1780 #: documents/serialisers.py:1793
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:1824 #: documents/serialisers.py:1837
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:1831 #: documents/serialisers.py:1844
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:1848 documents/serialisers.py:1858 #: documents/serialisers.py:1861 documents/serialisers.py:1871
msgid "" msgid ""
"Custom fields must be a list of integers or an object mapping ids to values." "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:1853 #: documents/serialisers.py:1866
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:1923 #: documents/serialisers.py:1936
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""

2480
uv.lock generated

File diff suppressed because it is too large Load Diff