Browse Source

Merge branch 'dev' into skoc/action

pull/24811/head
selman koc 1 day ago
committed by GitHub
parent
commit
d9bc5b82e9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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. 3
      docs/en/studio/version-mapping.md
  5. 8
      npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts
  6. 8
      npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts
  7. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts
  8. 14
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  9. 6
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts
  10. 18
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  11. 6
      npm/ng-packs/packages/components/page/src/page.component.html
  12. 15
      npm/ng-packs/packages/components/page/src/page.component.ts
  13. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html
  14. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts
  15. 5
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  16. 24
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts
  17. 7
      npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts
  18. 7
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  19. 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

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 |

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