Skip to content

[2.x] feat(core, mentions): implement ability to order groups#4350

Open
DavideIadeluca wants to merge 21 commits intoflarum:2.xfrom
glowingblue:di/user-group-ordering
Open

[2.x] feat(core, mentions): implement ability to order groups#4350
DavideIadeluca wants to merge 21 commits intoflarum:2.xfrom
glowingblue:di/user-group-ordering

Conversation

@DavideIadeluca
Copy link
Contributor

@DavideIadeluca DavideIadeluca commented Jan 24, 2026

Fixes #0000

Changes proposed in this pull request:

Reviewers should focus on:

  • Backend, how to handle the Guest and Member Group here? The DB migration does set a position, but the first time the order is changed in the frontend those groups' position would be set back to null. From a usability point of view it doesn't matter, but it could be argued it's a quirk we don't wanna have
  • Also the changes in the webpack config.. it appears like that the current minification step from Terser fails to parse the esm build of sortable js due to unsupported syntax. The alias I added forces webpack to use sortables UDM build which appears to fix the issues. Really unsure how we should address this issue and if maybe changes in flarums webpack config itself are needed.

This was the error message (reproducible both in CI and when running yarn build locally):

ERROR in admin.js
  admin.js from Terser plugin
  Unexpected token: punc ({) [webpack://../node_modules/sortablejs/modular/sortable.esm.js:1212,31][admin.js:22591,37]
      at js_error (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:536:11)
      at croak (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1264:9)
      at token_error (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1272:9)
      at unexpected (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1278:9)
      at semicolon (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1316:56)
      at simple_statement (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1577:73)
      at statement (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1390:19)
      at _embed_tokens_wrapper (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:1329:26)
      at block_ (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:2168:20)
      at _function_body (/home/runner/work/framework/framework/node_modules/terser/dist/bundle.min.js:2080:21)

Screenshot
https://github.com/user-attachments/assets/8dcb047b-bd52-435e-a5b1-a5da46673664

Necessity

  • Has the problem that is being solved here been clearly explained?
  • If applicable, have various options for solving this problem been considered?
  • For core PRs, does this need to be in core, or could it be in an extension?
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation.
  • Backend changes: tests are green (run composer test).
  • Core developer confirmed locally this works as intended.
  • Tests have been added, or are not appropriate here.

Required changes:

  • Related documentation PR: (Remove if irrelevant)

@DavideIadeluca DavideIadeluca changed the title [2.x ] feat(core, mentions): implement ability to order groups [2.x] feat(core, mentions): implement ability to order groups Jan 24, 2026
@imorland imorland added this to the 2.0.0-beta.7 milestone Jan 24, 2026
@DavideIadeluca DavideIadeluca marked this pull request as ready for review January 29, 2026 09:10
@DavideIadeluca DavideIadeluca requested a review from a team as a code owner January 29, 2026 09:10
Copy link
Member

@imorland imorland left a comment

Choose a reason for hiding this comment

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

Thanks for this PR — group ordering is a welcome addition and the overall structure is good. A few issues need addressing before this can merge, from correctness problems down to minor polish. See inline comments for specifics.

Summary of required changes:

  • sortGroups: handle null positions (same pattern as sortTags)
  • GroupBar: remove abstract or use a concrete subclass in PermissionsPage — cannot instantiate an abstract class directly
  • GroupResource: add ->readonly() to the position field
  • OrderGroupsController: add input validation (type-check, verify IDs are real groups, protect Guest/Member)
  • Webpack alias: this fix belongs in flarum-webpack-config, not just core — otherwise extension authors who bundle sortablejs will hit the same Terser error
  • Clean up the two chore: debugging commits before merge (squash or rebase)
  • Error handling on the app.request in onSortUpdate — revert on failure

Questions for you:

  • Is GroupBar intended to be a base class that extensions subclass (as the twofactor example suggests), or should it be directly usable? If the former, PermissionsPage needs a concrete DefaultGroupBar extends GroupBar.
  • For the webpack fix — do you want to explore moving sortablejs to a Flarum external (exposed from core bundle) rather than an alias? That would be cleaner long-term.

app.store.getById<Group>('groups', id)?.pushData({
attributes: { position: i },
});
});
Copy link
Member

Choose a reason for hiding this comment

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

app.request is fire-and-forget with no error handling. If the request fails, SortableJS has already reordered the DOM and the store has been updated via pushData, leaving the UI and server in an inconsistent state.

At minimum, revert the store positions on failure:

const previousPositions = order.map((id, i) => ({
  id,
  position: app.store.getById<Group>("groups", id)?.position() ?? null,
}));

app.request({ ... }).catch(() => {
  previousPositions.forEach(({ id, position }) => {
    app.store.getById<Group>("groups", id)?.pushData({ attributes: { position } });
  });
  m.redraw();
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

app.request is fire-and-forget with no error handling.

This is not quite correct. app.request() actually has a ton of error handling. But yes it's true that, should the request fail, that the UI and Server are in an inconsistent state.

This is the same behaviour as for when ordering Tags or other implementations of app.request() like dismissing flags or changing permissions. Should those implementations also be adapted to make sure that the UI state is correctly reflected when the server returns an error? If yes let's create a follow up issue for this, If no I'd say the implementation in this PR just follows existing standards

output: {
library: 'flarum.core',
},
resolve: {
Copy link
Member

Choose a reason for hiding this comment

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

This alias fixes the Terser/ESM issue for core, but any extension that bundles sortablejs directly will hit the same error. The fix should live in flarum-webpack-config so it applies everywhere, or sortablejs should be exposed as an external from the core bundle (similar to how mithril/jQuery are handled), so extensions can reference it without bundling their own copy.

Could you look into moving this to flarum-webpack-config? Or would you prefer we handle that separately?

Copy link
Contributor Author

@DavideIadeluca DavideIadeluca Feb 25, 2026

Choose a reason for hiding this comment

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

Yeah I'd prefer if we handle this separately. Open to moving this PR back to draft until that is done or proceed with this PR and make the webpack fix later. Can create a follow up issue if you woud like

if ($order === null) {
return new EmptyResponse(422);
}

Copy link
Member

Choose a reason for hiding this comment

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

A few input validation gaps here:

  1. No type check — $order could be a non-array (e.g. a string), which would make foreach throw.
  2. No validation that the IDs in $order are real group IDs — an invalid ID silently does nothing but the reset at line 33 (update position = null for all) still runs.
  3. Guest (ID 2) and Member (ID 3) groups could be included in the payload, which would set positions on groups the frontend intentionally excludes. Consider ignoring or rejecting those IDs.

Suggested additions:

if (\!is_array($order)) {
    return new EmptyResponse(422);
}

Copy link
Contributor Author

@DavideIadeluca DavideIadeluca Feb 25, 2026

Choose a reason for hiding this comment

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

No type check — $order could be a non-array (e.g. a string), which would make foreach throw.

Changed and added a test in 91de357

No validation that the IDs in $order are real group IDs — an invalid ID silently does nothing but the reset at line 33 (update position = null for all) still runs.

This is related to the quirk I described in the PR description. The DB Migration does set a position for all groups, including Member and Guest: so this behaviour is kind of desired with the above quirk. Essentially in the DB migration doesn't know for a fact that ID's 2 and 3 are indeed the Guest and Member Group without some runtime check which validates the class constants of Group.

Guest (ID 2) and Member (ID 3) groups could be included in the payload, which would set positions on groups the frontend intentionally excludes. Consider ignoring or rejecting those IDs.

Idk if this is a bug or a feature. The implementation in PermissionsPage does ignore those Groups, but the new component does technically support them as groups are passed into the component. There might be some cases where third party customizations want control over this. Not sure if it's worth it to restrict this

@imorland imorland modified the milestones: 2.0.0-beta.7, 2.0.0-beta.8 Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants