mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-07 21:55:20 +08:00
Merge pull request #7827 from artoscinote/ma_SCI_10865
Implement storage location sharing [SCI-10865]
This commit is contained in:
commit
34c8da949f
27 changed files with 429 additions and 232 deletions
|
@ -21,7 +21,6 @@ class RepositoriesController < ApplicationController
|
|||
before_action :check_manage_permissions, only: %i(rename_modal update)
|
||||
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
|
||||
before_action :check_archive_permissions, only: %i(archive restore)
|
||||
before_action :check_share_permissions, only: :share_modal
|
||||
before_action :check_create_permissions, only: %i(create_modal create)
|
||||
before_action :check_copy_permissions, only: %i(copy_modal copy)
|
||||
before_action :set_inline_name_editing, only: %i(show)
|
||||
|
@ -111,15 +110,6 @@ class RepositoriesController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def share_modal
|
||||
render json: { html: render_to_string(partial: 'share_repository_modal', formats: :html) }
|
||||
end
|
||||
|
||||
def shareable_teams
|
||||
teams = current_user.teams.order(:name) - [@repository.team]
|
||||
render json: teams, each_serializer: ShareableTeamSerializer, repository: @repository
|
||||
end
|
||||
|
||||
def hide_reminders
|
||||
# synchronously hide currently visible reminders
|
||||
if params[:visible_reminder_repository_row_ids].present?
|
||||
|
@ -532,10 +522,6 @@ class RepositoriesController < ApplicationController
|
|||
render_403 unless can_delete_repository?(@repository)
|
||||
end
|
||||
|
||||
def check_share_permissions
|
||||
render_403 unless can_share_repository?(@repository)
|
||||
end
|
||||
|
||||
def repository_params
|
||||
params.require(:repository).permit(:name)
|
||||
end
|
||||
|
|
|
@ -93,7 +93,7 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
end
|
||||
|
||||
def load_storage_location
|
||||
@storage_location = StorageLocation.where(team: current_team).find(
|
||||
@storage_location = StorageLocation.viewable_by_user(current_user).find(
|
||||
storage_location_repository_row_params[:storage_location_id]
|
||||
)
|
||||
render_404 unless @storage_location
|
||||
|
@ -110,12 +110,10 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
end
|
||||
|
||||
def check_read_permissions
|
||||
render_403 unless can_read_storage_location_containers?(current_team)
|
||||
render_403 unless can_read_storage_location?(@storage_location)
|
||||
end
|
||||
|
||||
def check_manage_permissions
|
||||
unless can_manage_storage_location_containers?(current_team) && can_read_repository?(@repository_row.repository)
|
||||
render_403
|
||||
end
|
||||
render_403 unless can_manage_storage_location?(@storage_location)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ class StorageLocationsController < ApplicationController
|
|||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
storage_locations = Lists::StorageLocationsService.new(current_team, params).call
|
||||
storage_locations = Lists::StorageLocationsService.new(current_user, current_team, params).call
|
||||
render json: storage_locations, each_serializer: Lists::StorageLocationSerializer,
|
||||
user: current_user, meta: pagination_dict(storage_locations)
|
||||
end
|
||||
|
@ -35,9 +35,11 @@ class StorageLocationsController < ApplicationController
|
|||
|
||||
def create
|
||||
@storage_location = StorageLocation.new(
|
||||
storage_location_params.merge({ team: current_team, created_by: current_user })
|
||||
storage_location_params.merge({ created_by: current_user })
|
||||
)
|
||||
|
||||
@storage_location.team = @storage_location.root_storage_location.team
|
||||
|
||||
@storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id]
|
||||
|
||||
if @storage_location.save
|
||||
|
@ -101,7 +103,7 @@ class StorageLocationsController < ApplicationController
|
|||
actions:
|
||||
Toolbars::StorageLocationsService.new(
|
||||
current_user,
|
||||
storage_location_ids: JSON.parse(params[:items]).map { |i| i['id'] }
|
||||
storage_location_ids: JSON.parse(params[:items]).pluck('id')
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
@ -114,7 +116,7 @@ class StorageLocationsController < ApplicationController
|
|||
|
||||
def storage_location_params
|
||||
params.permit(:id, :parent_id, :name, :container, :description,
|
||||
metadata: [:display_type, dimensions: [], parent_coordinations: []])
|
||||
metadata: [:display_type, { dimensions: [], parent_coordinations: [] }])
|
||||
end
|
||||
|
||||
def move_params
|
||||
|
@ -122,16 +124,12 @@ class StorageLocationsController < ApplicationController
|
|||
end
|
||||
|
||||
def load_storage_location
|
||||
@storage_location = current_team.storage_locations.find_by(id: storage_location_params[:id])
|
||||
@storage_location = StorageLocation.viewable_by_user(current_user).find_by(id: storage_location_params[:id])
|
||||
render_404 unless @storage_location
|
||||
end
|
||||
|
||||
def check_read_permissions
|
||||
if @storage_location.container
|
||||
render_403 unless can_read_storage_location_containers?(current_team)
|
||||
else
|
||||
render_403 unless can_read_storage_locations?(current_team)
|
||||
end
|
||||
render_403 unless can_read_storage_location?(@storage_location)
|
||||
end
|
||||
|
||||
def check_create_permissions
|
||||
|
@ -143,11 +141,7 @@ class StorageLocationsController < ApplicationController
|
|||
end
|
||||
|
||||
def check_manage_permissions
|
||||
if @storage_location.container
|
||||
render_403 unless can_manage_storage_location_containers?(current_team)
|
||||
else
|
||||
render_403 unless can_manage_storage_locations?(current_team)
|
||||
end
|
||||
render_403 unless can_manage_storage_location?(@storage_location)
|
||||
end
|
||||
|
||||
def set_breadcrumbs_items
|
||||
|
|
77
app/controllers/team_shared_objects_controller.rb
Normal file
77
app/controllers/team_shared_objects_controller.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TeamSharedObjectsController < ApplicationController
|
||||
before_action :load_vars
|
||||
before_action :check_sharing_permissions
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
# Global share
|
||||
if params[:select_all_teams]
|
||||
@model.update!(permission_level: params[:select_all_write_permission] ? :shared_write : :shared_read)
|
||||
@model.team_shared_objects.each(&:destroy!)
|
||||
next
|
||||
end
|
||||
|
||||
# Share to specific teams
|
||||
params[:team_share_params].each do |t|
|
||||
@model.update!(permission_level: :not_shared) if @model.globally_shareable?
|
||||
@model.team_shared_objects.find_or_initialize_by(team_id: t['id']).update!(
|
||||
permission_level: t['private_shared_with_write'] ? :shared_write : :shared_read
|
||||
)
|
||||
end
|
||||
|
||||
# Unshare
|
||||
@model.team_shared_objects.where.not(
|
||||
team_id: params[:team_share_params].filter { |t| t['private_shared_with'] }.pluck('id')
|
||||
).each(&:destroy!)
|
||||
end
|
||||
end
|
||||
|
||||
def shareable_teams
|
||||
teams = current_user.teams.order(:name) - [@model.team]
|
||||
render json: teams, each_serializer: ShareableTeamSerializer, model: @model
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
case params[:object_type]
|
||||
when 'Repository'
|
||||
@model = Repository.viewable_by_user(current_user).find_by(id: params[:object_id])
|
||||
when 'StorageLocation'
|
||||
@model = StorageLocation.viewable_by_user(current_user).find_by(id: params[:object_id])
|
||||
end
|
||||
|
||||
render_404 unless @model
|
||||
end
|
||||
|
||||
def create_params
|
||||
params.permit(:team_id, :object_type, :object_id, :target_team_id, :permission_level)
|
||||
end
|
||||
|
||||
def destroy_params
|
||||
params.permit(:team_id, :id)
|
||||
end
|
||||
|
||||
def update_params
|
||||
params.permit(permission_changes: {}, share_team_ids: [], write_permissions: [])
|
||||
end
|
||||
|
||||
def check_sharing_permissions
|
||||
object_name = @model.is_a?(RepositoryBase) ? 'repository' : @model.model_name.param_key
|
||||
render_403 unless public_send("can_share_#{object_name}?", @model)
|
||||
render_403 if !@model.shareable_write? && update_params[:write_permissions].present?
|
||||
end
|
||||
|
||||
def share_all_params
|
||||
{
|
||||
shared_with_all: params[:select_all_teams].present?,
|
||||
shared_permissions_level: params[:select_all_write_permission].present? ? 'shared_write' : 'shared_read'
|
||||
}
|
||||
end
|
||||
|
||||
def log_activity(type_of, team_shared_object)
|
||||
# log activity logic
|
||||
end
|
||||
end
|
|
@ -50,9 +50,10 @@
|
|||
:repository="duplicateRepository"
|
||||
@close="duplicateRepository = null"
|
||||
@duplicate="updateTable" />
|
||||
<ShareRepositoryModal
|
||||
<ShareObjectModal
|
||||
v-if="shareRepository"
|
||||
:repository="shareRepository"
|
||||
:object="shareRepository"
|
||||
:globalShareEnabled="true"
|
||||
@close="shareRepository = null"
|
||||
@share="updateTable" />
|
||||
</template>
|
||||
|
@ -66,7 +67,7 @@ import ExportRepositoryModal from './modals/export.vue';
|
|||
import NewRepositoryModal from './modals/new.vue';
|
||||
import EditRepositoryModal from './modals/edit.vue';
|
||||
import DuplicateRepositoryModal from './modals/duplicate.vue';
|
||||
import ShareRepositoryModal from './modals/share.vue';
|
||||
import ShareObjectModal from '../shared/share_modal.vue';
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
|
||||
export default {
|
||||
|
@ -78,7 +79,7 @@ export default {
|
|||
NewRepositoryModal,
|
||||
EditRepositoryModal,
|
||||
DuplicateRepositoryModal,
|
||||
ShareRepositoryModal
|
||||
ShareObjectModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
<div class="sci-divider my-4" v-if="index > 0"></div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
{{ i18n.t('repositories.locations.container') }}:
|
||||
<a :href="containerUrl(location.id)">{{ location.name }}</a>
|
||||
<a v-if="location.readable" :href="containerUrl(location.id)">{{ location.name }}</a>
|
||||
<span v-else>{{ location.name }}</span>
|
||||
<span v-if="location.metadata.display_type !== 'grid'">
|
||||
({{ location.positions.length }})
|
||||
</span>
|
||||
|
|
|
@ -7,29 +7,29 @@
|
|||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block">
|
||||
{{ i18n.t('repositories.index.modal_share.title', {name: repository.name }) }}
|
||||
{{ i18n.t('modal_share.title', {object_name: object.name }) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="col-span-2">
|
||||
{{ i18n.t("repositories.index.modal_share.share_with_team") }}
|
||||
{{ i18n.t("modal_share.share_with_team") }}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
{{ i18n.t("repositories.index.modal_share.can_edit") }}
|
||||
{{ i18n.t("modal_share.can_edit") }}
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center h-9 gap-1">
|
||||
<div v-if="globalShareEnabled" class="col-span-2 flex items-center h-9 gap-1">
|
||||
<span class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" v-model="sharedWithAllRead" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</span>
|
||||
{{ i18n.t("repositories.index.modal_share.all_teams") }}
|
||||
{{ i18n.t("modal_share.all_teams") }}
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<div v-if="globalShareEnabled" class="flex justify-center items-center">
|
||||
<span v-if="sharedWithAllRead" class="sci-toggle-checkbox-container">
|
||||
<input type="checkbox"
|
||||
class="sci-toggle-checkbox"
|
||||
:disabled="!repository.shareable_write"
|
||||
:disabled="!object.shareable_write"
|
||||
v-model="sharedWithAllWrite" />
|
||||
<span class="sci-toggle-checkbox-label"></span>
|
||||
</span>
|
||||
|
@ -48,8 +48,7 @@
|
|||
class="sci-toggle-checkbox-container">
|
||||
<input type="checkbox"
|
||||
class="sci-toggle-checkbox"
|
||||
@change="permission_changes[team.id] = true"
|
||||
:disabled="!repository.shareable_write"
|
||||
:disabled="!object.shareable_write"
|
||||
v-model="team.attributes.private_shared_with_write" />
|
||||
<span class="sci-toggle-checkbox-label"></span>
|
||||
</span>
|
||||
|
@ -60,7 +59,7 @@
|
|||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="submit" type="submit">
|
||||
{{ i18n.t('repositories.index.modal_share.submit') }}
|
||||
{{ i18n.t('modal_share.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,19 +70,20 @@
|
|||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import modalMixin from './modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'ShareRepositoryModal',
|
||||
name: 'ShareObjectModal',
|
||||
props: {
|
||||
repository: Object
|
||||
object: Object,
|
||||
globalShareEnabled: { type: Boolean, default: false }
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
sharedWithAllRead: this.repository.shared_read || this.repository.shared_write,
|
||||
sharedWithAllWrite: this.repository.shared_write,
|
||||
sharedWithAllRead: this.object.shared_read || this.object.shared_write,
|
||||
sharedWithAllWrite: this.object.shared_write,
|
||||
shareableTeams: [],
|
||||
permission_changes: {}
|
||||
};
|
||||
|
@ -93,7 +93,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
getTeams() {
|
||||
axios.get(this.repository.urls.shareable_teams).then((response) => {
|
||||
axios.get(this.object.urls.shareable_teams).then((response) => {
|
||||
this.shareableTeams = response.data.data;
|
||||
});
|
||||
},
|
||||
|
@ -101,14 +101,12 @@ export default {
|
|||
const data = {
|
||||
select_all_teams: this.sharedWithAllRead,
|
||||
select_all_write_permission: this.sharedWithAllWrite,
|
||||
share_team_ids: this.shareableTeams.filter((team) => team.attributes.private_shared_with).map((team) => team.id),
|
||||
write_permissions: this.shareableTeams.filter((team) => team.attributes.private_shared_with_write).map((team) => team.id),
|
||||
permission_changes: this.permission_changes
|
||||
team_share_params: this.shareableTeams.map((team) => { return { id: team.id, ...team.attributes } })
|
||||
};
|
||||
axios.post(this.repository.urls.share, data).then(() => {
|
||||
axios.post(this.object.urls.share, data).then(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t(
|
||||
'repositories.index.modal_share.success_message',
|
||||
{ inventory_name: this.repository.name }
|
||||
'modal_share.success_message',
|
||||
{ object_name: this.object.name }
|
||||
), 'success');
|
||||
this.$emit('share');
|
||||
});
|
|
@ -66,6 +66,10 @@ export default {
|
|||
RemindersRender
|
||||
},
|
||||
props: {
|
||||
canManage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true
|
||||
|
@ -143,13 +147,15 @@ export default {
|
|||
toolbarActions() {
|
||||
const left = [];
|
||||
|
||||
left.push({
|
||||
name: 'assign',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
label: this.i18n.t('storage_locations.show.toolbar.assign'),
|
||||
type: 'emit',
|
||||
buttonStyle: 'btn btn-primary'
|
||||
});
|
||||
if (this.canManage) {
|
||||
left.push({
|
||||
name: 'assign',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
label: this.i18n.t('storage_locations.show.toolbar.assign'),
|
||||
type: 'emit',
|
||||
buttonStyle: 'btn btn-primary'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
left,
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
@tableReloaded="reloadingTable = false"
|
||||
@move="move"
|
||||
@delete="deleteStorageLocation"
|
||||
@share="share"
|
||||
/>
|
||||
<Teleport to="body">
|
||||
<EditModal v-if="openEditModal"
|
||||
|
@ -34,6 +35,11 @@
|
|||
:confirmText="i18n.t('general.delete')"
|
||||
ref="deleteStorageLocationModal"
|
||||
></ConfirmationModal>
|
||||
<ShareObjectModal
|
||||
v-if="shareStorageLocation"
|
||||
:object="shareStorageLocation"
|
||||
@close="shareStorageLocation = null"
|
||||
@share="updateTable" />
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -46,6 +52,7 @@ import DataTable from '../shared/datatable/table.vue';
|
|||
import EditModal from './modals/new_edit.vue';
|
||||
import MoveModal from './modals/move.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import ShareObjectModal from '../shared/share_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoriesTable',
|
||||
|
@ -53,7 +60,8 @@ export default {
|
|||
DataTable,
|
||||
EditModal,
|
||||
MoveModal,
|
||||
ConfirmationModal
|
||||
ConfirmationModal,
|
||||
ShareObjectModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
|
@ -82,6 +90,7 @@ export default {
|
|||
editStorageLocation: null,
|
||||
objectToMove: null,
|
||||
moveToUrl: null,
|
||||
shareStorageLocation: null,
|
||||
storageLocationDeleteTitle: '',
|
||||
storageLocationDeleteDescription: ''
|
||||
};
|
||||
|
@ -111,11 +120,6 @@ export default {
|
|||
headerName: this.i18n.t('storage_locations.index.table.items'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'free_spaces',
|
||||
headerName: this.i18n.t('storage_locations.index.table.free_spaces'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'shared',
|
||||
headerName: this.i18n.t('storage_locations.index.table.shared'),
|
||||
|
@ -216,21 +220,28 @@ export default {
|
|||
nameRenderer(params) {
|
||||
const {
|
||||
name,
|
||||
urls
|
||||
urls,
|
||||
shared,
|
||||
ishared
|
||||
} = params.data;
|
||||
let containerIcon = '';
|
||||
if (params.data.container) {
|
||||
containerIcon = '<i class="sn-icon sn-icon-item"></i>';
|
||||
}
|
||||
let sharedIcon = '';
|
||||
if (shared || ishared) {
|
||||
sharedIcon = '<i class="fas fa-users"></i>';
|
||||
}
|
||||
return `<a class="hover:no-underline flex items-center gap-1"
|
||||
title="${name}" href="${urls.show}">
|
||||
${containerIcon}
|
||||
${sharedIcon}${containerIcon}
|
||||
<span class="truncate">${name}</span>
|
||||
</a>`;
|
||||
},
|
||||
updateTable() {
|
||||
this.reloadingTable = true;
|
||||
this.objectToMove = null;
|
||||
this.shareStorageLocation = null;
|
||||
},
|
||||
move(event, rows) {
|
||||
[this.objectToMove] = rows;
|
||||
|
@ -258,6 +269,10 @@ export default {
|
|||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
share(_event, rows) {
|
||||
const [storageLocation] = rows;
|
||||
this.shareStorageLocation = storageLocation;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
92
app/models/concerns/shareable.rb
Normal file
92
app/models/concerns/shareable.rb
Normal file
|
@ -0,0 +1,92 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Shareable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :team_shared_objects, as: :shared_object, dependent: :destroy
|
||||
has_many :teams_shared_with, through: :team_shared_objects, source: :team, dependent: :destroy
|
||||
|
||||
if column_names.include? 'permission_level'
|
||||
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS
|
||||
define_method :globally_shareable? do
|
||||
true
|
||||
end
|
||||
else
|
||||
# If model does not include the permission_level column for global sharing,
|
||||
# all related methods should just return false
|
||||
Extends::SHARED_OBJECTS_PERMISSION_LEVELS.each do |level|
|
||||
define_method "#{level[0]}?" do
|
||||
level[0] == :not_shared
|
||||
end
|
||||
|
||||
define_method :globally_shareable? do
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope :viewable_by_user, lambda { |user, teams = user.current_team|
|
||||
readable = readable_by_user(user).left_outer_joins(:team_shared_objects)
|
||||
readable
|
||||
.where(team: teams)
|
||||
.or(readable.where(team_shared_objects: { team: teams }))
|
||||
.or(readable
|
||||
.where(
|
||||
if column_names.include?('permission_level')
|
||||
{
|
||||
permission_level: [
|
||||
Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_read],
|
||||
Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_write]
|
||||
]
|
||||
}
|
||||
else
|
||||
{}
|
||||
end
|
||||
).where.not(team: teams))
|
||||
.distinct
|
||||
}
|
||||
end
|
||||
|
||||
def shareable_write?
|
||||
true
|
||||
end
|
||||
|
||||
def private_shared_with?(team)
|
||||
team_shared_objects.where(team: team).any?
|
||||
end
|
||||
|
||||
def private_shared_with_write?(team)
|
||||
team_shared_objects.where(team: team, permission_level: :shared_write).any?
|
||||
end
|
||||
|
||||
def i_shared?(team)
|
||||
shared_with_anybody? && self.team == team
|
||||
end
|
||||
|
||||
def globally_shared?
|
||||
shared_read? || shared_write?
|
||||
end
|
||||
|
||||
def shared_with_anybody?
|
||||
(!not_shared? || team_shared_objects.any?)
|
||||
end
|
||||
|
||||
def shared_with?(team)
|
||||
return false if self.team == team
|
||||
|
||||
!not_shared? || private_shared_with?(team)
|
||||
end
|
||||
|
||||
def shared_with_write?(team)
|
||||
return false if self.team == team
|
||||
|
||||
shared_write? || private_shared_with_write?(team)
|
||||
end
|
||||
|
||||
def shared_with_read?(team)
|
||||
return false if self.team == team
|
||||
|
||||
shared_read? || team_shared_objects.where(team: team, permission_level: :shared_read).any?
|
||||
end
|
||||
end
|
|
@ -7,12 +7,11 @@ class Repository < RepositoryBase
|
|||
include PermissionCheckableModel
|
||||
include RepositoryImportParser
|
||||
include ArchivableModel
|
||||
include Shareable
|
||||
|
||||
ID_PREFIX = 'IN'
|
||||
include PrefixedIdModel
|
||||
|
||||
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS
|
||||
|
||||
belongs_to :archived_by,
|
||||
foreign_key: :archived_by_id,
|
||||
class_name: 'User',
|
||||
|
@ -23,8 +22,6 @@ class Repository < RepositoryBase
|
|||
class_name: 'User',
|
||||
inverse_of: :restored_repositories,
|
||||
optional: true
|
||||
has_many :team_shared_objects, as: :shared_object, dependent: :destroy
|
||||
has_many :teams_shared_with, through: :team_shared_objects, source: :team, dependent: :destroy
|
||||
has_many :repository_snapshots,
|
||||
class_name: 'RepositorySnapshot',
|
||||
foreign_key: :parent_id,
|
||||
|
@ -48,17 +45,6 @@ class Repository < RepositoryBase
|
|||
scope :archived, -> { where(archived: true) }
|
||||
scope :globally_shared, -> { where(permission_level: %i(shared_read shared_write)) }
|
||||
|
||||
scope :viewable_by_user, lambda { |user, teams = user.current_team|
|
||||
readable_repositories = readable_by_user(user).left_outer_joins(:team_shared_objects)
|
||||
readable_repositories
|
||||
.where(team: teams)
|
||||
.or(readable_repositories.where(team_shared_objects: { team: teams }))
|
||||
.or(readable_repositories
|
||||
.where(permission_level: [Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_read], Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_write]])
|
||||
.where.not(team: teams))
|
||||
.distinct
|
||||
}
|
||||
|
||||
scope :assigned_to_project, lambda { |project|
|
||||
joins(repository_rows: { my_module_repository_rows: { my_module: { experiment: :project } } })
|
||||
.where(repository_rows: { my_module_repository_rows: { my_module: { experiments: { project: project } } } })
|
||||
|
@ -80,10 +66,6 @@ class Repository < RepositoryBase
|
|||
teams.blank? ? self : where(team: teams)
|
||||
end
|
||||
|
||||
def shareable_write?
|
||||
true
|
||||
end
|
||||
|
||||
def permission_parent
|
||||
team
|
||||
end
|
||||
|
@ -111,44 +93,6 @@ class Repository < RepositoryBase
|
|||
['repository_rows.name', RepositoryRow::PREFIXED_ID_SQL, 'users.full_name']
|
||||
end
|
||||
|
||||
def i_shared?(team)
|
||||
shared_with_anybody? && self.team == team
|
||||
end
|
||||
|
||||
def globally_shared?
|
||||
shared_read? || shared_write?
|
||||
end
|
||||
|
||||
def shared_with_anybody?
|
||||
(!not_shared? || team_shared_objects.any?)
|
||||
end
|
||||
|
||||
def shared_with?(team)
|
||||
return false if self.team == team
|
||||
|
||||
!not_shared? || private_shared_with?(team)
|
||||
end
|
||||
|
||||
def shared_with_write?(team)
|
||||
return false if self.team == team
|
||||
|
||||
shared_write? || private_shared_with_write?(team)
|
||||
end
|
||||
|
||||
def shared_with_read?(team)
|
||||
return false if self.team == team
|
||||
|
||||
shared_read? || team_shared_objects.where(team: team, permission_level: :shared_read).any?
|
||||
end
|
||||
|
||||
def private_shared_with?(team)
|
||||
team_shared_objects.where(team: team).any?
|
||||
end
|
||||
|
||||
def private_shared_with_write?(team)
|
||||
team_shared_objects.where(team: team, permission_level: :shared_write).any?
|
||||
end
|
||||
|
||||
def self.name_like(query)
|
||||
where('repositories.name ILIKE ?', "%#{query}%")
|
||||
end
|
||||
|
|
|
@ -174,17 +174,6 @@ class RepositoryRow < ApplicationRecord
|
|||
self[:archived]
|
||||
end
|
||||
|
||||
def grouped_storage_locations
|
||||
storage_location_repository_rows.joins(:storage_location).group(:storage_location_id).select(
|
||||
"storage_location_id as id,
|
||||
(ARRAY_AGG(storage_locations.metadata))[1] as metadata,
|
||||
MAX(storage_locations.name) as name,
|
||||
jsonb_agg(jsonb_build_object(
|
||||
'id', storage_location_repository_rows.id,
|
||||
'metadata', storage_location_repository_rows.metadata)
|
||||
) as positions").as_json
|
||||
end
|
||||
|
||||
def has_reminders?(user)
|
||||
stock_reminders = RepositoryCell.stock_reminder_repository_cells_scope(
|
||||
repository_cells.joins(:repository_column), user)
|
||||
|
|
|
@ -5,6 +5,7 @@ class StorageLocation < ApplicationRecord
|
|||
include Discard::Model
|
||||
ID_PREFIX = 'SL'
|
||||
include PrefixedIdModel
|
||||
include Shareable
|
||||
|
||||
default_scope -> { kept }
|
||||
|
||||
|
@ -16,16 +17,42 @@ class StorageLocation < ApplicationRecord
|
|||
|
||||
has_many :storage_location_repository_rows, inverse_of: :storage_location, dependent: :destroy
|
||||
has_many :storage_locations, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||
has_many :repository_rows, through: :storage_location_repository_row
|
||||
has_many :repository_rows, through: :storage_location_repository_rows
|
||||
|
||||
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||
validate :parent_validation, if: -> { parent.present? }
|
||||
|
||||
scope :readable_by_user, (lambda do |user, team = user.current_team|
|
||||
next StorageLocation.none unless team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_READ)
|
||||
|
||||
where(team: team)
|
||||
end)
|
||||
|
||||
after_discard do
|
||||
StorageLocation.where(parent_id: id).find_each(&:discard)
|
||||
storage_location_repository_rows.each(&:discard)
|
||||
end
|
||||
|
||||
def shared_with?(team)
|
||||
return false if self.team == team
|
||||
|
||||
(root? ? self : root_storage_location).private_shared_with?(team)
|
||||
end
|
||||
|
||||
def root?
|
||||
parent_id.nil?
|
||||
end
|
||||
|
||||
def root_storage_location
|
||||
return self if root?
|
||||
|
||||
storage_location = self
|
||||
|
||||
storage_location = storage_location.parent while storage_location.parent_id
|
||||
|
||||
storage_location
|
||||
end
|
||||
|
||||
def duplicate!
|
||||
ActiveRecord::Base.transaction do
|
||||
new_storage_location = dup
|
||||
|
|
39
app/permissions/storage_location.rb
Normal file
39
app/permissions/storage_location.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Canaid::Permissions.register_for(StorageLocation) do
|
||||
can :read_storage_location do |user, storage_location|
|
||||
root_storage_location = storage_location.root_storage_location
|
||||
|
||||
next true if root_storage_location.shared_with?(user.current_team)
|
||||
|
||||
user.current_team == root_storage_location.team && root_storage_location.team.permission_granted?(
|
||||
user,
|
||||
if root_storage_location.container?
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ
|
||||
else
|
||||
TeamPermissions::STORAGE_LOCATIONS_READ
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
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?(
|
||||
user,
|
||||
if root_storage_location.container?
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE
|
||||
else
|
||||
TeamPermissions::STORAGE_LOCATIONS_MANAGE
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
can :share_storage_location do |user, storage_location|
|
||||
user.current_team == storage_location.team &&
|
||||
storage_location.root? &&
|
||||
can_manage_storage_location?(user, storage_location)
|
||||
end
|
||||
end
|
|
@ -43,30 +43,14 @@ Canaid::Permissions.register_for(Team) do
|
|||
within_limits && team.permission_granted?(user, TeamPermissions::INVENTORIES_CREATE)
|
||||
end
|
||||
|
||||
can :read_storage_locations do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_READ)
|
||||
end
|
||||
|
||||
can :create_storage_locations do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_CREATE)
|
||||
end
|
||||
|
||||
can :manage_storage_locations do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_MANAGE)
|
||||
end
|
||||
|
||||
can :read_storage_location_containers do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ)
|
||||
end
|
||||
|
||||
can :create_storage_location_containers do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE)
|
||||
end
|
||||
|
||||
can :manage_storage_location_containers do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE)
|
||||
end
|
||||
|
||||
can :create_reports do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::REPORTS_CREATE)
|
||||
end
|
||||
|
|
42
app/serializers/concerns/shareable_serializer.rb
Normal file
42
app/serializers/concerns/shareable_serializer.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ShareableSerializer
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
attributes :shared, :shared_label, :ishared, :shared_read, :shared_write, :shareable_write
|
||||
end
|
||||
|
||||
def shared
|
||||
object.shared_with?(current_user.current_team)
|
||||
end
|
||||
|
||||
def shared_label
|
||||
case object[:shared]
|
||||
when 1
|
||||
I18n.t('libraries.index.shared')
|
||||
when 2
|
||||
I18n.t('libraries.index.shared_for_editing')
|
||||
when 3
|
||||
I18n.t('libraries.index.shared_for_viewing')
|
||||
when 4
|
||||
I18n.t('libraries.index.not_shared')
|
||||
end
|
||||
end
|
||||
|
||||
def ishared
|
||||
object.i_shared?(current_user.current_team)
|
||||
end
|
||||
|
||||
def shared_read
|
||||
object.shared_read?
|
||||
end
|
||||
|
||||
def shared_write
|
||||
object.shared_write?
|
||||
end
|
||||
|
||||
def shareable_write
|
||||
object.shareable_write?
|
||||
end
|
||||
end
|
|
@ -4,36 +4,14 @@ module Lists
|
|||
class RepositorySerializer < ActiveModel::Serializer
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
include ShareableSerializer
|
||||
|
||||
attributes :name, :code, :nr_of_rows, :shared, :shared_label, :ishared,
|
||||
:team, :created_at, :created_by, :archived_on, :archived_by,
|
||||
:urls, :shared_read, :shared_write, :shareable_write
|
||||
attributes :name, :code, :nr_of_rows, :team, :created_at, :created_by, :archived_on, :archived_by, :urls
|
||||
|
||||
def nr_of_rows
|
||||
object[:row_count]
|
||||
end
|
||||
|
||||
def shared
|
||||
object.shared_with?(current_user.current_team)
|
||||
end
|
||||
|
||||
def shared_label
|
||||
case object[:shared]
|
||||
when 1
|
||||
I18n.t('libraries.index.shared')
|
||||
when 2
|
||||
I18n.t('libraries.index.shared_for_editing')
|
||||
when 3
|
||||
I18n.t('libraries.index.shared_for_viewing')
|
||||
when 4
|
||||
I18n.t('libraries.index.not_shared')
|
||||
end
|
||||
end
|
||||
|
||||
def ishared
|
||||
object.i_shared?(current_user.current_team)
|
||||
end
|
||||
|
||||
def team
|
||||
object[:team_name]
|
||||
end
|
||||
|
@ -54,25 +32,15 @@ module Lists
|
|||
object[:archived_by_user]
|
||||
end
|
||||
|
||||
def shared_read
|
||||
object.shared_read?
|
||||
end
|
||||
|
||||
def shared_write
|
||||
object.shared_write?
|
||||
end
|
||||
|
||||
def shareable_write
|
||||
object.shareable_write?
|
||||
end
|
||||
|
||||
def urls
|
||||
{
|
||||
show: repository_path(object),
|
||||
update: team_repository_path(current_user.current_team, id: object, format: :json),
|
||||
shareable_teams: shareable_teams_team_repository_path(current_user.current_team, object),
|
||||
duplicate: team_repository_copy_path(current_user.current_team, repository_id: object, format: :json),
|
||||
share: team_repository_team_repositories_path(current_user.current_team, object)
|
||||
shareable_teams: shareable_teams_team_shared_objects_path(
|
||||
current_user.current_team, object_id: object.id, object_type: 'Repository'
|
||||
),
|
||||
share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: 'Repository')
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module Lists
|
|||
:have_reminders, :reminders_url
|
||||
|
||||
def row_id
|
||||
object.repository_row.id unless hidden
|
||||
object.repository_row.code
|
||||
end
|
||||
|
||||
def row_name
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Lists
|
||||
class StorageLocationSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
include ShareableSerializer
|
||||
|
||||
attributes :id, :code, :name, :container, :description, :owned_by, :created_by,
|
||||
:created_on, :urls, :metadata, :file_name, :sub_location_count
|
||||
|
@ -46,7 +47,11 @@ module Lists
|
|||
end
|
||||
{
|
||||
show: show_url,
|
||||
update: storage_location_path(@object)
|
||||
update: storage_location_path(@object),
|
||||
shareable_teams: shareable_teams_team_shared_objects_path(
|
||||
current_user.current_team, object_id: object.id, object_type: object.class.name
|
||||
),
|
||||
share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: object.class.name)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,16 +6,16 @@ class ShareableTeamSerializer < ActiveModel::Serializer
|
|||
attributes :id, :name, :private_shared_with, :private_shared_with_write
|
||||
|
||||
def private_shared_with
|
||||
repository.private_shared_with?(object)
|
||||
model.private_shared_with?(object)
|
||||
end
|
||||
|
||||
def private_shared_with_write
|
||||
repository.private_shared_with_write?(object)
|
||||
model.private_shared_with_write?(object)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def repository
|
||||
scope[:repository] || @instance_options[:repository]
|
||||
def model
|
||||
scope[:model] || @instance_options[:model]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
module Lists
|
||||
class StorageLocationsService < BaseService
|
||||
def initialize(team, params)
|
||||
def initialize(user, team, params)
|
||||
@user = user
|
||||
@team = team
|
||||
@parent_id = params[:parent_id]
|
||||
@filters = params[:filters] || {}
|
||||
|
@ -13,8 +14,8 @@ module Lists
|
|||
@records =
|
||||
StorageLocation.joins('LEFT JOIN storage_locations AS sub_locations ' \
|
||||
'ON storage_locations.id = sub_locations.parent_id')
|
||||
.viewable_by_user(@user, @team)
|
||||
.select('storage_locations.*, COUNT(sub_locations.id) AS sub_location_count')
|
||||
.where(team: @team)
|
||||
.group(:id)
|
||||
end
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ module Toolbars
|
|||
private
|
||||
|
||||
def unassign_action
|
||||
return unless can_manage_storage_location?(@storage_location)
|
||||
|
||||
{
|
||||
name: 'unassign',
|
||||
label: I18n.t('storage_locations.show.toolbar.unassign'),
|
||||
|
@ -37,7 +39,7 @@ module Toolbars
|
|||
end
|
||||
|
||||
def move_action
|
||||
return unless @single
|
||||
return unless @single && can_manage_storage_location?(@storage_location)
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
|
|
|
@ -21,16 +21,15 @@ module Toolbars
|
|||
edit_action,
|
||||
move_action,
|
||||
duplicate_action,
|
||||
delete_action
|
||||
delete_action,
|
||||
share_action
|
||||
].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def edit_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
return unless @single && can_manage_storage_location?(@storage_locations.first)
|
||||
|
||||
{
|
||||
name: 'edit',
|
||||
|
@ -42,13 +41,11 @@ module Toolbars
|
|||
end
|
||||
|
||||
def move_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
return unless @single && can_manage_storage_location?(@storage_locations.first)
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
label: I18n.t("storage_locations.index.toolbar.move"),
|
||||
label: I18n.t('storage_locations.index.toolbar.move'),
|
||||
icon: 'sn-icon sn-icon-move',
|
||||
path: move_storage_location_path(@storage_locations.first),
|
||||
type: :emit
|
||||
|
@ -56,9 +53,7 @@ module Toolbars
|
|||
end
|
||||
|
||||
def duplicate_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
return unless @single && can_manage_storage_location?(@storage_locations.first)
|
||||
|
||||
{
|
||||
name: 'duplicate',
|
||||
|
@ -70,9 +65,7 @@ module Toolbars
|
|||
end
|
||||
|
||||
def delete_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
return unless @single && can_manage_storage_location?(@storage_locations.first)
|
||||
|
||||
storage_location = @storage_locations.first
|
||||
|
||||
|
@ -90,5 +83,16 @@ module Toolbars
|
|||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
def share_action
|
||||
return unless @single && can_share_storage_location?(@storage_locations.first)
|
||||
|
||||
{
|
||||
name: :share,
|
||||
label: I18n.t('storage_locations.index.share'),
|
||||
icon: 'sn-icon sn-icon-shared',
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,7 +38,19 @@ json.actions do
|
|||
end
|
||||
|
||||
json.storage_locations do
|
||||
json.locations @repository_row.grouped_storage_locations
|
||||
json.locations(
|
||||
@repository_row.storage_locations.distinct.map do |storage_location|
|
||||
readable = can_read_storage_location?(storage_location)
|
||||
|
||||
{
|
||||
id: storage_location.id,
|
||||
readable: readable,
|
||||
name: readable ? storage_location.name : storage_location.code,
|
||||
metadata: storage_location.metadata,
|
||||
positions: readable ? storage_location.storage_location_repository_rows.where(repository_row: @repository_row) : []
|
||||
}
|
||||
end
|
||||
)
|
||||
json.enabled StorageLocation.storage_locations_enabled?
|
||||
end
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<storage-locations-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_manage_storage_location?(@storage_location) %>"
|
||||
:with-grid="<%= @storage_location.with_grid? %>"
|
||||
:grid-size="<%= @storage_location.grid_size.to_json %>"
|
||||
:container-id="<%= @storage_location.id %>"
|
||||
|
|
|
@ -2012,14 +2012,6 @@ en:
|
|||
name_placeholder: "My inventory"
|
||||
submit: "Create"
|
||||
success_flash_html: "Inventory <strong>%{name}</strong> successfully created."
|
||||
modal_share:
|
||||
title: "Share Inventory"
|
||||
submit: "Save sharing options"
|
||||
share_with_team: "Share with Team"
|
||||
can_edit: "Can Edit"
|
||||
all_teams: "All teams (current & new)"
|
||||
all_teams_tooltip: "This will disable individual team settings"
|
||||
success_message: "Selected sharing options for the Inventory %{inventory_name} have been saved."
|
||||
export:
|
||||
notification:
|
||||
error:
|
||||
|
@ -2768,6 +2760,7 @@ en:
|
|||
description_1_html: "You're about to delete <b>%{name}</b>. This action will delete the %{type}. <b>%{num_of_items}</b> items inside will lose their assigned positions."
|
||||
description_2_html: '<b>Are you sure you want to delete it?</b>'
|
||||
success_message: "%{type} %{name} successfully deleted."
|
||||
share: "Share"
|
||||
|
||||
libraries:
|
||||
manange_modal_column_index:
|
||||
|
@ -4521,6 +4514,15 @@ en:
|
|||
active_state: "Active state"
|
||||
archived_state: "Archived state"
|
||||
|
||||
modal_share:
|
||||
title: "Share %{object_name}"
|
||||
submit: "Save sharing options"
|
||||
share_with_team: "Share with Team"
|
||||
can_edit: "Can Edit"
|
||||
all_teams: "All teams (current & new)"
|
||||
all_teams_tooltip: "This will disable individual team settings"
|
||||
success_message: "Selected sharing options for the %{object_name} have been saved."
|
||||
|
||||
errors:
|
||||
general: "Something went wrong."
|
||||
general_text_too_long: 'Text is too long'
|
||||
|
|
|
@ -166,6 +166,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :user_notifications, only: :index do
|
||||
collection do
|
||||
get :filter_groups
|
||||
get :unseen_counter
|
||||
end
|
||||
end
|
||||
|
@ -255,6 +256,13 @@ Rails.application.routes.draw do
|
|||
via: [:get, :post, :put, :patch]
|
||||
end
|
||||
|
||||
resources :team_shared_objects, only: [] do
|
||||
collection do
|
||||
post 'update'
|
||||
get 'shareable_teams'
|
||||
end
|
||||
end
|
||||
|
||||
resources :reports, only: [:index, :new, :create, :update] do
|
||||
member do
|
||||
get :document_preview
|
||||
|
@ -819,6 +827,7 @@ Rails.application.routes.draw do
|
|||
post :duplicate
|
||||
post :unassign_rows
|
||||
get :available_positions
|
||||
get :shareable_teams
|
||||
end
|
||||
resources :storage_location_repository_rows, only: %i(index create destroy update) do
|
||||
collection do
|
||||
|
|
Loading…
Add table
Reference in a new issue