Merge branch 'features/new-datatable' into gc_SCI_10150

This commit is contained in:
G-Chubinidze 2024-02-21 04:27:36 +04:00 committed by GitHub
commit c48359c402
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 685 additions and 360 deletions

View file

@ -146,13 +146,11 @@
});
// initialize my_module tab remote loading
$('#experimentTable, .my-modules-protocols-index, #experiment-canvas')
.on('ajax:before', '.edit-tags-link', function() {
manageTagsModal.modal('show');
.on('click', '.edit-tags-link', function() {
if($('#tagsModalComponent').length) {
$('#tagsModalComponent').data('tagsModal').open()
}
})
.on('ajax:success', '.edit-tags-link', function(e, data) {
$('#manage-module-tags-modal-module').text(data.my_module.name);
initTagsModalBody(data);
});
}
bindEditTagsAjax();

View file

@ -933,17 +933,9 @@ function bindEditTagsAjax(elements) {
// initialize my_module tab remote loading
$(elements).find("a.edit-tags-link")
.on("ajax:before", function () {
var moduleId = $(this).closest(".panel-default").attr("data-module-id");
manageTagsModal.attr("data-module-id", moduleId);
manageTagsModal.modal('show');
})
.on("ajax:success", function (e, data) {
$("#manage-module-tags-modal-module").text(data.my_module.name);
initTagsModalBody(data);
})
.on('click', function(){
$(this).addClass('updated-module-tags');
var modal = $(this).closest(".panel-default").find('.tags-modal-component').data('tagsModal').open();
});
}

View file

@ -15,7 +15,6 @@
--ag-icon-font-code-checkbox-indeterminate: asset-url("checkbox/indeterminate.svg");
--ag-input-focus-box-shadow: none;
--ag-cell-horizontal-padding: .75rem;
border: 0;
.ag-cell {
@ -31,6 +30,10 @@
cursor: pointer;
}
.ag-header-cell-resize {
width: 1rem;
}
.ag-input-field-input:focus {
outline: none !important;
outline-offset: 0 !important;

View file

@ -90,5 +90,13 @@ input[type="checkbox"].sci-checkbox {
border: $border-tertiary;
}
}
&:checked + .sci-checkbox-label {
&::before {
background-color: var(--sn-sleepy-grey);
border: 1px solid var(--sn-sleepy-grey);
}
}
}
}

View file

@ -74,6 +74,8 @@ class ExperimentsController < ApplicationController
.select('COUNT(DISTINCT comments.id) as task_comments_count')
.select('my_modules.*').group(:id)
end
save_view_type('canvas')
end
def my_modules
@ -456,6 +458,12 @@ class ExperimentsController < ApplicationController
params.require(:experiment).require(:view_type)
end
def save_view_type(view_type)
view_state = @experiment.current_view_state(current_user)
view_state.state['my_modules']['view_type'] = view_type
view_state.save!
end
def check_read_permissions
current_team_switch(@experiment.project.team) if current_team != @experiment.project.team
render_403 unless can_read_experiment?(@experiment) ||

View file

@ -38,6 +38,7 @@ class MyModulesController < ApplicationController
meta: pagination_dict(my_modules)
end
format.html do
save_view_type('table')
render 'my_modules/index'
end
end
@ -573,6 +574,12 @@ class MyModulesController < ApplicationController
end
end
def save_view_type(view_type)
view_state = @experiment.current_view_state(current_user)
view_state.state['my_modules']['view_type'] = view_type
view_state.save!
end
def log_activity(type_of, my_module = nil, message_items = {})
my_module ||= @my_module
message_items = { my_module: my_module.id }.merge(message_items)

View file

@ -0,0 +1,68 @@
/* global I18n dropdownSelector */
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import TagsModal from '../../../vue/my_modules/modals/tags.vue';
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
window.initTagsModalComponent = (id) => {
const app = createApp({
data() {
return {
tagsModalOpen: false
};
},
mounted() {
$(this.$refs.tagsModal).data('tagsModal', this);
},
methods: {
open() {
this.tagsModalOpen = true;
},
close() {
this.tagsModalOpen = false;
},
syncTags(tags) {
// My module page
if ($('#module-tags-selector').length) {
const assignedTags = tags.filter((i) => i.assigned).map((i) => (
{
value: i.id,
label: i.attributes.name,
params: {
color: i.attributes.color
}
}
));
dropdownSelector.setData('#module-tags-selector', assignedTags);
}
// Canvas
if ($('#canvas-container').length) {
$.ajax({
url: $('#canvas-container').attr('data-module-tags-url'),
type: 'GET',
dataType: 'json',
success(data) {
$.each(data.my_modules, (index, myModule) => {
$(`div.panel[data-module-id='${myModule.id}']`)
.find('.edit-tags-link')
.html(myModule.tags_html);
});
}
});
}
}
}
});
app.component('tags-modal', TagsModal);
app.config.globalProperties.i18n = window.I18n;
mountWithTurbolinks(app, id);
};
document.addEventListener('turbolinks:load', () => {
const tagsModalContainers = document.querySelectorAll('.vue-tags-modal:not(.initialized)');
tagsModalContainers.forEach((container) => {
$(container).addClass('initialized')
window.initTagsModalComponent(`#${container.id}`);
});
});

View file

@ -122,7 +122,8 @@ export default {
{
field: 'created_at',
headerName: this.i18n.t('experiments.card.start_date'),
sortable: true
sortable: true,
minWidth: 130
},
{
field: 'updated_at',

View file

@ -26,6 +26,7 @@
<TagsModal v-if="tagsModalObject"
:params="tagsModalObject"
:tagsColors="tagsColors"
:projectName="projectName"
:projectTagsUrl="projectTagsUrl"
@close="updateTable" />
<NewModal v-if="newModalOpen"
@ -88,7 +89,8 @@ export default {
projectTagsUrl: { type: String, required: true },
assignedUsersUrl: { type: String, required: true },
usersFilterUrl: { type: String, required: true },
statusesList: { type: Array, required: true }
statusesList: { type: Array, required: true },
projectName: { type: String }
},
data() {
return {

View file

@ -6,89 +6,118 @@
<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">
<h4 v-if="canManage" class="modal-title truncate !block">
{{ i18n.t("experiments.canvas.modal_manage_tags.head_title") }}
</h4>
<h4 v-else class="modal-title truncate !block">
{{ i18n.t("experiments.canvas.modal_manage_tags.head_title_read") }}
</h4>
</div>
<div class="modal-body">
<div class="mb-4">
<div v-if="canManage" class="mb-4">
{{ i18n.t("experiments.canvas.modal_manage_tags.explanatory_text") }}
</div>
<div class="max-h-80 overflow-y-auto">
<div v-if="this.params.permissions.manage_tags" v-for="tag in tagsList" :key="tag.id"
class="flex items-center gap-2 px-3 py-2.5 hover:bg-sn-super-light-grey group">
<div class="mb-4">
<h5>{{ i18n.t("experiments.canvas.modal_manage_tags.project_tags", { project: this.projectName }) }}</h5>
</div>
<div class="max-h-80 overflow-y-auto" v-click-outside="finishEditMode">
<template v-for="tag in allTags" :key="tag.id">
<div
class="flex items-center gap-3 px-3 py-2.5 group"
:class="{
'!bg-sn-super-light-blue': tag.editing,
'hover:bg-sn-super-light-grey': canManage
}"
>
<div class="sci-checkbox-container">
<input type="checkbox"
:disabled="!canManage"
class="sci-checkbox"
:checked="tag.assigned" @change="toggleTag(tag)">
<label class="sci-checkbox-label"></label>
</div>
<div v-if="!tag.editing" @click="startEditMode(tag)"
class="h-6 px-1.5 flex items-center max-w-80 truncate text-sn-white rounded"
:class="{
'cursor-pointer': canManage
}"
:style="{ backgroundColor: tag.attributes.color }">
{{ tag.attributes.name }}
</div>
<template v-else>
<GeneralDropdown>
<template v-slot:field>
<div class="h-6 w-6 rounded relative flex items-center justify-center text-sn-white" :style="{ backgroundColor: tag.attributes.color }">
a
</div>
</template>
<template v-slot:flyout>
<div class="grid grid-cols-4 gap-1">
<div v-for="color in tagsColors" :key="color"
class="h-6 w-6 rounded relative flex items-center justify-center text-sn-white cursor-pointer"
@click.stop="updateTagColor(color, tag)"
:style="{ backgroundColor: color }">
<i v-if="color == tag.attributes.color" class="sn-icon sn-icon-check"></i>
<span v-else>a</span>
</div>
</div>
</template>
</GeneralDropdown>
<input type="text" :value="tag.attributes.name" class="border-0 grow focus:outline-none bg-transparent" @change="updateTagName($event.target.value, tag)"/>
<i @click.stop="finishEditMode($event, tag)" class="sn-icon sn-icon-check cursor-pointer ml-auto"></i>
</template>
<i v-if="canManage" @click.stop="deleteTag(tag)"
class="tw-hidden sn-icon sn-icon-delete cursor-pointer group-hover:block"
:class="{
'ml-auto': !tag.editing,
'!block': tag.editing
}"
></i>
</div>
</template>
</div>
<template v-if="canManage">
<div class="mb-4 mt-4">
{{ i18n.t('experiments.canvas.modal_manage_tags.create_new') }}
</div>
<div class="flex gap-2">
<GeneralDropdown>
<template v-slot:field>
<div class="h-8 w-8 rounded relative" :style="{ backgroundColor: tag.attributes.color }">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white"></div>
<div
class="h-6 w-6 border border-solid border-transparent rounded relative flex items-center justify-center text-sn-white"
:style="{ backgroundColor: newTag.color }"
:class="{'!border-sn-grey !text-sn-grey': !newTag.color}"
>
a
</div>
</template>
<template v-slot:flyout>
<div class="grid grid-cols-4 gap-1">
<div v-for="color in tagsColors" :key="color"
class="h-8 w-8 cursor-pointer rounded relative flex items-center justify-center"
@click="updateTagColor(color, tag)"
:style="{ backgroundColor: color }">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white"></div>
<i v-if="color == tag.attributes.color" class="sn-icon sn-icon-check text-white"></i>
class="h-6 w-6 rounded relative flex items-center justify-center text-sn-white cursor-pointer"
@click.stop="newTag.color = color"
:style="{ backgroundColor: color }">
<i v-if="color == newTag.color" class="sn-icon sn-icon-check"></i>
<span v-else>a</span>
</div>
</div>
</template>
</GeneralDropdown>
<div class="flex-grow truncate">
<InlineEdit
:value="tag.attributes.name"
:characterLimit="255"
attributeName='Tag name'
:allowBlank="false"
:singleLine="true"
@update="(value) => updateTagName(value, tag)"
/>
</div>
<i class="tw-hidden group-hover:block sn-icon sn-icon-close cursor-pointer" @click="unassignTag(tag)"></i>
<input type="text" v-model="newTag.name"
:placeholder="i18n.t('experiments.canvas.modal_manage_tags.new_tag_name')"
class="border-0 focus:outline-none bg-transparent" />
<i v-if="validNewTag" @click.stop="createTag" class="sn-icon sn-icon-check cursor-pointer ml-auto"></i>
<i @click.stop="newTag = { name: null, color: null }"
class="tw-hidden sn-icon sn-icon-delete cursor-pointer "
:class="{
'ml-auto': !validNewTag,
'!block': newTag.name || newTag.color
}"
></i>
</div>
<div v-else v-for="tag in tagsList" :key="tag.id" class="flex items-center gap-2 px-3 py-2.5">
<div class="h-8 w-8 rounded relative" :style="{ backgroundColor: tag.attributes.color }">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white"></div>
</div>
<div class="flex-grow truncate">
{{ tag.attributes.name }}
</div>
</div>
</div>
<div v-if="this.params.permissions.manage_tags"
class="text-sn-grey flex items-center gap-2 px-3 cursor-pointer
py-2.5 hover:bg-sn-super-light-grey"
@click="createTag()"
>
<div class="h-8 w-8 rounded relative border-sn-grey border-solid">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white border-sn-grey border-solid"></div>
</div>
<div>{{ i18n.t('experiments.canvas.modal_manage_tags.create_new') }}</div>
</div>
</template>
</div>
<div class="modal-footer">
<div v-if="(tagsToAssign.length > 0 || !this.loadingTags)
&& this.params.permissions.manage_tags" class="mr-auto">
<GeneralDropdown ref="assignDropdown">
<template v-slot:field>
<button class="btn btn-primary">{{ i18n.t('general.assign') }}</button>
</template>
<template v-slot:flyout>
<div class="max-h-80 overflow-y-auto">
<div v-for="tag in tagsToAssign" :key="tag.id"
@click="assignTag(tag)"
class="px-3 py-2.5 hover:bg-sn-super-light-grey cursor-pointer flex items-center gap-2">
<div class="h-6 w-6 rounded relative" :style="{ backgroundColor: tag.attributes.color }">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white"></div>
</div>
<div class="min-w-[10rem] truncate">{{ tag.attributes.name }}</div>
<i @click.stop="deleteTag(tag)" class="sn-icon sn-icon-delete cursor-pointer ml-auto"></i>
</div>
</div>
</template>
</GeneralDropdown>
</div>
<button class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
</div>
</div>
@ -103,7 +132,7 @@
></ConfirmationModal>
</template>
<script>
import { vOnClickOutside } from '@vueuse/components';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
import InlineEdit from '../../shared/inline_edit.vue';
@ -112,7 +141,7 @@ import ConfirmationModal from '../../shared/confirmation_modal.vue';
export default {
name: 'TagsModal',
emits: ['close'],
emits: ['close', 'tagsLoaded'],
props: {
params: {
required: true
@ -122,8 +151,14 @@ export default {
},
projectTagsUrl: {
required: true
},
projectName: {
required: true
}
},
directives: {
'click-outside': vOnClickOutside
},
components: {
InlineEdit,
GeneralDropdown,
@ -134,34 +169,48 @@ export default {
return {
allTags: [],
assignedTags: [],
newTag: {
name: null,
color: null
},
loadingTags: false,
tagToUpdate: null,
query: ''
};
},
computed: {
tagsList() {
return this.assignedTags.map((tag) => {
const tagObject = this.allTags.find((t) => parseInt(t.id, 10) === tag.attributes.tag_id);
const modifiedTag = tag;
modifiedTag.attributes = {
...tag.attributes,
name: tagObject.attributes.name,
color: tagObject.attributes.color
};
return modifiedTag;
});
validNewTag() {
return this.newTag.name && this.newTag.color;
},
tagsToAssign() {
return this.allTags.filter((tag) => (
!this.assignedTags.find((t) => t.attributes.tag_id === parseInt(tag.id, 10))
&& tag.attributes.name.toLowerCase().includes(this.query.toLowerCase())
));
canManage() {
return this.params.permissions.manage_tags;
}
},
created() {
this.loadAlltags();
},
methods: {
startEditMode(tag) {
if (!this.canManage) return;
this.finishEditMode();
tag.editing = true;
this.tagToUpdate = tag;
this.$nextTick(() => {
this.$refs.modal.querySelector('input').focus();
});
},
finishEditMode(e, tag = null) {
if (e && e.target.closest('.sn-dropdown')) return;
const tagToFinish = tag || this.allTags.find((t) => t.editing);
if (tagToFinish) {
tagToFinish.editing = false;
this.updateTag(this.tagToUpdate);
}
},
loadAlltags() {
this.loadingTags = true;
axios.get(this.projectTagsUrl).then((response) => {
@ -173,16 +222,32 @@ export default {
loadAssignedTags() {
axios.get(this.params.urls.assigned_tags).then((response) => {
this.assignedTags = response.data.data;
this.loadingTags = true;
this.allTags.forEach((tag) => {
const assignedTag = this.assignedTags.find((at) => at.attributes.tag_id === parseInt(tag.id, 10));
if (assignedTag) {
tag.assigned = true;
tag.attributes.urls.unassign = assignedTag.attributes.urls.update;
} else {
tag.assigned = false;
}
});
this.$emit('tagsLoaded', this.allTags);
this.loadingTags = false;
});
},
toggleTag(tag) {
if (tag.assigned) {
this.unassignTag(tag);
} else {
this.assignTag(tag);
}
},
unassignTag(tag) {
axios.delete(tag.attributes.urls.update).then(() => {
axios.delete(tag.attributes.urls.unassign).then(() => {
this.loadAssignedTags();
});
},
assignTag(tag) {
this.$refs.assignDropdown.closeMenu();
axios.post(this.params.urls.assign_tags, {
my_module_tag: {
tag_id: tag.id
@ -191,15 +256,11 @@ export default {
this.loadAssignedTags();
});
},
updateTagName(value, tag) {
const tagToUpdate = this.allTags.find((t) => parseInt(t.id, 10) === tag.attributes.tag_id);
tagToUpdate.attributes.name = value;
this.updateTag(tagToUpdate);
updateTagName(value) {
this.tagToUpdate.attributes.name = value;
},
updateTagColor(color, tag) {
const tagToUpdate = this.allTags.find((t) => parseInt(t.id, 10) === tag.attributes.tag_id);
tagToUpdate.attributes.color = color;
this.updateTag(tagToUpdate);
updateTagColor(color) {
this.tagToUpdate.attributes.color = color;
},
updateTag(tag) {
axios.put(tag.attributes.urls.update, {
@ -211,19 +272,15 @@ export default {
});
},
createTag() {
const randmonColor = this.tagsColors[Math.floor(Math.random() * this.tagsColors.length)];
axios.post(this.projectTagsUrl, {
tag: {
name: this.i18n.t('tags.create.new_name'),
color: randmonColor
},
tag: this.newTag,
my_module_id: this.params.id
}).then(() => {
this.newTag = { name: null, color: null };
this.loadAlltags();
});
},
async deleteTag(tag) {
this.$refs.assignDropdown.closeMenu();
const ok = await this.$refs.deleteTagModal.show();
if (ok) {
axios.delete(tag.attributes.urls.update, {

View file

@ -1,16 +1,45 @@
<template>
<span v-if="params.data.tags > 0" @click.stop="openModal" class="text-sn-blue cursor-pointer">
{{ params.data.tags }}
</span>
<span v-else-if="params.data.permissions.manage_tags" @click.stop="openModal" class="text-sn-blue cursor-pointer">
{{ i18n.t('experiments.table.add_tag') }}
</span>
<span v-else>{{ i18n.t('experiments.table.not_set') }}</span>
<div class="flex items-center gap-1.5 h-9 mt-0.5">
<template v-if="params.data.tags.length > 0 || params.data.permissions.manage_tags">
<div v-if="params.data.tags.length > 0"
class="h-6 px-1.5 flex items-center rounded text-white max-w-[150px]"
:style="{'background': params.data.tags[0].color}">
<div class="truncate">{{ params.data.tags[0].name }}</div>
</div>
<GeneralDropdown v-if="params.data.tags.length > 1" >
<template v-slot:field>
<div class="h-6 min-w-[24px] text-sn-dark-grey flex items-center justify-center rounded-full text-[.625rem]
bg-sn-light-grey border !border-sn-sleepy-grey cursor-pointer">
<span>+{{ params.data.tags.length - 1 }}</span>
</div>
</template>
<template v-slot:flyout>
<div>
{{ i18n.t('experiments.table.used_tags') }}
</div>
<hr class="my-2" />
<div class="max-h-[200px] overflow-y-auto flex flex-wrap gap-1.5 max-w-[240px]">
<div v-for="tag in params.data.tags" :key="tag.id"
class="h-6 px-1.5 flex items-center rounded text-white max-w-[150px]"
:style="{'background': tag.color}">
<div class="truncate">{{ tag.name }}</div>
</div>
</div>
</template>
</GeneralDropdown>
<div v-if="params.data.permissions.manage_tags" @click.stop="openModal"
class="cursor-pointer text-sn-sleep-grey border !border-dashed h-6 w-6 flex items-center
justify-center !border-sn-sleep-grey rounded-full ">
<i class="sn-icon sn-icon-new-task"></i>
</div>
</template>
<span v-else>{{ i18n.t('experiments.table.not_set') }}</span>
</div>
</template>
<script>
import GeneralDropdown from '../../shared/general_dropdown.vue';
export default {
name: 'TagsRenderer',
props: {
@ -18,6 +47,9 @@ export default {
required: true
}
},
components: {
GeneralDropdown
},
methods: {
openModal() {
this.params.dtComponent.$emit('editTags', null, [this.params.data]);

View file

@ -88,7 +88,7 @@ export default {
NewProjectModal,
NewFolderModal,
MoveModal,
AccessModal,
AccessModal
},
props: {
dataSource: { type: String, required: true },
@ -102,7 +102,7 @@ export default {
userRolesUrl: { type: String },
currentFolderId: { type: String },
foldersTreeUrl: { type: String },
moveToUrl: { type: String },
moveToUrl: { type: String }
},
data() {
return {
@ -114,41 +114,51 @@ export default {
objectsToMove: null,
reloadingTable: false,
folderDeleteDescription: '',
exportDescription: '',
columnDefs: [
{
field: 'name',
flex: 1,
headerName: this.i18n.t('projects.index.card.name'),
sortable: true,
cellRenderer: this.nameRenderer,
},
{
field: 'code',
headerName: this.i18n.t('projects.index.card.id'),
sortable: true,
},
{
field: 'created_at',
headerName: this.i18n.t('projects.index.card.start_date'),
sortable: true,
},
{
field: 'users',
headerName: this.i18n.t('projects.index.card.users'),
cellRenderer: 'UsersRenderer',
sortable: false,
minWidth: 210,
notSelectable: true
},
],
exportDescription: ''
};
},
computed: {
columnDefs() {
const columns = [{
field: 'name',
flex: 1,
headerName: this.i18n.t('projects.index.card.name'),
sortable: true,
cellRenderer: this.nameRenderer
},
{
field: 'code',
headerName: this.i18n.t('projects.index.card.id'),
sortable: true
},
{
field: 'created_at',
headerName: this.i18n.t('projects.index.card.start_date'),
sortable: true
},
{
field: 'users',
headerName: this.i18n.t('projects.index.card.users'),
cellRenderer: 'UsersRenderer',
sortable: false,
minWidth: 210,
notSelectable: true
}];
if (this.currentViewMode === 'archived') {
columns.push({
field: 'archived_on',
headerName: this.i18n.t('projects.index.card.archived_date'),
sortable: true
});
}
return columns;
},
viewRenders() {
return [
{ type: 'table' },
{ type: 'cards' },
{ type: 'cards' }
];
},
toolbarActions() {
@ -160,7 +170,7 @@ export default {
label: this.i18n.t('projects.index.new'),
type: 'emit',
path: this.createUrl,
buttonStyle: 'btn btn-primary',
buttonStyle: 'btn btn-primary'
});
}
if (this.createFolderUrl) {
@ -170,32 +180,32 @@ export default {
label: this.i18n.t('projects.index.new_folder'),
type: 'emit',
path: this.createFolderUrl,
buttonStyle: 'btn btn-light',
buttonStyle: 'btn btn-light'
});
}
return {
left,
right: [],
right: []
};
},
filters() {
const filters = [
{
key: 'query',
type: 'Text',
type: 'Text'
},
{
key: 'created_at',
type: 'DateRange',
label: this.i18n.t('filters_modal.created_on.label'),
},
label: this.i18n.t('filters_modal.created_on.label')
}
];
if (this.currentViewMode === 'archived') {
filters.push({
key: 'archived_at',
type: 'DateRange',
label: this.i18n.t('filters_modal.archived_on.label'),
label: this.i18n.t('filters_modal.archived_on.label')
});
}
@ -206,13 +216,13 @@ export default {
optionRenderer: this.usersFilterRenderer,
labelRenderer: this.usersFilterRenderer,
label: this.i18n.t('projects.index.filters_modal.members.label'),
placeholder: this.i18n.t('projects.index.filters_modal.members.placeholder'),
placeholder: this.i18n.t('projects.index.filters_modal.members.placeholder')
});
filters.push({
key: 'folder_search',
type: 'Checkbox',
label: this.i18n.t('projects.index.filters_modal.folders.label'),
label: this.i18n.t('projects.index.filters_modal.folders.label')
});
return filters;
@ -241,7 +251,7 @@ export default {
access(event, rows) {
this.accessModalParams = {
object: rows[0],
roles_path: this.userRolesUrl,
roles_path: this.userRolesUrl
};
},
async archive(event, rows) {
@ -302,7 +312,7 @@ export default {
if (ok) {
axios.post(event.path, {
project_ids: rows.filter((row) => !row.folder).map((row) => row.id),
project_folder_ids: rows.filter((row) => row.folder).map((row) => row.id),
project_folder_ids: rows.filter((row) => row.folder).map((row) => row.id)
}).then((response) => {
this.reloadingTable = true;
HelperModule.flashAlertMsg(response.data.message, 'success');

View file

@ -130,7 +130,8 @@ export default {
},
{
field: 'nr_of_rows',
headerName: this.i18n.t('libraries.index.table.number_of_items')
headerName: this.i18n.t('libraries.index.table.number_of_items'),
sortable: true
},
{
field: 'shared_label',

View file

@ -8,7 +8,7 @@
{{ i18n.t('action_toolbar.no_actions') }}
</div>
<div v-for="action in actions" :key="action.name" class="sn-action-toolbar__action shrink-0">
<a :class="`rounded flex gap-2 items-center py-1.5 px-2.5
<a :class="`rounded flex gap-2 items-center py-1.5 px-2.5 hover:text-sn-white hover:bg-sn-blue
bg-sn-white color-sn-blue hover:no-underline focus:no-underline ${action.button_class}`"
:href="(['link', 'remote-modal']).includes(action.type) ? action.path : '#'"
:id="action.button_id"

View file

@ -48,8 +48,10 @@
@first-data-rendered="onFirstDataRendered"
@sortChanged="setOrder"
@columnResized="saveTableState"
@columnMoved="saveTableState"
@columnMoved="onColumnMoved"
@bodyScroll="handleScroll"
@columnPinned="handlePin"
@columnVisible="handleVisibility"
@rowSelected="setSelectedRows"
@cellClicked="clickCell"
:CheckboxSelectionCallback="withCheckboxes"
@ -64,7 +66,7 @@
:params="actionsParams"
@toolbar:action="emitAction" />
</div>
<div v-if="scrollMode == 'pages'" class="flex items-center py-4">
<div v-if="scrollMode == 'pages'" class="flex items-center py-4" :class="{'opacity-0': initializing }">
<div class="mr-auto">
<Pagination
:totalPage="totalPage"
@ -166,6 +168,7 @@ export default {
order: null,
totalPage: 0,
selectedRows: [],
keepSelection: false,
searchValue: '',
initializing: true,
activeFilters: {},
@ -174,7 +177,9 @@ export default {
dataLoading: true,
lastPage: false,
tableState: null,
userSettingsUrl: null
userSettingsUrl: null,
fetchedTableState: null,
gridReady: false
};
},
components: {
@ -211,6 +216,7 @@ export default {
cellRendererParams: {
dtComponent: this
},
pinned: (column.field === 'name' ? 'left' : null),
comparator: () => false
}));
@ -272,10 +278,18 @@ export default {
},
perPage() {
this.saveTableState();
},
fetchedTableState(newValue) {
if (newValue !== null && this.gridReady) {
this.applyTableState(newValue);
}
}
},
mounted() {
created() {
this.userSettingsUrl = document.querySelector('meta[name="user-settings-url"]').getAttribute('content');
this.fetchTableState();
},
mounted() {
this.loadData();
window.addEventListener('resize', this.resize);
},
@ -300,39 +314,55 @@ export default {
this.loadData();
}
},
fetchAndApplyTableState() {
axios
.get(this.userSettingsUrl, {
params: {
key: this.stateKey
}
})
handlePin(event) {
if (event.pinned === 'right') {
this.columnApi.setColumnPinned(event.column.colId, null);
}
this.saveTableState();
},
handleVisibility(event) {
if (!event.visible && event.source !== 'api') {
this.columnApi.setColumnVisible(event.column.colId, true);
}
this.saveTableState();
},
fetchTableState() {
axios.get(this.userSettingsUrl, { params: { key: this.stateKey } })
.then((response) => {
if (response.data.data) {
const { currentViewRender, columnsState, perPage, order } = response.data.data;
this.tableState = response.data.data;
this.currentViewRender = currentViewRender;
this.columnsState = columnsState;
this.perPage = perPage;
this.order = order;
if (this.order) {
this.tableState.columnsState.forEach((column) => {
const updatedColumn = column;
updatedColumn.sort = this.order.column === column.colId ? this.order.dir : null;
return updatedColumn;
});
this.fetchedTableState = response.data.data;
if (this.gridReady && this.fetchedTableState) {
this.applyTableState(this.fetchedTableState);
}
this.columnApi.applyColumnState({
state: this.tableState.columnsState,
applyOrder: true
});
}
setTimeout(() => {
} else {
this.initializing = false;
}, 200);
this.saveTableState();
}
});
},
applyTableState(state) {
const { currentViewRender, columnsState, perPage, order } = state;
this.tableState = state;
this.currentViewRender = currentViewRender;
this.columnsState = columnsState;
this.perPage = perPage;
this.order = order;
if (this.order) {
this.tableState.columnsState.forEach((column) => {
const updatedColumn = column;
updatedColumn.sort = this.order.column === column.colId ? this.order.dir : null;
return updatedColumn;
});
}
this.columnApi.applyColumnState({
state: this.tableState.columnsState,
applyOrder: true
});
setTimeout(() => {
this.initializing = false;
}, 200);
},
getRowClass() {
if (this.currentViewMode === 'archived') {
return '!bg-sn-super-light-grey';
@ -358,11 +388,11 @@ export default {
this.reloadTable();
}
},
reloadTable() {
reloadTable(clearSelection = true) {
if (this.dataLoading) return;
this.dataLoading = true;
this.selectedRows = [];
if (clearSelection) this.selectedRows = [];
this.page = 1;
this.loadData(true);
},
@ -380,46 +410,47 @@ export default {
})
.then((response) => {
if (reload) {
if (this.gridApi) {
this.gridApi.setRowData([]);
}
if (this.gridApi) this.gridApi.setRowData([]);
this.rowData = [];
}
if (this.scrollMode === 'pages') {
this.selectedRows = [];
if (this.gridApi) {
this.gridApi.setRowData(this.formatData(response.data.data));
}
if (this.gridApi) this.gridApi.setRowData(this.formatData(response.data.data));
this.rowData = this.formatData(response.data.data);
} else {
const newRows = this.rowData.slice();
this.formatData(response.data.data).forEach((row) => {
newRows.push(row);
});
this.rowData = newRows;
if (this.gridApi) {
const viewport = document.querySelector('.ag-body-viewport');
const { scrollTop } = viewport;
this.gridApi.setRowData(this.rowData);
this.$nextTick(() => {
viewport.scrollTop = scrollTop;
});
}
this.lastPage = !response.data.meta.next_page;
this.handleInfiniteScroll(response);
}
this.totalPage = response.data.meta.total_pages;
this.$emit('tableReloaded');
this.dataLoading = false;
this.restoreSelection();
this.handleScroll();
});
},
handleInfiniteScroll(response) {
const newRows = this.rowData.slice();
this.formatData(response.data.data).forEach((row) => {
newRows.push(row);
});
this.rowData = newRows;
if (this.gridApi) {
const viewport = document.querySelector('.ag-body-viewport');
const { scrollTop } = viewport;
this.gridApi.setRowData(this.rowData);
this.$nextTick(() => {
viewport.scrollTop = scrollTop;
});
}
this.lastPage = !response.data.meta.next_page;
},
onGridReady(params) {
this.gridApi = params.api;
this.columnApi = params.columnApi;
this.fetchAndApplyTableState();
this.gridReady = true;
if (this.fetchedTableState) {
this.applyTableState(this.fetchedTableState);
}
},
onFirstDataRendered() {
this.resize();
@ -428,20 +459,23 @@ export default {
this.perPage = value;
this.page = 1;
this.lastPage = false;
this.reloadTable();
this.reloadTable(false);
},
setPage(page) {
this.page = page;
this.loadData();
this.loadData(false);
},
setOrder() {
const orderState = this.getOrder(this.columnApi.getColumnState());
const [order] = orderState;
this.order = order;
this.saveTableState();
this.reloadTable();
this.reloadTable(false);
},
saveTableState() {
if (this.initializing) {
return;
}
const columnsState = this.columnApi ? this.columnApi.getColumnState() : this.tableState?.columnsState || [];
const tableState = {
columnsState,
@ -456,8 +490,23 @@ export default {
axios.put(this.userSettingsUrl, { settings: [settings] });
this.tableState = tableState;
},
setSelectedRows() {
this.selectedRows = this.gridApi.getSelectedRows();
restoreSelection() {
if (this.gridApi) {
this.gridApi.forEachNode((node) => {
if (this.selectedRows.find((row) => row.id === node.data.id)) {
node.setSelected(true);
}
});
}
},
setSelectedRows(e) {
if (!this.rowData.find((row) => row.id === e.data.id)) return;
if (e.node.isSelected()) {
this.selectedRows.push(e.data);
} else {
this.selectedRows = this.selectedRows.filter((row) => row.id !== e.data.id);
}
},
emitAction(action) {
this.$emit(action.name, action, this.selectedRows);
@ -484,19 +533,15 @@ export default {
},
hideColumn(column) {
this.columnApi.setColumnVisible(column.field, false);
this.saveTableState();
},
showColumn(column) {
this.columnApi.setColumnVisible(column.field, true);
this.saveTableState();
},
pinColumn(column) {
this.columnApi.setColumnPinned(column.field, 'left');
this.saveTableState();
},
unPinColumn(column) {
this.columnApi.setColumnPinned(column.field, null);
this.saveTableState();
},
reorderColumns(columns) {
this.columnApi.moveColumns(columns, 1);
@ -522,7 +567,12 @@ export default {
dir
};
this.saveTableState();
this.reloadTable();
this.reloadTable(false);
},
onColumnMoved(event) {
if (event.finished) {
this.saveTableState();
}
}
}
};

View file

@ -1,6 +1,7 @@
export default {
template: `
<div class="w-full grid items-center gap-2 grid-cols-[auto_1.5rem] cursor-pointer"
<div class="w-full grid items-center gap-2 grid-cols-[auto_1.5rem]"
:class="{'cursor-pointer': params.enableSorting}"
:data-e2e="'e2e-CO-TableHeader-' + params.column.colId "
@click="onSortRequested((activeSort == 'asc' ? 'desc' : 'asc'), $event)">
<div v-if="params.html" class="customHeaderLabel truncate" v-html="params.html"></div>

View file

@ -75,7 +75,7 @@
:title="i18n.t('experiments.table.column_display_modal.title')"
class="btn btn-light icon-btn btn-black"
>
<i class="sn-icon sn-icon-manage-table"></i>
<i class="sn-icon sn-icon-manage-columns"></i>
</button>
<GeneralDropdown v-if="currentViewRender === 'cards'" ref="dropdown" position="right">
<template v-slot:field>
@ -252,6 +252,7 @@ export default {
if (ok) {
this.$emit('resetColumnsToDefault');
}
this.showColumnsModal = true;
}
}
};

View file

@ -19,7 +19,7 @@
<i class="sn-icon sn-icon-close"></i>
</button>
</div>
<div class="max-h-[400px] p-3.5 pt-0">
<div class="max-h-[40vh] px-3.5 overflow-y-auto">
<div v-for="filter in filters" :key="filter.key">
<Component
:is="`${filter.type}Filter`"
@ -28,7 +28,7 @@
@update="updateFilter" />
</div>
</div>
<div class="p-3.5 pt-0.5 flex items-center justify-end gap-4">
<div class="p-3.5 flex items-center justify-end gap-4">
<div @click.prevent="clearFilters" class="btn btn-secondary">
{{ i18n.t('filters_modal.clear_btn') }}
</div>

View file

@ -3,19 +3,20 @@
<div ref="field" class="cursor-pointer" @click.stop="isOpen = (!isOpen || fieldOnlyOpen)">
<slot name="field"></slot>
</div>
<teleport to="body">
<div ref="flyout"
class="sn-dropdown fixed z-[3000] bg-sn-white inline-block
rounded p-2.5 sn-shadow-menu-sm"
:class="{
'right-0': position === 'right',
'left-0': position === 'left',
}"
v-if="isOpen"
>
<slot name="flyout"></slot>
</div>
</teleport>
<template v-if="isOpen">
<teleport to="body">
<div ref="flyout"
class="sn-dropdown fixed z-[3000] bg-sn-white inline-block
rounded p-2.5 sn-shadow-menu-sm"
:class="{
'right-0': position === 'right',
'left-0': position === 'left',
}"
>
<slot name="flyout"></slot>
</div>
</teleport>
</template>
</div>
</template>

View file

@ -6,57 +6,58 @@
<i v-if="caret && isOpen" class="sn-icon sn-icon-up"></i>
<i v-else-if="caret" class="sn-icon sn-icon-down"></i>
</button>
<teleport to="body">
<div ref="flyout"
v-if="isOpen"
class="fixed z-[3000] sn-menu-dropdown bg-sn-white inline-block rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px]"
:class="{
'right-0': position === 'right',
'left-0': position === 'left',
}"
>
<span v-for="(item, i) in listItems" :key="i" class="contents">
<div v-if="item.dividerBefore" class="border-0 border-t border-solid border-sn-light-grey"></div>
<a :href="item.url" v-if="!item.submenu"
:target="item.url_target || '_self'"
:class="{ 'bg-sn-super-light-blue': item.active }"
:data-toggle="item.modalTarget && 'modal'"
:data-target="item.modalTarget"
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey leading-5"
@click="handleClick($event, item)"
>
{{ item.text }}
</a>
<div v-else class="-mx-2.5 px-2.5 group relative">
<span
<template v-if="isOpen">
<teleport to="body">
<div ref="flyout"
class="fixed z-[3000] sn-menu-dropdown bg-sn-white inline-block rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px]"
:class="{
'right-0': position === 'right',
'left-0': position === 'left',
}"
>
<span v-for="(item, i) in listItems" :key="i" class="contents">
<div v-if="item.dividerBefore" class="border-0 border-t border-solid border-sn-light-grey"></div>
<a :href="item.url" v-if="!item.submenu"
:target="item.url_target || '_self'"
:class="{ 'bg-sn-super-light-blue': item.active }"
class="flex group items-center rounded relative text-sn-blue whitespace-nowrap px-3 py-2.5 hover:no-underline cursor-pointer
group-hover:bg-sn-super-light-blue hover:!bg-sn-super-light-grey"
:data-toggle="item.modalTarget && 'modal'"
:data-target="item.modalTarget"
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey leading-5"
@click="handleClick($event, item)"
>
{{ item.text }}
<i class="sn-icon sn-icon-right ml-auto"></i>
</span>
<div
class="absolute bg-sn-white top-0 rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px] tw-hidden group-hover:block"
:class="{
'left-0 ml-[100%]': item.position === 'right',
'right-0 mr-[100%]': item.position === 'left'
}"
>
<a v-for="(sub_item, si) in item.submenu" :key="si"
:href="sub_item.url"
:traget="sub_item.url_target || '_self'"
</a>
<div v-else class="-mx-2.5 px-2.5 group relative">
<span
:class="{ 'bg-sn-super-light-blue': item.active }"
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey leading-5"
@click="handleClick($event, sub_item)"
class="flex group items-center rounded relative text-sn-blue whitespace-nowrap px-3 py-2.5 hover:no-underline cursor-pointer
group-hover:bg-sn-super-light-blue hover:!bg-sn-super-light-grey"
>
{{ sub_item.text }}
</a>
{{ item.text }}
<i class="sn-icon sn-icon-right ml-auto"></i>
</span>
<div
class="absolute bg-sn-white top-0 rounded p-2.5 sn-shadow-menu-sm flex flex-col gap-[1px] tw-hidden group-hover:block"
:class="{
'left-0 ml-[100%]': item.position === 'right',
'right-0 mr-[100%]': item.position === 'left'
}"
>
<a v-for="(sub_item, si) in item.submenu" :key="si"
:href="sub_item.url"
:traget="sub_item.url_target || '_self'"
:class="{ 'bg-sn-super-light-blue': item.active }"
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey leading-5"
@click="handleClick($event, sub_item)"
>
{{ sub_item.text }}
</a>
</div>
</div>
</div>
</span>
</div>
</teleport>
</span>
</div>
</teleport>
</template>
</div>
</template>

View file

@ -46,44 +46,46 @@
<i v-else class="sn-icon ml-auto"
:class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen, 'text-sn-grey': disabled}"></i>
</div>
<teleport to="body">
<div v-if="isOpen" ref="flyout"
class="sn-select-dropdown bg-white inline-block sn-shadow-menu-sm rounded w-full
fixed z-[3000]">
<div v-if="multiple && withCheckboxes" class="p-2.5 pb-0">
<div @click="selectAll" :class="sizeClass"
class="border-x-0 border-transparent border-solid border-b-sn-light-grey
py-1.5 px-3 cursor-pointer flex items-center gap-2 shrink-0">
<div class="sn-checkbox-icon"
:class="selectAllState"
></div>
{{ i18n.t('general.select_all') }}
</div>
</div>
<perfect-scrollbar class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
<template v-for="option in filteredOptions" :key="option[0]">
<div
@click.stop="setValue(option[0])"
class="py-1.5 px-3 rounded cursor-pointer flex items-center gap-2 shrink-0"
:class="[sizeClass, {'!bg-sn-super-light-blue': valueSelected(option[0])}]"
>
<div v-if="withCheckboxes"
class="sn-checkbox-icon"
:class="{
'checked': valueSelected(option[0]),
'unchecked': !valueSelected(option[0]),
}"
<template v-if="isOpen">
<teleport to="body">
<div ref="flyout"
class="sn-select-dropdown bg-white inline-block sn-shadow-menu-sm rounded w-full
fixed z-[3000]">
<div v-if="multiple && withCheckboxes" class="p-2.5 pb-0">
<div @click="selectAll" :class="sizeClass"
class="border-x-0 border-transparent border-solid border-b-sn-light-grey
py-1.5 px-3 cursor-pointer flex items-center gap-2 shrink-0">
<div class="sn-checkbox-icon"
:class="selectAllState"
></div>
<div v-if="optionRenderer" v-html="optionRenderer(option)"></div>
<div v-else >{{ option[1] }}</div>
{{ i18n.t('general.select_all') }}
</div>
</template>
<div v-if="filteredOptions.length === 0" class="text-sn-grey text-center py-2.5">
{{ noOptionsPlaceholder || this.i18n.t('general.select_dropdown.no_options_placeholder') }}
</div>
</perfect-scrollbar>
</div>
</teleport>
<perfect-scrollbar class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
<template v-for="option in filteredOptions" :key="option[0]">
<div
@click.stop="setValue(option[0])"
class="py-1.5 px-3 rounded cursor-pointer flex items-center gap-2 shrink-0"
:class="[sizeClass, {'!bg-sn-super-light-blue': valueSelected(option[0])}]"
>
<div v-if="withCheckboxes"
class="sn-checkbox-icon"
:class="{
'checked': valueSelected(option[0]),
'unchecked': !valueSelected(option[0]),
}"
></div>
<div v-if="optionRenderer" v-html="optionRenderer(option)"></div>
<div v-else >{{ option[1] }}</div>
</div>
</template>
<div v-if="filteredOptions.length === 0" class="text-sn-grey text-center py-2.5">
{{ noOptionsPlaceholder || this.i18n.t('general.select_dropdown.no_options_placeholder') }}
</div>
</perfect-scrollbar>
</div>
</teleport>
</template>
</div>
</template>
@ -228,6 +230,9 @@ export default {
this.fetchOptions();
},
watch: {
value(newValue) {
this.newValue = newValue;
},
isOpen() {
if (this.isOpen) {
this.$nextTick(() => {

View file

@ -132,7 +132,13 @@ module Lists
end
def tags
object.tags.length
object.tags.map do |tag|
{
id: tag.id,
name: tag.name,
color: tag.color
}
end
end
def comments

View file

@ -10,7 +10,7 @@ module Lists
:urls, :shared_read, :shared_write, :shareable_write
def nr_of_rows
object.repository_rows.count
object[:row_count]
end
def shared

View file

@ -18,7 +18,7 @@ class UserAssignmentSerializer < ActiveModel::Serializer
{
id: object.user.id,
name: object.user.name,
avatar_url: avatar_path(object, :icon_small)
avatar_url: avatar_path(object.user, :icon_small)
}
end

View file

@ -13,12 +13,14 @@ module Lists
'LEFT OUTER JOIN users AS archivers ' \
'ON repositories.archived_by_id = archivers.id'
)
.includes(:repository_rows)
.joins(: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')
.select('repositories.*')
.select('MAX(teams.name) AS team_name')
.select('COUNT(repository_rows.*) AS row_count')
.select('MAX(creators.full_name) AS created_by_user')
.select('MAX(archivers.full_name) AS archived_by_user')
.group('repositories.id')
view_mode = @params[:view_mode] || 'active'
@ -43,11 +45,12 @@ module Lists
def sortable_columns
@sortable_columns ||= {
name: 'repositories.name',
team: 'teams.name',
created_by: 'creators.full_name',
team: 'team_name',
created_by: 'created_by_user',
created_at: 'repositories.created_at',
archived_on: 'repositories.archived_on',
archived_by: 'archivers.full_name'
archived_by: 'archived_by_user',
nr_of_rows: 'row_count'
}
end
end

View file

@ -114,5 +114,31 @@
<div role="tabpanel" class="tab-pane" id="<%= my_module.id %>_comments" data-contents="comments"></div>
</div>
</div>
<div id="tagsModalContainer-<%= my_module.id %>" class="vue-tags-modal">
<div ref="tagsModal" class="tags-modal-component" id="tagsModalComponent-<%= my_module.id %>"></div>
<teleport to="body">
<tags-modal v-if="tagsModalOpen"
:params="<%=
{
id: my_module.id,
permissions: {
manage_tags: can_manage_my_module_tags?(my_module)
},
urls: {
assigned_tags: assigned_tags_my_module_my_module_tags_path(my_module),
assign_tags: my_module_my_module_tags_path(my_module)
}
}.to_json
%>"
:tags-colors="<%= Constants::TAG_COLORS.to_json %>"
project-name="<%= my_module.experiment.project.name %>"
project-tags-url="<%= project_tags_path(my_module.experiment.project) %>"
@close="close"
@tags-loaded="syncTags"
/>
</teleport>
</div>
<%= javascript_include_tag 'vue_legacy_tags_modal' %>
</div>

View file

@ -15,6 +15,7 @@
users-filter-url="<%= users_filter_projects_path %>"v
user-roles-url="<%= user_roles_projects_path %>"
:tags-colors="<%= Constants::TAG_COLORS.to_json %>"
project-name="<%= @experiment.project.name %>"
:statuses-list="<%= MyModuleStatus.all.order(:id).map{ |i| [i.id, i.name] }.to_json %>"
project-tags-url="<%= project_tags_path(@experiment.project) %>"
canvas-url="<%= view_mode == 'active' ? canvas_experiment_path(@experiment) : module_archive_experiment_path(@experiment) %>"

View file

@ -155,6 +155,32 @@
<!-- Consume Stock Modal -->
<%= render partial: 'my_modules/repositories/consume_stock_modal'%>
<!-- Tags modal -->
<div id="tagsModalContainer" class="vue-tags-modal">
<div ref="tagsModal" id="tagsModalComponent"></div>
<teleport to="body">
<tags-modal v-if="tagsModalOpen"
:params="<%=
{
id: @my_module.id,
permissions: {
manage_tags: can_manage_my_module_tags?(@my_module)
},
urls: {
assigned_tags: assigned_tags_my_module_my_module_tags_path(@my_module),
assign_tags: my_module_my_module_tags_path(@my_module)
}
}.to_json
%>"
:tags-colors="<%= Constants::TAG_COLORS.to_json %>"
project-name="<%= @experiment.project.name %>"
project-tags-url="<%= project_tags_path(@experiment.project) %>"
@close="close"
@tags-loaded="syncTags"
/>
</teleport>
</div>
<%= javascript_include_tag 'inputmask' %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag "handsontable.full" %>
@ -169,4 +195,4 @@
<%= javascript_include_tag "protocols/new_protocol" %>
<%= javascript_include_tag 'vue_protocol' %>
<%= javascript_include_tag 'vue_legacy_tags_modal' %>

View file

@ -570,7 +570,7 @@ en:
visibility: "Visible to"
users: "Access"
name: "Project name"
archived_date: "Archived"
archived_date: "Archived on"
end_of_list_placeholder: 'Youve reached the end of the list'
folder:
description: "%{projects_count} projects | %{folders_count} folders"
@ -1462,7 +1462,7 @@ en:
id: "ID"
start_date: "Started on"
modified_date: "Modified date"
archived_date: "Archived date"
archived_date: "Archived on"
completed_task: "Completed"
completed_value: "%{completed}/%{all} tasks"
description: "Description"
@ -1537,7 +1537,7 @@ en:
id_html: 'ID'
task_name_html: 'Task name'
due_date_html: 'Due'
archived_html: 'Archived'
archived_html: 'Archived on'
age_html: 'Age'
results_html: 'Results'
status_html: 'Status'
@ -1601,6 +1601,7 @@ en:
archived_tasks: 'Go to Archived tasks'
active_tasks: 'Go to Active tasks'
add_tag: '+ Add tag'
used_tags: 'Used tags'
search: 'Manage assignees'
not_set: 'not set'
archive_group:
@ -1630,17 +1631,20 @@ en:
reload_on_submit: "Save action is running. Reloading this page may cause unexpected behavior."
modal_manage_tags:
head_title: "Manage tags"
head_title_read: "Tags"
subtitle: "Showing tags of task %{module}"
no_tags: "No tags!"
edit_tag: "Edit tag."
remove_tag: "Remove tag from task %{module}."
delete_tag: "Permanently delete tag from all tasks."
delete_tag_confirmation: "Deleting a tag will remove it from all tagged tasks. Are you sure you wish to continue?"
explanatory_text: "Add a set of tags to mark the tasks inside this project. Changing the tag applies to all tagged tasks. Deleting a tag removes it from all tagged tasks."
delete_tag_confirmation: "Deleting a tag will remove it from all tagged tasks. You wont be able to get it back.<br><br><b>Are you sure you wish to continue?</b>"
explanatory_text: "Add a set of tags to mark the tasks inside this project."
project_tags: "%{project} project tags"
save_tag: "Save tag."
cancel_tag: "Cancel changes to the tag."
create: "Add"
create_new: "Create new tag"
create_new: "Create a new tag"
new_tag_name: "Tag name"
edit:
id: "ID:"
new_module: "New task"
@ -4096,7 +4100,7 @@ en:
updated_on:
label: "Modified date"
archived_on:
label: "Archived date"
label: "Archived on"
recent_searches_label: "Recent searches"
show_btn:
one: "Show results"

View file

@ -376,7 +376,9 @@ Rails.application.routes.draw do
end
get 'project_folders/:project_folder_id', to: 'projects#index', as: :project_folder_projects
resources :experiments, only: %i(index update) do
get 'projects/:project_id', to: 'experiments#index'
get 'projects/:project_id/experiments', to: 'experiments#index', as: :experiments
resources :experiments, only: %i(update) do
collection do
get 'inventory_assigning_experiment_filter'
get 'clone_modal', action: :clone_modal
@ -421,7 +423,9 @@ Rails.application.routes.draw do
# Show action is a popup (JSON) for individual module in full-zoom canvas,
# as well as 'module info' page for single module (HTML)
resources :my_modules, path: '/modules', only: [:show, :update, :index] do
get 'experiments/:experiment_id/table', to: 'my_modules#index'
get 'experiments/:experiment_id/modules', to: 'my_modules#index', as: :my_modules
resources :my_modules, path: '/modules', only: [:show, :update] do
post 'save_table_state', on: :collection, defaults: { format: 'json' }
collection do

View file

@ -58,7 +58,8 @@ const entryList = {
vue_reports_table: './app/javascript/packs/vue/reports_table.js',
vue_open_locally_menu: './app/javascript/packs/vue/open_locally_menu.js',
vue_scinote_edit_download: './app/javascript/packs/vue/scinote_edit_download.js',
vue_design_system_modals: './app/javascript/packs/vue/design_system/modals.js'
vue_design_system_modals: './app/javascript/packs/vue/design_system/modals.js',
vue_legacy_tags_modal: './app/javascript/packs/vue/legacy/tags_modal.js'
};
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949

Binary file not shown.

View file

@ -171,7 +171,9 @@
<glyph unicode="&#xe98e;" glyph-name="history-search" data-tags="history seaerch" d="M512 789.333c-128.853 0-240.64-71.68-298.667-176.64v91.307h-42.667v-170.667h170.667v42.667h-98.987c48.213 100.693 150.613 170.667 269.653 170.667 164.693 0 298.667-133.973 298.667-298.667s-133.973-298.667-298.667-298.667c-134.827 0-249.173 90.027-286.293 213.333h-43.947c37.973-147.2 171.093-256 330.24-256 188.587 0 341.333 152.747 341.333 341.333s-152.747 341.333-341.333 341.333zM676.267 292.267l-29.867-29.867-177.067 177.067v221.867h42.667v-204.8l164.267-164.267z" />
<glyph unicode="&#xe98f;" glyph-name="item" data-tags="item" d="M682.667 661.333c0-94.257-76.412-170.667-170.667-170.667-94.257 0-170.667 76.41-170.667 170.667s76.41 170.667 170.667 170.667c94.255 0 170.667-76.41 170.667-170.667zM725.333 106.667c-70.694 0-128 57.306-128 128s57.306 128 128 128c70.694 0 128-57.306 128-128s-57.306-128-128-128zM725.333 64c94.255 0 170.667 76.412 170.667 170.667s-76.412 170.667-170.667 170.667c-94.255 0-170.667-76.412-170.667-170.667s76.412-170.667 170.667-170.667zM298.667 106.667c-70.692 0-128 57.306-128 128s57.308 128 128 128c70.692 0 128-57.306 128-128s-57.308-128-128-128zM298.667 64c94.257 0 170.667 76.412 170.667 170.667s-76.41 170.667-170.667 170.667c-94.257 0-170.667-76.412-170.667-170.667s76.41-170.667 170.667-170.667z" />
<glyph unicode="&#xe990;" glyph-name="move-arrows" data-tags="move-arrows" d="M330.988 564.239l-81.66-81.675h134.673v-42.667h-134.69l81.677-81.694-30.165-30.165-133.184 133.184 133.184 133.182 30.165-30.165zM693.013 564.239l30.165 30.165 133.184-133.182-133.184-133.184-30.165 30.165 81.677 81.694h-134.69v42.667h134.673l-81.66 81.675zM512.597 520.956l60.352-60.331-60.352-60.352-60.331 60.352 60.331 60.331zM408.982 280.209l81.684-81.668v134.69h42.667v-134.694l81.685 81.673 30.165-30.165-133.184-133.184-133.183 133.184 30.165 30.165zM408.982 642.234l-30.165 30.165 133.183 133.184 133.184-133.184-30.165-30.165-81.685 81.669v-134.674h-42.667v134.673l-81.684-81.668z" />
<glyph unicode="&#xe991;" glyph-name="teams-small" data-tags="teams-small" d="M881.493 424.747c-8.107 10.658-18.347 19.614-31.147 26.010-28.16 13.641-55.467 23.023-83.2 29.841-28.16 6.822-84.907 10.231-84.907 10.231l0.427-42.633c0 0 50.347-2.982 75.093-8.951s49.067-14.498 72.96-26.010c7.253-3.836 12.8-8.525 16.64-13.641 3.84-5.542 5.973-11.511 5.973-18.334v-18.33h-128v-42.637h170.667v60.966c0 15.774-5.12 30.699-14.507 43.486zM691.2 268.284c10.667-5.542 19.2-12.791 25.173-20.894 5.973-8.098 8.96-17.476 8.96-27.285v-27.712h-426.667v27.712c0 9.809 2.987 19.187 8.96 27.285 5.973 8.102 14.507 14.925 25.173 20.894 104.030 50.095 252.523 50.987 358.4 0zM314.453 306.227c-17.92-9.378-31.147-20.463-40.96-34.108-11.52-15.347-17.493-33.681-17.493-52.442v-70.345h512v70.345c0 18.761-5.973 37.094-17.067 52.442-9.813 13.218-23.040 24.303-39.253 33.254-119.223 57.647-276.817 58.982-397.227 0.853zM512 618.7c46.933 0 85.333-38.371 85.333-85.268 0-46.895-38.4-85.27-85.333-85.27s-85.333 38.374-85.333 85.27c0 46.897 38.4 85.268 85.333 85.268zM512 661.333c-70.4 0-128-57.556-128-127.901s57.6-127.902 128-127.902c70.4 0 128 57.557 128 127.902s-57.6 127.901-128 127.901zM170.667 362.931v18.33c0 6.822 2.133 12.791 5.973 18.334s9.387 10.231 16.64 13.641c23.893 11.511 48.213 20.041 72.96 26.010s49.92 8.951 75.093 8.951v42.633c-28.16 0-56.747-3.409-84.907-10.231-27.733-6.396-55.040-16.201-81.493-28.988-14.507-7.676-24.747-16.205-32.427-26.863-9.387-12.787-14.507-27.712-14.507-43.486v-60.966h170.667v42.637h-128zM341.333 661.366c12.373 0 21.333-3.837 29.867-12.364l30.72 29.844c-16.64 16.627-36.693 25.154-60.587 25.154s-43.947-8.527-60.587-25.154c-16.64-16.627-24.747-36.665-24.747-60.113s8.107-43.913 24.747-60.54c16.64-16.627 36.693-24.728 60.587-24.728v42.634c-12.373 0-21.76 3.837-30.293 12.364s-12.373 17.906-12.373 30.27c0 12.364 3.84 21.317 12.8 30.27 8.533 8.527 17.92 12.364 29.867 12.364zM682.667 661.366c12.373 0 21.333-3.837 29.867-12.364 8.533-8.953 12.8-18.333 12.8-30.27s-3.84-21.743-12.373-30.27c-8.533-8.527-17.92-12.364-30.293-12.364v-42.634c23.893 0 43.947 8.101 60.587 24.728s24.747 36.665 24.747 60.54c0 23.875-8.107 43.060-24.747 60.113-16.64 16.627-36.693 25.154-60.587 25.154s-43.947-8.527-60.587-25.154l30.72-29.844c8.533 8.527 17.92 12.364 29.867 12.364z" />
<glyph unicode="&#xe992;" glyph-name="refresh" data-tags="refresh" d="M810.667 283.307c-58.027-104.96-169.813-176.64-298.667-176.64-188.587 0-341.333 152.747-341.333 341.333h42.667c0-164.693 133.973-298.667 298.667-298.667 119.040 0 221.44 69.973 269.653 170.667h-98.987v42.667h170.667v-170.667h-42.667v91.307zM213.333 612.693c58.027 104.96 169.813 176.64 298.667 176.64 188.587 0 341.333-152.747 341.333-341.333h-42.667c0 164.693-133.973 298.667-298.667 298.667-119.040 0-221.44-69.973-269.653-170.667h98.987v-42.667h-170.667v170.667h42.667v-91.307z" />
<glyph unicode="&#xe993;" glyph-name="pin" data-tags="pin" d="M411.605 357.077l-99.584 99.541c0 0 0.683 21.333 14.293 39.893 29.141 39.68 86.443 41.813 129.195 33.536l67.371 66.304c-14.933 49.963 71.509 107.648 71.509 107.648 65.152-65.195 130.347-130.389 195.541-195.584-4.736-6.357-9.515-12.715-14.293-19.072-22.912-30.421-54.144-56.107-91.136-49.877l-70.827-68.651c0.981-7.339 0.896-0.64 2.517-24.576 3.072-45.44-13.653-92.245-56.149-113.92l-15.915-7.765-102.357 102.357-155.563-155.605-30.208 30.208 155.605 155.563zM587.392 648.747c-14.677-12.117-30.208-30.933-21.845-44.203l10.453-15.403-106.368-106.411c-42.411 10.795-92.715 13.568-109.995-13.099l192.427-192.683c2.005 1.408 3.925 2.901 5.803 4.48 26.155 23.040 16.299 64.853 10.624 104.533l102.187 102.187c0 0 31.36-20.011 62.635 16.555l-144.853 144.853c-0.384-0.256-0.725-0.555-1.067-0.811z" />
<glyph unicode="&#xe994;" glyph-name="pinned" data-tags="pinned" d="M483.341 312.717l-140.803-0.030c0 0-14.602 15.569-18.102 38.319-7.452 48.661 31.558 90.688 67.641 115.068l0.754 94.52c-45.888 24.77-25.554 126.683-25.554 126.683 92.17-0.030 184.369-0.030 276.567-0.030 1.148-7.844 2.261-15.719 3.379-23.593 5.312-37.712 1.387-77.959-29.175-99.712l-1.536-98.624c5.884-4.497 1.084 0.179 19.157-15.599 34.304-29.961 55.573-74.88 40.849-120.256l-5.76-16.747h-144.755l0.030-220.028h-42.722l0.030 220.028zM401.399 643.26c-1.81-18.947 0.513-43.234 15.809-46.703l18.281-3.5 0.030-150.458c-37.62-22.353-75.151-55.966-68.514-87.040l272.311-0.179c0.422 2.415 0.725 4.826 0.939 7.27 2.202 34.786-34.334 57.382-66.406 81.429v144.513c0 0 36.326 8.025 32.585 55.995h-204.854c-0.090-0.453-0.12-0.905-0.181-1.327z" />
<glyph unicode="&#xe995;" glyph-name="manage-columns" data-tags="manage-columns" d="M810.667 789.333c23.467 0 42.667-19.2 42.667-42.667v-597.333c0-23.467-19.2-42.667-42.667-42.667h-597.333c-23.467 0-42.667 19.2-42.667 42.667v597.333c0 23.467 19.2 42.667 42.667 42.667h597.333zM810.667 746.667h-597.333v-128h597.333v128zM810.667 576h-170.667v-426.667h170.667v426.667zM597.333 576h-170.667v-426.667h170.667v426.667zM384 576h-170.667v-426.667h170.667v426.667z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Binary file not shown.

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'SN-icon-font';
src: url('fonts/SN-icon-font.eot?h4j5vh');
src: url('fonts/SN-icon-font.eot?h4j5vh#iefix') format('embedded-opentype'),
url('fonts/SN-icon-font.woff2?h4j5vh') format('woff2'),
url('fonts/SN-icon-font.ttf?h4j5vh') format('truetype'),
url('fonts/SN-icon-font.woff?h4j5vh') format('woff'),
url('fonts/SN-icon-font.svg?h4j5vh#SN-icon-font') format('svg');
src: url('fonts/SN-icon-font.eot?5l6t28');
src: url('fonts/SN-icon-font.eot?5l6t28#iefix') format('embedded-opentype'),
url('fonts/SN-icon-font.woff2?5l6t28') format('woff2'),
url('fonts/SN-icon-font.ttf?5l6t28') format('truetype'),
url('fonts/SN-icon-font.woff?5l6t28') format('woff'),
url('fonts/SN-icon-font.svg?5l6t28#SN-icon-font') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -470,3 +470,9 @@
.sn-icon-pinned:before {
content: "\e994";
}
.sn-icon-teams-small:before {
content: "\e991";
}
.sn-icon-manage-columns:before {
content: "\e995";
}