Skip to content

Append to cell #2035

Open
Open

Description

@Aveyron-RetD

Is your feature request related to a problem? Please describe.

Problem identified : Not possible on the same cell to append content. All content is available.

Issue I would like to solve : only one row for multiple users, instead of multiply the same row for multiple users.

We are working in a regulatory environment where we do track the readings of our documents by individual users. We usually have to sign the entry in a table to show that we did read the document on the date xxx.

I believe the Tables app could replace already that today with creating for each user an indiviual row of what they need to read, but this solution will require 100s of raws, while one raw could be sufficient with the proposed solution.

Describe the solution you'd like

.Have a column with all the users that must read the document XX
.Have a column with all the users that have read the document XX <= Append to cell userid to this column automatically filled in.
.Have a column which can append some data to the same cell (Tick box (I have read the following document )and . Date)

Create a filter view with userid that needs to read the document and have not read the document = People can track who has not read the document.

Describe alternatives you've considered

Like as said, one row for each user, but this seems unmanageable by someone with lots of users. Would need some automation...and the table app would be quickly filled in.
The solution above seems manageable by a human with groups and circles, and would reduce the size of the table drastically.

Additional context

Very good for small teams etc...

Note: the appended row will be added to a rich text field table.

Activity

  1. added
    enhancementNew feature or request
    0. Needs triagePending approval or rejection. This issue is pending approval.
    on Aug 29, 2025
  2. marcoambrosini commented on Jan 14, 2026

    @marcoambrosini
    Member

    Thanks for your contribution @Aveyron-RetD.

    It seems to me that what you need can be accomplished with just adding the ability to add more than one user to the users and group type. Is that correct? For the rest we can use views and toggles.

    cc @blizzz

  3. Aveyron-RetD commented on Jan 15, 2026

    @Aveyron-RetD
    Author

    The doable way today is to request each user to add his @username to the read column (user and group columns type), and the document to read will be filtered out of their view.

    But this seems not to be an elegant solution because they can see other people answers and I have no way of tracking other things like a comment or something.

    The row being the same they will share the same info, so I will not be able to track users input, and they will be able to overwrite other user's input.

    You could also do something like below create a table with rich text field but you cannot mention yourself or someone in a rich text field nextcloud/text#6879

    But again I have no way to enforce something like that, and they can overwrite or delete the table

    Image
  4. Aveyron-RetD commented on Jan 16, 2026

    @Aveyron-RetD
    Author

    Maybe could be solved by #2248 ?
    If understood, from the screenshot I would have a separate table read document which would be doing exactly that.

  5. Aveyron-RetD commented on Mar 30, 2026

    @Aveyron-RetD
    Author

    Ok so #2248 will not solve this problem i have tested this yesterday and this is not a good fit. It feels way too complex to achieve this.

    I believe we would need some kind of automations. It would need also #2258 to work properly.
    For the moment the work around is
    When a row is added to the parent table => There is an action => Create rows in another tables => extract all the users

  6. taiebot commented on Jul 19, 2026

    @taiebot
    Contributor

    Ok so this is my contribution with AI i would like a feedback from @Koc on the approach as you have worked on the multivalue and you are very active in development. Is it worth implementing ? This would bring the one to many functionality to the text fields. I have not tested any of those AI claim but I want to double check that i am on a good path

    AI work so far

    🧵 Turning append-only text columns into a mini collaborative log

    Hey folks 👋 — following up on the idea from #2035 (multi-user cell editing). Poked around the codebase and I think we can get 90% of the way there without touching the DB schema at all, by piggybacking on a mechanism that already exists for usergroup columns. Tagging @[contributor] since you've been deep in Row2Mapper lately — curious what you think!

    The core insight 💡

    RowCellMapperSuper::hasMultipleValues() already lets a column store multiple DB rows per (row_id, column_id) pair, which get merged into an array on read. Today only usergroup uses this. Turns out that's basically the plumbing we need for "append, never overwrite" — we just need to swap usergroup's delete-then-insert-all behavior for a plain insert-only behavior when a column is flagged append-only.

    Bonus: tables_row_cells_text rows already carry last_edit_by / last_edit_at per row (set in insertCell() via updateMetaData()), so we get "who wrote this entry + when" completely for free. No migration needed! 🎉

    Scope: this hooks in at the text mapper level, so it covers both the line and richtext subtypes (same table, same mapper) — gated per-column via a new custom setting, not a whole new column type.

    The patch

    1. Column.php — new flag, riding on the existing customSettings JSON blob (no schema change):

    public function getTextAppendOnly(): bool {
    	return (bool)($this->getCustomSettingsArray()['textAppendOnly'] ?? false);
    }

    2. RowCellMapperSuper.php — hasMultipleValues() becomes column-aware (⚠️ breaking signature change, see below):

    public function hasMultipleValues(Column $column): bool {
    	return false;
    }

    3. RowCellUsergroupMapper.php — same signature, same behavior:

    public function hasMultipleValues(Column $column): bool {
    	return true;
    }

    4. RowCellTextMapper.php — the actual opt-in:

    public function hasMultipleValues(Column $column): bool {
    	return $column->getTextAppendOnly();
    }

    5. Row2Mapper::insertOrUpdateCell() — the "never delete" branch:

    private function insertOrUpdateCell(int $rowId, int $columnId, $value): void {
    	$column = $this->columnMapper->find($columnId);
    	$cellMapper = $this->getCellMapper($column);
    	try {
    		if ($column->getTextAppendOnly()) {
    			// 🔒 append-only: no delete, ever. History is physically untouchable.
    			$this->insertCell($rowId, $columnId, $value);
    		} elseif ($cellMapper->hasMultipleValues($column)) {
    			$this->atomic(function () use ($cellMapper, $rowId, $columnId, $value) {
    				$cellMapper->deleteAllForColumnAndRow($columnId, $rowId);
    				$this->insertCell($rowId, $columnId, $value);
    			}, $this->db);
    		} else {
    			$cell = $cellMapper->findByRowAndColumn($rowId, $columnId);
    			$this->updateCell($cell, $cellMapper, $value, $column);
    		}
    	} catch (DoesNotExistException) {
    		$this->insertCell($rowId, $columnId, $value);
    	} catch (MultipleObjectsReturnedException|Exception $e) {
    		$this->logger->error($e->getMessage(), ['exception' => $e]);
    		throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
    	}
    }

    6. Row2Mapper::insertCell() — append-only sends a single string, not an array like usergroup does, so skip the loop:

    if ($cellMapper->hasMultipleValues($column) && !$column->getTextAppendOnly()) {
    	foreach ($value as $val) {
    		// existing usergroup behavior, untouched
    		$cell = new $cellClassName();
    		$cell->setRowIdWrapper($rowId);
    		$cell->setColumnIdWrapper($columnId);
    		$this->updateMetaData($cell, false, $lastEditAt, $lastEditBy);
    		$cellMapper->applyDataToEntity($column, $cell, $val);
    		$cellMapper->insert($cell);
    	}
    } else {
    	// single-value columns AND append-only columns: one entry per call
    	$cell = new $cellClassName();
    	$cell->setRowIdWrapper($rowId);
    	$cell->setColumnIdWrapper($columnId);
    	$this->updateMetaData($cell, false, $lastEditAt, $lastEditBy);
    	$cellMapper->applyDataToEntity($column, $cell, $value);
    	$cellMapper->insert($cell);
    }

    7. Row2Mapper::getRowsChunk() — grab id too (need it for stable ordering):

    $qbTmp->select('id', 'row_id', 'column_id', 'last_edit_at', 'last_edit_by')
    	->selectAlias($qb->expr()->castColumn('value', IQueryBuilder::PARAM_STR), 'value');
    $qb->select('row_id', 'column_id', 'created_by', 'created_at', 't1.last_edit_by', 't1.last_edit_at', 'value', 'table_id')
    	->addSelect('value_type')
    	->addSelect('t1.id')
    	// ...

    8. Row2Mapper::parseEntities() — turn each row into a little journal entry, newest first:

    if ($column->getTextAppendOnly()) {
    	$rowValues[$compositeKey][] = [
    		'id' => (int)$rowData['id'],
    		'userId' => $rowData['last_edit_by'],
    		'date' => $rowData['last_edit_at'],
    		'text' => $value,
    	];
    } elseif ($cellMapperCache[$columnType]->hasMultipleValues($column)) {
    	$rowValues[$compositeKey][] = $value;
    } else {
    	$rowValues[$compositeKey] = $value;
    }
    foreach ($rowValues as $compositeKey => $value) {
    	if (is_array($value) && isset($value[0]['id'])) {
    		usort($value, static fn ($a, $b) => $b['id'] <=> $a['id']); // newest first, stable order
    	}
    	$rows[$keyToRowId[$compositeKey]]->addCell($keyToColumnId[$compositeKey], $value);
    }

    What we get for free 🎁

    • No migration — id, last_edit_by, last_edit_at already exist on tables_row_cells_text.
    • RowCellBulkFetchTrait::findAllByRowIdsAndColumnIds() already does select('*') with no single-row assumption — works untouched.
    • RowController.php needs zero changes — update()/updateSet() already just forward columnId/value to RowService::updateSet(), where all the logic lives.

    Still open ⚠️

    • Breaking signature change on hasMultipleValues() — need to sweep for any other overrides I might've missed.
    • Frontend: the cell editor needs to stop loading existing content into the editor and instead render past entries read-only + present a blank input for the new one. Rich formatting (checkboxes etc.) can stay — NcRichText just gets rendered N times per cell instead of once.
    • Migration path: what happens if someone flips textAppendOnly on a column that already has plain-string data? (Probably: restrict to newly-created columns for v1.)
    • Whether RowService::validateColumnValueLimits() (textMaxLength) should apply per-entry (my assumption) or to the cumulative history.

    Would love a sanity check on the hasMultipleValues() signature change before I open a draft PR — does this match how you'd want this shaped, or is there a cleaner way to gate this that I'm missing? 🙏

  7. Koc commented on Jul 19, 2026

    @Koc
    Contributor

    @taiebot hey-hey!

    For sure, one2many relation worth to be implemented. But that's not something on my plate (at least for now). My next priorities are:

    Ai-suggested approach is very close to what I thought as well. Feel free to add it, but I'd recommend to wait until #2271 not merged to avoid possible merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    0. Needs triagePending approval or rejection. This issue is pending approval.enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions