Merge pull request #7067 from aignatov-bio/ai-sci-10105-update-tags-task-modal

Update tags modal [SCI-10105]
This commit is contained in:
aignatov-bio 2024-02-06 15:39:42 +01:00 committed by GitHub
commit ae56294670
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 171 additions and 101 deletions

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

@ -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="flex grow"
:class="{'cursor-pointer': canManage}"
>
<div class="h-6 px-1.5 flex items-center truncate text-sn-white rounded" :style="{ backgroundColor: tag.attributes.color }">
{{ tag.attributes.name }}
</div>
</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 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';
@ -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,31 @@ 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.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 +255,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 +271,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

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

@ -1630,17 +1630,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"