Skip to content

Commit 2d589c7

Browse files
committed
Merge remote-tracking branch 'origin/dev' into maliming/replace-timeago
2 parents c9aa8f9 + c551f8a commit 2d589c7

71 files changed

Lines changed: 1676 additions & 312 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/angular.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ jobs:
2727
build-test-lint:
2828
if: ${{ !github.event.pull_request.draft }}
2929
runs-on: ubuntu-latest
30+
timeout-minutes: 30
3031
steps:
31-
- uses: actions/checkout@v2
32+
- uses: actions/checkout@v4
3233
with:
3334
fetch-depth: 0
3435

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
name: Auto-merge forward
2+
3+
# Push to a rel-x.y branch opens a merge PR into the next newer rel-* line,
4+
# or into dev when this line is the newest. Merge of that PR retriggers the
5+
# next hop, so a bug-fix on rel-1.0 flows rel-1.0 -> rel-1.1 -> ... -> dev.
6+
on:
7+
push:
8+
branches:
9+
- 'rel-*'
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: auto-merge-forward-${{ github.ref_name }}
14+
cancel-in-progress: false
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
forward:
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: write
24+
pull-requests: write
25+
steps:
26+
- uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 0
29+
30+
- name: Resolve forward target
31+
id: target
32+
run: |
33+
set -euo pipefail
34+
SOURCE="${GITHUB_REF_NAME}"
35+
if [[ ! "$SOURCE" =~ ^rel-[0-9]+\.[0-9]+$ ]]; then
36+
echo "Not a rel-x.y branch ($SOURCE); skipping."
37+
echo "skip=true" >> "$GITHUB_OUTPUT"
38+
exit 0
39+
fi
40+
41+
git fetch origin --prune
42+
43+
mapfile -t RELS < <(
44+
git ls-remote --heads origin 'rel-*' \
45+
| awk '{print $2}' \
46+
| sed 's|refs/heads/||' \
47+
| grep -E '^rel-[0-9]+\.[0-9]+$' \
48+
| sort -t. -k1.5,1n -k2,2n
49+
)
50+
51+
TARGET="dev"
52+
found=0
53+
for branch in "${RELS[@]}"; do
54+
if [[ "$found" -eq 1 ]]; then
55+
TARGET="$branch"
56+
break
57+
fi
58+
if [[ "$branch" == "$SOURCE" ]]; then
59+
found=1
60+
fi
61+
done
62+
63+
if [[ "$found" -eq 0 ]]; then
64+
echo "::error::Source branch $SOURCE was not listed among origin rel-* heads."
65+
exit 1
66+
fi
67+
68+
if ! git rev-parse --verify "origin/$TARGET" >/dev/null 2>&1; then
69+
echo "::error::Target branch origin/$TARGET does not exist."
70+
exit 1
71+
fi
72+
73+
if git merge-base --is-ancestor "origin/$SOURCE" "origin/$TARGET"; then
74+
echo "origin/$SOURCE is already an ancestor of origin/$TARGET; nothing to forward."
75+
echo "skip=true" >> "$GITHUB_OUTPUT"
76+
exit 0
77+
fi
78+
79+
echo "skip=false" >> "$GITHUB_OUTPUT"
80+
echo "source=$SOURCE" >> "$GITHUB_OUTPUT"
81+
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
82+
echo "Auto-merge forward: $SOURCE -> $TARGET"
83+
84+
- name: Merge into forward branch
85+
if: steps.target.outputs.skip != 'true'
86+
id: merge
87+
run: |
88+
set -euo pipefail
89+
SOURCE="${{ steps.target.outputs.source }}"
90+
TARGET="${{ steps.target.outputs.target }}"
91+
FORWARD_BRANCH="auto-merge-forward/${SOURCE}-to-${TARGET}-${{ github.run_number }}"
92+
93+
git config user.name "github-actions[bot]"
94+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
95+
96+
git checkout -B "$FORWARD_BRANCH" "origin/$TARGET"
97+
if git merge --no-edit "origin/$SOURCE"; then
98+
echo "conflict=false" >> "$GITHUB_OUTPUT"
99+
else
100+
git merge --abort
101+
git checkout -B "$FORWARD_BRANCH" "origin/$SOURCE"
102+
echo "conflict=true" >> "$GITHUB_OUTPUT"
103+
echo "::warning::Merge conflict forwarding ${SOURCE} to ${TARGET}. PR left open for manual resolution."
104+
fi
105+
106+
git push origin "$FORWARD_BRANCH"
107+
echo "branch=$FORWARD_BRANCH" >> "$GITHUB_OUTPUT"
108+
109+
- name: Create pull request
110+
if: steps.target.outputs.skip != 'true'
111+
id: pr
112+
env:
113+
GH_TOKEN: ${{ github.token }}
114+
run: |
115+
set -euo pipefail
116+
SOURCE="${{ steps.target.outputs.source }}"
117+
TARGET="${{ steps.target.outputs.target }}"
118+
FORWARD_BRANCH="${{ steps.merge.outputs.branch }}"
119+
CONFLICT="${{ steps.merge.outputs.conflict }}"
120+
121+
BODY="Automated forward merge of \`${SOURCE}\` into \`${TARGET}\`."
122+
if [[ "$CONFLICT" == "true" ]]; then
123+
BODY+=$'\n\n**Merge conflict:** this branch is \`${SOURCE}\` as-is. Resolve against \`${TARGET}\` before merging.'
124+
fi
125+
126+
URL="$(gh pr create \
127+
--base "$TARGET" \
128+
--head "$FORWARD_BRANCH" \
129+
--title "Auto-merge forward ${SOURCE} → ${TARGET}" \
130+
--body "$BODY")"
131+
echo "url=$URL" >> "$GITHUB_OUTPUT"
132+
echo "Created $URL"
133+
134+
# BOT_SECRET, not github.token: a merge performed with the default token produces a push
135+
# that triggers no workflow, which would stop the chain at the first hop.
136+
- name: Approve and auto-merge
137+
if: steps.target.outputs.skip != 'true' && steps.merge.outputs.conflict != 'true'
138+
env:
139+
GH_TOKEN: ${{ secrets.BOT_SECRET }}
140+
run: |
141+
set -euo pipefail
142+
FORWARD_BRANCH="${{ steps.merge.outputs.branch }}"
143+
gh pr review "$FORWARD_BRANCH" --approve
144+
gh pr merge "$FORWARD_BRANCH" --merge --auto --delete-branch

.github/workflows/auto-pr.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

.github/workflows/image-compression.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ jobs:
2222
if: github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft
2323
name: calibreapp/image-actions
2424
runs-on: ubuntu-latest
25+
timeout-minutes: 15
2526
steps:
2627
- name: Checkout Repo
27-
uses: actions/checkout@v2
28+
uses: actions/checkout@v4
2829

2930
- name: Compress Images
3031
uses: calibreapp/image-actions@main

docs/en/framework/architecture/domain-driven-design/application-services.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ These methods are low level methods that can control how to query entities from
444444
* `ApplyPaging` is used to make paging on the query. If your `TGetListInput` already implements `IPagedResultRequest`, you don't need to override this since the ABP automatically understands it and performs the paging.
445445
* `ApplySorting` is used to sort (order by...) the query. If your `TGetListInput` already implements the `ISortedResultRequest`, ABP automatically sorts the query. If not, it fallbacks to the `ApplyDefaultSorting` which tries to sort by creation time, if your entity implements the standard `IHasCreationTime` interface.
446446
* `GetEntityByIdAsync` is used to get an entity by id, which calls `Repository.GetAsync(id)` by default.
447+
* `CreateEntityQueryOrNullAsync` is used to create a query for a single entity by id, which is only needed for the *Query Projection* explained below. It returns `null` if the application service can not create such a query, then `GetEntityByIdAsync` is used.
447448
* `DeleteByIdAsync` is used to delete an entity by id, which calls `Repository.DeleteAsync(id)` by default.
448449

449450
#### Object to Object Mapping
@@ -456,6 +457,103 @@ These methods are used to convert Entities to DTOs and vice verse. They use the
456457
* `MapToEntityAsync(TCreateInput)` is used to create an entity from `TCreateInput`.
457458
* `MapToEntityAsync(TUpdateInput, TEntity)` is used to update an existing entity from `TUpdateInput`.
458459

460+
#### Query Projection
461+
462+
`GetAsync` and `GetListAsync` get the entities from the database, then map them to DTOs in the memory. If your DTO uses only a few properties of a large entity, you can project the query to the DTO instead, so the database returns only the columns you need.
463+
464+
Implement the `IQueryProjector<TEntity, TDto>` interface to define a projection:
465+
466+
````csharp
467+
using System.Linq;
468+
using Volo.Abp.ObjectMapping;
469+
470+
namespace MyProject.Books;
471+
472+
public class BookProjector : IQueryProjector<Book, BookDto>
473+
{
474+
public IQueryable<BookDto> ProjectTo(IQueryable<Book> source)
475+
{
476+
return source.Select(book => new BookDto
477+
{
478+
Id = book.Id,
479+
Name = book.Name
480+
});
481+
}
482+
}
483+
````
484+
485+
You don't have to write the `Select` by hand. Both [Mapperly](https://mapperly.riok.app/) and [AutoMapper](https://docs.automapper.org) can project an `IQueryable`, refer to their own documentation for it and to the [object to object mapping document](../../infrastructure/object-to-object-mapping.md) for their ABP integrations. Your existing maps are not used for the projection, a projector is always a class implementing `IQueryProjector<TSource, TDestination>`.
486+
487+
ABP registers the projectors by convention, you don't need to configure anything else. Implement a projector once for an entity and DTO pair, and use the `ReplaceServices` option of the `DependencyAttribute` to replace an existing one. Filters (like soft delete and multi-tenancy), sorting and paging are still applied to the query before the projection.
488+
489+
> A projection must return one row per entity. The total count and the paging are calculated on the entity query before the projection runs, so a projection that filters out rows (an inner join to an optional relation) or multiplies them (a join to a collection) returns a page that doesn't match the reported total count. Use a left join for optional relations.
490+
491+
The projector is synchronous, so it can not obtain the query of another aggregate root, which is only
492+
available through the asynchronous `GetQueryableAsync`. Override `CreateGetOutputDtoQueryOrNullAsync` or
493+
`CreateGetListOutputDtoQueryOrNullAsync` for that. They replace the projector for that application service:
494+
495+
````csharp
496+
public class BookAppService : ReadOnlyAppService<Book, BookDto, Guid>
497+
{
498+
private readonly IBookDtoQuery _bookDtoQuery;
499+
500+
//...
501+
502+
protected override async Task<IQueryable<BookDto>?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable<Book> query)
503+
{
504+
return await _bookDtoQuery.ProjectAsync(query);
505+
}
506+
}
507+
508+
//The projection is a class of its own, so the other application services returning a BookDto reuse it
509+
public class BookDtoQuery : IBookDtoQuery, ITransientDependency
510+
{
511+
private readonly IReadOnlyRepository<Author, Guid> _authorRepository;
512+
513+
//...
514+
515+
public async Task<IQueryable<BookDto>> ProjectAsync(IQueryable<Book> books)
516+
{
517+
var authors = await _authorRepository.GetQueryableAsync();
518+
519+
return from book in books
520+
join author in authors on book.AuthorId equals author.Id into bookAuthors
521+
from bookAuthor in bookAuthors.DefaultIfEmpty()
522+
select new BookDto
523+
{
524+
Id = book.Id,
525+
Name = book.Name,
526+
AuthorName = bookAuthor != null ? bookAuthor.Name : null
527+
};
528+
}
529+
}
530+
````
531+
532+
Both queries must come from the same database context, otherwise they can not be executed as a single query,
533+
and the provider has to be able to translate the join. The one row per entity rule above applies here too,
534+
that's why the example uses a left join. A joined column can not be used for the sorting, and the paging is
535+
based on the entity query, since both are applied before this method is called.
536+
537+
A projector is resolved by the `(entity, DTO)` type pair, just like an `IObjectMapper<TSource, TDestination>`, so registering one enables the projection for every application service using that pair. It replaces the way the DTOs are read:
538+
539+
* `GetListAsync` doesn't use `MapToGetListOutputDtosAsync` anymore.
540+
* `GetAsync` doesn't use `GetEntityByIdAsync` and `MapToGetOutputDtoAsync` anymore, as long as the application service can create a query for a single entity. `ReadOnlyAppService` and `CrudAppService` already do that. A class deriving from `AbstractKeyReadOnlyAppService` has to override `CreateEntityQueryOrNullAsync`, otherwise `GetAsync` keeps loading the entity and mapping it.
541+
542+
The rest of the pipeline is untouched. The authorization policies are still checked, `CreateFilteredQueryAsync`, `ApplySorting` and `ApplyPaging` are still used, the data filters (like soft delete and multi-tenancy) are still applied, and the create, update and delete methods still use the [IObjectMapper](../../infrastructure/object-to-object-mapping.md).
543+
544+
> If an application service needs to keep using the entity based extension points, override the `GetOutputDtoQueryProjector` or `GetListOutputDtoQueryProjector` property and return `null`:
545+
546+
````csharp
547+
public class BookAppService : CrudAppService<Book, BookDto, Guid>
548+
{
549+
protected override IQueryProjector<Book, BookDto>? GetOutputDtoQueryProjector => null;
550+
551+
protected override IQueryProjector<Book, BookDto>? GetListOutputDtoQueryProjector => null;
552+
553+
//...
554+
}
555+
````
556+
459557
## Miscellaneous
460558

461559
### Working with Streams

docs/en/framework/ui/mvc-razor-pages/overall.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ There are a set of standard JavaScript/CSS libraries that comes pre-installed an
6464
- [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers.
6565
- [Select2](https://select2.org/) for better select/combo boxes.
6666
- [timeago.js](https://timeago.org/) to show automatically updating fuzzy timestamps.
67-
- [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars.
6867

6968
You can use these libraries directly in your applications, without needing to manually import your page.
7069

docs/en/framework/ui/mvc-razor-pages/theming.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ All the themes must depend on the [@abp/aspnetcore.mvc.ui.theme.shared](https://
5151
* [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers.
5252
* [Select2](https://select2.org/) for better select/combo boxes.
5353
* [timeago.js](https://timeago.org/) to show automatically updating fuzzy timestamps.
54-
* [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars.
5554

5655
These libraries are selected as the base libraries and available to the applications and modules.
5756

framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginScriptBundleContributor.cs

Lines changed: 0 additions & 12 deletions
This file was deleted.

framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginStyleBundleContributor.cs

Lines changed: 0 additions & 12 deletions
This file was deleted.

framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidationUnobtrusive;
88
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Lodash;
99
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Luxon;
10-
using Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar;
1110
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2;
1211
using Volo.Abp.AspNetCore.Mvc.UI.Packages.SweetAlert2;
1312
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago;
@@ -23,7 +22,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling;
2322
typeof(Select2ScriptContributor),
2423
typeof(DatatablesNetBs5ScriptContributor),
2524
typeof(Sweetalert2ScriptContributor),
26-
typeof(MalihuCustomScrollbarPluginScriptBundleContributor),
2725
typeof(LuxonScriptContributor),
2826
typeof(TimeagoScriptContributor),
2927
typeof(BootstrapDatepickerScriptContributor),

0 commit comments

Comments
 (0)