', {
+ class: 'sci-input-container text-field error-icon'
+ }).append($input);
+
+ $cell.html($div);
};
$.fn.dataTable.render.editRepositoryStockValue = function(formId, columnId, cell) {
diff --git a/app/assets/javascripts/repositories/renderers/new_renderers.js b/app/assets/javascripts/repositories/renderers/new_renderers.js
index a860e5428..c4f834c04 100644
--- a/app/assets/javascripts/repositories/renderers/new_renderers.js
+++ b/app/assets/javascripts/repositories/renderers/new_renderers.js
@@ -50,20 +50,31 @@ $.fn.dataTable.render.newRepositoryChecklistValue = function(formId, columnId, $
};
$.fn.dataTable.render.newRepositoryNumberValue = function(formId, columnId, $cell, $header) {
- let decimals = $header.data('metadata-decimals');
+ const decimals = $header.data('metadata-decimals');
- $cell.html(`
-
', {
+ class: 'sci-input-field',
+ form: formId,
+ type: 'text',
+ name: 'repository_cells[' + columnId + ']',
+ value: '',
+ placeholder: I18n.t('repositories.table.number.enter_number'),
+ 'data-type': 'RepositoryNumberValue'
+ });
+
+ $input.on('input', function() {
+ const decimalsRegex = new RegExp(`^\\d*(\\.\\d{0,${decimals}})?`);
+ let value = this.value;
+ value = value.replace(/[^0-9.]/g, '');
+ value = value.match(decimalsRegex)[0];
+ this.value = value;
+ });
+
+ let $div = $('
', {
+ class: 'sci-input-container text-field error-icon'
+ }).append($input);
+
+ $cell.html($div);
};
$.fn.dataTable.render.newRepositoryDateTimeValue = function(formId, columnId, $cell) {
diff --git a/app/assets/javascripts/shared/remote_modal.js b/app/assets/javascripts/shared/remote_modal.js
index 7478c304d..a13bb85f7 100644
--- a/app/assets/javascripts/shared/remote_modal.js
+++ b/app/assets/javascripts/shared/remote_modal.js
@@ -26,6 +26,19 @@
animateSpinner(null, false);
ShareModal.init();
}
+ if (['rename-repo-modal', 'copy-repo-modal'].includes($(this).attr('id'))) {
+ $(this).find('form')
+ .on('ajax:success', function(_e, data) {
+ if (data.url) {
+ window.location = data.url;
+ } else {
+ window.location.reload();
+ }
+ })
+ .on('ajax:error', function(_e, data) {
+ $(this).renderFormErrors('repository', data.responseJSON);
+ });
+ }
$(this).find('.selectpicker').selectpicker();
})
.on('hidden.bs.modal', function() {
diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js
index d15f216b4..ba32eebf4 100644
--- a/app/assets/javascripts/sitewide/dropdown_selector.js
+++ b/app/assets/javascripts/sitewide/dropdown_selector.js
@@ -716,7 +716,9 @@ var dropdownSelector = (function() {
} else {
// Or delete specific one
deleteValue(selector, container, tagLabel.data('ds-tag-id'), tagLabel.data('ds-tag-group'));
- removeOptionFromSelector(selector, tagLabel.data('ds-tag-id'));
+ if (selector.data('config').tagClass) {
+ removeOptionFromSelector(selector, tagLabel.data('ds-tag-id'));
+ }
}
}, 350);
}
@@ -1010,7 +1012,9 @@ var dropdownSelector = (function() {
currentData = getCurrentData($(selector).next());
currentData.push(value);
setData($(selector), currentData, skip_event);
- appendOptionToSelector(selector, value);
+ if (selector.data('config').tagClass) {
+ appendOptionToSelector(selector, value);
+ }
return this;
},
diff --git a/app/assets/stylesheets/navigation/navigator.scss b/app/assets/stylesheets/navigation/navigator.scss
index 4eca5b903..1b595fd38 100644
--- a/app/assets/stylesheets/navigation/navigator.scss
+++ b/app/assets/stylesheets/navigation/navigator.scss
@@ -2,22 +2,34 @@
.menu-item:not(.active):hover {
background-color: var(--sn-super-light-grey);
- a:not(.no-hover) {
+ :not(.no-hover) {
color: var(--sn-blue);
}
- a.no-hover {
+ .no-hover {
color: var(--sn-science-blue-hover);
}
}
.menu-item.active:hover {
- a:not(.no-hover) {
+ :not(.no-hover) {
color: var(--sn-blue);
}
- a.no-hover {
+ .no-hover {
color: var(--sn-science-blue-hover);
}
}
+
+ .menu-item,
+ .menu-item:not(.active):hover,
+ .menu-item.active:hover {
+ .disabled-link,
+ .disabled-link:not(.no-hover):hover,
+ .disabled-link.no-hover:hover {
+ color: var(--sn-grey);
+ pointer-events: none;
+ text-decoration: none;
+ }
+ }
}
diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss
index e4f8ee403..75d1beb73 100644
--- a/app/assets/stylesheets/projects.scss
+++ b/app/assets/stylesheets/projects.scss
@@ -747,6 +747,16 @@ li.module-hover {
a {
color: inherit;
+
+ &.disabled-link {
+ color: var(--sn-grey);
+ pointer-events: none;
+ text-decoration: none;
+
+ .name {
+ color: var(--sn-grey);
+ }
+ }
}
.name {
@@ -872,7 +882,7 @@ li.module-hover {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-name: placeholder-pulsing;
- background-color: $color-alto;
+ background-color: var(--sn-sleepy-grey);
border-radius: $border-radius-default;
height: 18px;
diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb
index 43733d2d4..1331ec058 100644
--- a/app/controllers/experiments_controller.rb
+++ b/app/controllers/experiments_controller.rb
@@ -447,22 +447,22 @@ class ExperimentsController < ApplicationController
end
def inventory_assigning_experiment_filter
- readable_experiments = Experiment.readable_by_user(current_user)
+ viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
- project = Project.readable_by_user(current_user)
+ project = Project.viewable_by_user(current_user, current_team)
.joins(experiments: :my_modules)
- .where(experiments: { id: readable_experiments })
+ .where(experiments: { id: viewable_experiments })
.where(my_modules: { id: assignable_my_modules })
.find_by(id: params[:project_id])
return render_404 if project.blank?
experiments = project.experiments
+ .active
.joins(:my_modules)
- .where(experiments: { id: readable_experiments })
+ .where(experiments: { id: viewable_experiments })
.where(my_modules: { id: assignable_my_modules })
- .search(current_user, false, params[:query], 1, current_team)
.distinct
.pluck(:id, :name)
diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb
index 3eddeb756..683224c2b 100644
--- a/app/controllers/my_modules_controller.rb
+++ b/app/controllers/my_modules_controller.rb
@@ -452,12 +452,12 @@ class MyModulesController < ApplicationController
end
def inventory_assigning_my_module_filter
- readable_experiments = Experiment.readable_by_user(current_user)
+ viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
- experiment = Experiment.readable_by_user(current_user)
+ experiment = Experiment.viewable_by_user(current_user, current_team)
.joins(:my_modules)
- .where(experiments: { id: readable_experiments })
+ .where(experiments: { id: viewable_experiments })
.where(my_modules: { id: assignable_my_modules })
.find_by(id: params[:experiment_id])
@@ -465,8 +465,6 @@ class MyModulesController < ApplicationController
my_modules = experiment.my_modules
.where(my_modules: { id: assignable_my_modules })
- .distinct
- .search(current_user, false, params[:query], 1, current_team)
.pluck(:id, :name)
return render plain: [].to_json if my_modules.blank?
diff --git a/app/controllers/navigator/base_controller.rb b/app/controllers/navigator/base_controller.rb
index ecc161fd9..61bc14e2f 100644
--- a/app/controllers/navigator/base_controller.rb
+++ b/app/controllers/navigator/base_controller.rb
@@ -12,7 +12,8 @@ module Navigator
archived: project.archived,
type: :project,
has_children: project.has_children,
- children_url: navigator_project_path(project)
+ children_url: navigator_project_path(project),
+ disabled: project.disabled
}
end
@@ -69,25 +70,35 @@ module Navigator
(projects.archived IS TRUE AND experiments.id IS NOT NULL)
THEN 1 ELSE 0 END) > 0 AS has_children'
end
+ disabled_sql = 'SUM(CASE WHEN project_user_roles IS NULL THEN 0 ELSE 1 END) < 1 AS disabled'
+
current_team.projects
.where(project_folder_id: folder)
.visible_to(current_user, current_team)
.with_children_viewable_by_user(current_user)
- .where('
- projects.archived = :archived OR
- (
- (
- experiments.archived = :archived OR
- my_modules.archived = :archived
- ) AND
- :archived IS TRUE
- ) OR
- projects.id = :project_id
- ', archived: archived, project_id: project&.id || -1)
+ .joins("LEFT OUTER JOIN user_assignments project_user_assignments
+ ON project_user_assignments.assignable_type = 'Project'
+ AND project_user_assignments.assignable_id = projects.id
+ AND project_user_assignments.user_id = #{current_user.id}
+ LEFT OUTER JOIN user_roles project_user_roles
+ ON project_user_roles.id = project_user_assignments.user_role_id
+ AND project_user_roles.permissions @> ARRAY['#{ProjectPermissions::READ}']::varchar[]")
+ .where('projects.archived = :archived OR
+ (
+ (
+ experiments.archived = :archived OR
+ my_modules.archived = :archived
+ ) AND
+ :archived IS TRUE
+ ) OR
+ projects.id = :project_id',
+ archived: archived,
+ project_id: project&.id || -1)
.select(
'projects.id',
'projects.name',
'projects.archived',
+ disabled_sql,
has_children_sql
).group('projects.id')
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 66d8a51a1..c64511770 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -107,14 +107,14 @@ class ProjectsController < ApplicationController
end
def inventory_assigning_project_filter
- readable_experiments = Experiment.readable_by_user(current_user)
+ viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
- projects = Project.readable_by_user(current_user)
+ projects = Project.viewable_by_user(current_user, current_team)
+ .active
.joins(experiments: :my_modules)
- .where(experiments: { id: readable_experiments })
+ .where(experiments: { id: viewable_experiments })
.where(my_modules: { id: assignable_my_modules })
- .search(current_user, false, params[:query], 1, current_team)
.distinct
.pluck(:id, :name)
@@ -167,64 +167,70 @@ class ProjectsController < ApplicationController
end
def update
+ @project.assign_attributes(project_update_params)
return_error = false
flash_error = t('projects.update.error_flash', name: escape_input(@project.name))
+ return render_403 unless can_manage_project?(@project) || @project.archived_changed?
+
# Check archive permissions if archiving/restoring
- if project_params.include? :archived
- if (project_params[:archived] == 'true' &&
- !can_archive_project?(@project)) ||
- (project_params[:archived] == 'false' &&
- !can_restore_project?(@project))
+ if @project.archived_changed? &&
+ ((@project.archived == 'true' && !can_archive_project?(@project)) ||
+ (@project.archived == 'false' && !can_restore_project?(@project)))
return_error = true
- is_archive = project_params[:archived] == 'true' ? 'archive' : 'restore'
+ is_archive = @project.archived? ? 'archive' : 'restore'
flash_error =
t("projects.#{is_archive}.error_flash", name: escape_input(@project.name))
- end
- elsif !can_manage_project?(@project)
- render_403 && return
end
- message_renamed = nil
- message_visibility = nil
- if (project_params.include? :name) &&
- (project_params[:name] != @project.name)
- message_renamed = true
- end
- if (project_params.include? :visibility) &&
- (project_params[:visibility] != @project.visibility)
- message_visibility = if project_params[:visibility] == 'visible'
- t('projects.activity.visibility_visible')
- else
- t('projects.activity.visibility_hidden')
- end
+ message_renamed = @project.name_changed?
+ message_visibility = if @project.visibility_changed?
+ nil
+ elsif @project.visible?
+ t('projects.activity.visibility_visible')
+ else
+ t('projects.activity.visibility_hidden')
+ end
+
+ message_archived = if !@project.archived_changed?
+ nil
+ elsif @project.archived?
+ 'archive'
+ else
+ 'restore'
+ end
+
+ default_public_user_name = nil
+ if @project.visibility_changed? && @project.default_public_user_role_id_changed?
+ default_public_user_name = UserRole.find(project_params[:default_public_user_role_id])&.name
end
@project.last_modified_by = current_user
- if !return_error && @project.update(project_params)
- # Add activities if needed
+ if !return_error && @project.save
+ # Add activities if needed
log_activity(:change_project_visibility, @project, visibility: message_visibility) if message_visibility.present?
log_activity(:rename_project) if message_renamed.present?
- log_activity(:archive_project) if project_params[:archived] == 'true'
- log_activity(:restore_project) if project_params[:archived] == 'false'
+ log_activity(:archive_project) if message_archived == 'archive'
+ log_activity(:restore_project) if message_archived == 'restore'
+
+ if default_public_user_name.present?
+ log_activity(:project_access_changed_all_team_members,
+ @project,
+ { team: @project.team.id, role: default_public_user_name })
+ end
flash_success = t('projects.update.success_flash', name: escape_input(@project.name))
- if project_params[:archived] == 'true'
+ if message_archived == 'archive'
flash_success = t('projects.archive.success_flash', name: escape_input(@project.name))
- elsif project_params[:archived] == 'false'
+ elsif message_archived == 'restore'
flash_success = t('projects.restore.success_flash', name: escape_input(@project.name))
end
respond_to do |format|
format.html do
- # Redirect URL for archive view is different as for other views.
- if project_params[:archived] == 'false'
- # The project should be restored
- @project.restore(current_user) unless @project.archived
- elsif @project.archived
- # The project should be archived
- @project.archive(current_user)
- end
+ @project.restore(current_user) if message_archived == 'restore'
+ @project.archive(current_user) if message_archived == 'archive'
+
redirect_to projects_path
flash[:success] = flash_success
end
@@ -404,12 +410,17 @@ class ProjectsController < ApplicationController
def project_params
params.require(:project)
.permit(
- :name, :team_id, :visibility,
+ :name, :visibility,
:archived, :project_folder_id,
:default_public_user_role_id
)
end
+ def project_update_params
+ params.require(:project)
+ .permit(:name, :visibility, :archived, :default_public_user_role_id)
+ end
+
def view_type_params
params.require(:project).require(:view_type)
end
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
index 83caeafa0..43040bf55 100644
--- a/app/controllers/repositories_controller.rb
+++ b/app/controllers/repositories_controller.rb
@@ -382,6 +382,16 @@ class RepositoriesController < ApplicationController
end
end
+ def export_repositories
+ repositories = Repository.viewable_by_user(current_user, current_team).where(id: params[:repository_ids])
+ if repositories.present?
+ RepositoriesExportJob.perform_later(repositories.pluck(:id), current_user, current_team)
+ render json: { message: t('zip_export.export_request_success') }
+ else
+ render json: { message: t('zip_export.export_error') }, status: :unprocessable_entity
+ end
+ end
+
def assigned_my_modules
my_modules = MyModule.joins(:repository_rows).where(repository_rows: { repository: @repository })
.readable_by_user(current_user).distinct
diff --git a/app/javascript/packs/vue/assign_items_to_task_modal.js b/app/javascript/packs/vue/assign_items_to_task_modal.js
index 2cc783622..ab7ddad48 100644
--- a/app/javascript/packs/vue/assign_items_to_task_modal.js
+++ b/app/javascript/packs/vue/assign_items_to_task_modal.js
@@ -1,8 +1,11 @@
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm';
import AssignItemsToTaskModalContainer from '../../vue/assign_items_to_tasks_modal/container.vue';
+import PerfectScrollbar from 'vue2-perfect-scrollbar';
+import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
Vue.use(TurbolinksAdapter);
+Vue.use(PerfectScrollbar);
Vue.prototype.i18n = window.I18n;
function initAssignItemsToTaskModalComponent() {
diff --git a/app/javascript/vue/assign_items_to_tasks_modal/container.vue b/app/javascript/vue/assign_items_to_tasks_modal/container.vue
index d5d54f371..b17ae20bf 100644
--- a/app/javascript/vue/assign_items_to_tasks_modal/container.vue
+++ b/app/javascript/vue/assign_items_to_tasks_modal/container.vue
@@ -43,6 +43,7 @@
ref="projectsSelector"
@change="changeProject"
:options="projects"
+ :isLoading="projectsLoading"
:placeholder="
i18n.t(
'repositories.modal_assign_items_to_task.body.project_select.placeholder'
@@ -76,6 +77,7 @@
ref="experimentsSelector"
@change="changeExperiment"
:options="experiments"
+ :isLoading="experimentsLoading"
:placeholder="experimentsSelectorPlaceholder"
:no-options-placeholder="
i18n.t(
@@ -105,6 +107,7 @@
ref="tasksSelector"
@change="changeTask"
:options="tasks"
+ :isLoading="tasksLoading"
:placeholder="tasksSelectorPlaceholder"
:no-options-placeholder="
i18n.t(
@@ -153,6 +156,9 @@ export default {
selectedProject: null,
selectedExperiment: null,
selectedTask: null,
+ projectsLoading: null,
+ experimentsLoading: null,
+ tasksLoading: null,
showCallback: null
};
},
@@ -164,12 +170,16 @@ export default {
},
mounted() {
$(this.$refs.modal).on("shown.bs.modal", () => {
+ this.projectsLoading = true;
+
$.get(this.projectURL, data => {
if (Array.isArray(data)) {
this.projects = data;
return false;
}
this.projects = [];
+ }).always(() => {
+ this.projectsLoading = false;
});
});
@@ -240,24 +250,30 @@ export default {
this.resetExperimentSelector();
this.resetTaskSelector();
+ this.experimentsLoading = true;
$.get(this.experimentURL, data => {
if (Array.isArray(data)) {
this.experiments = data;
return false;
}
this.experiments = [];
+ }).always(() => {
+ this.experimentsLoading = false;
});
},
changeExperiment(value) {
this.selectedExperiment = value;
this.resetTaskSelector();
+ this.tasksLoading = true;
$.get(this.taskURL, data => {
if (Array.isArray(data)) {
this.tasks = data;
return false;
}
this.tasks = [];
+ }).always(() => {
+ this.tasksLoading = false;
});
},
changeTask(value) {
diff --git a/app/javascript/vue/components/action_toolbar.vue b/app/javascript/vue/components/action_toolbar.vue
index 42ac75926..ffd0b513c 100644
--- a/app/javascript/vue/components/action_toolbar.vue
+++ b/app/javascript/vue/components/action_toolbar.vue
@@ -92,7 +92,10 @@
this.buttonOverflow = false;
this.$nextTick(() => {
- if (!this.$el.getBoundingClientRect) return;
+ if (
+ !(this.$el.getBoundingClientRect &&
+ document.querySelector('.sn-action-toolbar__action:last-child'))
+ ) return;
let containerRect = this.$el.getBoundingClientRect();
let lastActionRect = document.querySelector('.sn-action-toolbar__action:last-child').getBoundingClientRect();
diff --git a/app/javascript/vue/navigation/navigator_item.vue b/app/javascript/vue/navigation/navigator_item.vue
index 4ba4e7f31..2c29c7632 100644
--- a/app/javascript/vue/navigation/navigator_item.vue
+++ b/app/javascript/vue/navigation/navigator_item.vue
@@ -17,7 +17,8 @@
class="text-ellipsis overflow-hidden hover:no-underline pr-3"
:class="{
'text-sn-science-blue-hover': (!item.archived && archived),
- 'no-hover': (!item.archived && archived)
+ 'no-hover': (!item.archived && archived),
+ 'disabled-link': item.disabled
}">
(A)
{{ item.name }}
@@ -62,7 +63,7 @@ export default {
},
computed: {
hasChildren: function() {
- return this.item.has_children || this.children.length > 0;
+ return !this.item.disabled && (this.item.has_children || this.children.length > 0);
},
sortedMenuItems: function() {
return this.children.sort((a, b) => {
diff --git a/app/javascript/vue/protocol/step_elements/table.vue b/app/javascript/vue/protocol/step_elements/table.vue
index 4e87d377d..e19e80a4f 100644
--- a/app/javascript/vue/protocol/step_elements/table.vue
+++ b/app/javascript/vue/protocol/step_elements/table.vue
@@ -95,7 +95,8 @@
editingTable: false,
tableObject: null,
nameModalOpen: false,
- reloadHeader: 0
+ reloadHeader: 0,
+ updatingTableData: false
}
},
computed: {
@@ -104,10 +105,10 @@
}
},
updated() {
- this.loadTableData();
+ if(!this.updatingTableData) this.loadTableData();
},
beforeUpdate() {
- this.tableObject.destroy();
+ if(!this.updatingTableData) this.tableObject.destroy();
},
mounted() {
this.loadTableData();
@@ -130,6 +131,7 @@
},
disableTableEdit() {
this.editingTable = false;
+ this.updatingTableData = false;
},
enableNameEdit() {
this.editingName = true;
@@ -203,6 +205,7 @@
}
this.$emit('update', this.element)
this.ajax_update_url()
+ this.updatingTableData = false;
},
ajax_update_url() {
$.ajax({
@@ -229,7 +232,10 @@
formulas: formulasEnabled,
preventOverflow: 'horizontal',
readOnly: !this.editingTable,
- afterUnlisten: () => setTimeout(this.updateTable, 100) // delay makes cancel button work
+ afterUnlisten: () => {
+ this.updatingTableData = true;
+ setTimeout(this.updateTable, 100) // delay makes cancel button work
+ }
});
}
}
diff --git a/app/javascript/vue/shared/select.vue b/app/javascript/vue/shared/select.vue
index 83788fcf3..9d11dd0b5 100644
--- a/app/javascript/vue/shared/select.vue
+++ b/app/javascript/vue/shared/select.vue
@@ -14,7 +14,7 @@
{{ option[1] }}
@@ -47,7 +47,8 @@
data() {
return {
isOpen: false,
- optionPositionStyle: ''
+ optionPositionStyle: '',
+ blurPrevented: false
}
},
computed: {
@@ -64,13 +65,27 @@
document.addEventListener("scroll", this.updateOptionPosition);
},
methods: {
+ preventBlur() {
+ this.blurPrevented = true;
+ },
+ allowBlur() {
+ setTimeout(() => { this.blurPrevented = false }, 200);
+ },
blur() {
setTimeout(() => {
- this.isOpen = false;
- this.$emit('blur');
- }, 200);
+ if (this.blurPrevented) {
+ this.focusElement.focus();
+ } else {
+ this.isOpen = false;
+ this.$emit('blur');
+ }
+ }, 100);
},
toggle() {
+ if (this.isOpen && this.blurPrevented) {
+ return;
+ }
+
this.isOpen = !this.isOpen;
if (this.isOpen) {
@@ -80,6 +95,7 @@
});
this.$refs.optionsContainer.scrollTop = 0;
this.updateOptionPosition();
+ this.setUpBlurHandlers();
} else {
this.optionPositionStyle = '';
this.$emit('close');
@@ -104,6 +120,13 @@
}
this.optionPositionStyle = `position: fixed; top: ${top}px; left: ${left}px; width: ${width}px`
+ },
+ setUpBlurHandlers() {
+ setTimeout(() => {
+ this.$refs.optionsContainer.$el.querySelector('.ps__thumb-y').addEventListener('mousedown', this.preventBlur);
+ this.$refs.optionsContainer.$el.querySelector('.ps__thumb-y').addEventListener('mouseup', this.allowBlur);
+ document.addEventListener('mouseup', this.allowBlur);
+ }, 100);
}
}
}
diff --git a/app/javascript/vue/shared/select_search.vue b/app/javascript/vue/shared/select_search.vue
index 74b43d92c..cc93c8df1 100644
--- a/app/javascript/vue/shared/select_search.vue
+++ b/app/javascript/vue/shared/select_search.vue
@@ -4,7 +4,7 @@
:value="value"
:options="currentOptions"
:placeholder="placeholder"
- :noOptionsPlaceholder="noOptionsPlaceholder"
+ :noOptionsPlaceholder="isLoading ? i18n.t('general.loading') : noOptionsPlaceholder"
v-bind:disabled="disabled"
@change="change"
@blur="blur"
@@ -29,7 +29,8 @@
placeholder: { type: String },
searchPlaceholder: { type: String },
noOptionsPlaceholder: { type: String },
- disabled: { type: Boolean }
+ disabled: { type: Boolean },
+ isLoading: { type: Boolean, default: false }
},
components: { Select },
data() {
diff --git a/app/jobs/repositories_export_job.rb b/app/jobs/repositories_export_job.rb
new file mode 100644
index 000000000..11eabf078
--- /dev/null
+++ b/app/jobs/repositories_export_job.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+class RepositoriesExportJob < ApplicationJob
+ include StringUtility
+
+ def perform(repository_ids, user, team)
+ @user = user
+ @team = team
+ @repositories = Repository.viewable_by_user(@user, @team).where(id: repository_ids).order(:id)
+ zip_input_dir = FileUtils.mkdir_p(Rails.root.join("tmp/temp_zip_#{Time.now.to_i}")).first
+ zip_dir = FileUtils.mkdir_p(Rails.root.join('tmp/zip-ready')).first
+
+ zip_name = "inventories_export_#{Time.now.utc.strftime('%F_%H-%M-%S_UTC')}.zip"
+ full_zip_name = File.join(zip_dir, zip_name)
+
+ fill_content(zip_input_dir)
+ ZipExport.transaction do
+ @zip_export = ZipExport.create!(user: @user)
+ @zip_export.zip!(zip_input_dir, full_zip_name)
+ @zip_export.zip_file.attach(io: File.open(full_zip_name), filename: zip_name)
+ generate_notification
+ end
+ ensure
+ FileUtils.rm_rf([zip_input_dir, full_zip_name], secure: true)
+ end
+
+ private
+
+ def fill_content(tmp_dir)
+ # Create team dir
+ team_path = "#{tmp_dir}/#{to_filesystem_name(@team.name)}"
+ FileUtils.mkdir_p(team_path)
+ @repositories.each_with_index do |repository, idx|
+ save_repository_to_csv(team_path, repository, idx)
+ end
+ end
+
+ def save_repository_to_csv(path, repository, idx)
+ repository_name = "#{to_filesystem_name(repository.name)} (#{idx})"
+
+ # Attachments dir
+ relative_attachments_path = "#{repository_name} attachments"
+ attachments_path = "#{path}/#{relative_attachments_path}"
+ FileUtils.mkdir_p(attachments_path)
+
+ # CSV file
+ csv_file = FileUtils.touch("#{path}/#{repository_name}.csv").first
+
+ # Define headers and columns IDs
+ col_ids = [-3, -4, -5, -6] + repository.repository_columns.map(&:id)
+
+ # Define callback function for file name
+ assets = {}
+ asset_counter = 0
+ handle_name_func = lambda do |asset|
+ file_name = append_file_suffix(asset.file_name, "_#{asset_counter}").to_s
+
+ # Save pair for downloading it later
+ assets[asset] = "#{attachments_path}/#{file_name}"
+
+ asset_counter += 1
+ relative_path = "#{relative_attachments_path}/#{file_name}"
+ return "=HYPERLINK(\"#{relative_path}\", \"#{relative_path}\")"
+ end
+
+ # Generate CSV
+ csv_data = RepositoryZipExport.to_csv(repository.repository_rows, col_ids, @user, repository, handle_name_func)
+ File.binwrite(csv_file, csv_data)
+
+ # Save all attachments (it doesn't work directly in callback function
+ assets.each do |asset, asset_path|
+ asset.file.open do |file|
+ FileUtils.mv(file.path, asset_path)
+ end
+ end
+ end
+
+ def append_file_suffix(file_name, suffix)
+ file_name = to_filesystem_name(file_name)
+ ext = File.extname(file_name)
+ File.basename(file_name, ext) + suffix + ext
+ end
+
+ def generate_notification
+ notification = Notification.create!(
+ type_of: :deliver,
+ title: I18n.t('zip_export.notification_title'),
+ message: "
" \
+ "#{@zip_export.zip_file_name} "
+ )
+ UserNotification.create!(notification: notification, user: @user)
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index b513a598e..2735d78a7 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -113,23 +113,21 @@ class Project < ApplicationRecord
joins("
LEFT OUTER JOIN experiments ON experiments.project_id = projects.id
LEFT OUTER JOIN user_assignments experiment_user_assignments
- ON experiment_user_assignments.assignable_id = experiments.id AND
- experiment_user_assignments.assignable_type = 'Experiment'
+ ON experiment_user_assignments.assignable_id = experiments.id
+ AND experiment_user_assignments.assignable_type = 'Experiment'
+ AND experiment_user_assignments.user_id = #{user.id}
LEFT OUTER JOIN user_roles experiment_user_roles
ON experiment_user_roles.id = experiment_user_assignments.user_role_id
+ AND experiment_user_roles.permissions @> ARRAY['#{ExperimentPermissions::READ}']::varchar[]
LEFT OUTER JOIN my_modules ON my_modules.experiment_id = experiments.id
LEFT OUTER JOIN user_assignments my_module_user_assignments
- ON my_module_user_assignments.assignable_id = my_modules.id AND
- my_module_user_assignments.assignable_type = 'MyModule'
+ ON my_module_user_assignments.assignable_id = my_modules.id
+ AND my_module_user_assignments.assignable_type = 'MyModule'
+ AND my_module_user_assignments.user_id = #{user.id}
LEFT OUTER JOIN user_roles my_module_user_roles
ON my_module_user_roles.id = my_module_user_assignments.user_role_id
+ AND my_module_user_roles.permissions @> ARRAY['#{MyModulePermissions::READ}']::varchar[]
")
- .where('
- (experiment_user_assignments.user_id = ? AND experiment_user_roles.permissions @> ARRAY[?]::varchar[]
- OR experiments.id IS NULL) AND
- (my_module_user_assignments.user_id = ? AND my_module_user_roles.permissions @> ARRAY[?]::varchar[]
- OR my_modules.id IS NULL)
- ', user.id, ExperimentPermissions::READ, user.id, MyModulePermissions::READ)
end
def self.filter_by_teams(teams = [])
diff --git a/app/models/step.rb b/app/models/step.rb
index f8bd376e6..128053908 100644
--- a/app/models/step.rb
+++ b/app/models/step.rb
@@ -97,7 +97,7 @@ class Step < ApplicationRecord
end
def self.viewable_by_user(user, teams)
- where(protocol: Protocol.viewable_by_user(user, teams))
+ joins(:protocol).where(protocol: { my_module: MyModule.viewable_by_user(user, teams) })
end
def can_destroy?
diff --git a/app/models/team_zip_export.rb b/app/models/team_zip_export.rb
index a75292289..b55ad9ee2 100644
--- a/app/models/team_zip_export.rb
+++ b/app/models/team_zip_export.rb
@@ -13,16 +13,15 @@ class TeamZipExport < ZipExport
).first
zip_dir = FileUtils.mkdir_p(File.join(Rails.root, 'tmp/zip-ready')).first
- zip_name = "projects_export_#{Time.now.strftime('%F_%H-%M-%S_UTC')}.zip"
+ zip_name = "projects_export_#{Time.now.utc.strftime('%F_%H-%M-%S_UTC')}.zip"
full_zip_name = File.join(zip_dir, zip_name)
- zip_file = File.new(full_zip_name, 'w+')
fill_content(zip_input_dir, data, type, options)
- zip!(zip_input_dir, zip_file)
- self.zip_file.attach(io: File.open(zip_file), filename: zip_name)
+ zip!(zip_input_dir, full_zip_name)
+ zip_file.attach(io: File.open(full_zip_name), filename: zip_name)
generate_notification(user) if save
ensure
- FileUtils.rm_rf([zip_input_dir, zip_file], secure: true)
+ FileUtils.rm_rf([zip_input_dir, full_zip_name], secure: true)
end
handle_asynchronously :generate_exportable_zip,
@@ -320,38 +319,4 @@ class TeamZipExport < ZipExport
csv_file_path
end
-
- # Recursive zipping
- def zip!(input_dir, output_file)
- files = Dir.entries(input_dir)
-
- # Don't zip current/above directory
- files.delete_if { |el| ['.', '..'].include?(el) }
-
- Zip::File.open(output_file.path, Zip::File::CREATE) do |zipfile|
- write_entries(input_dir, files, '', zipfile)
- end
- end
-
- # A helper method to make the recursion work.
- def write_entries(input_dir, entries, path, io)
- entries.each do |e|
- zip_file_path = path == '' ? e : File.join(path, e)
- disk_file_path = File.join(input_dir, zip_file_path)
- puts 'Deflating ' + disk_file_path
- if File.directory?(disk_file_path)
- io.mkdir(zip_file_path)
- subdir = Dir.entries(disk_file_path)
-
- # Remove current/above directory to prevent infinite recursion
- subdir.delete_if { |el| ['.', '..'].include?(el) }
-
- write_entries(input_dir, subdir, zip_file_path, io)
- else
- io.get_output_stream(zip_file_path) do |f|
- f.write(File.open(disk_file_path, 'rb').read)
- end
- end
- end
- end
end
diff --git a/app/models/zip_export.rb b/app/models/zip_export.rb
index abfba3e89..f04b33185 100644
--- a/app/models/zip_export.rb
+++ b/app/models/zip_export.rb
@@ -36,19 +36,27 @@ class ZipExport < ApplicationRecord
zip_file.blob&.filename&.to_s
end
+ def zip!(input_dir, output_file)
+ entries = Dir.glob('**/*', base: input_dir)
+ Zip::File.open(output_file, create: true) do |zipfile|
+ entries.each do |entry|
+ zipfile.add(entry, "#{input_dir}/#{entry}")
+ end
+ end
+ end
+
def generate_exportable_zip(user, data, type, options = {})
I18n.backend.date_format = user.settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
zip_input_dir = FileUtils.mkdir_p(File.join(Rails.root, "tmp/temp_zip_#{Time.now.to_i}")).first
tmp_zip_dir = FileUtils.mkdir_p(File.join(Rails.root, 'tmp/zip-ready')).first
- tmp_zip_name = "export_#{Time.now.strftime('%F %H-%M-%S_UTC')}.zip"
- tmp_zip_file = File.new(File.join(tmp_zip_dir, tmp_zip_name), 'w+')
+ tmp_full_zip_name = File.join(tmp_zip_dir, "export_#{Time.now.strftime('%F %H-%M-%S_UTC')}.zip")
fill_content(zip_input_dir, data, type, options)
zip!(zip_input_dir, tmp_zip_file)
- zip_file.attach(io: File.open(tmp_zip_file), filename: tmp_zip_name)
+ zip_file.attach(io: File.open(tmp_full_zip_name), filename: tmp_zip_name)
generate_notification(user) if save
ensure
- FileUtils.rm_rf([zip_input_dir, tmp_zip_file], secure: true)
+ FileUtils.rm_rf([zip_input_dir, tmp_full_zip_name], secure: true)
end
handle_asynchronously :generate_exportable_zip
@@ -60,14 +68,14 @@ class ZipExport < ApplicationRecord
.delete_expired_export(id)
end
- def method_missing(m, *args, &block)
- puts 'Method is missing! To use this zip_export you have to ' \
- 'define a method: generate_( type )_zip.'
- object.public_send(m, *args, &block)
+ def method_missing(method_name, *args, &block)
+ return super unless method_name.to_s.start_with?('generate_')
+
+ raise StandardError, 'Method is missing! To use this zip_export you have to define a method: generate_( type )_zip.'
end
def respond_to_missing?(method_name, include_private = false)
- method_name.to_s.start_with?(' generate_') || super
+ method_name.to_s.start_with?('generate_') || super
end
def fill_content(dir, data, type, options = {})
@@ -89,16 +97,6 @@ class ZipExport < ApplicationRecord
UserNotification.create(notification: notification, user: user)
end
- def zip!(input_dir, output_file)
- files = Dir.entries(input_dir)
- files.delete_if { |el| el == '..' || el == '.' }
- Zip::File.open(output_file.path, Zip::File::CREATE) do |zipfile|
- files.each do |filename|
- zipfile.add(filename, input_dir + '/' + filename)
- end
- end
- end
-
def generate_repositories_zip(tmp_dir, data, _options = {})
file = FileUtils.touch("#{tmp_dir}/export.csv").first
File.open(file, 'wb') { |f| f.write(data) }
diff --git a/app/permissions/project.rb b/app/permissions/project.rb
index fca0547cd..4a255b5ac 100644
--- a/app/permissions/project.rb
+++ b/app/permissions/project.rb
@@ -19,8 +19,7 @@ Canaid::Permissions.register_for(Project) do
export_project)
.each do |perm|
can perm do |user, project|
- project.permission_granted?(user, ProjectPermissions::READ) ||
- project.team.permission_granted?(user, TeamPermissions::MANAGE)
+ project.permission_granted?(user, ProjectPermissions::READ)
end
end
diff --git a/app/services/repository_zip_export.rb b/app/services/repository_zip_export.rb
index e8ad574ac..83a609f30 100644
--- a/app/services/repository_zip_export.rb
+++ b/app/services/repository_zip_export.rb
@@ -57,7 +57,7 @@ module RepositoryZipExport
when -8
I18n.t('repositories.table.archived_on')
else
- column = RepositoryColumn.find_by_id(c_id)
+ column = repository.repository_columns.find_by(id: c_id)
column ? column.name : nil
end
end
@@ -88,8 +88,7 @@ module RepositoryZipExport
.find_by(repository_column_id: c_id)
if cell
- if cell.value_type == 'RepositoryAssetValue' &&
- handle_file_name_func
+ if cell.value_type == 'RepositoryAssetValue' && handle_file_name_func
handle_file_name_func.call(cell.value.asset)
else
SmartAnnotations::TagToText.new(
diff --git a/app/views/experiments/_show_header.html.erb b/app/views/experiments/_show_header.html.erb
index 957c42c39..f3ced98d2 100644
--- a/app/views/experiments/_show_header.html.erb
+++ b/app/views/experiments/_show_header.html.erb
@@ -2,8 +2,8 @@
- <% if @experiment.archived_branch? %>
-
+ <% if @experiment.archived? %>
+ <%= t('labels.archived')%>
<% end %>
<% if @inline_editable_title_config.present? %>
<%= render partial: "shared/inline_editing",
diff --git a/app/views/global_activities/_date_picker.html.erb b/app/views/global_activities/_date_picker.html.erb
index 88999d54a..8e6e30229 100644
--- a/app/views/global_activities/_date_picker.html.erb
+++ b/app/views/global_activities/_date_picker.html.erb
@@ -1,7 +1,8 @@
+ data-use-current="<%= use_current %>"
+ data-datetime-picker-format="<%= datetime_picker_format_date_only %>">
<% if label %>
<%= label %>
<% end %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 24361ae2a..88bc2c66b 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -16,7 +16,7 @@
<% if ::NewRelic::Agent.instance.started? %>
<%= ::NewRelic::Agent.browser_timing_header(controller.request.content_security_policy_nonce) %>
<% end %>
- <%= javascript_include_tag 'jquery_bundle' %>
+ <%= javascript_include_tag 'jquery_bundle', nonce: true %>
<%= javascript_include_tag 'application' %>
<%= javascript_include_tag 'application_pack' %>
<%= javascript_include_tag 'session_end' %>
@@ -59,7 +59,7 @@
data-atwho-repositories-url="<%= atwho_menu_team_path(current_team) %>"
data-atwho-rep-items-url="<%= atwho_rep_items_team_path(current_team) %>"
data-atwho-menu-items="<%= atwho_menu_items_team_path(current_team) %>"
- data-datetime-picker-format-date-only="<%= datetime_picker_format_date_only %>"
+ data-datetime-picker-format="<%= datetime_picker_format_date_only %>"
<% end %>
>
diff --git a/app/views/projects/index/_header.html.erb b/app/views/projects/index/_header.html.erb
index df6d9f7e1..6b43a27b9 100644
--- a/app/views/projects/index/_header.html.erb
+++ b/app/views/projects/index/_header.html.erb
@@ -15,7 +15,7 @@
<% end %>
-
+ <%= t('labels.archived')%>
<%= current_folder&.name || t('projects.index.head_title_archived') %>
diff --git a/app/views/projects/index/_project_card.html.erb b/app/views/projects/index/_project_card.html.erb
index 3ff946f92..4462ac663 100644
--- a/app/views/projects/index/_project_card.html.erb
+++ b/app/views/projects/index/_project_card.html.erb
@@ -13,14 +13,17 @@
+ <%
+ disabled_link = 'disabled-link' unless can_read_project?(project)
+ %>
<% if project.archived? %>
- <%= link_to project_url(project, view_mode: :archived) do %>
+ <%= link_to project_url(project, view_mode: :archived), class: disabled_link do %>
<%= project.name %>
<% end %>
<% else %>
- <%= link_to project_url(project) do %>
+ <%= link_to project_url(project), class: disabled_link do %>
<%= project.name %>
diff --git a/app/views/projects/show/_header.html.erb b/app/views/projects/show/_header.html.erb
index f156f7b62..66cd56cd3 100644
--- a/app/views/projects/show/_header.html.erb
+++ b/app/views/projects/show/_header.html.erb
@@ -3,7 +3,7 @@
<% if @project.archived? %>
-
+ <%= t('labels.archived')%>
<% end %>
<% if @inline_editable_title_config.present? %>
<%= render partial: "shared/inline_editing",
diff --git a/app/views/protocols/index.html.erb b/app/views/protocols/index.html.erb
index 6fe3b527f..49a0c80e0 100644
--- a/app/views/protocols/index.html.erb
+++ b/app/views/protocols/index.html.erb
@@ -16,7 +16,7 @@
<% if templates_view_mode_archived?(type: @type) %>
-
+ <%= t('labels.archived')%>
<%= t('protocols.index.head_title_archived') %>
<% else %>
diff --git a/app/views/protocols/show.html.erb b/app/views/protocols/show.html.erb
index fe668796a..e227114a8 100644
--- a/app/views/protocols/show.html.erb
+++ b/app/views/protocols/show.html.erb
@@ -15,7 +15,7 @@
} %>
<% else %>
<% if @protocol.archived %>
-
+
<%= t('labels.archived')%>
<% end %>
<% if @protocol.in_repository_draft? %>
diff --git a/app/views/repositories/_sidebar.html.erb b/app/views/repositories/_sidebar.html.erb
index 4248f14e6..cb7588ccc 100644
--- a/app/views/repositories/_sidebar.html.erb
+++ b/app/views/repositories/_sidebar.html.erb
@@ -5,7 +5,7 @@
class: "sidebar-link repository-link #{ 'selected' if current_page?(repository_path(repository)) }",
data: {type: 'repository', id: repository.id } do %>
<% if repository.archived? %>
-
+
<%= t('labels.archived')%>
<% end %>
<%= repository.name %>
<%= inventory_shared_status_icon(repository, current_team) %>
diff --git a/app/views/repositories/_view_archived_btn.html.erb b/app/views/repositories/_view_archived_btn.html.erb
index fa1e391ed..3be64c929 100644
--- a/app/views/repositories/_view_archived_btn.html.erb
+++ b/app/views/repositories/_view_archived_btn.html.erb
@@ -3,7 +3,7 @@
diff --git a/app/views/repositories/show.html.erb b/app/views/repositories/show.html.erb
index c2eafc7a5..cb0914144 100644
--- a/app/views/repositories/show.html.erb
+++ b/app/views/repositories/show.html.erb
@@ -37,7 +37,7 @@
<% end %>
<% if @repository.archived? %>
-
+ <%= t('labels.archived')%>
<%= t('repositories.show.archived_inventory', repository_name: @repository.name) %>
<% else %>
<%= t('repositories.show.archived_inventory_items', repository_name: @repository.name) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 678f9a21b..f0abc8223 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -430,6 +430,9 @@ en:
delete_error: "Error occurred while deleting comment."
step_url: "Go to step"
+ labels:
+ archived: "(A)"
+
projects:
index:
header:
@@ -3444,6 +3447,7 @@ en:
download: "Download"
access: "Access"
select: "Select"
+ loading: "Loading..."
# In order to use the strings 'yes' and 'no' as keys, you need to wrap them with quotes
'yes': "Yes"
'no': "No"
diff --git a/config/locales/global_activities/en.yml b/config/locales/global_activities/en.yml
index cc3ba3fbb..022374de4 100644
--- a/config/locales/global_activities/en.yml
+++ b/config/locales/global_activities/en.yml
@@ -87,7 +87,7 @@ en:
uncomplete_task_html: "%{user} uncompleted task %{my_module}."
assign_repository_record_html: "%{user} assigned inventory item(s) %{record_names} from inventory %{repository} to task %{my_module}."
unassign_repository_record_html: "%{user} unassigned inventory item(s) %{record_names} from inventory %{repository} to task %{my_module}."
- assign_user_to_project_html: "%{user} granted with access %{user_target} with user role %{role} to project %{project}."
+ assign_user_to_project_html: "%{user} granted access to %{user_target} with user role %{role} to project %{project}."
unassign_user_from_project_html: "%{user} removed %{user_target} with user role %{role} from project %{project}."
change_user_role_on_project_html: "%{user} changed %{user_target}'s role on project %{project} to %{role}."
change_user_role_on_experiment_html: "%{user} changed %{user_target}'s role on experiment %{experiment} to %{role}."
@@ -258,10 +258,10 @@ en:
protocol_template_revision_notes_updated_html: "%{user} edited revision notes of %{protocol}."
protocol_template_draft_deleted_html: "%{user} deleted draft of %{protocol}."
protocol_template_draft_created_html: "%{user} created draft of %{protocol}."
- protocol_template_access_granted_html: "%{user} granted with access %{user_target} with user role %{role} to protocol template %{protocol}."
+ protocol_template_access_granted_html: "%{user} granted access to %{user_target} with user role %{role} to protocol template %{protocol}."
protocol_template_access_changed_html: "%{user} changed %{user_target}’s role on protocol template %{protocol} to %{role}."
protocol_template_access_revoked_html: "%{user} removed %{user_target} with user role %{role} from protocol template %{protocol}."
- protocol_template_access_granted_all_team_members_html: "%{user} granted with access all team members of %{team} team with user role %{role} to protocol template %{protocol}."
+ protocol_template_access_granted_all_team_members_html: "%{user} granted access to all team members of %{team} team with user role %{role} to protocol template %{protocol}."
protocol_template_access_changed_all_team_members_html: "%{user} changed %{team}’s role on protocol template %{protocol} to %{role}."
protocol_template_access_revoked_all_team_members_html: "%{user} removed %{team} team members with user role %{role} from protocol template %{protocol}."
task_protocol_save_to_template_html: "%{user} created a new protocol template %{protocol} from a task."
diff --git a/config/routes.rb b/config/routes.rb
index 253b8d88b..5b372f97e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -219,6 +219,7 @@ Rails.application.routes.draw do
member do
post 'parse_sheet', defaults: { format: 'json' }
post 'export_repository', to: 'repositories#export_repository'
+ post 'export_repositories', to: 'repositories#export_repositories'
post 'export_projects'
get 'sidebar'
get 'export_projects_modal'