Skip to content

feat: allow multiple values on relation columns - #2979

Open
he-is-talha wants to merge 6 commits into
nextcloud:mainfrom
he-is-talha:feat/2952/relation-multiple-values
Open

feat: allow multiple values on relation columns#2979
he-is-talha wants to merge 6 commits into
nextcloud:mainfrom
he-is-talha:feat/2952/relation-multiple-values

Conversation

@he-is-talha

Copy link
Copy Markdown
Contributor

Summary

  • Add a Select multiple items option on relation columns (customSettings.allowMultiple)
  • Store relation values as one cell row per related id (same pattern as usergroup)
  • Update create/edit UI, cell display, validation, import parsing, and analytics formatting for single and multi relations

Closes #2952

🖼️ Screenshots

image image image

Notes

  • API cell values for relation columns are now always int[] (legacy scalars are still accepted and normalized)
  • No DB migration; existing single-value cells continue to work

🤖 AI (if applicable)

The content of this PR was partly using AI

… Updated related components and services to handle multiple relation values, including UI adjustments for selection and display. Added tests to verify correct formatting of multiple relation values in analytics.

Signed-off-by: Talha Bin Yousaf <talhayousaf4420@gmail.com>
@samin-z

samin-z commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@he-is-talha please clean you second commit message before merging.

Keep only the first related id per row when switching to single-select,
fix multi filters / API scalar shape, and soften CSV import parsing.

Signed-off-by: Talha Bin Yousaf <talhayousaf4420@gmail.com>
@he-is-talha
he-is-talha force-pushed the feat/2952/relation-multiple-values branch from 9b6cdef to 888a5f9 Compare September 9, 2026 10:40
@he-is-talha

Copy link
Copy Markdown
Contributor Author

@samin-z Cleaned the second commit message and force-pushed.

@samin-z

samin-z commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

few points:

  1. for multiple item when column is edited and it is switched to single item, table still shows multiple items until page is refreshed and then the correct value is shown.
  2. editing multiple items and switching to single item, it shows no warning that existing data might be loss so maybe a nice addition to let users know, or show a warning underneath this option in regard to how toggling this will effect.
  3. when the column is mandatory, clearing the relation still save with no error, it's better to make sure empty values are rejected both in the UI and on the API. same point for multi-selection, empty array shouldnt count as a valid value. Inline cell edit should also refuse to clear a mandatory relation. though i think this is pre-existing issue as if we have columns which rows which are empty and then the column is updated to mandatory but existing empty rows columns are still as they are.

for point 4 lets fix it for now for this case though.

Reload truncated values after switching multi→single, warn about data
loss, reject empty mandatory relations in UI/API, and block enabling
mandatory when some rows still have no related value.

Signed-off-by: Talha Bin Yousaf <talhayousaf4420@gmail.com>
@he-is-talha

Copy link
Copy Markdown
Contributor Author
  1. Implemented: Multi → single updates without refresh
  2. Warning when turning off multi:
image 3. Mandatory rejects empty image 4. Enabling mandatory with empty rows blocked image

@he-is-talha

Copy link
Copy Markdown
Contributor Author

@samin-z Ready for the review again

@Aveyron-RetD

Copy link
Copy Markdown

Maybe @Koc would be a good reviewer

@blizzz blizzz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contriubtion!

If it is correct, that the PR is also AI assisted, please add the "Assisted-by: $AGENT_NAME:$MODEL_VERSION" trailer to each commit where it applies to.

<div style="width: 100%">
<div class="row space-T">
<div class="fix-col-4 title">
<div class="form-col-4 title">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The rename does not rename the styling. Intentional? Why is the rename necessary in first place?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted — the rename wasn’t needed.

Comment on lines +119 to +120
} elseif (str_contains($value, ',')) {
$value = array_map(trim(...), explode(',', $value));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i.e. labels would not be able to contains , I am not aware that such a restriction is in place anyhwere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Names with commas are kept as one label now; we only split when the full name doesn’t match.

}

if (is_string($value)) {
$decoded = json_decode($value, true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

when this is not a json string, integer values would turn into a string, and then may not survive the check in resolveRelationId()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — number IDs are handled correctly now.

Comment thread lib/Db/Row2Mapper.php Outdated
try {
$column = $this->columnMapper->find($columnId);
if ($column->getType() === Column::TYPE_RELATION
&& !(bool)($column->getCustomSettingsArray()[Column::RELATION_ALLOW_MULTIPLE] ?? false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this bleeds customSettings into Row2Mapper which it should not know about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Moved that logic out of Row2Mapper.

Comment thread lib/Db/Row2Mapper.php Outdated
Comment on lines +805 to +807
} catch (DoesNotExistException) {
// Column may have been deleted; keep aggregated value as-is
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this column was already fetched in line 776, hence the check is not necessary know (but does not hurt). What would be better is to use the object that was already fetched.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — we reuse the column we already loaded.

$this->normalizeToIds($value, $column, throwOnInvalid: true);
return true;
} catch (BadRequestError) {
return $this->normalizeToIds($value, $column, throwOnInvalid: false) !== [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this probably does not need to be called a second time and can be solved differently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplified — only one pass now.

Comment thread lib/Service/RowService.php Outdated
if (is_array($value)) {
return count($value) > 0;
}
return $value !== null && $value !== '';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this check was already run in line 503

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the extra check.

if (this.isMandatory && newValue.length === 0) {
showError(t('tables', 'This column is mandatory and cannot be empty.'))
this.cancelEdit()
this.localLoading = false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it is already false (line 168)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the redundant line.

const cellLabel = this.getLabel(cell.value)?.toLowerCase()
const rawFilter = filter.magicValuesEnriched ? filter.magicValuesEnriched : filter.value
const filterValue = String(rawFilter ?? '').toLowerCase()
const ids = this.normalizeIds(cell?.value ?? cell)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
const ids = this.normalizeIds(cell?.value ?? cell)
const ids = this.normalizeIds(cell?.value)

should be just this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied, thanks.

Comment thread lib/Db/Row2Mapper.php
$qb->expr()->eq($alias . '.column_id', $qb->createNamedParameter($sortData['columnId']))
)
);
$qb->addOrderBy($qb->func()->max($alias . '.value'), $sortData['mode']);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

fyi, sorting would be by label id not display value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the note — left as-is for now.

Move single-value unwrapping into the relation cell mapper, harden
label/id parsing, dedupe exclusion filters, and apply the other
requested cleanups from code review.

Signed-off-by: Talha Bin Yousaf <talhayousaf4420@gmail.com>
Assisted-by: Cursor:Composer
@he-is-talha

Copy link
Copy Markdown
Contributor Author

@blizzz Thanks for the review! Addressed the inline comments. Also added the Assisted-by trailer on the follow-up commit.

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.

feat: add relation column type (multiple)

4 participants