Merge branch 'develop' into features/file-versioning

This commit is contained in:
Martin Artnik 2024-10-14 13:09:42 +02:00
commit 8c356f7293
27 changed files with 251 additions and 92 deletions

View file

@ -6,7 +6,7 @@ class StorageLocationRepositoryRowsController < ApplicationController
before_action :load_storage_location
before_action :load_repository_row, only: %i(create update destroy move)
before_action :check_read_permissions, except: %i(create actions_toolbar)
before_action :check_manage_permissions, only: %i(create update destroy)
before_action :check_manage_permissions, only: %i(create update destroy move)
def index
storage_location_repository_row = Lists::StorageLocationRepositoryRowsService.new(
@ -54,6 +54,9 @@ class StorageLocationRepositoryRowsController < ApplicationController
def move
ActiveRecord::Base.transaction do
@original_storage_location = @storage_location_repository_row.storage_location
@original_position = @storage_location_repository_row.human_readable_position
@storage_location_repository_row.discard
@storage_location_repository_row = StorageLocationRepositoryRow.create!(
repository_row: @repository_row,
@ -61,7 +64,13 @@ class StorageLocationRepositoryRowsController < ApplicationController
metadata: storage_location_repository_row_params[:metadata] || {},
created_by: current_user
)
log_activity(:storage_location_repository_row_moved)
log_activity(
:storage_location_repository_row_moved,
{
storage_location_original: @original_storage_location.id,
position_original: @original_position
}
)
render json: @storage_location_repository_row,
serializer: Lists::StorageLocationRepositoryRowSerializer
rescue ActiveRecord::RecordInvalid => e
@ -125,7 +134,7 @@ class StorageLocationRepositoryRowsController < ApplicationController
end
def check_manage_permissions
render_403 unless can_create_storage_location_repository_rows?(@storage_location)
render_403 unless can_manage_storage_location_repository_rows?(@storage_location)
end
def log_activity(type_of, message_items = {})

View file

@ -10,6 +10,7 @@ class StorageLocationsController < ApplicationController
before_action :check_storage_locations_enabled, except: :unassign_rows
before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container)
before_action :check_read_permissions, except: %i(index create tree actions_toolbar import_container unassign_rows)
before_action :check_manage_repository_rows_permissions, only: %i(import_container unassign_rows)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy duplicate move)
before_action :set_breadcrumbs_items, only: %i(index show)
@ -86,7 +87,7 @@ class StorageLocationsController < ApplicationController
def duplicate
ActiveRecord::Base.transaction do
new_storage_location = @storage_location.duplicate!(current_user)
new_storage_location = @storage_location.duplicate!(current_user, current_team)
if new_storage_location
@storage_location = new_storage_location
log_activity('storage_location_created')
@ -104,9 +105,11 @@ class StorageLocationsController < ApplicationController
if move_params[:destination_storage_location_id] == 'root_storage_location'
nil
else
current_team.storage_locations.find(move_params[:destination_storage_location_id])
StorageLocation.find(move_params[:destination_storage_location_id])
end
render_403 and return if destination_storage_location && !can_manage_storage_location?(destination_storage_location)
@storage_location.update!(parent: destination_storage_location)
log_activity('storage_location_moved', {
@ -228,6 +231,10 @@ class StorageLocationsController < ApplicationController
render_403 unless can_manage_storage_location?(@storage_location)
end
def check_manage_repository_rows_permissions
render_403 unless can_manage_storage_location_repository_rows?(@storage_location)
end
def set_breadcrumbs_items
@breadcrumbs_items = []

View file

@ -8,26 +8,24 @@ class TeamSharedObjectsController < ApplicationController
ActiveRecord::Base.transaction do
@activities_to_log = []
global_permission_level =
if params[:select_all_teams]
params[:select_all_write_permission] ? :shared_write : :shared_read
else
:not_shared
end
# Global share
if @model.globally_shareable?
permission_level =
if params[:select_all_teams]
params[:select_all_write_permission] ? :shared_write : :shared_read
else
:not_shared
end
@model.permission_level = permission_level
@model.permission_level = global_permission_level
if @model.permission_level_changed?
@model.save!
@model.team_shared_objects.each(&:destroy!) unless permission_level == :not_shared
@model.team_shared_objects.each(&:destroy!) unless global_permission_level == :not_shared
case @model
when Repository
setup_repository_global_share_activity
end
log_activities and next
end
end
@ -35,11 +33,10 @@ class TeamSharedObjectsController < ApplicationController
params[:team_share_params].each do |t|
next unless t['private_shared_with']
@model.update!(permission_level: :not_shared) if @model.globally_shareable?
team_shared_object = @model.team_shared_objects.find_or_initialize_by(team_id: t['id'])
new_record = team_shared_object.new_record?
team_shared_object.update!(
permission_level: t['private_shared_with_write'] ? :shared_write : :shared_read
)

View file

@ -225,6 +225,7 @@
:title="i18n.t('protocols.reorder_steps.modal.title')"
:items="steps"
:includeNumbers="true"
dataE2e="protocol-templateSteps-reorder"
@reorder="updateStepOrder"
@close="closeStepReorderModal"
/>

View file

@ -162,7 +162,7 @@
<ReorderableItemsModal v-if="reordering"
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
:items="reorderableElements"
:dataE2e="`e2e-BT-protocol-step${step.id}-reorder`"
:dataE2e="`protocol-step${step.id}-reorder`"
@reorder="updateElementOrder"
@close="closeReorderModal"
/>

View file

@ -31,7 +31,12 @@
</div>
<div class="mt-6" :class="{'hidden': !visible}">
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.role_label") }}</label>
<SelectDropdown :options="userRoles" :value="defaultRole" @change="changeRole" />
<SelectDropdown
:options="userRoles"
:value="defaultRole"
:data-e2e="`e2e-DD-newProtocolModal-defaultUserRole`"
@change="changeRole"
/>
</div>
</div>
<div class="modal-footer">

View file

@ -54,8 +54,27 @@
v-if="shareRepository"
:object="shareRepository"
:globalShareEnabled="true"
:confirmationModal="$refs.shareConfirmationModal"
@close="shareRepository = null"
@share="updateTable" />
<ConfirmationModal
ref="shareConfirmationModal"
:title="i18n.t('repositories.index.modal_confirm_sharing.title')"
:description="`
<p>${i18n.t('repositories.index.modal_confirm_sharing.description_1')}</p>
<p><b>${i18n.t('repositories.index.modal_confirm_sharing.description_2')}</b></p>
`"
:confirmClass="'btn btn-danger'"
:confirmText="i18n.t('repositories.index.modal_confirm_sharing.confirm')"
:e2eAttributes="{
modalName: 'e2e-MD-confirmSharingChanges',
title: 'e2e-TX-confirmSharingChangesModal-title',
content: 'e2e-TX-confirmSharingChangesModal-content',
close: 'e2e-BT-confirmSharingChangesModal-close',
cancel: 'e2e-BT-confirmSharingChangesModal-cancel',
confirm: 'e2e-BT-confirmSharingChangesModal-delete'
}"
></ConfirmationModal>
</template>
<script>

View file

@ -97,6 +97,7 @@
<ReorderableItemsModal v-if="reordering"
:title="i18n.t('my_modules.modals.reorder_results.title')"
:items="reorderableElements"
:dataE2e="`task-result${result.id}-reorder`"
@reorder="updateElementOrder"
@close="closeReorderModal"
/>

View file

@ -1,22 +1,27 @@
<template>
<div class="mb-6">
<div class="sci-label mb-2">
<div class="sci-label mb-2" :data-e2e="`e2e-TX-${dataE2e}-grantAccessLabel`">
{{ i18n.t('access_permissions.partials.new_assignments_form.grant_access') }}
</div>
<GeneralDropdown ref="dropdown" @open="$emit('assigningNewUsers', true)" @close="$emit('assigningNewUsers', false)" :fieldOnlyOpen="true" :fixed-width="true">
<template v-slot:field>
<div class="sci-input-container-v2 left-icon">
<input type="text" v-model="query" class="sci-input-field"
:placeholder="i18n.t('access_permissions.partials.new_assignments_form.find_people_html')" />
<i class="sn-icon sn-icon-search"></i>
<input
type="text"
v-model="query"
class="sci-input-field"
:placeholder="i18n.t('access_permissions.partials.new_assignments_form.find_people_html')"
:data-e2e="`e2e-IF-${dataE2e}-searchUsers`"
/>
<i class="sn-icon sn-icon-search" :data-e2e="`e2e-IC-${dataE2e}-searchUsers`"></i>
</div>
</template>
<template v-slot:flyout>
<div v-if="!visible && roles.length > 0" class="py-2 flex border-solid border-0 border-b border-b-sn-sleepy-grey items-center gap-2">
<div>
<img src="/images/icon/team.png" class="rounded-full w-8 h-8">
<img src="/images/icon/team.png" class="rounded-full w-8 h-8" :data-e2e="`e2e-IC-${dataE2e}-grantAccessTeam`">
</div>
<div>
<div :data-e2e="`e2e-TX-${dataE2e}-grantAccessTeam`">
{{ i18n.t('user_assignment.assign_all_team_members') }}
</div>
<MenuDropdown
@ -25,16 +30,23 @@
btnText="Assign"
:position="'right'"
:caret="true"
:data-e2e="`e2e-DD-${dataE2e}-grantAccessTeam`"
@setRole="(...args) => this.assignRole('all', ...args)"
></MenuDropdown>
</div>
<perfect-scrollbar class="max-h-80 relative">
<div v-for="user in filteredUsers" :key="user.id" class="py-2 flex items-center w-full">
<div>
<img :src="user.attributes.avatar_url" class="rounded-full w-8 h-8">
<img :src="user.attributes.avatar_url" class="rounded-full w-8 h-8" :data-e2e="`e2e-IC-${dataE2e}-${user.attributes.name.replace(/\W/g, '')}-grantAccess`">
</div>
<div class="truncate ml-2" :title="user.attributes.name">{{ user.attributes.name }}</div>
<div v-if="user.attributes.current_user" class="text-nowrap ml-1">
<div
class="truncate ml-2"
:title="user.attributes.name"
:data-e2e="`e2e-TX-${dataE2e}-${user.attributes.name.replace(/\W/g, '')}-grantAccess-name`"
>
{{ user.attributes.name }}
</div>
<div v-if="user.attributes.current_user" class="text-nowrap ml-1" :data-e2e="`e2e-TX-${dataE2e}-${user.attributes.name.replace(/\W/g, '')}-grantAccess-permission`">
{{ `(${i18n.t('access_permissions.you')})` }}
</div>
<MenuDropdown
@ -43,10 +55,11 @@
btnText="Assign"
:position="'right'"
:caret="true"
:data-e2e="`e2e-DD-${dataE2e}-${user.attributes.name.replace(/\W/g, '')}-grantAccess`"
@setRole="(...args) => this.assignRole(user.id, ...args)"
></MenuDropdown>
</div>
<div v-if="filteredUsers.length === 0" class="p-2 flex items-center w-full">
<div v-if="filteredUsers.length === 0" class="p-2 flex items-center w-full" :data-e2e="`e2e-TX-${dataE2e.replace(/\W/g, '')}-grantAccess-noResults`">
{{ i18n.t('access_permissions.no_results') }}
</div>
</perfect-scrollbar>
@ -76,6 +89,10 @@ export default {
},
reloadUsers: {
type: Boolean
},
dataE2e: {
type: String,
default: ''
}
},
mounted() {

View file

@ -3,14 +3,14 @@
<perfect-scrollbar class="h-[50vh] relative">
<div v-if="roles.length > 0 && visible && default_role" class="p-2 flex items-center gap-2 border-solid border-0 border-b border-b-sn-sleepy-grey">
<div>
<img src="/images/icon/team.png" class="rounded-full w-8 h-8">
<img src="/images/icon/team.png" class="rounded-full w-8 h-8" :data-e2e="`e2e-IC-${dataE2e}-everyoneElse-team`">
</div>
<div>
<div :data-e2e="`e2e-TX-${dataE2e}-everyoneElse`">
{{ i18n.t('access_permissions.everyone_else', { team_name: params.object.team }) }}
</div>
<GeneralDropdown @open="loadUsers" @close="closeFlyout">
<template v-slot:field>
<i class="sn-icon sn-icon-info"></i>
<i class="sn-icon sn-icon-info" :data-e2e="`e2e-IC-${dataE2e}-everyoneElse-info`"></i>
</template>
<template v-slot:flyout>
<perfect-scrollbar class="flex flex-col max-h-96 max-w-[280px] relative pr-4 gap-y-px">
@ -18,8 +18,12 @@
:key="user.attributes.user.id"
:title="user.attributes.user.name"
class="rounded px-3 py-2.5 flex items-center hover:no-underline leading-5 gap-2">
<img :src="user.attributes.user.avatar_url" class="w-6 h-6 rounded-full">
<span class="truncate">{{ user.attributes.user.name }}</span>
<img
:src="user.attributes.user.avatar_url"
class="w-6 h-6 rounded-full"
:data-e2e="`e2e-IC-${dataE2e}-everyoneElse-${user.attributes.user.name.replace(/\W/g, '')}`"
>
<span class="truncate" :data-e2e="`e2e-TX-${dataE2e}-everyoneElse-${user.attributes.user.name.replace(/\W/g, '')}`">{{ user.attributes.user.name }}</span>
</div>
</perfect-scrollbar>
</template>
@ -31,6 +35,7 @@
:btnText="this.roles.find((role) => role[0] == default_role)[1]"
:position="'right'"
:caret="true"
:data-e2e="`e2e-DD-${dataE2e}-everyoneElse-roles`"
@setRole="(...args) => this.changeDefaultRole(...args)"
@removeRole="() => this.changeDefaultRole()"
></MenuDropdown>
@ -43,18 +48,29 @@
:key="userAssignment.id"
class="p-2 flex items-center gap-2">
<div>
<img :src="userAssignment.attributes.user.avatar_url" class="rounded-full w-8 h-8">
<img
class="rounded-full w-8 h-8"
:src="userAssignment.attributes.user.avatar_url"
:data-e2e="`e2e-IC-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}`"
>
</div>
<div class="truncate">
<div class="flex flex-row gap-2">
<div class="truncate"
:title="userAssignment.attributes.user.name"
:data-e2e="`e2e-TX-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}-name`"
>{{ userAssignment.attributes.user.name }}</div>
<div v-if="userAssignment.attributes.current_user" class="text-nowrap">
<div
v-if="userAssignment.attributes.current_user"
class="text-nowrap"
:data-e2e="`e2e-TX-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}-currentUserLabel`"
>
{{ `(${i18n.t('access_permissions.you')})` }}
</div>
</div>
<div class="text-xs text-sn-grey text-nowrap">{{ userAssignment.attributes.inherit_message }}</div>
<div class="text-xs text-sn-grey text-nowrap" :data-e2e="`e2e-TX-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}-inheritLabel`">
{{ userAssignment.attributes.inherit_message }}
</div>
</div>
<MenuDropdown
v-if="!userAssignment.attributes.last_owner && params.object.urls.update_access && !(userAssignment.attributes.current_user && userAssignment.attributes.inherit_message)"
@ -63,10 +79,11 @@
:btnText="userAssignment.attributes.user_role.name"
:position="'right'"
:caret="true"
:data-e2e="`e2e-DD-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}-role`"
@setRole="(...args) => this.changeRole(userAssignment.attributes.user.id, ...args)"
@removeRole="() => this.removeRole(userAssignment.attributes.user.id)"
></MenuDropdown>
<div class="ml-auto btn btn-light pointer-events-none" v-else>
<div v-else class="ml-auto btn btn-light pointer-events-none" :data-e2e="`e2e-TX-${dataE2e}-${userAssignment.attributes.user.name.replace(/\W/g, '')}-role`">
{{ userAssignment.attributes.user_role.name }}
<div class="h-6 w-6"></div>
</div>
@ -78,7 +95,7 @@
<script>
/* global HelperModule */
import MenuDropdown from '../menu_dropdown.vue';
import GeneralDropdown from '../../shared/general_dropdown.vue';
import GeneralDropdown from '../general_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
export default {
@ -96,6 +113,10 @@ export default {
},
reloadUsers: {
type: Boolean
},
dataE2e: {
type: String,
default: ''
}
},
mounted() {

View file

@ -1,13 +1,14 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-content" data-e2e="e2e-MD-manageAccess">
<div class="modal-header">
<button type="button"
class="close"
data-dismiss="modal"
aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<h4 class="modal-title truncate !block">
aria-label="Close"
data-e2e="e2e-BT-manageAccess-close"><i class="sn-icon sn-icon-close"></i></button>
<h4 class="modal-title truncate !block" data-e2e="e2e-TX-manageAccessModal-title">
{{ i18n.t(`access_permissions.${params.object.type}.modals.edit_modal.title`, {
resource_name: params.object.name
}) }}
@ -20,12 +21,13 @@
:visible="visible"
:default_role="default_role"
:reloadUsers="reloadUnAssignedUsers"
:dataE2e="`manageAccessModal-flyout`"
@modified="modified = true; reloadUsers = true"
@assigningNewUsers="(v) => { assigningNewUsers = v }"
@usersReloaded="reloadUnAssignedUsers = false"
@changeVisibility="changeVisibility"
/>
<h5 class="py-2.5">
<h5 class="py-2.5" data-e2e="e2e-TX-manageAccessModal-peopleWithAccess">
{{ i18n.t('access_permissions.partials.new_assignments_form.people_with_access') }}
</h5>
<editView
@ -34,6 +36,7 @@
:visible="visible"
:default_role="default_role"
:reloadUsers="reloadUsers"
:dataE2e="`manageAccessModal-usersWithAccess`"
@modified="modified = true; reloadUnAssignedUsers = true"
@usersReloaded="reloadUsers = false"
@changeVisibility="changeVisibility"

View file

@ -11,6 +11,8 @@
<a :class="`rounded flex gap-2 items-center py-1.5 px-1.5 xl:px-2.5 hover:text-sn-white hover:bg-sn-blue
bg-sn-white color-sn-blue hover:no-underline focus:no-underline ${action.button_class}`"
:href="(['link', 'remote-modal']).includes(action.type) ? action.path : '#'"
:data-target="action.target"
:data-toggle="action.type === 'modal' && 'modal'"
:id="action.button_id"
:title="action.label"
:data-e2e="`e2e-BT-actionToolbar-${action.name}`"
@ -73,6 +75,9 @@ export default {
this.$emit('toolbar:action', action);
// do nothing, this is handled by legacy code based on the button class
break;
case 'modal':
// do nothihg, boostrap modal handled by data-toggle="modal" and data-target
break;
case 'link':
// do nothing, already handled by href
break;

View file

@ -1,10 +1,12 @@
<template>
<div ref="modal" @keydown.esc="close" class="modal sci-reorderable-items" tabindex="-1" role="dialog">
<div ref="modal" @keydown.esc="close" class="modal sci-reorderable-items" tabindex="-1" role="dialog" :data-e2e="`e2e-MD-${dataE2e}`">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button @click="close" type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<h4 class="modal-title">
<button @click="close" type="button" class="close" data-dismiss="modal" aria-label="Close" :data-e2e="`e2e-BT-${dataE2e}-close`">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title" :data-e2e="`e2e-TX-${dataE2e}-title`">
{{ title }}
</h4>
</div>
@ -20,13 +22,23 @@
<template #item="{element, index}">
<div class="step-element-header flex items-center">
<div class="step-element-grip step-element-grip--draggable">
<i class="sn-icon sn-icon-drag"></i>
<i class="sn-icon sn-icon-drag" :data-e2e="`e2e-BT-${dataE2e}-element${index + 1}-drag`"></i>
</div>
<div class="step-element-name text-center flex items-center gap-2">
<strong v-if="includeNumbers" class="step-element-number">{{ index + 1 }}</strong>
<i v-if="element.attributes.icon" class="fas" :class="element.attributes.icon"></i>
<span :title="nameWithFallbacks(element)" v-if="nameWithFallbacks(element)">{{ nameWithFallbacks(element) }}</span>
<span :title="element.attributes.placeholder" v-else class="step-element-name-placeholder">{{ element.attributes.placeholder }}</span>
<strong v-if="includeNumbers" class="step-element-number" :data-e2e="`e2e-TX-${dataE2e}-element${index + 1}-position`">
{{ index + 1 }}
</strong>
<i v-if="element.attributes.icon" class="fas" :class="element.attributes.icon" :data-e2e="`e2e-IC-${dataE2e}-element${index + 1}`"></i>
<span
:title="nameWithFallbacks(element)"
v-if="nameWithFallbacks(element)"
:data-e2e="`e2e-TX-${dataE2e}-element${index + 1}-name`"
>
{{ nameWithFallbacks(element) }}
</span>
<span :title="element.attributes.placeholder" v-else class="step-element-name-placeholder" :data-e2e="`e2e-TX-${dataE2e}-element${index + 1}-name`">
{{ element.attributes.placeholder }}
</span>
</div>
</div>
</template>
@ -56,6 +68,10 @@ export default {
includeNumbers: {
type: Boolean,
default: false
},
dataE2e: {
type: String,
default: ''
}
},
data() {

View file

@ -77,31 +77,56 @@ export default {
name: 'ShareObjectModal',
props: {
object: Object,
globalShareEnabled: { type: Boolean, default: false }
globalShareEnabled: { type: Boolean, default: false },
confirmationModal: { type: Object }
},
mixins: [modalMixin],
data() {
return {
sharedWithAllRead: this.object.shared_read || this.object.shared_write,
sharedWithAllWrite: this.object.shared_write,
shareableTeams: [],
permission_changes: {}
initialState: {},
shareableTeams: []
};
},
mounted() {
this.getTeams();
this.initTeams();
},
computed: {
willUnshare() {
if (this.globalShareEnabled && !this.sharedWithAllRead && this.initialState.sharedWithAllRead) return true;
// true if any team would switch from shared to unshared, based on initial state
return this.shareableTeams.some((t) => {
return this.initialState.shareableTeams.find((it) => t.id === it.id).attributes.private_shared_with && !t.attributes.private_shared_with;
});
}
},
methods: {
getTeams() {
initTeams() {
axios.get(this.object.urls.shareable_teams).then((response) => {
this.initialState = {
shareableTeams: JSON.parse(JSON.stringify(response.data.data)), // object needs to be deep cloned to get rid of references
sharedWithAllRead: this.sharedWithAllRead,
sharedWithAllWrite: this.sharedWithAllWrite
};
this.shareableTeams = response.data.data;
});
},
submit() {
async submit() {
$(this.$refs.modal).hide();
if (this.confirmationModal ? !this.willUnshare || await this.confirmationModal.show() : true) {
this.doRequest();
} else {
$(this.$refs.modal).show();
}
},
doRequest() {
const data = {
select_all_teams: this.sharedWithAllRead,
select_all_write_permission: this.sharedWithAllWrite,
team_share_params: this.shareableTeams.map((team) => { return { id: team.id, ...team.attributes } })
team_share_params: this.sharedWithAllRead ? [] : this.shareableTeams.map((team) => { return { id: team.id, ...team.attributes } })
};
axios.post(this.object.urls.share, data).then(() => {
HelperModule.flashAlertMsg(this.i18n.t(

View file

@ -34,6 +34,7 @@
:selectedContainer="assignToContainer"
:selectedPosition="assignToPosition"
:selectedRow="rowIdToMove"
:selectedRowName="rowNameToMove"
:cellId="cellIdToUnassign"
@close="openAssignModal = false; resetTableSearch(); this.reloadingTable = true"
></AssignModal>
@ -115,6 +116,7 @@ export default {
assignToPosition: null,
assignToContainer: null,
rowIdToMove: null,
rowNameToMove: null,
cellIdToUnassign: null,
assignMode: 'assign',
storageLocationUnassignDescription: ''
@ -179,15 +181,15 @@ export default {
type: 'emit',
buttonStyle: 'btn btn-primary'
});
}
left.push({
name: 'import',
icon: 'sn-icon sn-icon-import',
label: this.i18n.t('storage_locations.show.import_modal.import_button'),
type: 'emit',
buttonStyle: 'btn btn-light'
});
left.push({
name: 'import',
icon: 'sn-icon sn-icon-import',
label: this.i18n.t('storage_locations.show.import_modal.import_button'),
type: 'emit',
buttonStyle: 'btn btn-light'
});
}
return {
left,
@ -219,6 +221,7 @@ export default {
assignRow() {
this.openAssignModal = true;
this.rowIdToMove = null;
this.rowNameToMove = null;
this.assignToContainer = this.containerId;
this.assignToPosition = null;
this.cellIdToUnassign = null;
@ -227,6 +230,7 @@ export default {
assignRowToPosition(position) {
this.openAssignModal = true;
this.rowIdToMove = null;
this.rowNameToMove = null;
this.assignToContainer = this.containerId;
this.assignToPosition = position;
this.cellIdToUnassign = null;
@ -235,6 +239,7 @@ export default {
moveRow(_event, data) {
this.openAssignModal = true;
this.rowIdToMove = data[0].row_id;
this.rowNameToMove = data[0].row_name || this.i18n.t('storage_locations.show.hidden');
this.assignToContainer = null;
this.assignToPosition = null;
this.cellIdToUnassign = data[0].id;

View file

@ -10,31 +10,37 @@
<h4 v-if="selectedPosition" class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.selected_position_title`, { position: formattedPosition }) }}
</h4>
<h4 v-else-if="selectedRow && selectedRowName" class="modal-title truncate !block">
<h4 v-else-if="assignMode === 'assign' && selectedRow && selectedRowName" class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.selected_row_title`) }}
</h4>
<h4 v-else-if="assignMode === 'move'" class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.move_title`, { name: selectedRowName }) }}
</h4>
<h4 v-else class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_title`) }}
{{ i18n.t(`storage_locations.show.assign_modal.assign_title`) }}
</h4>
</div>
<div class="modal-body">
<p v-if="selectedRow && selectedRowName" class="mb-4">
{{ i18n.t(`storage_locations.show.assign_modal.selected_row_description`, { name: selectedRowName }) }}
</p>
<h4 v-else-if="assignMode === 'move'" class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.move_description`, { name: selectedRowName }) }}
</h4>
<p v-else class="mb-4">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_description`) }}
{{ i18n.t(`storage_locations.show.assign_modal.assign_description`) }}
</p>
<RowSelector v-if="!selectedRow" @change="this.rowId = $event" class="mb-4"></RowSelector>
<ContainerSelector v-if="!selectedContainer" @change="this.containerId = $event"></ContainerSelector>
<PositionSelector
v-if="containerId && !selectedPosition"
v-if="containerId && containerId > 0 && !selectedPosition"
:key="containerId"
:selectedContainerId="containerId"
@change="this.position = $event"></PositionSelector>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" type="submit">
<button class="btn btn-primary" type="submit" :disabled="!validObject">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_action`) }}
</button>
</div>
@ -70,6 +76,9 @@ export default {
},
mixins: [modalMixin],
computed: {
validObject() {
return this.rowId && this.containerId && this.containerId > 0;
},
createUrl() {
return storage_location_storage_location_repository_rows_path({
storage_location_id: this.containerId

View file

@ -43,7 +43,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" type="submit">
<button class="btn btn-primary" :disabled="!validContainer" type="submit">
{{ i18n.t('general.move') }}
</button>
</div>
@ -69,6 +69,11 @@ export default {
created() {
this.teamId = this.selectedObject.team_id;
},
computed: {
validContainer() {
return (this.selectedStorageLocationId && this.selectedStorageLocationId > 0) || this.selectedStorageLocationId === null;
}
},
mixins: [modalMixin, MoveTreeMixin],
data() {
return {

View file

@ -61,14 +61,15 @@ class StorageLocation < ApplicationRecord
storage_location_repository_rows.count.zero?
end
def duplicate!(user)
def duplicate!(user, team)
ActiveRecord::Base.transaction do
new_storage_location = dup
new_storage_location.name = next_clone_name
new_storage_location.team = team unless parent_id
new_storage_location.created_by = user
new_storage_location.save!
copy_image(self, new_storage_location)
recursive_duplicate(id, new_storage_location.id, user)
recursive_duplicate(id, new_storage_location.id, user, new_storage_location.team)
new_storage_location
rescue ActiveRecord::RecordInvalid
false
@ -144,14 +145,15 @@ class StorageLocation < ApplicationRecord
private
def recursive_duplicate(old_parent_id = nil, new_parent_id = nil, user = nil)
def recursive_duplicate(old_parent_id = nil, new_parent_id = nil, user = nil, team = nil)
StorageLocation.where(parent_id: old_parent_id).find_each do |child|
new_child = child.dup
new_child.parent_id = new_parent_id
new_child.team = team
new_child.created_by = user
new_child.save!
copy_image(child, new_child)
recursive_duplicate(child.id, new_child.id, user)
recursive_duplicate(child.id, new_child.id, user, team)
end
end

View file

@ -19,9 +19,7 @@ Canaid::Permissions.register_for(StorageLocation) do
can :manage_storage_location do |user, storage_location|
root_storage_location = storage_location.root_storage_location
next true if root_storage_location.shared_with_write?(user.current_team)
user.current_team == root_storage_location.team && root_storage_location.team.permission_granted?(
next false unless user.current_team.permission_granted?(
user,
if root_storage_location.container?
TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE
@ -29,10 +27,15 @@ Canaid::Permissions.register_for(StorageLocation) do
TeamPermissions::STORAGE_LOCATIONS_MANAGE
end
)
next true if user.current_team == root_storage_location.team
root_storage_location.shared_with_write?(user.current_team)
end
can :create_storage_location_repository_rows do |user, storage_location|
can_read_storage_location?(user, storage_location)
can :manage_storage_location_repository_rows do |user, storage_location|
can_read_storage_location?(user, storage_location) &&
user.current_team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE)
end
can :share_storage_location do |user, storage_location|

View file

@ -9,7 +9,7 @@ module RepositoryDatatable
def value
@user = scope[:user]
{
view: value_object.has_smart_annotation? ? custom_auto_link(value_object.data, simple_format: true, team: scope[:team]) : escape_input(value_object.data),
view: value_object.has_smart_annotation? ? custom_auto_link(value_object.data, simple_format: true, team: scope[:team]) : sanitize_input(value_object.data),
edit: value_object.data
}
end

View file

@ -83,7 +83,7 @@ class RepositoryDatatableService
repository_rows =
if @repository.archived? || @repository.is_a?(RepositorySnapshot)
# don't load reminders for archived repositories or snapshots
repository_rows.select('FALSE AS has_active_stock_reminders, FALSE AS has_active_datetime_reminders')
repository_rows.select('FALSE AS has_active_reminders')
else
repository_rows.left_outer_joins_active_reminders(@repository, @user)
.select('COUNT(repository_cells_with_active_reminders.id) > 0 AS has_active_reminders')

View file

@ -20,6 +20,8 @@ class RepositorySnapshotDatatableService < RepositoryDatatableService
repository_rows = fetch_rows(search_value).preload(Extends::REPOSITORY_ROWS_PRELOAD_RELATIONS)
repository_rows = repository_rows.preload(:repository_columns, repository_cells: { value: @repository.cell_preload_includes }) if @preload_cells
repository_rows = repository_rows.preload(:repository_stock_cell, :repository_stock_value) if @repository.has_stock_management?
# don't load reminders for snapshots
repository_rows = repository_rows.select('FALSE AS has_active_reminders') if Repository.reminders_enabled?
sort_rows(order_by_column, repository_rows)
end

View file

@ -15,7 +15,7 @@ module StorageLocations
end
def import_items
@rows = SpreadsheetParser.spreadsheet_enumerator(@sheet).reject { |r| r.all?(&:blank?) }
@rows = SpreadsheetParser.spreadsheet_enumerator(@sheet).to_a
# Check if the file has proper headers
header = SpreadsheetParser.parse_row(@rows[0], @sheet)
@ -73,6 +73,8 @@ module StorageLocations
repository_row_id: (row[1].to_s.gsub('IT', '') if row[1].present?)
}
end
@rows.reject! { |r| r[:repository_row_id].blank? && r[:position].blank? }
end
def import_row!(row)

View file

@ -27,7 +27,7 @@ module Toolbars
private
def unassign_action
return unless can_read_storage_location?(@storage_location)
return unless can_manage_storage_location_repository_rows?(@storage_location)
{
name: 'unassign',
@ -39,7 +39,7 @@ module Toolbars
end
def move_action
return unless @single && can_read_storage_location?(@storage_location)
return unless @single && can_manage_storage_location_repository_rows?(@storage_location)
{
name: 'move',

View file

@ -14,7 +14,7 @@
ref="container"
actions-url="<%= actions_toolbar_storage_location_storage_location_repository_rows_path(@storage_location) %>"
data-source="<%= storage_location_storage_location_repository_rows_path(@storage_location) %>"
:can-manage="<%= can_create_storage_location_repository_rows?(@storage_location) %>"
:can-manage="<%= can_manage_storage_location_repository_rows?(@storage_location) %>"
:with-grid="<%= @storage_location.with_grid? %>"
:grid-size="<%= @storage_location.grid_size.to_json %>"
:container-id="<%= @storage_location.id %>"

View file

@ -2017,6 +2017,11 @@ en:
name_placeholder: "My inventory"
submit: "Create"
success_flash_html: "Inventory <strong>%{name}</strong> successfully created."
modal_confirm_sharing:
title: "Inventory sharing changes"
description_1: "You will no longer share this inventory with some of the teams. All unshared inventory items assigned to tasks will be automatically removed and this action is irreversible. Any item relationship links (if they exist) will also be deleted."
description_2: "Are you sure you want to apply the changes you made?"
confirm: "Apply"
export:
notification:
error:
@ -2699,9 +2704,9 @@ en:
selected_position_title: 'Assign to position %{position}'
selected_row_title: 'Assign new location'
assign_title: 'Assign position'
move_title: 'Move item'
move_title: 'Move %{name}'
assign_description: 'Select an item to assign it to a location.'
move_description: 'Select a new location for your item.'
move_description: 'Select where you want to move %{name}.'
selected_row_description: "Select a location for the item %{name}."
assign_action: 'Assign'
move_action: 'Move'
@ -2712,7 +2717,7 @@ en:
import_modal:
import_button: 'Import items'
title: "Import items to a box"
description: "Import items to a box allows for assigning items to a box and updating positions within a grid box. First, export the current box data to download a file listing the items already in the box. Then, edit the exported file to add or update the items you want to place in the box. When importing the file, ensure it includes the Position and Item ID columns for a successful import."
description: "Import items to a box allows for assigning items to a box and updating positions within a grid box. First, export the current box data to download a file listing the items already in the box. Then, edit the exported file to add or update the items you want to place in the box. When importing the file, ensure it includes the Box position and Item ID columns for a successful import."
export: "Export"
export_button: "Export current box"
import: "Import"

View file

@ -338,7 +338,7 @@ en:
container_storage_location_sharing_updated_html: "%{user} changed permission of shared box %{storage_location} with team %{team} to %{permission_level}."
storage_location_repository_row_created_html: "%{user} assigned %{repository_row} to box %{storage_location} %{position}."
storage_location_repository_row_deleted_html: "%{user} unassigned %{repository_row} from box %{storage_location} %{position}."
storage_location_repository_row_moved_html: "%{user} moved item %{repository_row} from box %{storage_location_original} %{positions} to box %{storage_location_destination} %{positions}."
storage_location_repository_row_moved_html: "%{user} moved item %{repository_row} from box %{storage_location_original} %{position_original} to box %{storage_location} %{position}."
container_storage_location_imported_html: "%{user} assigned %{assigned_count} item(s) and unassigned %{unassigned_count} item(s) by import to %{storage_location}."
activity_name:
create_project: "Project created"