Merge pull request #8379 from artoscinote/ma_SCI_11736

Implement repository row field value snapshot view in sidebar [SCI-11736]
This commit is contained in:
Martin Artnik 2025-04-01 14:07:26 +02:00 committed by GitHub
commit 6e34ca408c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 77 additions and 42 deletions

View file

@ -385,7 +385,15 @@ class RepositoryRowsController < ApplicationController
end end
def load_repository_row def load_repository_row
@repository_row = @repository.repository_rows.eager_load(:repository_columns).find_by(id: params[:id]) @repository_row =
if params[:form_repository_rows_field_value_id]
FormRepositoryRowsFieldValue
.find_by(id: params[:form_repository_rows_field_value_id])
&.reified_repository_row_by_id(params[:id])
else
@repository.repository_rows.eager_load(:repository_columns).find_by(id: params[:id])
end
render_404 unless @repository_row render_404 unless @repository_row
end end

View file

@ -63,7 +63,7 @@ export default {
}, },
methods: { methods: {
itemCardUrl(row) { itemCardUrl(row) {
return repository_repository_row_path(row.repository_id, row.id); return repository_repository_row_path(row.repository_id, row.id, { form_repository_rows_field_value_id: this.field.field_value.id });
}, },
addValue(rows) { addValue(rows) {
const rowIds = this.assignedIds; const rowIds = this.assignedIds;

View file

@ -10,7 +10,7 @@
<div id="repository-item-sidebar" data-e2e="e2e-CO-itemCard" class="w-full h-full pl-6 bg-white flex flex-col"> <div id="repository-item-sidebar" data-e2e="e2e-CO-itemCard" class="w-full h-full pl-6 bg-white flex flex-col">
<div ref="stickyHeaderRef" id="sticky-header-wrapper" <div ref="stickyHeaderRef" id="sticky-header-wrapper"
class="sticky top-0 right-0 bg-white flex z-50 flex-col h-[78px] pt-6"> class="sticky top-0 right-0 bg-white flex z-50 flex-col pt-6">
<div class="header flex w-full h-[30px] pr-6"> <div class="header flex w-full h-[30px] pr-6">
<repository-item-sidebar-title v-if="defaultColumns" <repository-item-sidebar-title v-if="defaultColumns"
:editable="permissions.can_manage && !defaultColumns?.archived" :editable="permissions.can_manage && !defaultColumns?.archived"
@ -22,6 +22,9 @@
<i id="close-icon" data-e2e="e2e-BT-itemCard-close" @click="toggleShowHideSidebar(null)" <i id="close-icon" data-e2e="e2e-BT-itemCard-close" @click="toggleShowHideSidebar(null)"
class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i> class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i>
</div> </div>
<div v-if="snapshotAt" class="text-sn-grey mt-2">
{{ i18n.t('repositories.item_card.snapshot_label', { timestamp: snapshotAt, name: snapshotByName }) }}
</div>
<div id="divider" class="bg-sn-light-grey flex items-center self-stretch h-px mt-5 mr-6"></div> <div id="divider" class="bg-sn-light-grey flex items-center self-stretch h-px mt-5 mr-6"></div>
</div> </div>
@ -466,7 +469,9 @@ export default {
relationshipDetailsState: {}, relationshipDetailsState: {},
selectedToUnlink: null, selectedToUnlink: null,
initialSectionId: null, initialSectionId: null,
loadingError: false loadingError: false,
snapshotAt: null,
snapshotByName: null
}; };
}, },
provide() { provide() {
@ -624,6 +629,8 @@ export default {
this.icons = result.icons; this.icons = result.icons;
this.dataLoading = false; this.dataLoading = false;
this.notification = result.notification; this.notification = result.notification;
this.snapshotByName = result.snapshot_by_name;
this.snapshotAt = result.snapshot_at;
this.$nextTick(() => { this.$nextTick(() => {
this.generateBarCode(this.defaultColumns.code); this.generateBarCode(this.defaultColumns.code);

View file

@ -13,35 +13,18 @@
}} }}
</div> </div>
</div> </div>
<text-area :initialValue="colVal?.edit"
<div v-if="canEdit" class="w-full contents"> :noContentPlaceholder="i18n.t('repositories.item_card.repository_text_value.placeholder')"
<text-area :initialValue="colVal?.edit" :placeholder="i18n.t('repositories.item_card.repository_text_value.placeholder')"
:noContentPlaceholder="i18n.t('repositories.item_card.repository_text_value.placeholder')" :unEditableRef="`textRef`"
:placeholder="i18n.t('repositories.item_card.repository_text_value.placeholder')" :smartAnnotation="true"
:unEditableRef="`textRef`" :expandable="expandable"
:smartAnnotation="true" :collapsed="collapsed"
:expandable="expandable" @toggleExpandableState="toggleExpandableState"
:collapsed="collapsed" @update="update"
@toggleExpandableState="toggleExpandableState" className="px-3"
@update="update" :data-e2e="'e2e-IF-repoItemSBcustomColumns-input' + colId"
className="px-3" />
:data-e2e="'e2e-IF-repoItemSBcustomColumns-input' + colId"
/>
</div>
<div v-else-if="colVal?.view"
ref="textRef"
v-html="colVal?.view"
class="text-sn-dark-grey box-content text-sm font-normal leading-5
overflow-y-auto pr-3 rounded w-[calc(100%-2rem)]]"
:class="{
'max-h-[4rem]': collapsed,
'max-h-[40rem]': !collapsed
}"
>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5 pr-3 py-2 w-[calc(100%-2rem)]]">
{{ i18n.t("repositories.item_card.repository_text_value.no_text") }}
</div>
</div> </div>
</template> </template>

View file

@ -166,12 +166,12 @@ export default {
if (this.formFieldValues.find((formFieldValue) => formFieldValue.form_field_id === formFieldId)) { if (this.formFieldValues.find((formFieldValue) => formFieldValue.form_field_id === formFieldId)) {
this.formFieldValues = this.formFieldValues.map((formFieldValue) => { this.formFieldValues = this.formFieldValues.map((formFieldValue) => {
if (formFieldValue.form_field_id === formFieldId) { if (formFieldValue.form_field_id === formFieldId) {
return response.data.data.attributes; return { ...response.data.data.attributes, id: response.data.data.id };
} }
return formFieldValue; return formFieldValue;
}); });
} else { } else {
this.formFieldValues.push(response.data.data.attributes); this.formFieldValues.push({ ...response.data.data.attributes, id: response.data.data.id });
} }
}); });
}, },

View file

@ -115,6 +115,8 @@ export default {
}); });
}, },
enableEdit(e) { enableEdit(e) {
if (!this.canEdit) return;
if (e && $(e.target).hasClass('atwho-user-popover')) return; if (e && $(e.target).hasClass('atwho-user-popover')) return;
if (e && $(e.target).hasClass('sa-name')) return; if (e && $(e.target).hasClass('sa-name')) return;
if (e && $(e.target).hasClass('sa-link')) return; if (e && $(e.target).hasClass('sa-link')) return;

View file

@ -25,6 +25,35 @@ class FormRepositoryRowsFieldValue < FormFieldValue
data data
end end
def reified_repository_row_by_id(repository_row_id)
# build an in-memory, read-only representation of the repository row in the snapshot
row_attributes = data.find { |r| r['id'] == repository_row_id.to_i }
row = RepositoryRow.new(row_attributes.except('repository_cells'))
row.repository_cells = row_attributes['repository_cells'].map do |cell_attributes|
repository_cell = row.repository_cells.build(cell_attributes.except('value', 'repository_column'))
repository_column = RepositoryColumn.new(cell_attributes['repository_column'])
repository_column.readonly!
repository_cell.assign_attributes(
repository_column_id: cell_attributes['repository_column']['id'],
repository_row_id: repository_row_id.to_i,
repository_column: repository_column,
value:
cell_attributes['repository_column']['data_type'].constantize.new(
cell_attributes['value'].except('repository_column')
)
)
repository_cell.readonly!
repository_cell
end
row.snapshot_by_name = User.find_by(id: row.snapshot_by_id)&.full_name
row.readonly!
row
end
def formatted def formatted
value&.map { |i| "#{i['name']} (#{RepositoryRow::ID_PREFIX}#{i['id']})" }&.join(' | ') value&.map { |i| "#{i['name']} (#{RepositoryRow::ID_PREFIX}#{i['id']})" }&.join(' | ')
end end

View file

@ -112,7 +112,7 @@ class RepositoryRow < ApplicationRecord
length: { maximum: Constants::NAME_MAX_LENGTH } length: { maximum: Constants::NAME_MAX_LENGTH }
validates :created_by, presence: true validates :created_by, presence: true
attr_accessor :import_status, :import_message attr_accessor :import_status, :import_message, :snapshot_at, :snapshot_by_id, :snapshot_by_name
scope :active, -> { where(archived: false) } scope :active, -> { where(archived: false) }
scope :archived, -> { where(archived: true) } scope :archived, -> { where(archived: true) }

View file

@ -3,7 +3,7 @@
class FormFieldValueSerializer < ActiveModel::Serializer class FormFieldValueSerializer < ActiveModel::Serializer
include Canaid::Helpers::PermissionsHelper include Canaid::Helpers::PermissionsHelper
attributes :form_field_id, :type, :value, :submitted_at, :submitted_by_full_name, attributes :id, :form_field_id, :type, :value, :submitted_at, :submitted_by_full_name,
:unit, :not_applicable, :selection, :datetime, :datetime_to :unit, :not_applicable, :selection, :datetime, :datetime_to
def submitted_by_full_name def submitted_by_full_name

View file

@ -1,10 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
json.id @repository_row.id json.id @repository_row.id
if @repository_row.snapshot_at
json.snapshot_at DateTime.parse(@repository_row.snapshot_at).strftime("#{I18n.backend.date_format} %H:%M")
json.snapshot_by_name @repository_row.snapshot_by_name
end
json.repository do json.repository do
json.id @repository.id json.id @repository.id
json.name @repository.name json.name @repository.name
json.is_snapshot @repository.is_a?(RepositorySnapshot) json.is_snapshot @repository.is_a?(RepositorySnapshot) || !@repository_row.snapshot_at.nil?
end end
json.editable @repository_row.editable? json.editable @repository_row.editable?
json.notification @notification json.notification @notification
@ -12,9 +17,9 @@ json.notification @notification
json.update_path update_cell_repository_repository_row_path(@repository, @repository_row) json.update_path update_cell_repository_repository_row_path(@repository, @repository_row)
json.permissions do json.permissions do
json.can_export_repository_stock can_export_repository_stock?(@repository) json.can_export_repository_stock @repository_row.snapshot_at.nil? && can_export_repository_stock?(@repository)
json.can_manage can_manage_repository_rows?(@repository) if @repository.is_a?(Repository) && !@repository.is_a?(SoftLockedRepository) json.can_manage @repository_row.snapshot_at.nil? && can_manage_repository_rows?(@repository) if @repository.is_a?(Repository) && !@repository.is_a?(SoftLockedRepository)
json.can_connect_rows can_connect_repository_rows?(@repository) if @repository.is_a?(Repository) && !@repository.is_a?(SoftLockedRepository) json.can_connect_rows @repository_row.snapshot_at.nil? && can_connect_repository_rows?(@repository) if @repository.is_a?(Repository) && !@repository.is_a?(SoftLockedRepository)
end end
json.actions do json.actions do
@ -106,7 +111,7 @@ end
json.custom_columns do json.custom_columns do
json.array! repository_columns_ordered_by_state(@repository_row.repository).each do |repository_column| json.array! repository_columns_ordered_by_state(@repository_row.repository).each do |repository_column|
repository_cell = @repository_row.repository_cells.find_by(repository_column: repository_column) repository_cell = @repository_row.repository_cells.find { |c| c.repository_column_id == repository_column.id }
options = case repository_column.data_type options = case repository_column.data_type
when 'RepositoryListValue' when 'RepositoryListValue'

View file

@ -2714,6 +2714,7 @@ en:
one: "day" one: "day"
other: "days" other: "days"
stock_export: "Export" stock_export: "Export"
snapshot_label: "Snapshot on %{timestamp} by %{name}"
custom_columns_label: "Custom columns" custom_columns_label: "Custom columns"
no_custom_columns_label: "No custom columns" no_custom_columns_label: "No custom columns"
repository_time_range_value: repository_time_range_value: