Merge pull request #7827 from artoscinote/ma_SCI_10865

Implement storage location sharing [SCI-10865]
This commit is contained in:
Martin Artnik 2024-09-09 10:25:03 +02:00 committed by GitHub
commit 34c8da949f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 429 additions and 232 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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: {

View file

@ -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>

View file

@ -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');
});

View file

@ -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,

View file

@ -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;
}
}
};

View 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

View file

@ -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

View file

@ -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)

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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 %>"

View file

@ -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'

View file

@ -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