Skip to content

637301 Report layout override lifecycle - #9428

Merged
V-CWurtzen merged 20 commits into
mainfrom
features/637301-report-layout-override-lifecycle
Aug 17, 2026
Merged

637301 Report layout override lifecycle#9428
V-CWurtzen merged 20 commits into
mainfrom
features/637301-report-layout-override-lifecycle

Conversation

@V-CWurtzen

@V-CWurtzen V-CWurtzen commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Application-side implementation of CP0529-338: lifecycle/property changes to an extension-installed report layout now write a Tenant Report Layout Override record instead of copying the layout into Tenant Report Layout (which left the original visible with its default values).

  • Layout Status (Set Approved/Draft/Pending/Retired) on an extension layout writes an override for all companies. An extension layout is the same layout in every company, and the override records metadata about its lifecycle rather than changing the layout itself, so tenant-wide is the default rather than a special case.
  • Edit info overrides Description and IsObsolete (one-way — a layout already obsolete in metadata cannot be un-obsoleted), also for all companies. There is no scope control in the dialog; Available in All Companies is shown read-only and states the scope the override is written at. "Save Changes to a Copy" is kept as an opt-in escape hatch: ticking it makes the layout name, the copy's own company scope and the obsolete flag editable, because the copy is an ordinary tenant layout; unticking it restores the values the in-place override will actually write, so a read-only field never displays something the write contradicts.
  • Company precedence is preserved. Where a company already has a status override of its own, a further status change stays in that company. Such rows can no longer be created from the UI but may exist from an earlier version or a vendor's install codeunit. A batch run must still be single-scope — a mix is rejected — so one run is never ambiguous.
  • Only fields the user actually changed are written. Opening Edit info and pressing OK without editing anything creates no override record at all.
  • User-defined layouts are unchanged (in-place update in Tenant Report Layout).

The update path is chosen by Report Layout List."User Defined": true → in-place update; false → upsert a Tenant Report Layout Override record. Scope is resolved per field — from the Override Layout Status flag rather than from the mere existence of a row — so a description-only or obsolete-only row never drags a status change into the wrong scope.

Telemetry for the override path uses its own allocated event id 0000RTQ with metadata-only dimensions; the layout description is reported as a changed/not-changed flag, never as content.

Tests

Report Layouts Test (codeunit 139595) — 33 tests, covering the all-companies default for both the status and edit paths, company precedence where a company already owns the status, field-granular scope resolution in both directions, no-op edits writing nothing, one-way IsObsolete, the copy escape hatch, both the success and mixed-scope-rejection paths of a multi-layout batch run, and the pre-existing user-defined regression suite. A second shipped layout (MYLAYOUT2) was added to the test report so multi-layout scope scenarios are exercisable.

Two of those cover the dialog transitions raised in review: unticking Save Changes to a Copy after selecting company-only must restore the all-companies scope, and taking a copy of an obsolete extension layout must re-enable the obsolete field so the copy is not stuck obsolete.

Modal handlers record what they observe and the test asserts it after the dialog closes, rather than asserting inside the handler where a failure can be swallowed by the calling UI operation. LibraryVariableStorage is cleared at the start of every test and asserted empty at the end, so a leaked value cannot cascade and each dialog is proven to have fired exactly once.

The tests that cover the all-companies paths deliberately register no ConfirmHandler, so an unexpected prompt fails them — that is what pins the removal of the former confirmation.

Scope behaviour is additionally checked by hand across two companies, using a helper extension that surfaces the override scope the product page does not display.

Notes for reviewers

  • The all-companies confirmation that an earlier revision raised on the status path has been removed. Once tenant-wide is the normal scope, prompting on the everyday path trains users to dismiss the dialog without reading it; the change is layout metadata, not layout content. The mixed-scope rejection is kept.
  • AccessByPermission = tabledata "Tenant Report Layout" = M is deliberate: BaseSystemPermissionSet grants the tenant layout and override tables together, so it correctly represents "may manage layout status" and matches the page's own permission.
  • A dedicated permission for tenant-wide writes was considered and deliberately not introduced here — it would touch every layout object and belongs to a permission design owned by the application team; a request to revisit that model is being raised with them separately.
  • Deferred with the reasoning recorded on the relevant threads, and carried in the work item: improving the mixed-scope error to name the split (belongs with per-layout scope visibility), a pre-existing company-scoped row taking precedence in its own company, StatusChangedMessageHandler not inspecting the message it consumes (pre-existing), and the parameter shape of UpsertLayoutOverride (changes again with the company-scoped option).

AB#637301

@V-CWurtzen
V-CWurtzen requested a review from a team July 14, 2026 12:35
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Copilot PR Review

Iteration 1 · Outcome: completed

Knowledge source: https://github.com/microsoft/BCQuality@be1b92b624679f8c031061602e7d3a3b5f71a688

Findings by domain

Findings split into Knowledge-backed (cite a BCQuality article) and Agent (the agent's own judgement, no matching BCQuality rule).

Domain Findings Knowledge-backed Agent Inline Fallback
Accessibility 1 1 0 1 0
Agent 1 0 1 1 0
Privacy 1 1 0 1 0
Style 1 1 0 1 0

Totals: 3 knowledge-backed · 1 agent findings.

Orchestrator pre-filter (16 file(s) excluded)

  • layer-disabled (knowledge) : 16 file(s)

Findings produced by the Copilot CLI agent against BCQuality at be1b92b624679f8c031061602e7d3a3b5f71a688. Reply 👎 on any inline comment to flag false positives.

@github-actions github-actions Bot added this to the Version 29.0 milestone Jul 14, 2026
@JesperSchulz Jesper Schulz-Wedde (JesperSchulz) added the Integration GitHub request for Integration area label Jul 16, 2026
@V-CWurtzen

Copy link
Copy Markdown
Contributor Author

⚠️ CI compile is blocked on a platform artifact version, not on this code

The failing Compile Apps steps (all country builds, Clean + Default) report:

Foundation\Reporting\ReportLayoutsImpl.codeunit.al
AL0185  Table 'Tenant Report Layout Override' is missing

This PR references the platform table Tenant Report Layout Override (object 2000000248), which is the prerequisite platform change for CP0529-338. That table first ships in platform 29.0.52163.0, but this repo currently pins:

.github/AL-Go-Settings.json →  "artifact": "bcinsider/Sandbox/29.0.51936.0//latest"

51936 < 52163, so the table isn't present in the symbols CI compiles against, and the compile fails. The AL code itself is correct — it compiles cleanly against the newer platform symbols, and Report Layouts Test (codeunit 139595) passes 23/23 locally (status/edit overrides, global-scope confirmation, and mixed-scope rejection).

To unblock: the repo's platform artifact pin needs to advance to a public build that includes object 2000000248 (≥ 29.0.52163.0). Could a maintainer / the platform owner advise on when that version is available in the public bcinsider/Sandbox stream and when main's pin will move? I'll rebase and re-run once it's in.

@V-CWurtzen V-CWurtzen changed the title Features/637301 report layout override lifecycle 637301 Report layout override lifecycle Jul 20, 2026
Setting the Layout Status of an extension-installed layout (User Defined =
false) now writes a Tenant Report Layout Override record instead of a no-op,
rather than copying the layout into Tenant Report Layout.

- ReportLayoutsImpl: add UpsertLayoutOverride helper (writes only the flagged
  fields with their Override * flags; IsObsolete is one-way); reroute
  SetLayoutStatus non-user-defined branch to it; grant RIMD on
  Tenant Report Layout Override.
- ReportLayouts.page: relax CanModifyStatus so extension layouts can use the
  status actions.
- Tests: add TestSetExtensionLayoutStatusWritesOverride (asserts an override
  is written with no tenant copy); clear overrides in EnsureNewLayoutsAreCleaned.

Verified: codeunit 139595 all 19 tests pass.
Editing an extension-installed layout's info now writes a Tenant Report Layout
Override record (Description, one-way IsObsolete) instead of copying the layout.

- ReportLayoutsImpl.EditReportLayout: for a non-user-defined layout not being
  copied, write an override via UpsertLayoutOverride and exit; existing
  copy/in-place logic unchanged.
- ReportLayoutEditDialog: override mode for extension layouts - name read-only,
  Description editable, IsObsolete editable one-way (locked when already obsolete
  in metadata), AvailableInAllCompanies drives global vs company scope; copy kept
  as an opt-in escape hatch.
- ReportLayouts.page: EditLayout no longer forces the 'edit a copy instead'
  confirm for extension layouts; removed the now-unused label.
- Tests: add global-description and company-obsolete override tests (assert
  override written, no tenant copy).

Verified: codeunit 139595 all 21 tests pass.
Batch status actions (Set Approved/Draft/Pending/Retired) now resolve the
override scope per extension-installed layout and keep each run to one scope:

- LayoutStatusIsGlobalScope: current-company override wins (company scope);
  else an existing global override => global scop
- SetLayoutStatus: extension layouts override at the classified scope.
- SetLayoutStatusBatch: reject mixed-scope select
  confirm all-global changes (GlobalScopeConfirmQst) before applying;
  user-defined layouts still update in place and

Fresh extension layouts stay company-specific wit
shipped user-defined behaviour with minimal user-facing change.

Tests: add TestSetGlobalScopeExtensionLayoutStatusConfirmsAndUpdatesGlobal
(global override updated after confirm; no compan
Mixed-scope Error is not auto-tested (test report ships a single extension
layout); manually verifiable.
Wording/permission (ReportLayouts.page):
- Retire "user-defined" from the four Set<Status> tooltips; the status actions
  now cover extension-installed layouts too (via override).
- Document why the AccessByPermission gate stays tabledata "Tenant Report
  Layout" = M: an action takes a single tabledata permission, and the platform's
  BaseSystemPermissionSet grants Tenant Report Layout and Tenant Report Layout
  Override together, so this gate represents "may manage layout status" and
  matches the page's own permission.

Mixed-scope test coverage (Tests-Report):
- Add TestMixedScopeBatchStatusIsRejected, driving the internal Report Layouts
  Impl. codeunit directly (Tests-Report is in Bas a
  filtered Report Layout List spanning two extension layouts, asserting the
  mixed-scope error. A TestPage cannot multi-sele
- Add a second shipped layout (MYLAYOUT2, same rdl) to test report 139595 so
  multi-layout scope scenarios are exercisable.

Verified: codeunit 139595 all 23 tests pass.
@V-CWurtzen
V-CWurtzen force-pushed the features/637301-report-layout-override-lifecycle branch from 8257d10 to e17c5ff Compare July 21, 2026 09:47
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
CI compile (CodeCop/analyzers) rejects two report layouts sharing one
LayoutFile: AL0835 "The LayoutFile '…TestReportLayoutsReport.rdl' is already
being used." MYLAYOUT2 (added for mixed-scope test coverage) reused MYLAYOUT's
rdl. Point it at a dedicated copy, Layouts/TestReportLayoutsReport2.rdl.
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Stale Status Check Deleted

The Pull Request Build workflow run for this PR was older than 72 hours and has been deleted.

📋 Why was it deleted?

Status checks that are too old may no longer reflect the current state of the target branch. To ensure this PR is validated against the latest code and passes up-to-date checks, a fresh build is required.


🔄 How to trigger a new status check:

  1. 📤 Push a new commit to the PR branch, or
  2. 🔁 Close and reopen the PR

This will automatically trigger a new Pull Request Build workflow run.

Address the Copilot review findings on the extension-layout override edit path:

A (major usability) — stop the silent, invisible all-companies override:
- Add a dedicated "Override for all companies" control (page 9661), shown only
  for extension layouts; do NOT reuse "Available in All Companies" (layout
  availability) for override scope. Default = current company.
- EditReportLayout: write an override only for fields the user actually changed
  (no-op OK writes nothing); Confirm before any all-companies (global) override.

B — copy can be renamed: checking "Save Changes to a Copy" now re-enables the
Layout Name field.

C/D — telemetry: comment out the override Log for now (it reused event id
0000N0H with a changed schema and shipped the free-text description/PII);
re-enable later with a dedicated event id and no content dimension.

Tests: switch edit handlers to the new control, add ConfirmHandler to the
global-edit test, add TestEditExtensionLayoutNoOpWritesNoOverride.
Manual step-4 finding: answering No to the all-companies confirmation exited
EditReportLayout, so the Description the user had typed was silently discarded
with no feedback.

- ReportLayoutsImpl: run the edit dialog in a repeat..until loop. On a declined
  global confirmation, reopen the dialog with the entered values and the scope
  reset to the current company, so the edit can still be saved company-scoped
  (or cancelled deliberately).
- Confirm text now states the consequence: "Choose No to go back to the dialog
  with your changes, where you can apply them to the current company only."
- ReportLayoutEditDialog: add SetOverrideValues(Description, IsObsolete,
  ScopeIsGlobal) to re-seed the reopened dialog without SetupDialog (which would
  wipe the edits); IsObsolete only re-applied while that field is editable.
- Tests: add TestEditExtensionLayoutDeclineGlobalKeepsEditsAndSavesCompanyScope
  (+ ConfirmHandlerNo). The modal handler runs twice: pass 1 types a description
  and asks for global; pass 2 asserts the description survived and the scope was
  reset, then accepts. Asserts a company override with the typed description and
  NO global override.

Note: the post-loop body is kept inside a plain begin..end block so the existing
override/copy/in-place paths stay as they were (small diff); can be flattened if
preferred.
AL does not short-circuit `and`, so
  if AvailableInAllCompanies and (not Confirm(GlobalOverrideConfirmQst, false))
raised the prompt on every extension-layout edit, including company-scoped ones —
TestEditExtensionLayoutWritesCompanyObsoleteOverride failed with "Unhandled UI:
Confirm". Restored the nested ifs with a comment explaining why they must stay
nested.
Two manual-test findings.

Step 4 — declining the all-companies confirm gave no feedback. Reopening the
dialog (the earlier 1+3 attempt) is impossible: an AL Page variable cannot be
RunModal'd twice ("The object has been run. Use the function CLEAR(Page)") and
Clear would discard the entries we wanted to keep. That loop passed the automated
test — a ModalPageHandler never exercises the real page lifecycle — but failed
manually, so it is reverted. Now the confirm says No discards the changes (and how
to apply them to this company instead) and declining shows an explicit
"No changes were applied" message. Removed the now-unused SetOverrideValues.

Step 10e — "Save Changes to a Copy" silently created a company-scoped copy that
never appeared in other companies: Available in All Companies was hidden for
extension layouts and the override toggle was fed into the copy path. Restore the
shipped default (copy = all companies), show/enable the field once Copy is ticked,
and read the override control only when not copying.

Tests: TestEditExtensionLayoutDeclineGlobalWritesNothing (no override at either
scope) and TestCopyOfExtensionLayoutKeepsAllCompaniesScope (copy lands with empty
Company Name, no override row) — the copy path's scope had no coverage before.
AL evaluates a field's Visible property at page initialisation and does not
re-evaluate it when a variable changes, so Visible = (not OverrideMode) or
CreateCopy never showed the field after ticking Copy. Keep the field always
visible and let Editable — which is re-evaluated — carry the meaning: read-only
Yes while overriding an extension layout (a true statement), editable once the
user opts into a copy, whose company scope it controls. Override scope remains a
separate control, so the two meanings stay distinct.
A copy writes a tenant layout and no override, so leaving "Override for all
companies" live there offered a control that silently did nothing. Ticking Copy
now locks and clears it, and unlocks "Available in All Companies" (scope of the
copy); unticking restores the reverse. Tooltip states when each applies.
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
The company user is the audience: extension layouts join lifecycle management,
and the tenant-wide dimension is hidden from the everyday path. Scope is a
governance decision — in the sunrise case a company user usually has neither the
permission nor the judgement for it, and in the sunset case (obsoleting is
one-way) forcing all companies is the gatekeeper's act, not a side effect of an
edit dialog. Guarding the choice (default + confirm + a second toggle) kept
adding UI without removing the confusion; removing the choice removes the class.

- Edit dialog: drop the "Override for all companies" control (field, vars,
  accessor). An in-place edit of an extension layout always writes a
  CURRENT-COMPANY override; no prompt, no "no changes applied" message needed.
- "Available in All Companies" keeps its own meaning and serves only the copy
  path, so nothing is overloaded.
- Kept: one-way obsolete, only-changed-field writes, the copy escape hatch with
  rename, and the status path's derive+confirm as a safety net — a global
  override can still exist (e.g. seeded by a vendor install codeunit) and must
  never be flipped silently.
- Tests: TestEditExtensionLayoutWritesGlobalDescriptionOverride becomes
  TestEditExtensionLayoutWritesCompanyDescriptionOverride and now also asserts an
  everyday edit NEVER creates a global override; the decline test is removed
  (unreachable by design).
@V-CWurtzen

Copy link
Copy Markdown
Contributor Author

Review follow-up — all four findings addressed

Thanks — the review caught a real defect and two correctness issues. Summary of what changed since the review, plus one item deliberately left open.

🟡 A — silent all-companies override on Edit info ✅ fixed, by removing the choice

This was worse than reported: because the dialog hard-coded all-companies scope and the override
was applied unconditionally, opening Edit info and pressing OK without changing anything silently
wrote a global Override Description row
— invisible on the page, and (per the platform) it
outranks later layout-metadata updates in every company.

We first guarded it (explicit scope control defaulting to the current company + a confirmation), but
testing that design convinced us the guard rail was the wrong answer. Scope is a governance
decision, not a per-edit preference:
a company user adding layouts and moving them Draft → Approved
should not be deciding — or even seeing — that a change hits every company. That is most obvious
when sunsetting, where marking obsolete is one-way and tenant-wide.

So the everyday path no longer offers the choice at all:

  • An in-place edit of an extension layout always writes a CURRENT-COMPANY override. No scope
    control, no prompt.
  • Only changed fields are overridden — a no-op OK writes nothing (the original defect).
  • Available in All Companies keeps its own meaning and is used only by the copy path, so nothing is
    overloaded.
  • The batch status path keeps derive + confirm as a safety net: a global override may exist from
    elsewhere (e.g. seeded by a vendor's install codeunit), and we must never flip one silently.

A gatekeeper affordance for deliberate tenant-wide acts is a candidate follow-up. Note it would need a
permission story: BaseSystemPermissionSet grants Tenant Report Layout Override = RIMD, so today
anyone who can manage layouts can write a global override — a role gate would be UI-level unless the
app introduces its own permission set. Happy to take direction here.

🟡 B — copy could not be renamed ✅ fixed

LayoutNameEditable := CreateCopy in the override branch, so a copy can be given a distinct name (and
copying the same layout twice works).

Manual testing then found a related regression we introduced: while the scope toggle existed, the
copy path took its company scope from it, so copies landed company-scoped and never appeared in other
companies. With the toggle gone, the copy again uses Available in All Companies (default: all
companies, as before) — read-only while editing an extension layout in place, editable once you opt
into a copy.

🟠 C + 🟡 D — telemetry ⏸ deliberately parked (not an oversight)

Both are valid. The override path's Log is currently commented out with a TODO rather than
shipped wrong:

  • C: it reused event ID 0000N0H with a different custom-dimension schema — needs its own
    registered event ID
    .
  • D: it passed the user-typed Description as a custom dimension — that dimension is dropped; a
    non-content signal (e.g. a boolean) can replace it.

Question for a maintainer: what's the process for allocating a new telemetry event ID for BaseApp?
I'd rather register one than reuse or invent an ID.

Verification

  • Codeunit 139595, all green — incl. no-op-writes-nothing, an everyday edit writing a
    company-scoped override and never a global one, global-scope confirm on the status path,
    mixed-scope rejection, and copy-scope.
  • Manual two-company matrix (company-vs-global scope, precedence, one-way obsolete, user-defined
    regression). Worth noting: three issues were only catchable by hand — a [ModalPageHandler] never
    exercises the real page lifecycle (an AL Page can't be RunModal'd twice), TestPage ignores
    Visible, and nothing covered the copy path's scope.

Known caveat

The simplification landed after the manual matrix run, so the trimmed dialog is compile-clean and
covered by the automated suite but has had only a partial UI re-check — flagging it rather than
implying full manual coverage.

…00RTQ

Allocated a fresh tag from the number series rather than reusing 0000N0H, whose dimension schema (Old/New layout name and description) doesn't fit this path and is consumed by existing queries. Dimensions are metadata only - ReportId, LayoutName, DescriptionChanged, ObsoleteSet, OverrideScope (stable non-localized token via ScopeDimension) - so no user-entered text reaches telemetry. Closes review findings C and D.
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Security}$

The new in-place "Edit info" path for extension-installed layouts now reaches code that inserts or modifies "Tenant Report Layout Override" rows using the codeunit's elevated rimd permissions, but the EditLayout page action itself declares no AccessByPermission gate (unlike the SetApproved/SetDraft/SetPendingApproval/SetRetired actions, which explicitly gate on tabledata "Tenant Report Layout" = M). Because "Report Layouts Impl." has Access = Internal and Permissions = tabledata "Tenant Report Layout Override" = rimd, any user who can open the Report Layouts page and invoke Edit info can now write tenant-wide override rows for extension-installed layouts, whereas before this change extension layouts were read-only from this action for those users. Consider adding an explicit AccessByPermission (or equivalent) gate on EditLayout consistent with the other status-changing actions, or documenting why the existing read access to the page is an acceptable gate for override writes.

Agent judgement — not directly backed by a BCQuality knowledge article.

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.32.4

Comment thread src/Layers/W1/Tests/Report/ReportLayoutsTest.Codeunit.al
@V-CWurtzen

V-CWurtzen commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Compile Apps fails on every country — warning gate flags files this PR does not touch

All 44 Build Apps jobs fail with "New warnings were introduced in this PR." Eighteen warnings are reported, and ten of them are in files this PR never touches:

Warnings File In this PR?
4 × AL0607 (with deprecated) APIV1CustomersE2E.Codeunit.al no
3 × AL0607 APIV1VendorsE2E.Codeunit.al no
2 × AL0607 APIV1CompanyInfoE2E.Codeunit.al no
1 × AL0607 APIV1ShipmentMethodsE2E.Codeunit.al no
3 × AA0139, 5 × AA0217 ReportLayoutsTest.Codeunit.al yes — mine

This PR changes six files, all under Foundation/Reporting and Tests/Report. None of the APIV1*E2E codeunits is among them.

What changed: the head commit 3adc5c7636 is unchanged since 7 August and the full matrix passed on it (170 green) on 9 and 10 August. It began failing after the BCArtifact bump to 29.0.53514.0 (#10121). The source did not move; the compiler did.

Not transient: two complete matrix runs nine hours apart, with main having advanced in between, produce byte-identical annotations.

mirkobiasini — is the warning baseline being rebuilt against the new artifact, or are PRs expected to absorb the new compiler's warnings?

I have eight genuine warnings of my own in ReportLayoutsTest.Codeunit.al and will fix them — but since the gate fails on the union, fixing only mine still leaves the ten AL0607 and the job still red. I would rather push that fix once, when the gate can actually pass, than spend a matrix run per attempt.

Happy to be told I have misread this.

@mynjj Joshua (mynjj) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very verbose PR, I recommend removing most of the comments, for doc comments local procedure shouldn't need them. I haven't gone through everything, but there is at least one unnecessary procedure (overly verbose code).

Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutEditDialog.page.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated
Comment thread src/Layers/W1/BaseApp/Foundation/Reporting/ReportLayoutsImpl.codeunit.al Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes:

S1 - Copy-to-override toggle displays the wrong scope

In ReportLayoutEditDialog.page.al, switching Save Changes to a Copy back off only disables Available in All Companies; it does not restore its value to true. If the user selected company-only while copy mode was enabled and then returns to override mode, the dialog displays a read-only No, while EditReportLayout silently forces AvailableInAllCompanies := true and writes a tenant-wide override. The displayed scope can therefore contradict the actual write.

Please restore the value to true when CreateCopy = false in override mode and add a regression test covering toggle on, select company-only, toggle off, and save.

S2 - Copying an obsolete extension layout cannot clear obsolete

For an extension layout already marked obsolete, SetupDialog sets IsObsoleteEditable := false. Selecting Save Changes to a Copy does not re-enable the field, so the new user-defined copy is forced to remain obsolete. This regresses the previous copy flow, where the copy's metadata was editable.

Copy mode should restore normal tenant-layout editability, while switching back to override mode should reapply the one-way obsolete lock. Please add coverage for copying an obsolete extension layout.

S3 - Current head does not pass validation

The current head SHA fails the PR build because it introduces three AA0139 warnings and five AA0217 warnings. There is also an accepted unresolved test issue in this PR: assertions inside two new modal handlers can be swallowed, so those tests do not reliably pin the obsolete-lock and default-scope behavior.

The core override and field-granular scope implementation is thoughtfully covered, but these UI transition defects and the current validation failures should be resolved before merge.

Mirko and Joshua reviewed the comment density and both landed on the same point:
implementation comments are a second thing to maintain and go out of sync, and
too many of them read as generated. Trimmed across all four files on one rule -
keep a comment only where it records why and the reason cannot be recovered from
the code, delete anything restating what. The note on OverrideCompany survives
because the fallback looks like belt-and-braces without it, but at two lines
rather than six. Telling Niels-Henrik earlier that this was prototype quality and
the comments would come out later does not survive the app team joining, so they
come out now.

Inline ScopeDimension, which had a single consumer, and drop the parentheses
around single conditions - both flagged as unidiomatic. The parenthesised lines
predate this branch but a re-indent had already pulled them into the diff, so
cleaning them adds no attribution that was not there.

Convert the two modal handlers this pull request introduced. An assertion that
fails inside a handler can be swallowed by the calling UI operation, so both
tests could have stayed green while the obsolete lock or the default scope
regressed - which defeats the reason they were written. The handlers now record
the observed value and the tests assert it after the modal closes, which is
stronger than asserting in the handler at all. The pre-existing-handler argument
that deferred this last round does not apply to handlers added here.

Fix the eight warnings the new compiler reports in the test codeunit: CopyStr
around CompanyName for the three Text to Text[30] assignments, and labels for the
five StrSubstNo format strings.

Verified: test project compiles clean with CodeCop and no AA0139 or AA0217 remain
in the codeunit; BaseApp has no diagnostics in Foundation/Reporting; codeunit
139595 is 31/31 including both converted handlers.
Comment thread src/Layers/W1/Tests/Report/ReportLayoutsTest.Codeunit.al
…pass

Predrag filed these as requested changes on 13 August. His approval an hour
later replaced the review state, so the body stopped showing in every summary
view and nobody actioned it for two days.

S1 - unticking Save Changes to a Copy disabled the availability field without
restoring its value, so a user who had chosen company-only saw a read-only No
while the in-place edit wrote an all-companies override. The dialog contradicted
what was written. Both the scope and the obsolete value are now restored when
copy mode is left, so a locked field never displays something the write denies.

S2 - the one-way obsolete lock was applied once at setup and never revisited, so
taking a copy of an obsolete extension layout produced a copy that could not be
un-obsoleted. The lock now belongs to the override path only: copy mode restores
normal tenant-layout editability and switching back reapplies it, which is the
shape he asked for. Remembering the metadata value in ObsoleteInMetadata is what
makes reapplying possible.

Two regression tests written to his sequences: toggle on, choose company-only,
toggle off, save; and copying an obsolete layout. 33/33.

Finish the comment pass. Joshua's review body asked for most comments removed and
said local procedures should not carry doc comments, so the last doc block on a
local procedure is gone along with a duplicate of the SetLayoutStatus summary and
four comments that restated their own code. Comment density on added BaseApp
lines is 10 percent, against 24 at the start of the pass; the implementation
codeunit sits at 6 percent where it was 5 before this branch existed.

Close the queue-lifecycle finding the way its own knowledge article prescribes:
Clear when a test starts, AssertEmpty when it ends, so a leaked value cannot
cascade and every enqueued interaction is proven to have fired exactly once.
@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Security}$

The 'Edit info' action (EditLayout) on page 9660 "Report Layouts" now unconditionally calls ReportLayoutsImpl.EditReportLayout for both user-defined and extension-installed layouts. Previously, editing a non-user-defined layout required a Dialog.Confirm warning and only ever created a separately-owned copy. Now, confirming OK on the edit dialog for a non-user-defined layout calls UpsertLayoutOverride, writing a global (Company Name = '') 'Tenant Report Layout Override' record that changes Description/Obsolete status of a vendor-supplied layout for every company on the tenant, with no confirmation and no AccessByPermission gate on the action. By contrast, the sibling status-change actions (SetApproved, SetDraft, SetPendingApproval, SetRetired) that mutate the same class of data via the same internal codeunit are explicitly gated with AccessByPermission = tabledata "Tenant Report Layout" = M. EditLayout has no equivalent gate, widening the mutation's blast radius without a matching authorization boundary. Recommend adding an AccessByPermission guard on EditLayout consistent with the other mutation actions, and/or restoring an explicit confirmation before writing a global override record.

Agent judgement — not directly backed by a BCQuality knowledge article.

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@github-actions

Copy link
Copy Markdown
Contributor

$\textbf{🟡\ Medium\ Severity\ —\ Testing}$

SetLayoutStatusBatch's new mixed-scope guard only classifies scope for non-user-defined (extension-installed) rows; user-defined rows are skipped by the classification loop entirely even though a user-defined Tenant Report Layout also has a company scope of its own. The new test suite only exercises batches made up entirely of extension-installed layouts — there is no test mixing a user-defined layout with company-scoped/global-scope extension-installed layouts in one batch, so intended behavior for that combination is unverified. Recommend adding such a test.

Agent judgement — not directly backed by a BCQuality knowledge article.

Line mapping was unavailable, so this was posted as an issue comment.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4

@V-CWurtzen

Copy link
Copy Markdown
Contributor Author

All review points addressed — b7dd614

Predrag Maricic (@PredragMaricic) — your requested changes of 13 August were not actioned for two days, and that is on me: the approval that followed replaced the review state, so the body stopped appearing in the summary views I was working from. Both defects are real and both are now fixed.

S1 — copy-to-override toggle displayed the wrong scope. Unticking Save Changes to a Copy disabled Available in All Companies without restoring its value, so a user who had chosen company-only saw a read-only No while EditReportLayout wrote an all-companies override. Leaving copy mode now restores both the scope and the obsolete value, so a locked field cannot display something the write contradicts. Regression test written to your sequence — toggle on, choose company-only, toggle off, save — as TestUntickingCopyRestoresAllCompaniesScope, asserting the displayed value and that the override landed globally with no company row.

S2 — copying an obsolete extension layout could not clear obsolete. The one-way lock was applied once in SetupDialog and never revisited, so copy mode inherited it. It now belongs to the override path only: IsObsoleteEditable := CreateCopy or (not ObsoleteInMetadata), with the metadata value remembered so switching back reapplies the lock — the shape you asked for. Covered by TestCopyOfObsoleteExtensionLayoutCanClearObsolete.

S3 — head did not pass validation. The three AA0139 and five AA0217 warnings are fixed, and the two modal handlers no longer assert inside the handler body. All Build Apps jobs are green on this head.

Joshua (@mynjj) — on your review comment, the comment pass went further than the three inline threads. Comment density on the added BaseApp lines is 10%, down from 24%; ReportLayoutsImpl.codeunit.al is at 6% against the 5% it had before this branch existed; and no local procedure added here carries a doc comment. ReportLayoutEditDialog.page.al now has none of my comments at all. Looking for drift also turned up a comment referencing a test that had been renamed when its behaviour inverted — so it named something that no longer existed and asserted the opposite of the code. You said you had not gone through everything; anything further is welcome.

State: codeunit 139595 is 33/33, including the two new regression tests, run against a locally built Base Application carrying these fixes. Remaining open threads are answered individually — two deferred to the company-scoped follow-up with the reasoning recorded in the work item, and two declined on the substance of the cited rules.

@V-CWurtzen
V-CWurtzen added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 85bc9e2 Aug 17, 2026
332 of 338 checks passed
@V-CWurtzen
V-CWurtzen deleted the features/637301-report-layout-override-lifecycle branch August 17, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Integration GitHub request for Integration area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants