project access_dropdown.vue - selection, save payload, preselect, and toggle label
## Context
| | |
|---|---|
| **Phase** | 5 of 6 |
| **Parallel with** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594887+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594889+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594890+ |
| **Blocked by** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594885+ <br> https://gitlab.com/gitlab-org/gitlab/-/work_items/594886+ |
| **Unblocks** | https://gitlab.com/gitlab-org/gitlab/-/work_items/594891+ |
## Summary
Update `access_dropdown.vue` (project level) to correctly build the save payload, handle preselection from server data, and update the toggle label for custom role selections.
## Background
This issue covers the data-flow side of the custom roles dropdown integration. The display side (fetching and rendering the custom roles section) is covered in Issue 13. This issue handles:
1. **`setDataForSave`** — parsing server-returned access level data to populate initial selected state
2. **`selection` computed** — including `member_role_id` in the save payload emitted to the parent
3. **`setSelected`** — restoring selected state on initial load from preselected items
4. **`toggleLabel` computed** — showing "N custom roles" in the dropdown button label
## Relevant file
`app/assets/javascripts/projects/settings/components/access_dropdown.vue`
## Changes required
### `setDataForSave(items)` method
Add a branch for `member_role_id`:
```javascript
setDataForSave(items) {
this.selected = items.reduce(
(selected, item) => {
if (item.group_id) {
selected[LEVEL_TYPES.GROUP].push({ id: item.group_id, ...item });
} else if (item.user_id) {
selected[LEVEL_TYPES.USER].push({ id: item.user_id, ...item });
} else if (item.member_role_id) {
selected[LEVEL_TYPES.MEMBER_ROLE].push({ id: item.member_role_id, ...item });
} else if (item.access_level) {
const level = this.accessLevelsData.find(({ id }) => item.access_level === id);
selected[LEVEL_TYPES.ROLE].push(level);
} else if (item.deploy_key_id) {
selected[LEVEL_TYPES.DEPLOY_KEY].push({ id: item.deploy_key_id, ...item });
}
return selected;
},
{ /* ...empty initial state including MEMBER_ROLE */ }
);
},
```
Note: `member_role_id` must be checked before `access_level` because a `member_role`-type access level row also has an `access_level` integer set (to the role's `base_access_level`). Without this ordering, it would be misclassified as a plain role.
### `selection` computed property
```javascript
selection() {
return [
...this.getDataForSave(LEVEL_TYPES.ROLE, 'access_level'),
...this.getDataForSave(LEVEL_TYPES.GROUP, 'group_id'),
...this.getDataForSave(LEVEL_TYPES.USER, 'user_id'),
...this.getDataForSave(LEVEL_TYPES.DEPLOY_KEY, 'deploy_key_id'),
...this.getDataForSave(LEVEL_TYPES.MEMBER_ROLE, 'member_role_id'),
];
},
```
### `setSelected({ initial })` method
Add intersection logic for `MEMBER_ROLE`, similar to groups:
```javascript
const selectedCustomRoles = intersectionWith(
this.customRoles,
this.preselectedItems,
(role, selected) => {
return selected.type === LEVEL_TYPES.MEMBER_ROLE && role.id === selected.member_role_id;
},
);
this.selected[LEVEL_TYPES.MEMBER_ROLE] = selectedCustomRoles;
```
### `toggleLabel` computed
Add custom role count:
```javascript
if (counts[LEVEL_TYPES.MEMBER_ROLE] > 0) {
labelPieces.push(n__('1 custom role', '%d custom roles', counts[LEVEL_TYPES.MEMBER_ROLE]));
}
```
And update `isOnlyRoleSelected` to also require `counts[LEVEL_TYPES.MEMBER_ROLE] === 0`.
### `clearSelection()` method
Ensure `LEVEL_TYPES.MEMBER_ROLE` is included:
```javascript
clearSelection() {
Object.values(LEVEL_TYPES).forEach((level) => {
this.selected[level] = [];
});
},
```
This already works if `LEVEL_TYPES.MEMBER_ROLE` is defined and `selected` has it as a key.
## Notes
- The ordering in `setDataForSave` matters: check `member_role_id` before `access_level` since member role rows have both set
- The save payload `{ member_role_id: N }` is what gets sent to the protected branches API endpoint (`allowed_to_merge`, `allowed_to_push`, `allowed_to_unprotect`) — this matches the format expected by Issue 6
## Testing
- Jest: `setDataForSave` correctly identifies `member_role_id` items
- Jest: `setDataForSave` does NOT misclassify a `member_role_id` item as a `role` type when `access_level` is also present
- Jest: `selection` computed includes `{ member_role_id: N }` entries for selected custom roles
- Jest: `selection` includes `_destroy: true` for deselected custom roles
- Jest: `setSelected` restores custom role selection from preselected items on initial load
- Jest: `toggleLabel` shows "1 custom role" / "N custom roles" correctly
- Jest: `toggleLabel` shows combined label when both role and custom role are selected
## Dependencies
- Issue 12 (`MEMBER_ROLE` constant)
- Issue 13 (custom roles data loading and rendering) — shares the same file; coordinate to avoid conflicts
## Labels
issue