Browse Source

Merge branch 'dev' of https://github.com/abpframework/abp into dev

pull/24703/merge
Ebicoglu 17 hours ago
parent
commit
a2a3193bb4
  1. 406
      .github/scripts/test_update_dependency_changes.py
  2. 331
      .github/scripts/update_dependency_changes.py
  3. 71
      .github/workflows/nuget-packages-version-change-detector.yml
  4. 261
      .github/workflows/update-studio-docs.yml
  5. 9
      docs/en/modules/ai-management/index.md
  6. 3
      docs/en/studio/version-mapping.md
  7. 47
      framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerUIOptionsExtensions.cs
  8. 6
      framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.js
  9. 2
      framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js
  10. 8
      npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts
  11. 8
      npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts
  12. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts
  13. 14
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  14. 6
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts
  15. 18
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  16. 6
      npm/ng-packs/packages/components/page/src/page.component.html
  17. 15
      npm/ng-packs/packages/components/page/src/page.component.ts
  18. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html
  19. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts
  20. 5
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  21. 24
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts
  22. 7
      npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts
  23. 7
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  24. 10
      npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts

406
.github/scripts/test_update_dependency_changes.py

@ -0,0 +1,406 @@
#!/usr/bin/env python3
"""
Comprehensive test suite for update_dependency_changes.py
Tests cover:
- Basic update/add/remove scenarios
- Version revert scenarios
- Complex multi-step change sequences
- Edge cases and duplicate operations
- Document format validation
"""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from update_dependency_changes import merge_changes, render_section
def test_update_then_revert():
"""Test: PR1 updates A->B, PR2 reverts B->A. Should be removed."""
print("Test 1: Update then revert")
existing = (
{"PackageA": ("1.0.0", "2.0.0", "#1")}, # updated
{}, # added
{} # removed
)
new = (
{"PackageA": ("2.0.0", "1.0.0", "#2")}, # updated back
{},
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageA" not in updated, f"Expected PackageA removed, got: {updated}"
assert len(added) == 0 and len(removed) == 0
print("✓ Passed: Package correctly removed from updates\n")
def test_add_then_remove_same_version():
"""Test: PR1 adds v1.0, PR2 removes v1.0. Should be completely removed."""
print("Test 2: Add then remove same version")
existing = (
{},
{"PackageB": ("1.0.0", "#1")}, # added
{}
)
new = (
{},
{},
{"PackageB": ("1.0.0", "#2")} # removed
)
updated, added, removed = merge_changes(existing, new)
assert "PackageB" not in added, f"Expected PackageB removed from added, got: {added}"
assert "PackageB" not in removed, f"Expected PackageB removed from removed, got: {removed}"
assert "PackageB" not in updated
print("✓ Passed: Package correctly removed from all sections\n")
def test_remove_then_add_same_version():
"""Test: PR1 removes v1.0, PR2 adds v1.0. Should be removed."""
print("Test 3: Remove then add same version")
existing = (
{},
{},
{"PackageC": ("1.0.0", "#1")} # removed
)
new = (
{},
{"PackageC": ("1.0.0", "#2")}, # added back
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageC" not in updated, f"Expected PackageC removed from updated, got: {updated}"
assert "PackageC" not in added, f"Expected PackageC removed from added, got: {added}"
assert "PackageC" not in removed, f"Expected PackageC removed from removed, got: {removed}"
print("✓ Passed: Package correctly removed from all sections\n")
def test_add_then_remove_different_version():
"""Test: PR1 adds v1.0, PR2 removes v2.0. Should show as removed v2.0."""
print("Test 4: Add then remove different version")
existing = (
{},
{"PackageD": ("1.0.0", "#1")}, # added
{}
)
new = (
{},
{},
{"PackageD": ("2.0.0", "#2")} # removed different version
)
updated, added, removed = merge_changes(existing, new)
assert "PackageD" not in added, f"Expected PackageD removed from added, got: {added}"
assert "PackageD" in removed, f"Expected PackageD in removed, got: {removed}"
assert removed["PackageD"][0] == "2.0.0", f"Expected version 2.0.0, got: {removed['PackageD']}"
print(f"✓ Passed: Package correctly tracked as removed with version {removed['PackageD'][0]}\n")
def test_update_in_added():
"""Test: PR1 adds v1.0, PR2 updates to v2.0. Should show as updated 1.0->2.0."""
print("Test 5: Update a package that was added")
existing = (
{},
{"PackageE": ("1.0.0", "#1")}, # added
{}
)
new = (
{"PackageE": ("1.0.0", "2.0.0", "#2")}, # updated
{},
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageE" not in added, f"Expected PackageE removed from added, got: {added}"
assert "PackageE" in updated, f"Expected PackageE in updated, got: {updated}"
assert updated["PackageE"] == ("1.0.0", "2.0.0", "#1, #2"), \
f"Expected ('1.0.0', '2.0.0', '#1, #2'), got: {updated['PackageE']}"
print(f"✓ Passed: Package correctly converted to updated: {updated['PackageE']}\n")
def test_multiple_updates():
"""Test: PR1 updates A->B, PR2 updates B->C. Should show A->C."""
print("Test 6: Multiple updates")
existing = (
{"PackageF": ("1.0.0", "2.0.0", "#1")}, # updated
{},
{}
)
new = (
{"PackageF": ("2.0.0", "3.0.0", "#2")}, # updated again
{},
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageF" in updated
assert updated["PackageF"] == ("1.0.0", "3.0.0", "#1, #2"), \
f"Expected ('1.0.0', '3.0.0', '#1, #2'), got: {updated['PackageF']}"
print(f"✓ Passed: Package correctly shows full range: {updated['PackageF']}\n")
def test_multiple_updates_back_to_original():
"""Test: PR1 updates 1->2, PR2 updates 2->3, PR3 updates 3->1. Should be removed."""
print("Test 7: Multiple updates ending back at original version")
# Simulate PR1 and PR2 already merged
existing = (
{"PackageG": ("1.0.0", "3.0.0", "#1, #2")}, # updated through PR1 and PR2
{},
{}
)
# PR3 changes back to 1.0.0
new = (
{"PackageG": ("3.0.0", "1.0.0", "#3")}, # updated back to original
{},
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageG" not in updated, f"Expected PackageG removed, got: {updated}"
assert len(added) == 0 and len(removed) == 0
print("✓ Passed: Package correctly removed (version returned to original)\n")
def test_update_remove_add_same_version():
"""Test: PR1 updates 1->2, PR2 updates 2->3, PR3 removes, PR4 adds v3. Should show updated 1->3."""
print("Test 8: Update-Update-Remove-Add same version")
# After PR1, PR2, PR3
existing = (
{},
{},
{"PackageH": ("1.0.0", "#1, #2, #3")} # removed (original was 1.0.0)
)
# PR4 adds back the same version that was removed
new = (
{},
{"PackageH": ("3.0.0", "#4")}, # added
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageH" in updated, f"Expected PackageH in updated, got: updated={updated}, added={added}, removed={removed}"
assert updated["PackageH"] == ("1.0.0", "3.0.0", "#1, #2, #3, #4"), \
f"Expected ('1.0.0', '3.0.0', '#1, #2, #3, #4'), got: {updated['PackageH']}"
print(f"✓ Passed: Package correctly shows as updated: {updated['PackageH']}\n")
def test_update_remove_add_original_version():
"""Test: PR1 updates 1->2, PR2 updates 2->3, PR3 removes, PR4 adds v1. Should be removed."""
print("Test 9: Update-Update-Remove-Add original version")
# After PR1, PR2, PR3
existing = (
{},
{},
{"PackageI": ("1.0.0", "#1, #2, #3")} # removed (original was 1.0.0)
)
# PR4 adds back the original version
new = (
{},
{"PackageI": ("1.0.0", "#4")}, # added back to original
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageI" not in updated, f"Expected PackageI removed, got: updated={updated}"
assert "PackageI" not in added, f"Expected PackageI removed, got: added={added}"
assert "PackageI" not in removed, f"Expected PackageI removed, got: removed={removed}"
print("✓ Passed: Package correctly removed (added back to original version)\n")
def test_update_remove_add_different_version():
"""Test: PR1 updates 1->2, PR2 updates 2->3, PR3 removes, PR4 adds v4. Should show updated 1->4."""
print("Test 10: Update-Update-Remove-Add different version")
# After PR1, PR2, PR3
existing = (
{},
{},
{"PackageJ": ("1.0.0", "#1, #2, #3")} # removed (original was 1.0.0)
)
# PR4 adds a completely different version
new = (
{},
{"PackageJ": ("4.0.0", "#4")}, # added new version
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageJ" in updated, f"Expected PackageJ in updated, got: updated={updated}, added={added}, removed={removed}"
assert updated["PackageJ"] == ("1.0.0", "4.0.0", "#1, #2, #3, #4"), \
f"Expected ('1.0.0', '4.0.0', '#1, #2, #3, #4'), got: {updated['PackageJ']}"
print(f"✓ Passed: Package correctly shows as updated: {updated['PackageJ']}\n")
def test_add_update_remove():
"""Test: PR1 adds v1, PR2 updates to v2, PR3 removes v2. Should be completely removed."""
print("Test 11: Add-Update-Remove")
# After PR1 and PR2
existing = (
{"PackageK": ("1.0.0", "2.0.0", "#1, #2")}, # updated (was added in PR1, updated in PR2)
{},
{}
)
# PR3 removes v2
new = (
{},
{},
{"PackageK": ("2.0.0", "#3")} # removed
)
updated, added, removed = merge_changes(existing, new)
assert "PackageK" not in updated, f"Expected PackageK removed from updated, got: {updated}"
assert "PackageK" not in added, f"Expected PackageK removed from added, got: {added}"
assert "PackageK" in removed, f"Expected PackageK in removed, got: {removed}"
# The removed should track from the original first version
assert removed["PackageK"][0] == "1.0.0", f"Expected removed from 1.0.0, got: {removed['PackageK']}"
print(f"✓ Passed: Package correctly shows as removed from original: {removed['PackageK']}\n")
def test_add_remove_add_same_version():
"""Test: PR1 adds v1, PR2 removes v1, PR3 adds v1 again. Should show as added v1."""
print("Test 12: Add-Remove-Add same version")
# After PR1 and PR2 (added then removed)
existing = (
{},
{},
{} # Completely removed after PR2
)
# PR3 adds v1 again
new = (
{},
{"PackageL": ("1.0.0", "#3")}, # added
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageL" in added, f"Expected PackageL in added, got: added={added}"
assert added["PackageL"] == ("1.0.0", "#3"), f"Expected ('1.0.0', '#3'), got: {added['PackageL']}"
print(f"✓ Passed: Package correctly shows as added: {added['PackageL']}\n")
def test_update_remove_remove():
"""Test: PR1 updates 1->2, PR2 removes v2, PR3 tries to remove again. Should show removed from v1."""
print("Test 13: Update-Remove (duplicate remove)")
# After PR1 and PR2
existing = (
{},
{},
{"PackageM": ("1.0.0", "#1, #2")} # removed (original was 1.0.0)
)
# PR3 tries to remove again (edge case, might not happen in practice)
new = (
{},
{},
{"PackageM": ("1.0.0", "#3")} # removed again
)
updated, added, removed = merge_changes(existing, new)
assert "PackageM" in removed, f"Expected PackageM in removed, got: {removed}"
# Should keep the original information
assert removed["PackageM"][0] == "1.0.0", f"Expected removed from 1.0.0, got: {removed['PackageM']}"
print(f"✓ Passed: Package correctly maintains removed state: {removed['PackageM']}\n")
def test_add_add():
"""Test: PR1 adds v1, PR2 adds v2 (version changed externally). Should show added v2."""
print("Test 14: Add-Add (version changed between PRs)")
# After PR1
existing = (
{},
{"PackageN": ("1.0.0", "#1")}, # added
{}
)
# PR2 adds different version (edge case)
new = (
{},
{"PackageN": ("2.0.0", "#2")}, # added different version
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageN" in added, f"Expected PackageN in added, got: {added}"
assert added["PackageN"][0] == "2.0.0", f"Expected version 2.0.0, got: {added['PackageN']}"
print(f"✓ Passed: Package correctly shows latest added version: {added['PackageN']}\n")
def test_complex_chain_ending_in_original():
"""Test: Complex chain - Add v1, Update to v2, Remove, Add v2, Update to v1. Should be removed."""
print("Test 15: Complex chain ending at nothing changed")
# After PR1 (add), PR2 (update), PR3 (remove), PR4 (add back)
existing = (
{"PackageO": ("1.0.0", "2.0.0", "#1, #2, #3, #4")}, # Complex history
{},
{}
)
# PR5 updates back to v1 (original from perspective of first state)
new = (
{"PackageO": ("2.0.0", "1.0.0", "#5")}, # back to start
{},
{}
)
updated, added, removed = merge_changes(existing, new)
assert "PackageO" not in updated, f"Expected PackageO removed, got: {updated}"
print(f"✓ Passed: Complex chain correctly removed when ending at original\n")
def test_document_format():
"""Test: Verify the document rendering format."""
print("Test 16: Document format validation")
updated = {
"Microsoft.Extensions.Logging": ("8.0.0", "8.0.1", "#123"),
"Newtonsoft.Json": ("13.0.1", "13.0.3", "#456, #789"),
}
added = {
"Azure.Identity": ("1.10.0", "#567"),
}
removed = {
"System.Text.Json": ("7.0.0", "#890"),
}
document = render_section("9.0.0", updated, added, removed)
# Verify document structure
assert "## 9.0.0" in document, "Version header missing"
assert "| Package | Old Version | New Version | PR |" in document, "Updated table header missing"
assert "Microsoft.Extensions.Logging" in document, "Updated package missing"
assert "**Added:**" in document, "Added section missing"
assert "Azure.Identity" in document, "Added package missing"
assert "**Removed:**" in document, "Removed section missing"
assert "System.Text.Json" in document, "Removed package missing"
print("✓ Passed: Document format is correct")
print("\nSample output:")
print("-" * 60)
print(document)
print("-" * 60 + "\n")
def run_all_tests():
"""Run all test cases."""
print("=" * 70)
print("Testing update_dependency_changes.py")
print("=" * 70 + "\n")
test_update_then_revert()
test_add_then_remove_same_version()
test_remove_then_add_same_version()
test_add_then_remove_different_version()
test_update_in_added()
test_multiple_updates()
test_multiple_updates_back_to_original()
test_update_remove_add_same_version()
test_update_remove_add_original_version()
test_update_remove_add_different_version()
test_add_update_remove()
test_add_remove_add_same_version()
test_update_remove_remove()
test_add_add()
test_complex_chain_ending_in_original()
test_document_format()
print("=" * 70)
print("All 16 tests passed! ✓")
print("=" * 70)
print("\nTest coverage summary:")
print(" ✓ Basic scenarios (update, add, remove)")
print(" ✓ Version revert handling")
print(" ✓ Complex multi-step sequences")
print(" ✓ Edge cases and duplicates")
print(" ✓ Document format validation")
print("=" * 70)
if __name__ == "__main__":
run_all_tests()

331
.github/scripts/update_dependency_changes.py

@ -0,0 +1,331 @@
import subprocess
import re
import os
import sys
import xml.etree.ElementTree as ET
HEADER = "# Package Version Changes\n"
DOC_PATH = os.environ.get("DOC_PATH", "docs/en/package-version-changes.md")
def get_version():
"""Read the current version from common.props."""
try:
tree = ET.parse("common.props")
root = tree.getroot()
version_elem = root.find(".//Version")
if version_elem is not None:
return version_elem.text
except FileNotFoundError:
print("Error: 'common.props' file not found.", file=sys.stderr)
except ET.ParseError as ex:
print(f"Error: Failed to parse 'common.props': {ex}", file=sys.stderr)
return None
def get_diff(base_ref):
"""Get diff of Directory.Packages.props against the base branch."""
result = subprocess.run(
["git", "diff", f"origin/{base_ref}", "--", "Directory.Packages.props"],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(
f"Failed to get diff for base ref 'origin/{base_ref}': {result.stderr}"
)
return result.stdout
def get_existing_doc_from_base(base_ref):
"""Read the existing document from the base branch."""
result = subprocess.run(
["git", "show", f"origin/{base_ref}:{DOC_PATH}"],
capture_output=True,
text=True,
)
if result.returncode == 0:
return result.stdout
return ""
def parse_diff_packages(lines, prefix):
"""Parse package versions from diff lines with the given prefix (+ or -)."""
packages = {}
# Use separate patterns to handle different attribute orders
include_pattern = re.compile(r'Include="([^"]+)"')
version_pattern = re.compile(r'Version="([^"]+)"')
for line in lines:
if line.startswith(prefix) and "PackageVersion" in line and not line.startswith(prefix * 3):
include_match = include_pattern.search(line)
version_match = version_pattern.search(line)
if include_match and version_match:
packages[include_match.group(1)] = version_match.group(1)
return packages
def classify_changes(old_packages, new_packages, pr_number):
"""Classify diff into updated, added, and removed with PR attribution."""
updated = {}
added = {}
removed = {}
all_packages = sorted(set(list(old_packages.keys()) + list(new_packages.keys())))
for pkg in all_packages:
if pkg in old_packages and pkg in new_packages:
if old_packages[pkg] != new_packages[pkg]:
updated[pkg] = (old_packages[pkg], new_packages[pkg], pr_number)
elif pkg in new_packages:
added[pkg] = (new_packages[pkg], pr_number)
else:
removed[pkg] = (old_packages[pkg], pr_number)
return updated, added, removed
def parse_existing_section(section_text):
"""Parse an existing markdown section to extract package records with PR info."""
updated = {}
added = {}
removed = {}
mode = "updated"
for line in section_text.split("\n"):
if "**Added:**" in line:
mode = "added"
continue
if "**Removed:**" in line:
mode = "removed"
continue
if not line.startswith("|") or line.startswith("| Package") or line.startswith("|---"):
continue
parts = [p.strip() for p in line.split("|")[1:-1]]
if mode == "updated" and len(parts) >= 3:
pr = parts[3] if len(parts) >= 4 else ""
updated[parts[0]] = (parts[1], parts[2], pr)
elif len(parts) >= 2:
pr = parts[2] if len(parts) >= 3 else ""
if mode == "added":
added[parts[0]] = (parts[1], pr)
else:
removed[parts[0]] = (parts[1], pr)
return updated, added, removed
def merge_prs(existing_pr, new_pr):
"""Merge PR numbers, avoiding duplicates."""
if not existing_pr or not existing_pr.strip():
return new_pr
if not new_pr or not new_pr.strip():
return existing_pr
# Parse existing PRs
existing_prs = [p.strip() for p in existing_pr.split(",") if p.strip()]
# Add new PR if not already present
if new_pr not in existing_prs:
existing_prs.append(new_pr)
return ", ".join(existing_prs)
def merge_changes(existing, new):
"""Merge new changes into existing records for the same version."""
ex_updated, ex_added, ex_removed = existing
new_updated, new_added, new_removed = new
merged_updated = dict(ex_updated)
merged_added = dict(ex_added)
merged_removed = dict(ex_removed)
for pkg, (old_ver, new_ver, pr) in new_updated.items():
if pkg in merged_updated:
existing_old_ver, existing_new_ver, existing_pr = merged_updated[pkg]
merged_pr = merge_prs(existing_pr, pr)
merged_updated[pkg] = (existing_old_ver, new_ver, merged_pr)
elif pkg in merged_added:
existing_ver, existing_pr = merged_added[pkg]
merged_pr = merge_prs(existing_pr, pr)
# Convert added to updated since the version changed again
del merged_added[pkg]
merged_updated[pkg] = (existing_ver, new_ver, merged_pr)
else:
merged_updated[pkg] = (old_ver, new_ver, pr)
for pkg, (ver, pr) in new_added.items():
if pkg in merged_removed:
removed_ver, removed_pr = merged_removed.pop(pkg)
merged_pr = merge_prs(removed_pr, pr)
merged_updated[pkg] = (removed_ver, ver, merged_pr)
elif pkg in merged_added:
existing_ver, existing_pr = merged_added[pkg]
merged_pr = merge_prs(existing_pr, pr)
merged_added[pkg] = (ver, merged_pr)
else:
merged_added[pkg] = (ver, pr)
for pkg, (ver, pr) in new_removed.items():
if pkg in merged_added:
existing_ver, existing_pr = merged_added[pkg]
# Only delete if versions match (added then removed the same version)
if existing_ver == ver:
del merged_added[pkg]
else:
# Version changed between add and remove, convert to updated then removed
del merged_added[pkg]
merged_removed[pkg] = (ver, merge_prs(existing_pr, pr))
elif pkg in merged_updated:
old_ver, new_ver, existing_pr = merged_updated.pop(pkg)
merged_pr = merge_prs(existing_pr, pr)
# Only keep as removed if the final state is different from original
merged_removed[pkg] = (old_ver, merged_pr)
else:
merged_removed[pkg] = (ver, pr)
# Remove updated entries where old and new versions are the same
merged_updated = {k: v for k, v in merged_updated.items() if v[0] != v[1]}
# Remove added entries that are also in removed with the same version
for pkg in list(merged_added.keys()):
if pkg in merged_removed:
added_ver, added_pr = merged_added[pkg]
removed_ver, removed_pr = merged_removed[pkg]
if added_ver == removed_ver:
# Package was added and removed at the same version, cancel out
del merged_added[pkg]
del merged_removed[pkg]
return merged_updated, merged_added, merged_removed
def render_section(version, updated, added, removed):
"""Render a version section as markdown."""
lines = [f"## {version}\n"]
if updated:
lines.append("| Package | Old Version | New Version | PR |")
lines.append("|---------|-------------|-------------|-----|")
for pkg in sorted(updated):
old_ver, new_ver, pr = updated[pkg]
lines.append(f"| {pkg} | {old_ver} | {new_ver} | {pr} |")
lines.append("")
if added:
lines.append("**Added:**\n")
lines.append("| Package | Version | PR |")
lines.append("|---------|---------|-----|")
for pkg in sorted(added):
ver, pr = added[pkg]
lines.append(f"| {pkg} | {ver} | {pr} |")
lines.append("")
if removed:
lines.append("**Removed:**\n")
lines.append("| Package | Version | PR |")
lines.append("|---------|---------|-----|")
for pkg in sorted(removed):
ver, pr = removed[pkg]
lines.append(f"| {pkg} | {ver} | {pr} |")
lines.append("")
return "\n".join(lines)
def parse_document(content):
"""Split document into a list of (version, section_text) tuples."""
sections = []
current_version = None
current_lines = []
for line in content.split("\n"):
match = re.match(r"^## (.+)$", line)
if match:
if current_version:
sections.append((current_version, "\n".join(current_lines)))
current_version = match.group(1).strip()
current_lines = [line]
elif current_version:
current_lines.append(line)
if current_version:
sections.append((current_version, "\n".join(current_lines)))
return sections
def main():
if len(sys.argv) < 3:
print("Usage: update_dependency_changes.py <base-ref> <pr-number>")
sys.exit(1)
base_ref = sys.argv[1]
pr_arg = sys.argv[2]
# Validate PR number is numeric
if not re.fullmatch(r"\d+", pr_arg):
print("Invalid PR number; must be numeric.")
sys.exit(1)
# Validate base_ref doesn't contain dangerous characters
if not re.fullmatch(r"[a-zA-Z0-9/_.-]+", base_ref):
print("Invalid base ref; contains invalid characters.")
sys.exit(1)
pr_number = f"#{pr_arg}"
version = get_version()
if not version:
print("Could not read version from common.props.")
sys.exit(1)
diff = get_diff(base_ref)
if not diff:
print("No diff found for Directory.Packages.props.")
sys.exit(0)
diff_lines = diff.split("\n")
old_packages = parse_diff_packages(diff_lines, "-")
new_packages = parse_diff_packages(diff_lines, "+")
new_updated, new_added, new_removed = classify_changes(old_packages, new_packages, pr_number)
if not new_updated and not new_added and not new_removed:
print("No package version changes detected.")
sys.exit(0)
# Load existing document from the base branch
existing_content = get_existing_doc_from_base(base_ref)
sections = parse_document(existing_content) if existing_content else []
# Find existing section for this version
version_index = None
for i, (v, _) in enumerate(sections):
if v == version:
version_index = i
break
if version_index is not None:
existing = parse_existing_section(sections[version_index][1])
merged = merge_changes(existing, (new_updated, new_added, new_removed))
section_text = render_section(version, *merged)
sections[version_index] = (version, section_text)
else:
section_text = render_section(version, new_updated, new_added, new_removed)
sections.insert(0, (version, section_text))
# Write document
doc_dir = os.path.dirname(DOC_PATH)
if doc_dir:
os.makedirs(doc_dir, exist_ok=True)
with open(DOC_PATH, "w") as f:
f.write(HEADER + "\n")
for _, text in sections:
f.write(text.rstrip("\n") + "\n\n")
print(f"Updated {DOC_PATH} for version {version}")
if __name__ == "__main__":
main()

71
.github/workflows/nuget-packages-version-change-detector.yml

@ -0,0 +1,71 @@
# Automatically detects and documents NuGet package version changes in PRs.
# Triggers on changes to Directory.Packages.props and:
# - Adds 'dependency-change' label to the PR
# - Updates docs/en/package-version-changes.md with version changes
# - Commits the documentation back to the PR branch
# Note: Only runs for PRs from the same repository (not forks) to ensure write permissions.
name: Nuget Packages Version Change Detector
on:
pull_request:
paths:
- 'Directory.Packages.props'
types:
- opened
- synchronize
- reopened
- ready_for_review
permissions:
contents: read
concurrency:
group: dependency-changes-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
label:
if: ${{ !github.event.pull_request.draft && !startsWith(github.head_ref, 'auto-merge/') && github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.head_commit.message, '[skip ci]') }}
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
env:
DOC_PATH: docs/en/package-version-changes.md
steps:
- run: gh pr edit "$PR_NUMBER" --add-label "dependency-change"
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.BOT_SECRET }}
GH_REPO: ${{ github.repository }}
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 1
- name: Fetch base branch
run: git fetch origin ${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} --depth=1
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- run: python .github/scripts/update_dependency_changes.py ${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.number }}
- name: Commit changes
run: |
set -e
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "$DOC_PATH"
if git diff --staged --quiet; then
echo "No changes to commit."
else
git commit -m "docs: update package version changes [skip ci]"
if ! git push; then
echo "Error: Failed to push changes. This may be due to conflicts or permission issues."
exit 1
fi
echo "Successfully committed and pushed documentation changes."
fi

261
.github/workflows/update-studio-docs.yml

@ -0,0 +1,261 @@
name: Update ABP Studio Docs
on:
repository_dispatch:
types: [update_studio_docs]
jobs:
update-docs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
models: read
steps:
# -------------------------------------------------
# Validate payload (safe & strict)
# -------------------------------------------------
- name: Validate payload
run: |
required_keys=(version name notes url target_branch)
for key in "${required_keys[@]}"; do
value="$(jq -r --arg k "$key" '.client_payload[$k] // ""' "$GITHUB_EVENT_PATH")"
if [ -z "$value" ] || [ "$value" = "null" ]; then
echo "Missing payload field: $key"
exit 1
fi
done
# -------------------------------------------------
# Checkout target branch
# -------------------------------------------------
- uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.target_branch }}
fetch-depth: 0
- name: Configure git
run: |
git config user.name "docs-bot"
git config user.email "docs-bot@users.noreply.github.com"
# -------------------------------------------------
# Create working branch
# -------------------------------------------------
- name: Create branch
run: |
VERSION="${{ github.event.client_payload.version }}"
BRANCH="docs/studio-${VERSION}"
git checkout -B "$BRANCH"
echo "BRANCH=$BRANCH" >> $GITHUB_ENV
# -------------------------------------------------
# Save raw release notes
# -------------------------------------------------
- name: Save raw release notes
run: |
mkdir -p .tmp
jq -r '.client_payload.notes' "$GITHUB_EVENT_PATH" > .tmp/raw-notes.txt
# -------------------------------------------------
# Try AI formatting (OPTIONAL)
# -------------------------------------------------
- name: Generate release notes with AI (optional)
id: ai
continue-on-error: true
uses: actions/ai-inference@v1
with:
model: openai/gpt-4.1
prompt: |
You are a technical writer.
Convert the following release notes into concise, user-facing bullet points.
Rules:
- Use "-" bullets
- Keep it short and clear
- Skip internal or low-level changes
Release notes:
${{ github.event.client_payload.notes }}
# -------------------------------------------------
# Decide final release notes (AI or fallback)
# -------------------------------------------------
- name: Decide final release notes
run: |
if [ -n "${{ steps.ai.outputs.response }}" ]; then
echo "✅ Using AI formatted notes"
echo "${{ steps.ai.outputs.response }}" > .tmp/final-notes.txt
else
echo "⚠️ AI unavailable – using raw notes"
sed 's/^/- /' .tmp/raw-notes.txt > .tmp/final-notes.txt
fi
# -------------------------------------------------
# Update release-notes.md (UNDER Latest)
# -------------------------------------------------
- name: Update release-notes.md
run: |
FILE="docs/en/studio/release-notes.md"
VERSION="${{ github.event.client_payload.version }}"
NAME="${{ github.event.client_payload.name }}"
URL="${{ github.event.client_payload.url }}"
DATE="$(date +%Y-%m-%d)"
mkdir -p docs/en/studio
touch "$FILE"
if grep -q "## $VERSION" "$FILE"; then
echo "Release notes already contain $VERSION"
exit 0
fi
ENTRY=$(cat <<EOF
## $VERSION ($DATE)
$(cat .tmp/final-notes.txt)
[Release Link]($URL)
EOF
)
awk -v entry="$ENTRY" '
/Latest/ && !inserted {
print
print entry
inserted=1
next
}
{ print }
END {
if (!inserted) print entry
}' "$FILE" > "$FILE.new"
mv "$FILE.new" "$FILE"
# -------------------------------------------------
# Update version-mapping.md (INSIDE table)
# -------------------------------------------------
- name: Update version-mapping.md (smart)
run: |
FILE="docs/en/studio/version-mapping.md"
STUDIO_VERSION="${{ github.event.client_payload.version }}"
ABP_VERSION="dev" # gerekiyorsa payload’dan alabilirsin
mkdir -p docs/en/studio
if [ ! -f "$FILE" ]; then
echo "| ABP Studio Version | ABP Version |" > "$FILE"
echo "|-------------------|-------------|" >> "$FILE"
echo "| $STUDIO_VERSION | $ABP_VERSION |" >> "$FILE"
exit 0
fi
python3 <<'EOF'
import re
from packaging.version import Version
file_path = "docs/en/studio/version-mapping.md"
studio = Version("${STUDIO_VERSION}")
abp = "${ABP_VERSION}"
with open(file_path) as f:
lines = f.readlines()
header = lines[:2]
rows = lines[2:]
new_rows = []
handled = False
for row in rows:
m = re.match(r"\|\s*(.+?)\s*\|\s*(.+?)\s*\|", row)
if not m:
new_rows.append(row)
continue
studio_range, abp_version = m.groups()
if abp_version != abp:
new_rows.append(row)
continue
# range cases
if "-" in studio_range:
start, end = [Version(v.strip()) for v in studio_range.split("-")]
if start <= studio <= end:
handled = True
new_rows.append(row) # already covered
elif studio == end.next_patch():
handled = True
new_rows.append(f"| {start} - {studio} | {abp} |\n")
else:
new_rows.append(row)
else:
v = Version(studio_range)
if studio == v:
handled = True
new_rows.append(row)
elif studio == v.next_patch():
handled = True
new_rows.append(f"| {v} - {studio} | {abp} |\n")
else:
new_rows.append(row)
if not handled:
new_rows.insert(0, f"| {studio} | {abp} |\n")
with open(file_path, "w") as f:
f.writelines(header + new_rows)
EOF
# -------------------------------------------------
# Check for changes
# -------------------------------------------------
- name: Check for changes
id: changes
run: |
git add docs/en/studio
if git diff --cached --quiet; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# -------------------------------------------------
# Commit & push
# -------------------------------------------------
- name: Commit & push
if: steps.changes.outputs.has_changes == 'true'
run: |
git commit -m "docs(studio): release ${{ github.event.client_payload.version }}"
git push -u origin "$BRANCH"
# -------------------------------------------------
# Create PR
# -------------------------------------------------
- name: Create PR
if: steps.changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.BOT_SECRET }}
run: |
PR_URL=$(gh pr create \
--title "docs(studio): release ${{ github.event.client_payload.version }}" \
--body "Automated documentation update for ABP Studio." \
--base "${{ github.event.client_payload.target_branch }}" \
--head "$BRANCH")
echo "PR_URL=$PR_URL" >> $GITHUB_ENV
# -------------------------------------------------
# Enable auto-merge (branch protection safe)
# -------------------------------------------------
- name: Enable auto-merge
if: steps.changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.BOT_SECRET }}
run: |
gh pr merge "$PR_URL" --squash --auto

9
docs/en/modules/ai-management/index.md

@ -9,14 +9,11 @@
> You must have an ABP Team or a higher license to use this module.
> **⚠️ Important Notice**
> The **AI Management Module** is currently in **preview**. The documentation and implementation are subject to change.
This module implements AI (Artificial Intelligence) management capabilities on top of the [Artificial Intelligence Workspaces](../../framework/infrastructure/artificial-intelligence/index.md) feature of the ABP Framework and allows to manage workspaces dynamically from the application including UI components and API endpoints.
This module implements AI (Artificial Intelligence) management capabilities on top of the [Artificial Intelligence Workspaces](../../framework/infrastructure/artificial-intelligence/index.md) feature of the ABP Framework and allows managing workspaces dynamically from the application, including UI components and API endpoints.
## How to Install
AI Management module is not pre-installed in [the startup templates](../solution-templates/layered-web-application). You can install it using the ABP CLI or ABP Studio.
The **AI Management Module** is not included in [the startup templates](../solution-templates/layered-web-application) by default. However, when creating a new application with [ABP Studio](../../tools/abp-studio/index.md), you can easily enable it during setup via the *AI Integration* step in the project creation wizard. Alternatively, you can install it using the ABP CLI or ABP Studio:
**Using ABP CLI:**
@ -40,7 +37,7 @@ AI Management module packages are designed for various usage scenarios. Packages
### Menu Items
AI Management module adds the following items to the "Main" menu:
The **AI Management Module** adds the following items to the "Main" menu:
* **AI Management**: Root menu item for AI Management module. (`AIManagement`)
* **Workspaces**: Workspace management page. (`AIManagement.Workspaces`)

3
docs/en/studio/version-mapping.md

@ -11,7 +11,8 @@ This document provides a general overview of the relationship between various ve
| **ABP Studio Version** | **ABP Version of Startup Template** |
|------------------------|---------------------------|
| 2.1.0 - 2.1.3 | 10.0.1 |
| 2.1.5 - 2.1.9 | 10.0.2 |
| 2.1.0 - 2.1.4 | 10.0.1 |
| 2.0.0 to 2.0.2 | 10.0.0 |
| 1.4.2 | 9.3.6 |
| 1.3.3 to 1.4.1 | 9.3.5 |

47
framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerUIOptionsExtensions.cs

@ -0,0 +1,47 @@
using System;
using System.Text;
using System.Text.Json;
using JetBrains.Annotations;
using Swashbuckle.AspNetCore.SwaggerUI;
using Volo.Abp;
namespace Microsoft.Extensions.DependencyInjection;
public static class AbpSwaggerUIOptionsExtensions
{
/// <summary>
/// Sets the abp.appPath used by the Swagger UI scripts.
/// </summary>
/// <param name="options">The Swagger UI options.</param>
/// <param name="appPath">The application base path.</param>
public static void AbpAppPath([NotNull] this SwaggerUIOptions options, [NotNull] string appPath)
{
Check.NotNull(options, nameof(options));
Check.NotNull(appPath, nameof(appPath));
var normalizedAppPath = NormalizeAppPath(appPath);
options.HeadContent = BuildAppPathScript(normalizedAppPath, options.HeadContent ?? string.Empty);
}
private static string NormalizeAppPath(string appPath)
{
return string.IsNullOrWhiteSpace(appPath)
? "/"
: appPath.Trim().EnsureStartsWith('/').EnsureEndsWith('/');
}
private static string BuildAppPathScript(string normalizedAppPath, string headContent)
{
var builder = new StringBuilder(headContent);
if (builder.Length > 0)
{
builder.AppendLine();
}
builder.AppendLine("<script>");
builder.AppendLine(" var abp = abp || {};");
builder.AppendLine($" abp.appPath = {JsonSerializer.Serialize(normalizedAppPath)};");
builder.AppendLine("</script>");
return builder.ToString();
}
}

6
framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.js

@ -2,11 +2,7 @@ var abp = abp || {};
(function () {
/* Application paths *****************************************/
//Current application root path (including virtual directory if exists).
var baseElement = document.querySelector('base');
var baseHref = baseElement ? baseElement.getAttribute('href') : null;
abp.appPath = baseHref || abp.appPath || '/';
abp.appPath = abp.appPath || '/';
/* UTILS ***************************************************/

2
framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js

@ -11,7 +11,7 @@ var abp = abp || {};
var oidcSupportedScopes = configObject.oidcSupportedScopes || [];
var oidcDiscoveryEndpoint = configObject.oidcDiscoveryEndpoint || [];
var tenantPlaceHolders = ["{{tenantId}}", "{{tenantName}}", "{0}"]
abp.appPath = configObject.baseUrl || abp.appPath;
abp.appPath = abp.appPath || "/";
var requestInterceptor = configObject.requestInterceptor;
var responseInterceptor = configObject.responseInterceptor;

8
npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts

@ -1,4 +1,4 @@
import { Component, inject, OnInit, ViewChild } from '@angular/core';
import { Component, inject, OnInit, viewChild } from '@angular/core';
import { DynamicFormComponent, FormFieldConfig } from '@abp/ng.components/dynamic-form';
import { FormConfigService } from './form-config.service';
@ -8,7 +8,7 @@ import { FormConfigService } from './form-config.service';
imports: [DynamicFormComponent],
})
export class DynamicFormPageComponent implements OnInit {
@ViewChild(DynamicFormComponent, { static: false }) dynamicFormComponent: DynamicFormComponent;
readonly dynamicFormComponent = viewChild(DynamicFormComponent);
protected readonly formConfigService = inject(FormConfigService);
formFields: FormFieldConfig[] = [];
@ -27,12 +27,12 @@ export class DynamicFormPageComponent implements OnInit {
alert('✅ Form submitted successfully! Check the console for details.');
// Reset form after submission
this.dynamicFormComponent.resetForm();
this.dynamicFormComponent().resetForm();
}
cancel() {
console.log('❌ Form Cancelled');
alert('Form cancelled');
this.dynamicFormComponent.resetForm();
this.dynamicFormComponent().resetForm();
}
}

8
npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts

@ -1,6 +1,5 @@
import {
Component,
ViewChild,
ViewContainerRef,
ChangeDetectionStrategy,
forwardRef,
@ -9,6 +8,7 @@ import {
DestroyRef,
inject,
input,
viewChild
} from '@angular/core';
import {
ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule
@ -34,7 +34,7 @@ export class DynamicFieldHostComponent implements ControlValueAccessor {
component = input<Type<ControlValueAccessor>>();
inputs = input<Record<string, any>>({});
@ViewChild('vcRef', { read: ViewContainerRef, static: true }) viewContainerRef!: ViewContainerRef;
readonly viewContainerRef = viewChild.required('vcRef', { read: ViewContainerRef });
private componentRef?: any;
private value: any;
@ -55,10 +55,10 @@ export class DynamicFieldHostComponent implements ControlValueAccessor {
}
private createChild() {
this.viewContainerRef.clear();
this.viewContainerRef().clear();
if (!this.component()) return;
this.componentRef = this.viewContainerRef.createComponent(this.component());
this.componentRef = this.viewContainerRef().createComponent(this.component());
this.applyInputs();
const instance: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl;

10
npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts

@ -6,7 +6,7 @@ import {
input,
Optional,
SkipSelf,
ViewChild,
viewChild
} from '@angular/core';
import { ControlContainer, ReactiveFormsModule } from '@angular/forms';
import {
@ -76,14 +76,14 @@ export class ExtensibleDateTimePickerComponent {
meridian = input<boolean>(false);
placement = input<Placement>('bottom-left');
@ViewChild(NgbInputDatepicker) date!: NgbInputDatepicker;
@ViewChild(NgbTimepicker) time!: NgbTimepicker;
readonly date = viewChild.required(NgbInputDatepicker);
readonly time = viewChild.required(NgbTimepicker);
setDate(dateStr: string) {
this.date.writeValue(dateStr);
this.date().writeValue(dateStr);
}
setTime(dateStr: string) {
this.time.writeValue(dateStr);
this.time().writeValue(dateStr);
}
}

14
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -19,9 +19,7 @@ import {
Optional,
SimpleChanges,
SkipSelf,
ViewChild,
signal,
effect,
viewChild,
} from '@angular/core';
import {
ControlContainer,
@ -72,8 +70,8 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
AsyncPipe,
NgComponentOutlet,
NgTemplateOutlet,
FormsModule
],
FormsModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ExtensibleFormPropService],
viewProviders: [
@ -98,7 +96,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
@Input() prop!: FormProp;
@Input() first?: boolean;
@Input() isFirstGroup?: boolean;
@ViewChild('field') private fieldRef!: ElementRef<HTMLElement>;
private readonly fieldRef = viewChild.required<ElementRef<HTMLElement>>('field');
injectorForCustomComponent?: Injector;
asterisk = '';
@ -158,9 +156,9 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
}
ngAfterViewInit() {
if (this.isFirstGroup && this.first && this.fieldRef) {
if (this.isFirstGroup && this.first && this.fieldRef()) {
requestAnimationFrame(() => {
this.fieldRef.nativeElement.focus();
this.fieldRef().nativeElement.focus();
});
}
}

6
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts

@ -6,9 +6,8 @@ import {
inject,
Input,
Optional,
QueryList,
SkipSelf,
ViewChildren,
viewChildren
} from '@angular/core';
import { ControlContainer, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
@ -41,8 +40,7 @@ export class ExtensibleFormComponent<R = any> {
private readonly extensions = inject(ExtensionsService);
private readonly identifier = inject(EXTENSIONS_IDENTIFIER);
@ViewChildren(ExtensibleFormPropComponent)
formProps!: QueryList<ExtensibleFormPropComponent>;
readonly formProps = viewChildren(ExtensibleFormPropComponent);
@Input()
set selectedRecord(record: R) {

18
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

@ -4,7 +4,6 @@ import {
ChangeDetectorRef,
Component,
computed,
ContentChild,
EventEmitter,
inject,
Injector,
@ -18,7 +17,8 @@ import {
SimpleChanges,
TemplateRef,
TrackByFunction,
ViewChild,
contentChild,
viewChild
} from '@angular/core';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
@ -141,17 +141,16 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
@Input() rowDetailHeight: string | number = '100%';
@Output() rowDetailToggle = new EventEmitter<R>();
@ContentChild(ExtensibleTableRowDetailComponent)
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
readonly rowDetailComponent = contentChild(ExtensibleTableRowDetailComponent);
@ViewChild('table', { static: false }) table!: DatatableComponent;
readonly table = viewChild.required<DatatableComponent>('table');
protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined {
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate;
return this.rowDetailComponent()?.template() ?? this.rowDetailTemplate;
}
protected get effectiveRowDetailHeight(): string | number {
return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight;
return this.rowDetailComponent()?.rowHeight() ?? this.rowDetailHeight;
}
hasAtLeastOnePermittedAction: boolean;
@ -318,8 +317,9 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
}
toggleExpandRow(row: R): void {
if (this.table && this.table.rowDetail) {
this.table.rowDetail.toggleExpandRow(row);
const table = this.table();
if (table && table.rowDetail) {
table.rowDetail.toggleExpandRow(row);
}
this.rowDetailToggle.emit(row);
}

6
npm/ng-packs/packages/components/page/src/page.component.html

@ -1,6 +1,6 @@
@if (shouldRenderRow) {
<div class="row entry-row">
@if (customTitle) {
@if (customTitle()) {
<ng-content select="abp-page-title-container"></ng-content>
} @else {
@if (title) {
@ -12,7 +12,7 @@
}
}
@if (customBreadcrumb) {
@if (customBreadcrumb()) {
<ng-content select="abp-page-breadcrumb-container"></ng-content>
} @else {
@if (breadcrumb) {
@ -22,7 +22,7 @@
}
}
@if (customToolbar) {
@if (customToolbar()) {
<ng-content select="abp-page-toolbar-container"></ng-content>
} @else {
@if (toolbarVisible) {

15
npm/ng-packs/packages/components/page/src/page.component.ts

@ -1,4 +1,4 @@
import { Component, Input, ViewEncapsulation, ContentChild } from '@angular/core';
import { Component, Input, ViewEncapsulation, contentChild } from '@angular/core';
import {
PageTitleContainerComponent,
PageBreadcrumbContainerComponent,
@ -37,19 +37,18 @@ export class PageComponent {
toolbar: PageParts.toolbar,
};
@ContentChild(PageTitleContainerComponent) customTitle?: PageTitleContainerComponent;
@ContentChild(PageBreadcrumbContainerComponent)
customBreadcrumb?: PageBreadcrumbContainerComponent;
@ContentChild(PageToolbarContainerComponent) customToolbar?: PageToolbarContainerComponent;
readonly customTitle = contentChild(PageTitleContainerComponent);
readonly customBreadcrumb = contentChild(PageBreadcrumbContainerComponent);
readonly customToolbar = contentChild(PageToolbarContainerComponent);
get shouldRenderRow() {
return !!(
this.title ||
this.toolbarVisible ||
this.breadcrumb ||
this.customTitle ||
this.customBreadcrumb ||
this.customToolbar ||
this.customTitle() ||
this.customBreadcrumb() ||
this.customToolbar() ||
this.pageParts
);
}

8
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html

@ -7,7 +7,7 @@
[nzData]="nodes"
[nzTreeTemplate]="treeTemplate"
[nzExpandedKeys]="expandedKeys"
[nzExpandedIcon]="expandedIconTemplate?.template || defaultIconTemplate"
[nzExpandedIcon]="expandedIconTemplate()?.template || defaultIconTemplate"
(nzExpandChange)="onExpandedKeysChange($event)"
(nzCheckboxChange)="onCheckboxChange($event)"
(nzOnDrop)="onDrop($event)"
@ -26,13 +26,13 @@
<div class="d-inline-flex align-items-center abp-ellipsis-inline">
<ng-container
*ngTemplateOutlet="
customNodeTemplate ? customNodeTemplate?.template : defaultNodeTemplate;
customNodeTemplate() ? customNodeTemplate()?.template : defaultNodeTemplate;
context: { $implicit: node }
"
/>
</div>
@if (menu) {
@if (menu()) {
<div
#dropdown="ngbDropdown"
class="d-inline-block ms-1"
@ -48,7 +48,7 @@
aria-hidden="true"
></i>
<div ngbDropdownMenu>
<ng-template *ngTemplateOutlet="menu; context: { $implicit: node }" />
<ng-template *ngTemplateOutlet="menu(); context: { $implicit: node }" />
</div>
</div>
}

8
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts

@ -2,7 +2,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
contentChild,
EventEmitter,
inject,
Input,
@ -60,9 +60,9 @@ export class TreeComponent implements OnInit {
dropdowns = {} as { [key: string]: NgbDropdown };
@ContentChild('menu') menu: TemplateRef<any>;
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective;
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective;
readonly menu = contentChild<TemplateRef<any>>('menu');
readonly customNodeTemplate = contentChild(TreeNodeTemplateDirective);
readonly expandedIconTemplate = contentChild(ExpandedIconTemplateDirective);
@Output() readonly checkedKeysChange = new EventEmitter();
@Output() readonly expandedKeysChange = new EventEmitter<string[]>();
@Output() readonly selectedNodeChange = new EventEmitter();

5
npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

@ -39,7 +39,7 @@ import {
OnInit,
TemplateRef,
TrackByFunction,
ViewChild,
viewChild
} from '@angular/core';
import {
AbstractControl,
@ -99,8 +99,7 @@ export class UsersComponent implements OnInit {
data: PagedResultDto<IdentityUserDto> = { items: [], totalCount: 0 };
@ViewChild('modalContent', { static: false })
modalContent!: TemplateRef<any>;
readonly modalContent = viewChild.required<TemplateRef<any>>('modalContent');
form!: UntypedFormGroup;

24
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

@ -15,21 +15,22 @@ import {
UpdatePermissionDto,
} from '@abp/ng.permission-management/proxy';
import {
afterNextRender,
Component,
computed,
DOCUMENT,
ElementRef,
EventEmitter,
inject,
Injector,
Input,
Output,
QueryList,
signal,
TrackByFunction,
ViewChildren,
viewChildren
} from '@angular/core';
import { concat, of } from 'rxjs';
import { finalize, switchMap, take, tap } from 'rxjs/operators';
import { of } from 'rxjs';
import { finalize, switchMap, tap } from 'rxjs/operators';
import { PermissionManagement } from '../models';
import { FormsModule } from '@angular/forms';
@ -116,6 +117,7 @@ export class PermissionManagementComponent
protected readonly service = inject(PermissionsService);
protected readonly configState = inject(ConfigStateService);
protected readonly toasterService = inject(ToasterService);
private readonly injector = inject(Injector);
private document = inject(DOCUMENT);
@Input()
@ -146,11 +148,9 @@ export class PermissionManagementComponent
this.openModal().subscribe(() => {
this._visible = true;
this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes)
.pipe(take(1))
.subscribe(() => {
this.initModal();
});
afterNextRender(() => {
this.initModal();
}, { injector: this.injector });
});
} else {
this.setSelectedGroup(null);
@ -162,10 +162,8 @@ export class PermissionManagementComponent
@Output() readonly visibleChange = new EventEmitter<boolean>();
@ViewChildren('selectAllInThisTabsRef')
selectAllInThisTabsRef!: QueryList<ElementRef<HTMLInputElement>>;
@ViewChildren('selectAllInAllTabsRef')
selectAllInAllTabsRef!: QueryList<ElementRef<HTMLInputElement>>;
selectAllInThisTabsRef = viewChildren<ElementRef<HTMLInputElement>>('selectAllInThisTabsRef');
selectAllInAllTabsRef = viewChildren<ElementRef<HTMLInputElement>>('selectAllInAllTabsRef');
data: GetPermissionListResultDto = { groups: [], entityDisplayName: '' };

7
npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts

@ -11,10 +11,9 @@ import {
ElementRef,
inject,
Input,
QueryList,
Renderer2,
TrackByFunction,
ViewChildren,
viewChildren
} from '@angular/core';
import { NgTemplateOutlet, AsyncPipe } from '@angular/common';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@ -41,7 +40,7 @@ export class RoutesComponent {
@Input() smallScreen?: boolean;
@ViewChildren('childrenContainer') childrenContainers!: QueryList<ElementRef<HTMLDivElement>>;
readonly childrenContainers = viewChildren<ElementRef<HTMLDivElement>>('childrenContainer');
rootDropdownExpand = {} as { [key: string]: boolean };
@ -52,7 +51,7 @@ export class RoutesComponent {
}
closeDropdown() {
this.childrenContainers.forEach(({ nativeElement }) => {
this.childrenContainers().forEach(({ nativeElement }) => {
this.renderer.addClass(nativeElement, 'd-none');
setTimeout(() => this.renderer.removeClass(nativeElement, 'd-none'), 0);
});

7
npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

@ -7,8 +7,8 @@ import {
OnInit,
Output,
Renderer2,
ViewChild,
inject,
viewChild
} from '@angular/core';
import { ABP, StopPropagationDirective } from '@abp/ng.core';
@ -70,8 +70,7 @@ export class ButtonComponent implements OnInit {
@Output() readonly abpBlur = new EventEmitter<FocusEvent>();
@ViewChild('button', { static: true })
buttonRef!: ElementRef<HTMLButtonElement>;
readonly buttonRef = viewChild.required<ElementRef<HTMLButtonElement>>('button');
get icon(): string {
return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`;
@ -81,7 +80,7 @@ export class ButtonComponent implements OnInit {
if (this.attributes) {
Object.keys(this.attributes).forEach(key => {
if (this.attributes?.[key]) {
this.renderer.setAttribute(this.buttonRef.nativeElement, key, this.attributes[key]);
this.renderer.setAttribute(this.buttonRef().nativeElement, key, this.attributes[key]);
}
});
}

10
npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts

@ -6,12 +6,12 @@ import {
ElementRef,
EmbeddedViewRef,
Type,
ViewChild,
AfterViewInit,
OnDestroy,
createComponent,
EnvironmentInjector,
DestroyRef,
viewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DOCUMENT } from '@angular/common';
@ -53,8 +53,7 @@ export class HttpErrorWrapperComponent implements OnInit, AfterViewInit, OnDestr
isHomeShow = true;
@ViewChild('container', { static: false })
containerRef?: ElementRef<HTMLDivElement>;
readonly containerRef = viewChild<ElementRef<HTMLDivElement>>('container');
get statusText(): string {
return this.status ? `[${this.status}]` : '';
@ -86,8 +85,9 @@ export class HttpErrorWrapperComponent implements OnInit, AfterViewInit, OnDestr
this.appRef.attachView(customComponentRef.hostView);
if (this.containerRef) {
this.containerRef.nativeElement.appendChild(
const containerRef = this.containerRef();
if (containerRef) {
containerRef.nativeElement.appendChild(
(customComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0],
);
}

Loading…
Cancel
Save