Merge pull request #6896 from aignatov-bio/ai-sci-9804-migrate-inventories-table

Migrate inventory tables [SCI-9804]
This commit is contained in:
aignatov-bio 2024-01-08 18:33:45 +01:00 committed by GitHub
commit 8eb5aba948
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 856 additions and 126 deletions

View file

@ -11,7 +11,7 @@ class RepositoriesController < ApplicationController
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
export_modal export_repositories)
before_action :load_repositories, only: %i(index show sidebar)
before_action :load_repositories, only: :index
before_action :load_repositories_for_archiving, only: :archive
before_action :load_repositories_for_restoring, only: :restore
before_action :check_view_all_permissions, only: %i(index sidebar)
@ -35,7 +35,9 @@ class RepositoriesController < ApplicationController
render 'index'
end
format.json do
render json: prepare_repositories_datatable(@repositories, current_team, params)
repositories = Lists::RepositoriesService.new(@repositories, params).call
render json: repositories, each_serializer: Lists::RepositorySerializer, user: current_user,
meta: pagination_dict(repositories)
end
end
end
@ -93,6 +95,11 @@ class RepositoriesController < ApplicationController
render json: { html: render_to_string(partial: 'share_repository_modal', formats: :html) }
end
def shareable_teams
teams = current_user.teams - [@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?
@ -122,12 +129,9 @@ class RepositoriesController < ApplicationController
if @repository.save
log_activity(:create_inventory)
flash[:success] = t('repositories.index.modal_create.success_flash_html', name: @repository.name)
render json: { url: repository_path(@repository) }
render json: { message: t('repositories.index.modal_create.success_flash_html', name: @repository.name) }
else
render json: @repository.errors,
status: :unprocessable_entity
render json: @repository.errors, status: :unprocessable_entity
end
end
@ -163,14 +167,14 @@ class RepositoriesController < ApplicationController
end
def destroy
flash[:success] = t('repositories.index.delete_flash',
name: @repository.name)
log_activity(:delete_inventory) # Log before delete id
@repository.discard
@repository.destroy_discarded(current_user.id)
redirect_to team_repositories_path(archived: true)
render json: {
message: t('repositories.index.delete_flash', name: @repository.name)
}
end
def rename_modal
@ -240,13 +244,8 @@ class RepositoriesController < ApplicationController
if !copied_repository
render json: { name: ['Server error'] }, status: :unprocessable_entity
else
flash[:success] = t(
'repositories.index.copy_flash',
old: @repository.name,
new: copied_repository.name
)
render json: {
url: repository_path(copied_repository)
message: t('repositories.index.copy_flash', old: @repository.name, new: copied_repository.name)
}
end
end
@ -425,7 +424,7 @@ class RepositoriesController < ApplicationController
Toolbars::RepositoriesService.new(
current_user,
current_team,
repository_ids: params[:repository_ids].split(',')
repository_ids: JSON.parse(params[:items]).map { |i| i['id'] }
).actions
}
end
@ -449,12 +448,7 @@ class RepositoriesController < ApplicationController
end
def load_repositories
@repositories = Repository.accessible_by_teams(current_team).order('repositories.created_at ASC')
@repositories = if params[:archived] == 'true' || @repository&.archived?
@repositories.archived
else
@repositories.active
end
@repositories = Repository.accessible_by_teams(current_team)
end
def load_repositories_for_archiving

View file

@ -0,0 +1,10 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import PerfectScrollbar from 'vue3-perfect-scrollbar';
import RepositoriesTable from '../../vue/repositories/table.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp();
app.component('RepositoriesTable', RepositoriesTable);
app.config.globalProperties.i18n = window.I18n;
app.use(PerfectScrollbar);
mountWithTurbolinks(app, '#repositoriesTable');

View file

@ -59,7 +59,7 @@ export default {
})
},
beforeUnmount() {
document.body.style.overflow = 'scroll';
document.body.style.overflow = 'auto';
},
computed: {
filteredNotifications() {

View file

@ -0,0 +1,68 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<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" id="edit-project-modal-label" :title="repository.name">
{{ i18n.t('repositories.index.modal_copy.title_html', {name: repository.name }) }}
</h4>
</div>
<div class="modal-body">
<div class="mb-6">
<label class="sci-label">{{ i18n.t("repositories.index.modal_copy.name") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name" class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('repositories.index.modal_copy.name_placeholder')" />
</div>
</div>
</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" @click="submit" type="submit">
{{ i18n.t('repositories.index.modal_copy.copy') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
/* global HelperModule */
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'DuplicateRepositoryModal',
props: {
repository: Object
},
mixins: [modalMixin],
data() {
return {
name: this.repository.name,
error: null
};
},
methods: {
submit() {
axios.post(this.repository.urls.duplicate, {
repository: {
name: this.name
}
}).then((response) => {
this.error = null;
this.$emit('duplicate');
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
this.error = error.response.data.name;
});
}
}
};
</script>

View file

@ -0,0 +1,66 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<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" id="edit-project-modal-label" :title="repository.name">
{{ i18n.t('repositories.index.modal_rename.title_html', {name: repository.name }) }}
</h4>
</div>
<div class="modal-body">
<div class="mb-6">
<label class="sci-label">{{ i18n.t("repositories.index.modal_rename.name") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name" class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('repositories.index.modal_rename.name_placeholder')" />
</div>
</div>
</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" @click="submit" type="submit">
{{ i18n.t('repositories.index.modal_rename.rename') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'EditRepositoryModal',
props: {
repository: Object
},
mixins: [modalMixin],
data() {
return {
name: this.repository.name,
error: null
};
},
methods: {
submit() {
axios.put(this.repository.urls.update, {
repository: {
name: this.name
}
}).then(() => {
this.error = null;
this.$emit('update');
}).catch((error) => {
this.error = error.response.data.name;
});
}
}
};
</script>

View file

@ -0,0 +1,69 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<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" id="edit-project-modal-label">
{{ i18n.t('repositories.index.modal_create.title') }}
</h4>
</div>
<div class="modal-body">
<div class="mb-6">
<label class="sci-label">{{ i18n.t("repositories.index.modal_create.name_label") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name"
class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('repositories.index.modal_create.name_placeholder')" />
</div>
</div>
</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" @click="submit" type="submit">
{{ i18n.t('repositories.index.modal_create.submit') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
/* global HelperModule */
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'NewRepositoryModal',
props: {
createUrl: String
},
mixins: [modalMixin],
data() {
return {
name: '',
error: null
};
},
methods: {
submit() {
axios.post(this.createUrl, {
repository: {
name: this.name
}
}).then((response) => {
this.error = null;
this.$emit('create');
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
this.error = error.response.data.name;
});
}
}
};
</script>

View file

@ -0,0 +1,118 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<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">
{{ i18n.t('repositories.index.modal_share.title', {name: repository.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") }}
</div>
<div class="text-center">
{{ i18n.t("repositories.index.modal_share.can_edit") }}
</div>
<div 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") }}
</div>
<div 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"
v-model="sharedWithAllWrite" />
<span class="sci-toggle-checkbox-label"></span>
</span>
</div>
<template v-for="team in shareableTeams">
<div class="col-span-2 flex items-center h-9 gap-1">
<span class="sci-checkbox-container" :class="{'opacity-0 pointer-events-none': sharedWithAllRead}">
<input type="checkbox" class="sci-checkbox" v-model="team.attributes.private_shared_with" />
<span class="sci-checkbox-label"></span>
</span>
{{ team.attributes.name }}
</div>
<div class="flex justify-center items-center">
<span v-if="team.attributes.private_shared_with"
:class="{'opacity-0 pointer-events-none': sharedWithAllRead}"
class="sci-toggle-checkbox-container">
<input type="checkbox"
class="sci-toggle-checkbox"
@change="permission_changes[team.id] = true"
:disabled="!repository.shareable_write"
v-model="team.attributes.private_shared_with_write" />
<span class="sci-toggle-checkbox-label"></span>
</span>
</div>
</template>
</div>
</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" @click="submit" type="submit">
{{ i18n.t('repositories.index.modal_share.submit') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
/* global HelperModule */
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'ShareRepositoryModal',
props: {
repository: Object
},
mixins: [modalMixin],
data() {
return {
sharedWithAllRead: this.repository.shared_read || this.repository.shared_write,
sharedWithAllWrite: this.repository.shared_write,
shareableTeams: [],
permission_changes: {}
};
},
mounted() {
this.getTeams();
},
methods: {
getTeams() {
axios.get(this.repository.urls.shareable_teams).then((response) => {
this.shareableTeams = response.data.data;
});
},
submit() {
const data = {
select_all_teams: this.sharedWithAllRead,
select_all_write_permission: this.sharedWithAllWrite,
share_team_ids: this.shareableTeams.filter((team) => team.private_shared_with).map((team) => team.id),
write_permissions: this.shareableTeams.filter((team) => team.private_shared_with_write).map((team) => team.id),
permission_changes: this.permission_changes
};
axios.post(this.repository.urls.share, data).then(() => {
HelperModule.flashAlertMsg(this.i18n.t(
'repositories.index.modal_share.success_message',
{ inventory_name: this.repository.name }
), 'success');
this.$emit('share');
});
}
}
};
</script>

View file

@ -0,0 +1,299 @@
<template>
<div class="h-full">
<DataTable :columnDefs="columnDefs"
tableId="repositories"
:dataUrl="dataSource"
:reloadingTable="reloadingTable"
:currentViewMode="currentViewMode"
:toolbarActions="toolbarActions"
:activePageUrl="activePageUrl"
:archivedPageUrl="archivedPageUrl"
:actionsUrl="actionsUrl"
@archive="archive"
@restore="restore"
@delete="deleteRepository"
@update="update"
@duplicate="duplicate"
@export="exportRepositories"
@share="share"
@create="newRepository = true"
@tableReloaded="reloadingTable = false"
/>
</div>
<ConfirmationModal
:title="deleteModal.title"
:description="deleteModal.description"
confirmClass="btn btn-danger"
:confirmText="i18n.t('repositories.index.modal_delete.delete')"
ref="deleteModal"
></ConfirmationModal>
<ConfirmationModal
:title="exportModal.title"
:description="exportModal.description"
confirmClass="btn btn-primary"
:confirmText="i18n.t('repositories.index.modal_export.export')"
ref="exportModal"
></ConfirmationModal>
<NewRepositoryModal
v-if="newRepository"
:createUrl="createUrl"
@close="newRepository = false"
@create="updateTable" />
<EditRepositoryModal
v-if="editRepository"
:repository="editRepository"
@close="editRepository = null"
@update="updateTable" />
<DuplicateRepositoryModal
v-if="duplicateRepository"
:repository="duplicateRepository"
@close="duplicateRepository = null"
@duplicate="updateTable" />
<ShareRepositoryModal
v-if="shareRepository"
:repository="shareRepository"
@close="shareRepository = null"
@share="updateTable" />
</template>
<script>
/* global HelperModule */
import axios from '../../packs/custom_axios.js';
import ConfirmationModal from '../shared/confirmation_modal.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 DataTable from '../shared/datatable/table.vue';
export default {
name: 'LabelTemplatesTable',
components: {
DataTable,
ConfirmationModal,
NewRepositoryModal,
EditRepositoryModal,
DuplicateRepositoryModal,
ShareRepositoryModal
},
props: {
dataSource: {
type: String,
required: true
},
actionsUrl: {
type: String,
required: true
},
createUrl: {
type: String
},
currentViewMode: {
type: String,
required: true
},
activePageUrl: {
type: String,
required: true
},
archivedPageUrl: {
type: String,
required: true
}
},
data() {
return {
reloadingTable: false,
newRepository: false,
editRepository: null,
duplicateRepository: null,
shareRepository: null,
deleteModal: {
title: '',
description: ''
},
exportModal: {
title: '',
description: ''
}
};
},
computed: {
columnDefs() {
const columns = [{
field: 'name',
headerName: this.i18n.t('libraries.index.table.name'),
sortable: true,
notSelectable: true,
cellRenderer: this.nameRenderer
},
{
field: 'nr_of_rows',
headerName: this.i18n.t('libraries.index.table.number_of_items')
},
{
field: 'shared_label',
headerName: this.i18n.t('libraries.index.table.shared')
},
{
field: 'team',
headerName: this.i18n.t('libraries.index.table.ownership'),
sortable: true
},
{
field: 'created_at',
headerName: this.i18n.t('libraries.index.table.added_on'),
sortable: true
},
{
field: 'created_by',
headerName: this.i18n.t('libraries.index.table.added_by'),
sortable: true
}];
if (this.currentViewMode === 'archived') {
columns.push({
field: 'archived_on',
headerName: this.i18n.t('libraries.index.table.archived_on'),
sortable: true
});
columns.push({
field: 'archived_by',
headerName: this.i18n.t('libraries.index.table.archived_by'),
sortable: true
});
}
return columns;
},
toolbarActions() {
const left = [];
if (this.createUrl) {
left.push({
name: 'create',
icon: 'sn-icon sn-icon-new-task',
label: this.i18n.t('libraries.index.no_libraries.create_new_button'),
type: 'emit',
path: this.createUrl,
buttonStyle: 'btn btn-primary'
});
}
return {
left,
right: []
};
}
},
methods: {
updateTable() {
this.reloadingTable = true;
this.newRepository = false;
this.editRepository = null;
this.duplicateRepository = null;
this.shareRepository = null;
},
archive(event, rows) {
axios.post(event.path, { repository_ids: rows.map((row) => row.id) }).then((response) => {
this.updateTable();
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
restore(event, rows) {
axios.post(event.path, { repository_ids: rows.map((row) => row.id) }).then((response) => {
this.updateTable();
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
async exportRepositories(event, rows) {
this.exportModal.title = this.i18n.t('repositories.index.modal_export.title');
this.exportModal.description = `
<p class="description-p1">
${this.i18n.t('repositories.index.modal_export.description_p1_html', {
team_name: rows[0].team,
count: rows.length
})}
</p>
<p class="bg-sn-super-light-blue p-3">
${this.i18n.t('repositories.index.modal_export.description_alert')}
</p>
<p class="mt-3">
${this.i18n.t('repositories.index.modal_export.description_p2')}
</p>
<p>
${this.i18n.t('repositories.index.modal_export.description_p3_html', {
remaining_export_requests: event.num_of_requests_left,
requests_limit: event.export_limit
})}
</p>
`;
const ok = await this.$refs.exportModal.show();
if (ok) {
axios.post(event.path, { repository_ids: rows.map((row) => row.id) }).then((response) => {
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
}
},
async deleteRepository(event, rows) {
const [repository] = rows;
this.deleteModal.title = this.i18n.t('repositories.index.modal_delete.title_html', { name: repository.name });
this.deleteModal.description = `
<p>${this.i18n.t('repositories.index.modal_delete.message_html', { name: repository.name })}</p>
<div class="alert alert-danger" role="alert">
<span class="fas fa-exclamation-triangle"></span>
${this.i18n.t('repositories.index.modal_delete.alert_heading')}
<ul>
<li>${this.i18n.t('repositories.index.modal_delete.alert_line_1')}</li>
<li>${this.i18n.t('repositories.index.modal_delete.alert_line_2')}</li>
</ul>
</div>
`;
const ok = await this.$refs.deleteModal.show();
if (ok) {
axios.delete(event.path).then((response) => {
this.updateTable();
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
}
},
update(_event, rows) {
const [repository] = rows;
this.editRepository = repository;
},
duplicate(_event, rows) {
const [repository] = rows;
this.duplicateRepository = repository;
},
share(_event, rows) {
const [repository] = rows;
this.shareRepository = repository;
},
// Renderers
nameRenderer(params) {
const {
name,
urls,
shared,
ishared
} = params.data;
let sharedIcon = '';
if (shared || ishared) {
sharedIcon = '<i class="fas fa-users"></i>';
}
return `<a class="hover:no-underline flex items-center gap-1" href="${urls.show}">${sharedIcon}${name}</a>`;
}
}
};
</script>

View file

@ -19,8 +19,11 @@
v-for="column in columnDefs"
:key="column.field"
@click="toggleColumn(column, columnVisbile(column))"
class="flex items-center gap-4 py-2.5 px-3 cursor-pointer"
:class="{'hover:bg-sn-super-light-grey': column.field !== 'name'}"
class="flex items-center gap-4 py-2.5 px-3"
:class="{
'cursor-pointer': column.field !== 'name',
'hover:bg-sn-super-light-grey': column.field !== 'name'
}"
>
<div v-if="column.field === 'name'" class="w-6 h-6"></div>
<template v-else>
@ -61,6 +64,8 @@ export default {
return !this.currentTableState.columnsState?.find((col) => col.colId === column.field).hide;
},
toggleColumn(column, visible) {
if (column.field === 'name') return;
this.currentTableState.columnsState.find((col) => col.colId === column.field).hide = visible;
if (visible) {
this.$emit('hideColumn', column);

View file

@ -0,0 +1,80 @@
# frozen_string_literal: true
module Lists
class RepositorySerializer < ActiveModel::Serializer
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
attributes :name, :nr_of_rows, :shared, :shared_label, :ishared,
:team, :created_at, :created_by, :archived_on, :archived_by,
:urls, :shared_read, :shared_write, :shareable_write
def nr_of_rows
object.repository_rows.count
end
def shared
object.shared_with?(current_user.current_team)
end
def shared_label
if object.i_shared?(current_user.current_team)
I18n.t('libraries.index.shared')
elsif object.shared_with?(current_user.current_team)
if object.shared_with_read?(current_user.current_team)
I18n.t('libraries.index.shared_for_viewing')
else
I18n.t('libraries.index.shared_for_editing')
end
else
I18n.t('libraries.index.not_shared')
end
end
def ishared
object.i_shared?(current_user.current_team)
end
def team
object[:team_name]
end
def created_at
I18n.l(object.created_at, format: :full)
end
def created_by
object[:created_by_user]
end
def archived_on
I18n.l(object.archived_on, format: :full) if object.archived_on
end
def archived_by
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)
}
end
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class ShareableTeamSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :name, :private_shared_with, :private_shared_with_write
def private_shared_with
repository.private_shared_with?(object)
end
def private_shared_with_write
repository.private_shared_with_write?(object)
end
private
def repository
scope[:repository] || @instance_options[:repository]
end
end

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
module Lists
class RepositoriesService < BaseService
private
def fetch_records
@records = @raw_data.joins(
'LEFT OUTER JOIN users AS creators ' \
'ON repositories.created_by_id = creators.id'
)
.joins(
'LEFT OUTER JOIN users AS archivers ' \
'ON repositories.archived_by_id = archivers.id'
)
.includes(:repository_rows)
.joins(:team)
.select('repositories.* AS repositories')
.select('teams.name AS team_name')
.select('creators.full_name AS created_by_user')
.select('archivers.full_name AS archived_by_user')
view_mode = @params[:view_mode] || 'active'
@records = @records.archived if view_mode == 'archived'
@records = @records.active if view_mode == 'active'
end
def filter_records
return unless @params[:search]
@records = @records.where_attributes_like(
[
'repositories.name',
'teams.name',
'creators.full_name',
'archivers.full_name'
],
@params[:search]
)
end
def sortable_columns
@sortable_columns ||= {
name: 'repositories.name',
team: 'teams.name',
created_by: 'creators.full_name',
created_at: 'repositories.created_at',
archived_on: 'repositories.archived_on',
archived_by: 'archivers.full_name'
}
end
end
end

View file

@ -34,12 +34,10 @@ module Toolbars
return unless @single && can_manage_repository?(@repository)
{
name: 'rename',
name: :update,
label: I18n.t('libraries.index.buttons.edit'),
button_id: 'renameRepoBtn',
icon: 'sn-icon sn-icon-edit',
path: team_repository_rename_modal_path(@current_team, repository_id: @repository),
type: 'remote-modal'
type: :emit
}
end
@ -47,12 +45,10 @@ module Toolbars
return unless @single && can_create_repositories?(@current_team)
{
name: 'duplicate',
name: :duplicate,
label: I18n.t('libraries.index.buttons.duplicate'),
button_id: 'copyRepoBtn',
icon: 'sn-icon sn-icon-duplicate',
path: team_repository_copy_modal_path(@current_team, repository_id: @repository),
type: 'remote-modal'
type: :emit
}
end
@ -60,12 +56,13 @@ module Toolbars
return unless @repositories.all? { |repository| can_read_repository?(repository) }
{
name: 'export',
name: :export,
label: I18n.t('libraries.index.buttons.export'),
button_id: 'exportRepoBtn',
icon: 'sn-icon sn-icon-export',
path: export_modal_team_repositories_path(@current_team, counter: @repositories.length),
type: 'remote-modal'
path: export_repositories_team_path(@current_team),
export_limit: TeamZipExport.exports_limit,
num_of_requests_left: @current_user.exports_left - 1,
type: :emit
}
end
@ -73,13 +70,11 @@ module Toolbars
return unless @repositories.all? { |repository| can_archive_repository?(repository) }
{
name: 'archive',
name: :archive,
label: I18n.t('libraries.index.buttons.archive'),
button_id: 'archiveRepoBtn',
icon: 'sn-icon sn-icon-archive',
path: archive_team_repositories_path(@current_team),
type: :request,
request_method: :post
type: :emit
}
end
@ -87,12 +82,10 @@ module Toolbars
return unless @single && can_share_repository?(@repository)
{
name: 'share',
name: :share,
label: I18n.t('repositories.index.share_inventory'),
icon: 'sn-icon sn-icon-shared',
button_class: 'share-repository-button',
path: team_repository_share_modal_path(@current_team, repository_id: @repository),
type: 'remote-modal'
type: :emit
}
end
@ -100,13 +93,11 @@ module Toolbars
return unless @repositories.all? { |repository| can_archive_repository?(repository) }
{
name: 'restore',
name: :restore,
label: I18n.t('libraries.index.buttons.restore'),
icon: 'sn-icon sn-icon-restore',
button_id: 'restoreRepoBtn',
path: restore_team_repositories_path(@current_team),
type: :request,
request_method: :post
type: :emit
}
end
@ -114,12 +105,11 @@ module Toolbars
return unless @single && can_delete_repository?(@repository)
{
name: 'delete',
name: :delete,
label: I18n.t('libraries.index.buttons.delete'),
icon: 'sn-icon sn-icon-delete',
button_id: 'deleteRepoBtn',
path: team_repository_destroy_modal_path(@current_team, repository_id: @repository),
type: 'remote-modal'
path: team_repository_path(@current_team, @repository),
type: :emit
}
end
end

View file

@ -1,10 +0,0 @@
<div class="content-pane flexible <%= params[:archived] ? :archived : :active %> repositories-index">
<div class="content-header">
<div class="title-row">
<h1 data-view-mode="active"><%= t('libraries.index.head_title') %></h1>
<h1 data-view-mode="archived"><span><%= t('labels.archived')%></span>&nbsp;<%= t('libraries.index.head_title_archived') %></h1>
</div>
</div>
<div class="content-body" data-e2e="e2e-inventories-container">
</div>
</div>

View file

@ -3,65 +3,26 @@
<% if current_team %>
<% provide(:sidebar_url, sidebar_repositories_path) %>
<% provide(:sidebar_title, t('sidebar.repositories.sidebar_title')) %>
<%= content_for :sidebar do %>
<%= render partial: "sidebar", locals: { repositories: @repositories, archived: params[:archived] } %>
<% end %>
<%= render "view_archived_btn" %>
<% end %>
<!-- table template -->
<template id="RepositoriesListTable">
<table id="repositoriesList" class="table"
data-source="<%= team_repositories_path(current_team, format: :json) %>">
<thead>
<tr>
<th>
<div class="sci-checkbox-container">
<input value="1" type="checkbox" class="sci-checkbox select-all-checkbox">
<span class="sci-checkbox-label"></span>
</div>
</th>
<th><%= t('libraries.index.table.name') %></th>
<th><%= t('libraries.index.table.number_of_items') %></th>
<th><%= t('libraries.index.table.shared') %></th>
<th><%= t('libraries.index.table.ownership') %></th>
<th><%= t('libraries.index.table.added_on') %></th>
<th><%= t('libraries.index.table.added_by') %></th>
<th><%= t('libraries.index.table.archived_on') %></th>
<th><%= t('libraries.index.table.archived_by') %></th>
</tr>
</thead>
</table>
</template>
<!-- Repositories action buttons -->
<template id="repositoriesListButtons">
<% if can_create_repositories?(current_team) %>
<a id="createRepoBtn" class="btn btn-primary" title="<%= t('libraries.index.no_libraries.create_new_button_tooltip') %>"
data-remote="true"
data-view-mode="active"
href="<%= create_modal_team_repositories_path(current_team) %>">
<span class="sn-icon sn-icon-new-task"></span>
<span class="hidden-xs"><%= t('libraries.index.no_libraries.create_new_button') %></span>
</a>
<% end %>
<%= render partial: 'shared/state_view_switch', locals: {
disabled: false,
switchable: true,
archived: params[:archived],
active_url: repositories_path,
archived_url: repositories_path(archived: true),
} %>
<div class="content-pane flexible <%= params[:archived] ? :archived : :active %> repositories-index">
<div class="content-header">
<div class="title-row">
<h1 data-view-mode="active"><%= t('libraries.index.head_title') %></h1>
<h1 data-view-mode="archived"><span><%= t('labels.archived')%></span>&nbsp;<%= t('libraries.index.head_title_archived') %></h1>
</div>
</div>
<div class="content-body" data-e2e="e2e-inventories-container">
<div id="repositoriesTable" class="fixed-content-body">
<repositories-table
actions-url="<%= actions_toolbar_team_repositories_path(current_team) %>"
data-source="<%= repositories_path(format: :json) %>"
create-url="<%= repositories_path if can_create_repositories?(current_team) %>"
active-page-url="<%= repositories_path %>"
archived-page-url="<%= repositories_path(view_mode: :archived) %>"
current-view-mode="<%= params[:view_mode] || :active %>"
/>
</div>
</div>
</div>
</template>
<div id="actionToolbar" data-behaviour="vue">
<action-toolbar actions-url="<%= actions_toolbar_team_repositories_url(current_team) %>" />
</div>
<%= javascript_include_tag "repositories/index" %>
<%= javascript_include_tag "repositories/share_modal" %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag "vue_components_action_toolbar" %>
<%= javascript_include_tag 'vue_repositories_table' %>
<% end %>

View file

@ -69,6 +69,7 @@ test: &test
# for a full rundown on how to provide these environment variables in a
# production deployment.
#
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#

View file

@ -197,6 +197,9 @@ Rails.application.routes.draw do
get 'actions_toolbar'
get 'export_modal'
end
member do
get :shareable_teams
end
get 'destroy_modal', to: 'repositories#destroy_modal',
defaults: { format: 'json' }
get 'rename_modal', to: 'repositories#rename_modal',

View file

@ -52,7 +52,8 @@ const entryList = {
vue_experiments_list: './app/javascript/packs/vue/experiments_list.js',
vue_my_modules_list: './app/javascript/packs/vue/my_modules_list.js',
vue_design_system_select: './app/javascript/packs/vue/design_system/select.js',
vue_protocols_list: './app/javascript/packs/vue/protocols_list.js'
vue_protocols_list: './app/javascript/packs/vue/protocols_list.js',
vue_repositories_table: './app/javascript/packs/vue/repositories_table.js'
};
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949