I noticed that if you enable domain config override on system information, if you go to account settings the button displays as if it was enabled, because it shows remove, but `user.mail` has definitely not been enabled yet. Not sure what connection there is between these two forms, but I saw this on my project and on a bare install.

Issue fork domain-3593113

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

hanoii created an issue. See original summary.

mably’s picture

Status: Active » Postponed (maintainer needs more info)

Probably the same configuration ID.

hanoii’s picture

hmm, no, one is system.settings, the other is user.mail. And I can confirm that saving any domain config on the account settings saves the default collection.

hanoii’s picture

One is: /admin/config/system/site-information

The other is: /admin/config/people/accounts

hanoii’s picture

Status: Postponed (maintainer needs more info) » Active
mably’s picture

Confirmed, and it is a pre-existing bug, unrelated to the recent edit-context or switcher work. The relevant line dates back to 2025-09-03.

What is happening

Core's AccountSettingsForm::getEditableConfigNames() returns three config names, with system.site first:

['system.site', 'user.mail', 'user.settings']

domain_config_ui reduces that list to its first element when checking registration:

// DomainConfigFactory::isConfigurationRegisteredForDomain()
$name = is_array($names) ? current($names) : $names;
if (isset($overridable_configurations[$name])) {
  return in_array($domain_id, $overridable_configurations[$name]);
}

So the account settings form is judged entirely by system.site. Once you enable the override on the site information form, system.site is registered for the domain, and on the account settings form the check looks at current($names) (which is system.site), finds it registered, and renders the "remove" button, even though user.mail and user.settings were never enabled.

That also explains why saving account settings writes to the default collection: the per-domain override only engages for registered names, and user.mail / user.settings are not registered. The button is wrong; the save is correct. The same first-name reduction is also in getActiveEditingDomainId().

The only "connection" between the two forms is that AccountSettingsForm lists system.site first and domain_config_ui only inspects the first name.

Possible solutions

The root cause is the current($names) reduction. Dropping it and evaluating per config name is the direction, but the toggler is a single button for a multi-config form, so "registered" needs a defined meaning:

  1. Treat the form as registered only when all of its config names are registered for the domain, and make enable/remove act on all of them. The button then reflects the whole form rather than just its first config.
  2. Decide the button state from the form's own names while ignoring incidentally shared, globally registered names like system.site. More targeted, but needs a rule for what counts as the form's own config.

There is a real subtlety either way: system.site is legitimately shared, so once registered it is genuinely overridable wherever it is edited, including this form. The mismatch is a binary toggle sitting on a heterogeneous set of config names. Whatever rule we pick, the per-name save behavior should stay name-scoped (the edit context already supports getDomainId($config_name) per name; the factory collapses it to the first name before calling it).

mably’s picture

Digging into the fix, I found the root-cause method is shared between two concerns, which makes the naive fix risky.

isConfigurationRegisteredForDomain() drives the toggler (via isRegisteredConfiguration()), but the same path also gates the config load routing through isPerDomainEditable():

// DomainConfigFactory::doLoadMultiple(array $names, ...)
if ($immutable || !$this->isPerDomainEditable($names)) {
  return parent::doLoadMultiple($names, $immutable);
}

doLoadMultiple() passes the whole array, and today's current($names) reduction makes it all-or-nothing on the first name. So simply switching isConfigurationRegisteredForDomain() to "all names registered" would regress a batch load: loading ['system.site', 'user.mail', ...] when only system.site is registered would then return FALSE and load system.site from the default collection, dropping its override in that batch.

The save path is not affected: it goes through doGet($name) with a single name, where "all names" equals that one name. That is also why account settings correctly saves user.mail / user.settings to the default collection.

Plan

  1. Change isConfigurationRegisteredForDomain() to "all names registered for the domain", which fixes the toggler showing "remove" when only a shared config like system.site is registered.
  2. Make doLoadMultiple() route per name: load each registered name from the domain collection and each unregistered name from the parent, instead of all-or-nothing. This removes the regression above and is more correct than the current first-name behavior.

doGet() (single name) is unchanged. The toggler description will also list the config names the enable/remove action touches, so the shared-config behavior is transparent.

Tests: one asserting the toggler shows "Enable" until all of a multi-config form's names are registered (using AccountSettingsForm's ['system.site', 'user.mail', 'user.settings']), and one asserting a registered config still loads from its domain collection when batch-loaded with an unregistered sibling.

mably’s picture

Status: Active » Needs review
mably’s picture

Implemented in MR !390 (against 3.x), with a correction to my earlier comment.

On a closer look the load path is not affected, so the fix is simpler than the two-part plan I described. doGet() calls doLoadMultiple([$name], ...) with a single-element array, and loadMultiple() is immutable, so doLoadMultiple() returns before the registration check is ever reached with a multi-name array. The only caller passing a genuine multi-name set is the toggler, so changing isConfigurationRegisteredForDomain() to require all names needs no matching doLoadMultiple() change.

The fix: isConfigurationRegisteredForDomain() now returns true only when every config name in the set is registered for the domain, so the account settings toggler correctly shows "Enable" until user.mail and user.settings are registered, not just system.site. The toggler also lists the affected config names as a hover title.

Added a kernel test that fails before the change and passes after. Setting to needs review.

mably’s picture

@hanoii does it fix your problem? Can we merge it?

hanoii’s picture

@mably, sorry, I didn't know you were waiting for my feedback. I've seen you merge things quicker than I could review them and I thought this was going to be the same :) - reviewing now.

hanoii’s picture

Will continue in a bit, thought it was doing something odd but it was me misunderstanding something.

hanoii’s picture

Ok, it seems to work very good: this are a the things I noticed which confused me, only noting them here:

- Saving a config that's the same as the default stores the config with an empty { }, at first this got be worried as I didn't understand why it was doing that. In my was it was a unique part where I manually configured the default system.site to be the same as the main domain which happen to also have a override. I understand now that only what's different is stored in the config collection. There is a bug around this though, I'll mention it below, but it's unrelated to this issue.
- Overriding the account settings for a domain and clicking save, also saves system.site. I guess that's how it is, but modified by configs in the same way as above.

The bug:

- When the configs are saved empty with {}, they are left in a state that both drush cim and drush cex report them as modified, and you cannot remove those. You import them, you run drush cim again and it's still report as modified. Obviously --diff shows no difference. EDIT: I think when the override is empty (same as default) it should remove the config, not save it empty.

mably’s picture

Thanks @hanoii for your valuable feedback.

I'll merge this one and create a dedicated issue for the bug you mention.

  • mably committed 6534c48e on 3.x
    fix: #3593113 domain_config_ui: account settings shows "remove" button...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably’s picture

@hanoii can you verify that MR 391 in related issue fixes the bug you reported?

  • mably committed 74db1dca on 3.0.x
    fix: #3593113 domain_config_ui: account settings shows "remove" button...
hanoii’s picture

Status: Fixed » Needs work
StatusFileSize
new126.24 KB

I am reopening this issue because I found an unwanted side effect. Not sure if you would have preferred a new issue but I thought best to keep it around this one:

When you remove the domain config override on account settings (the remove button), it warns you about removing all three configs:

screenshot of config removal

This will make you lose any other override on system.site that you probably don't want. This is likely a hard one to fix.

Thinking out loud:

- I. checked the code on this form (account settings) and a quick look shows most of the fields have #config_target - not sure why the are still using getEditableConfigNames(), maybe they just didn't remove it yet. ON purely config_target'ed forms you the path is clear, you would need to revert to default only the config_target’ed elements of each config. Not sure how hard would that be to implement.
- But... as a lot of forms doesn't have that and some might have a mixed of config_target'ed ones and not, what to do there? One option might to give the user one control button per config on geteditableconfignames(). So you enable and disable overrides individually.
- You could track the getFormId()s where the override happen, so if you have overridden system.site on system_site_information_settings and you are removing it from user_admin_settings you can do a warning or simply reject this removal.

mably’s picture

Agreed, let us go with one control per config (one enable/remove button per config name) instead of a single button for the whole form.

Why this fits: the remove button keeps its meaning, a total removal of an override, but it now acts on a single config at a time. On the account settings form you would get separate controls for system.site, user.mail and user.settings, so clearing the user-related overrides no longer takes system.site (and the name and slogan set on Site information) down with it. You only remove system.site if you explicitly act on its own control.

It also tidies up the original fix here: instead of one toggler whose state had to be derived from "all config names registered", each config shows its own state independently (enable if not yet overridden for the domain, remove if it is). Registration is already per config name, so this is the natural model.

Background that makes the bundling the real culprit: the account settings form contributes only system.site:mail_notification to system.site (a single #config_target); the name and slogan belong to Site information. So both forms share system.site but edit different keys.

Scope note: this is a UX change to the toggler introduced in this issue, so it can land here or as a dedicated "per-config override controls" follow-up. A shared config is still fully removed when you act on its own control (that is intended), so an extra confirmation note for shared configs is optional.

hanoii’s picture

Hi @mably,

Just checking in to see if you had any chance to look into this. Thanks!

mably’s picture

Looking into what exactly?

hanoii’s picture

About adding one button per override to prevent accidentally removing configs that are you don{t want it to be removed. What I mentioned above on https://www.drupal.org/project/domain/issues/3593113#comment-16607227

mably’s picture

Ah, ok. Will see what I can do.

mably’s picture

Hi @hanoii, could you give a try to MR 395?

The implemented solution uses checkboxes on the deletion confirmation page instead of multiple buttons.

Deletion confirmation

mably’s picture

Status: Needs work » Needs review
hanoii’s picture

It seems to work nice. The one thing I'd add is that when there's a single config, there shouldn't be a need for the checkbox.

Also a side comment - the UI of the review setting is a little bit intimidating. config is small, title is huge, and content in the middle. I wonder if something like a review setting title and then one collapsible detail per config or simply a bit of more reasonable font sizes. I also wonder if there could bea diff like view, like when you see a config difference in the configuration synchronize tab.

mably’s picture

Thanks for testing. I pushed updates to !395:

  • A single configuration no longer shows a checkbox: there is nothing to choose, so it is confirmed directly.
  • Each configuration's stored values now sit in their own collapsible section, so a long override no longer crowds the page.
  • A registered configuration that carries no override values for the domain now shows a "Not overridden for this domain." note instead of an empty block.

On the font sizes and the large title: those come from the core confirmation form and the admin theme, so I would rather not fight them with custom CSS in this module.

The config-sync style diff view is a nice idea, but it is a fair amount of work and a bit outside the scope of a delete confirmation. Could we track it as a separate feature request so it does not hold up this fix?

Deletion confirmation

mably’s picture

hanoii’s picture

Agreed on the separate feature request for the diff and more than OK with not fighting core's css. It looks a lot better, with the different collapsible blocks. I noticed the single domain starts expanded, which is good. I'd think I prefer if all collapsible blocks are expanded by default. More often than not you will want to see the differences, and immediately gather those that have no overrides. Having them all expanded and you manually collapsing them as you see best is preferable IMO.

mably’s picture

Good point, done in MR 395: every collapsible section, including the per-config ones, is now expanded by default, so all the overrides and any non-overridden configs are visible at a glance. They can still be collapsed individually as needed.

hanoii’s picture

Great! What an amazing job you are doing with this module suite!

mably’s picture

Thanks @hanoii! It's a lot of work for sure :)

Merging.

  • mably committed 1fd9b94c on 3.x
    Fix: #3593113 Add per-config checkboxes to the override delete...

mably’s picture

Opened !396 as the 3.0.x backport of !395. The DeleteForm change is identical; only the unrelated @internal docblock (absent on 3.0.x) was dropped, and the test update went to the domain_config_ui copy of DomainConfigUiSavedConfigTest since the language UI is not a separate submodule on 3.0.x.

  • mably committed c4e43dfc on 3.0.x
    task: #3593113 [3.0.x backport] Per-config checkboxes on the override...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.