Merge branch 'develop' into features/user-groups

This commit is contained in:
Martin Artnik 2025-06-30 15:24:22 +02:00
commit a6e5cc9d2e
16 changed files with 150 additions and 81 deletions

View file

@ -1 +1 @@
1.42.0
1.42.0.1

View file

@ -255,7 +255,6 @@ label {
.user-time {
color: $color-silver-chalice;
white-space: nowrap;
}
.report-element-body {
@ -462,7 +461,6 @@ label {
.user-time {
display: inline-block;
white-space: nowrap;
}
}

View file

@ -41,7 +41,12 @@ class ResultsController < ApplicationController
end
def list
if params[:with_linked_step_id].present?
step = @my_module.protocol.steps.find_by(id: params[:with_linked_step_id])
@results = @my_module.results.where(archived: false).or(@my_module.results.where(id: step.results.select(:id)))
else
@results = @my_module.results.active
end
update_and_apply_user_sort_preference!
end

View file

@ -87,7 +87,8 @@ class StepResultsController < ApplicationController
my_module: my_module.id,
step: step.id,
result: result.id,
position: step.position + 1
step_position: { id: step.id,
value_for: 'position_plus_one' }
})
end
end

View file

@ -285,7 +285,7 @@ class StepsController < ApplicationController
render json: {
paginated: true,
next_page: steps.next_page,
data: steps.map { |step| [step.id, step.name] }
data: steps.map { |step| [step.id, step.name, { position: step.position }] }
}
end

View file

@ -11,7 +11,10 @@
{{ i18n.t('protocols.steps.modals.link_results.title') }}
</h4>
</div>
<div v-if="results.length > 0" class="modal-body">
<div v-if="loading" class="modal-body h-40 flex items-center justify-center">
<div class="sci-loader"></div>
</div>
<div v-else-if="results.length > 0" class="modal-body">
<p>
{{ i18n.t('protocols.steps.modals.link_results.description') }}
</p>
@ -32,7 +35,7 @@
{{ i18n.t('protocols.steps.modals.link_results.empty_description') }}
</p>
</div>
<div class="modal-footer">
<div v-if="!loading" class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{{ i18n.t('general.cancel') }}
</button>
@ -89,12 +92,13 @@ export default {
return {
results: [],
initialResults: [],
selectedResults: []
selectedResults: [],
loading: true
};
},
computed: {
resultsListUrl() {
return list_my_module_results_path({ my_module_id: this.step.attributes.my_module_id });
return list_my_module_results_path({ my_module_id: this.step.attributes.my_module_id, with_linked_step_id: this.step.id });
},
resultsPageUrl() {
return my_module_results_path({ my_module_id: this.step.attributes.my_module_id });
@ -132,6 +136,10 @@ export default {
axios.get(this.resultsListUrl)
.then((response) => {
this.results = response.data;
this.loading = false;
}).catch(() => {
HelperModule.flashAlertMsg(I18n.t('general.error'), 'danger');
this.loading = false;
});
}
}

View file

@ -83,14 +83,15 @@
:data-object-type="step.attributes.type"
tabindex="0"
></span> <!-- Hidden element to support legacy code -->
<template v-if="!inRepository">
<template v-if="step.attributes.results.length == 0">
<button v-if="urls.update_url" :title="i18n.t('protocols.steps.link_results')" class="btn btn-light icon-btn" @click="this.openLinkResultsModal = true">
<button ref="linkButton" v-if="urls.update_url" :title="i18n.t('protocols.steps.link_results')" class="btn btn-light icon-btn" @click="this.openLinkResultsModal = true">
<i class="sn-icon sn-icon-results"></i>
</button>
</template>
<GeneralDropdown v-else ref="linkedResultsDropdown" position="right">
<template v-slot:field>
<button class="btn btn-light icon-btn" :title="i18n.t('protocols.steps.linked_results')">
<button ref="linkButton" class="btn btn-light icon-btn" :title="i18n.t('protocols.steps.linked_results')">
<i class="sn-icon sn-icon-results"></i>
<span class="absolute top-1 right-1 h-4 min-w-4 bg-sn-science-blue text-white flex items-center justify-center rounded-full text-[10px]">
{{ step.attributes.results.length }}
@ -117,6 +118,7 @@
</template>
</template>
</GeneralDropdown>
</template>
<a href=" #"
v-if="!inRepository"
ref="comments"
@ -214,12 +216,14 @@
@close="openFormSelectModal = false"
@submit="createElement('form_response', null, null, $event); openFormSelectModal = false"
/>
<Teleport to="body">
<LinkResultsModal
v-if="openLinkResultsModal"
:step="step"
@updateStep="updateLinkedResults"
@close="openLinkResultsModal = false"
/>
</Teleport>
</div>
</template>
@ -399,6 +403,11 @@
$(this.$refs.elementsDropdownButton).on('shown.bs.dropdown hidden.bs.dropdown', () => {
this.handleDropdownPosition(this.$refs.elementsDropdownButton, this.$refs.elementsDropdown)
});
window.initTooltip(this.$refs.linkButton);
},
beforeUnmount() {
window.destroyTooltip(this.$refs.linkButton);
},
computed: {
reorderableElements() {
@ -801,10 +810,14 @@
});
},
updateLinkedResults(results) {
window.destroyTooltip(this.$refs.linkButton);
this.$emit('step:update', {
results: results,
position: this.step.attributes.position
})
});
this.$nextTick(() => window.initTooltip(this.$refs.linkButton));
},
resultUrl(result_id, archived) {
return my_module_results_path({my_module_id: this.step.attributes.my_module_id, result_id: result_id, view_mode: (archived ? 'archived' : 'active') });

View file

@ -11,7 +11,10 @@
{{ i18n.t('my_modules.results.modals.link_steps.title') }}
</h4>
</div>
<div v-if="steps.length > 0" class="modal-body">
<div v-if="loading" class="modal-body h-40 flex items-center justify-center">
<div class="sci-loader"></div>
</div>
<div v-else-if="steps.length > 0" class="modal-body">
<p>
{{ i18n.t('my_modules.results.modals.link_steps.description') }}
</p>
@ -22,6 +25,7 @@
:value="selectedSteps"
:searchable="true"
@change="changeSteps"
:option-renderer="stepRenderer"
:multiple="true"
:withCheckboxes="true"
:placeholder="i18n.t('my_modules.results.modals.link_steps.placeholder')" />
@ -32,7 +36,7 @@
{{ i18n.t('my_modules.results.modals.link_steps.empty_description') }}
</p>
</div>
<div class="modal-footer">
<div v-if="!loading" class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
{{ i18n.t('general.cancel') }}
</button>
@ -93,7 +97,8 @@ export default {
return {
steps: [],
selectedSteps: [],
initialSteps: []
initialSteps: [],
loading: true
};
},
computed: {
@ -135,8 +140,15 @@ export default {
loadSteps() {
axios.get(this.stepsListUrl)
.then((response) => {
this.loading = false;
this.steps = response.data;
}).catch(() => {
HelperModule.flashAlertMsg(I18n.t('general.error'), 'danger');
this.loading = false;
});
},
stepRenderer(option) {
return `${option[2].position + 1}. ${option[1]}`;
}
}
};

View file

@ -66,13 +66,13 @@
tabindex="0"
></span> <!-- Hidden element to support legacy code -->
<tempplate v-if="result.attributes.steps.length == 0">
<button v-if="urls.update_url" :title="i18n.t('my_modules.results.link_steps')" class="btn btn-light icon-btn" @click="this.openLinkStepsModal = true">
<button v-if="urls.update_url" ref="linkButton" :title="i18n.t('my_modules.results.link_steps')" class="btn btn-light icon-btn" @click="this.openLinkStepsModal = true">
{{ i18n.t('my_modules.results.link_to_step') }}
</button>
</tempplate>
<GeneralDropdown v-else ref="linkedStepsDropdown" position="right">
<template v-slot:field>
<button class="btn btn-light icon-btn" :title="i18n.t('my_modules.results.linked_steps')">
<button ref="linkButton" class="btn btn-light icon-btn" :title="i18n.t('my_modules.results.linked_steps')">
<i class="sn-icon sn-icon-steps"></i>
<span class="absolute top-1 -right-1 h-4 min-w-4 bg-sn-science-blue text-white flex items-center justify-center rounded-full text-[10px]">
{{ result.attributes.steps.length }}
@ -181,6 +181,7 @@
@cancel="closeCustomWellPlateModal"
@create:table="(...args) => this.createElement('table', ...args)"
/>
<Teleport to="body">
<LinkStepsModal
v-if="openLinkStepsModal"
:result="result"
@ -188,6 +189,7 @@
@updateResult="updateLinkedSteps"
@close="openLinkStepsModal = false"
/>
</Teleport>
</div>
</div>
</div>
@ -302,6 +304,11 @@ export default {
}
this.$emit('result:collapsed');
});
window.initTooltip(this.$refs.linkButton);
},
beforeUnmount() {
window.destroyTooltip(this.$refs.linkButton);
},
computed: {
reorderableElements() {
@ -627,9 +634,13 @@ export default {
});
},
updateLinkedSteps(steps) {
window.destroyTooltip(this.$refs.linkButton);
this.$emit('result:update', this.result.id,{
steps: steps
})
});
this.$nextTick(() => window.initTooltip(this.$refs.linkButton));
},
protocolUrl(step_id) {
return protocols_my_module_path({ id: this.result.attributes.my_module_id }, { step_id: step_id })

View file

@ -270,7 +270,8 @@ export default {
maxWidth: 40,
resizable: true,
pinned: 'left',
lockPosition: 'left'
lockPosition: 'left',
sortable: false
});
}
@ -453,6 +454,9 @@ export default {
currentViewRender: this.currentViewRender,
perPage: this.perPage
};
columnsState.find((column) => column.colId === 'checkbox').pinned = 'left';
const settings = {
key: this.stateKey,
data: tableState
@ -532,6 +536,11 @@ export default {
this.restoreSelection();
this.handleScroll();
})
.catch(() => {
this.dataLoading = false;
this.$emit('tableReloaded', [], { filtered: this.searchValue.length > 0 });
window.HelperModule.flashAlertMsg(this.i18n.t('general.error'), 'danger');
});
},
handleInfiniteScroll(response) {

View file

@ -260,7 +260,7 @@ class Protocol < ApplicationRecord
def self.viewable_by_user_my_module_protocols(user, teams)
distinct.joins(:my_module)
.where(my_modules: MyModule.viewable_by_user(user, teams))
.where(my_modules: { id: MyModule.viewable_by_user(user, teams).select(:id) })
end
def self.filter_by_teams(teams = [])

View file

@ -39,6 +39,7 @@ class Result < ApplicationRecord
accepts_nested_attributes_for :tables
before_save :ensure_default_name
after_discard :delete_step_results
after_discard do
CleanupUserSettingsJob.perform_later('result_states', id)
end
@ -51,8 +52,12 @@ class Result < ApplicationRecord
teams = options[:teams] || current_team || user.teams.select(:id)
new_query = joins(:my_module)
.where(my_modules: MyModule.with_granted_permissions(user, MyModulePermissions::READ)
.where(user_assignments: { team: teams }))
.where(
my_modules: {
id: MyModule.with_granted_permissions(user, MyModulePermissions::READ)
.where(user_assignments: { team: teams }).select(:id)
}
)
unless include_archived
new_query = new_query.joins(my_module: { experiment: :project })
@ -187,4 +192,8 @@ class Result < ApplicationRecord
def ensure_default_name
self.name = name.presence || I18n.t('my_modules.results.default_name')
end
def delete_step_results
step_results.destroy_all
end
end

View file

@ -81,6 +81,9 @@ module Scinote
config.x.export_all_limit_24h = (ENV['EXPORT_ALL_LIMIT_24_HOURS'] || 3).to_i
# Fallback to old variant behaviour (pre 7.2)
config.active_storage.track_variants = false
# SciNote Core Application version
VERSION = File.read(Rails.root.join('VERSION')).strip.freeze

View file

@ -430,7 +430,7 @@ en:
attachments:
menu:
office_file: "New Office file"
office_file: "New Microsoft 365 file"
chemical_drawing: "New chemical drawing"
file_from_pc: "File from your PC"
modified_label: "Modified:"
@ -2215,9 +2215,9 @@ en:
success_flash: "File result successfully deleted."
wopi_open_file: "Open in %{app}"
wopi_edit_file: "Open in %{app}"
wopi_word: "Microsoft Word for the Web"
wopi_excel: "Microsoft Excel for the Web"
wopi_powerpoint: "Microsoft PowerPoint for the Web"
wopi_word: "Microsoft Word for the web"
wopi_excel: "Microsoft Excel for the web"
wopi_powerpoint: "Microsoft PowerPoint for the web"
error_flash: 'Something went wrong! Please try again later.'
result_tables:
@ -4440,9 +4440,9 @@ en:
wopi_supported_table_formats_title: 'Only .xlsx, .xlsm, .xlsb, .ods file formats are supported for editing in Excel for the web.'
wopi_supported_presentation_formats_title: 'Only .pptx, ppsx, .odp file formats are supported for editing in PowerPoint for the web.'
create_wopi_file:
button_text: 'New Microsoft Office file'
li_text: "Office file"
modal_title: 'Create new Microsoft Office document'
button_text: 'New Microsoft 365 file'
li_text: "Microsoft 365 file"
modal_title: 'Create new Microsoft 365 document'
text_field_label: 'Document name'
type_select_label: 'Document type'
text_field_placeholder: 'Enter document name...'

View file

@ -122,12 +122,12 @@ en:
move_experiment_html: "%{user} moved experiment %{experiment} from project %{project_original} to project %{project_new}."
clone_experiment_html: "%{user} duplicated experiment %{experiment_new} from experiment %{experiment_original} as template."
archive_experiment_html: "%{user} archived experiment %{experiment}."
edit_wopi_file_on_result_html: "%{user} edited Office online file %{asset_name} on result %{result}: %{action}."
edit_wopi_file_on_result_html: "%{user} edited Microsoft 365 file %{asset_name} on result %{result}: %{action}."
export_protocol_from_task_html: "%{user} exported protocol from task %{my_module}"
copy_protocol_in_repository_html: "%{user} duplicated protocol %{protocol_new} from protocol %{protocol_original} as a template"
user_leave_team_html: "%{user} left team %{team}"
edit_wopi_file_on_step_html: "%{user} edited Office online file %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}: %{action}."
edit_wopi_file_on_step_in_repository_html: "%{user} edited Office online file %{asset_name} on protocol %{protocol}'s step %{step_position} %{step} in Protocol repository: %{action}."
edit_wopi_file_on_step_html: "%{user} edited Microsoft 365 file %{asset_name} on protocol's step %{step_position} %{step} on task %{my_module}: %{action}."
edit_wopi_file_on_step_in_repository_html: "%{user} edited Microsoft 365 file %{asset_name} on protocol %{protocol}'s step %{step_position} %{step} in Protocol repository: %{action}."
restore_experiment_html: "%{user} restored experiment %{experiment}."
rename_task_html: "%{user} renamed task %{my_module}."
move_task_html: "%{user} moved task %{my_module} from experiment %{experiment_original} to experiment %{experiment_new}."
@ -313,7 +313,7 @@ en:
edit_task_result_file_locally_html: "%{user} locally edited file %{file} on result %{result}"
export_inventories_html: "%{user} exported inventory %{inventories}"
edit_image_on_inventory_item_html: "%{user} edited image %{asset_name} on inventory item %{repository_row} in inventory %{repository}: %{action}."
edit_wopi_file_on_inventory_item_html: "%{user} edited Office online file %{asset_name} on inventory item %{repository_row} in inventory %{repository}: %{action}."
edit_wopi_file_on_inventory_item_html: "%{user} edited Microsoft 365 file %{asset_name} on inventory item %{repository_row} in inventory %{repository}: %{action}."
export_inventory_stock_consumption_html: "%{user} exported stock consumption for inventory item(s) %{inventory_items} in inventory %{repository}."
task_step_file_duplicated_html: "%{user} duplicated file <strong>%{file}</strong> on protocol's step <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
result_file_duplicated_html: "%{user} duplicated file <strong>%{file}</strong> on result <strong>%{result}</strong> on task <strong>%{my_module}</strong>."
@ -410,6 +410,8 @@ en:
my_module_access_changed_user_group_html: "%{user} changed %{user_group}'s role on task %{my_module} to %{role}."
step_and_result_linked_html: "%{user} linked result <strong>%{result}</strong> and step <strong>%{position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
step_and_result_unlinked_html: "%{user} unlinked result <strong>%{result}</strong> and step <strong>%{position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
step_and_result_linked_html: "%{user} linked result <strong>%{result}</strong> and step <strong>%{step_position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
step_and_result_unlinked_html: "%{user} unlinked result <strong>%{result}</strong> and step <strong>%{step_position}</strong> <strong>%{step}</strong> on task <strong>%{my_module}</strong>."
activity_name:
create_project: "Project created"
edit_project: "Project edited"
@ -486,9 +488,9 @@ en:
move_experiment: "Experiment moved"
clone_experiment: "Experiment duplicated"
archive_experiment: "Experiment archived"
edit_wopi_file_on_result: "Office online file on result edited"
edit_wopi_file_on_step: "Office online file on task step edited"
edit_wopi_file_on_step_in_repository: "Office online file on step edited"
edit_wopi_file_on_result: "Microsoft 365 file on result edited"
edit_wopi_file_on_step: "Microsoft 365 file on task step edited"
edit_wopi_file_on_step_in_repository: "Microsoft 365 file on step edited"
restore_experiment: "Experiment restored"
rename_task: "Task renamed"
move_task: "Task moved"

View file

@ -1,8 +1,6 @@
include UsersGenerator
if ActiveRecord::Base.connection.migration_context.needs_migration?
raise "There are pending migrations. Run 'rails db:migrate' first."
end
ActiveRecord::Migration.check_all_pending!
MyModuleStatusFlow.ensure_default