mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-11 06:16:32 +08:00
Add new tags managment modal [SCI-12241]
This commit is contained in:
parent
832a43b69d
commit
8a8b11da6a
9 changed files with 251 additions and 66 deletions
|
@ -4,9 +4,10 @@ module TaggableActions
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :load_taggable_item, only: %i(tag_resource untag_resource)
|
before_action :load_taggable_item, only: %i(tag_resource untag_resource tag_resource_with_new_tag)
|
||||||
before_action :load_tag, only: %i(tag_resource untag_resource)
|
before_action :load_tag, only: %i(tag_resource untag_resource)
|
||||||
before_action :check_tag_manage_permissions, only: %i(tag_resource untag_resource)
|
before_action :check_tag_manage_permissions, only: %i(tag_resource untag_resource tag_resource_with_new_tag)
|
||||||
|
before_action :check_tag_create_permissions, only: %i(tag_resource_with_new_tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_resource
|
def tag_resource
|
||||||
|
@ -18,6 +19,20 @@ module TaggableActions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tag_resource_with_new_tag
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
params[:tag][:color] = Constants::TAG_COLORS.sample.to_s if params[:tag][:color].blank?
|
||||||
|
|
||||||
|
@tag = current_team.tags.create!(tag_params.merge(created_by: current_user, last_modified_by: current_user))
|
||||||
|
tagging = @taggable_item.taggings.new(tag: @tag, created_by: current_user)
|
||||||
|
tagging.save!
|
||||||
|
render json: { tag: [@tag.id, @tag.name, @tag.color] }
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render json: { status: :error, error: e.message }, status: :unprocessable_entity
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def untag_resource
|
def untag_resource
|
||||||
tagging = @taggable_item.taggings.find_by(tag_id: @tag.id)
|
tagging = @taggable_item.taggings.find_by(tag_id: @tag.id)
|
||||||
if tagging&.destroy
|
if tagging&.destroy
|
||||||
|
@ -29,6 +44,10 @@ module TaggableActions
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def tag_params
|
||||||
|
params.require(:tag).permit(:name, :color)
|
||||||
|
end
|
||||||
|
|
||||||
def load_taggable_item
|
def load_taggable_item
|
||||||
@taggable_item = controller_name.singularize.camelize.constantize.find(params[:id])
|
@taggable_item = controller_name.singularize.camelize.constantize.find(params[:id])
|
||||||
end
|
end
|
||||||
|
@ -41,4 +60,8 @@ module TaggableActions
|
||||||
def check_tag_manage_permissions
|
def check_tag_manage_permissions
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_tag_create_permissions
|
||||||
|
true # TODO: implement
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,7 +72,11 @@ module Users
|
||||||
tags_to_merge = @team.tags.where(id: params[:merge_ids]).where.not(id: @tag.id)
|
tags_to_merge = @team.tags.where(id: params[:merge_ids]).where.not(id: @tag.id)
|
||||||
|
|
||||||
taggings_to_update = Tagging.where(tag_id: tags_to_merge.select(:id))
|
taggings_to_update = Tagging.where(tag_id: tags_to_merge.select(:id))
|
||||||
.where.not(id: Tagging.where(tag_id: @tag.id).select(:id))
|
.where.not(
|
||||||
|
Tagging.where(tag_id: @tag.id).map{|i|
|
||||||
|
Arel.sql("(taggable_type = '#{i.taggable_type}' AND taggable_id = #{i.taggable_id})")
|
||||||
|
}.join(" OR ")
|
||||||
|
)
|
||||||
|
|
||||||
taggings_to_update.update!(tag_id: @tag.id)
|
taggings_to_update.update!(tag_id: @tag.id)
|
||||||
tags_to_merge.each(&:destroy!)
|
tags_to_merge.each(&:destroy!)
|
||||||
|
|
|
@ -695,6 +695,7 @@ export default {
|
||||||
},
|
},
|
||||||
applyFilters(filters) {
|
applyFilters(filters) {
|
||||||
this.activeFilters = filters;
|
this.activeFilters = filters;
|
||||||
|
console.log(this.activeFilters);
|
||||||
this.reloadTable();
|
this.reloadTable();
|
||||||
},
|
},
|
||||||
switchViewRender(view) {
|
switchViewRender(view) {
|
||||||
|
|
91
app/javascript/vue/shared/mixins/tags_mixin.js
Normal file
91
app/javascript/vue/shared/mixins/tags_mixin.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import axios from '../../../packs/custom_axios.js';
|
||||||
|
import {
|
||||||
|
tags_path
|
||||||
|
} from '../../../routes.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
subject: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canManage() {
|
||||||
|
return this.subject.attributes.permissions.manage_tags;
|
||||||
|
},
|
||||||
|
canAssign() {
|
||||||
|
return this.subject.attributes.permissions.assign_tags;
|
||||||
|
},
|
||||||
|
tagResourceUrl() {
|
||||||
|
return this.subject.attributes.urls.tag_resource;
|
||||||
|
},
|
||||||
|
untagResourceUrl() {
|
||||||
|
return this.subject.attributes.urls.untag_resource;
|
||||||
|
},
|
||||||
|
createTagUrl() {
|
||||||
|
return this.subject.attributes.urls.tag_resource_with_new_tag;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadAllTags();
|
||||||
|
this.tags = this.subject.attributes.tags || [];
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: [],
|
||||||
|
allTags: [],
|
||||||
|
linkingTag: false,
|
||||||
|
searchQuery: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadAllTags() {
|
||||||
|
axios.get(tags_path()).then((response) => {
|
||||||
|
this.allTags = response.data.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
linkTag(tag) {
|
||||||
|
if (this.tags.map(t => t[0]).includes(tag[0])) {
|
||||||
|
this.unlinkTag(tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.linkingTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.linkingTag = true;
|
||||||
|
|
||||||
|
axios.post(this.tagResourceUrl, {
|
||||||
|
tag_id: tag[0],
|
||||||
|
}).then((response) => {
|
||||||
|
this.tags.push(response.data.tag);
|
||||||
|
this.linkingTag = false;
|
||||||
|
this.searchQuery = '';
|
||||||
|
}).catch(() => {
|
||||||
|
this.linkingTag = false;
|
||||||
|
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
unlinkTag(tag) {
|
||||||
|
if (this.linkingTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.linkingTag = true;
|
||||||
|
|
||||||
|
axios.post(this.untagResourceUrl, {
|
||||||
|
tag_id: tag[0],
|
||||||
|
}).then((response) => {
|
||||||
|
this.tags = this.tags.filter(t => t[0] !== tag[0]);
|
||||||
|
this.linkingTag = false;
|
||||||
|
this.searchQuery = '';
|
||||||
|
}).catch(() => {
|
||||||
|
this.linkingTag = false;
|
||||||
|
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,26 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<GeneralDropdown @open="opened = true" @close="opened = false">
|
<GeneralDropdown :canOpen="canAssign" @open="openSearch" @close="closeSearch">
|
||||||
<template v-slot:field>
|
<template v-slot:field>
|
||||||
<div class="w-full flex flex-wrap rounded gap-2 p-1 border border-solid cursor-pointer"
|
<div class="w-full flex flex-wrap rounded gap-2 p-1 border border-solid"
|
||||||
:class="{
|
:class="{
|
||||||
'!border-sn-science-blue': opened,
|
'!border-sn-science-blue cursor-pointer': opened,
|
||||||
'hover:!border-sn-light-grey !border-transparent': !opened,
|
'hover:!border-sn-light-grey !border-transparent cursor-pointer': !opened && canAssign,
|
||||||
|
'!border-transparent': !canAssign
|
||||||
}">
|
}">
|
||||||
<template v-if="tags.length > 0">
|
<template v-if="tags.length > 0">
|
||||||
<div v-for="tag in tags" :key="tag[0]" class="sci-tag text-white" :style="{ backgroundColor: tag[2] }" >
|
<div v-for="tag in tags" :key="tag[0]" class="sci-tag text-white" :style="{ backgroundColor: tag[2] }" >
|
||||||
{{ tag[1] }}
|
{{ tag[1] }}
|
||||||
<i @click.stop="unlinkTag(tag)" class="sn-icon sn-icon-close"></i>
|
<i v-if="canAssign" @click.stop="unlinkTag(tag)" class="sn-icon sn-icon-close"></i>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="sci-tag bg-sn-super-light-grey">
|
<div v-else-if="!opened" class="sci-tag bg-sn-super-light-grey">
|
||||||
{{ i18n.t('tags.tags_input.add_tag') }}
|
{{ i18n.t('tags.tags_input.add_tag') }}
|
||||||
</div>
|
</div>
|
||||||
|
<input v-if="opened" @click.stop="handleInputClick" @keydown.enter.stop="handleInputEnter" type="text" ref="tagSearch" v-model="searchQuery" class="flex-grow outline-none border-none bg-transparent p-1" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:flyout>
|
<template v-slot:flyout>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div v-for="tag in allTags" :key="tag[0]" @click="linkTag(tag)" class="py-2 cursor-pointer hover:bg-sn-super-light-grey px-3 flex items-center gap-2" >
|
<div v-if="validTagName && canManage" @click="createTag" class="py-2 cursor-pointer hover:bg-sn-super-light-grey px-3 flex items-center gap-2">
|
||||||
|
<i class="sn-icon sn-icon-new-task"></i>
|
||||||
|
{{ i18n.t('tags.tags_input.create_tag') }}
|
||||||
|
</div>
|
||||||
|
<div v-for="tag in filteredTags" :key="tag[0]" @click="linkTag(tag)" class="py-2 cursor-pointer hover:bg-sn-super-light-grey px-3 flex items-center gap-2" >
|
||||||
<div class="sci-checkbox-container pointer-events-none" >
|
<div class="sci-checkbox-container pointer-events-none" >
|
||||||
<input type="checkbox" :checked="tags.map(t => t[0]).includes(tag[0])" class="sci-checkbox" />
|
<input type="checkbox" :checked="tags.map(t => t[0]).includes(tag[0])" class="sci-checkbox" />
|
||||||
<span class="sci-checkbox-label"></span>
|
<span class="sci-checkbox-label"></span>
|
||||||
|
@ -41,90 +47,74 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from '../../packs/custom_axios.js';
|
|
||||||
import GeneralDropdown from './general_dropdown.vue';
|
import GeneralDropdown from './general_dropdown.vue';
|
||||||
import {
|
import TagsMixin from './mixins/tags_mixin.js';
|
||||||
tags_path
|
|
||||||
} from '../../routes.js';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TagsInput',
|
name: 'TagsInput',
|
||||||
props: {
|
|
||||||
subject: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
GeneralDropdown,
|
GeneralDropdown,
|
||||||
},
|
},
|
||||||
|
mixins: [TagsMixin],
|
||||||
computed: {
|
computed: {
|
||||||
tagResourceUrl() {
|
filteredTags() {
|
||||||
return this.subject.attributes.urls.tag_resource;
|
if (this.searchQuery.trim() === '') {
|
||||||
|
return this.allTags;
|
||||||
|
}
|
||||||
|
const lowerQuery = this.searchQuery.toLowerCase();
|
||||||
|
return filter(this.allTags, tag => tag[1].toLowerCase().includes(lowerQuery));
|
||||||
},
|
},
|
||||||
untagResourceUrl() {
|
validTagName() {
|
||||||
return this.subject.attributes.urls.untag_resource;
|
return this.searchQuery.trim().length > GLOBAL_CONSTANTS.NAME_MIN_LENGTH &&
|
||||||
},
|
!this.allTags.map(t => t[1].toLowerCase()).includes(this.searchQuery.toLowerCase());
|
||||||
},
|
}
|
||||||
created() {
|
|
||||||
this.loadAllTags();
|
|
||||||
this.tags = this.subject.attributes.tags || [];
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tags: [],
|
|
||||||
allTags: [],
|
|
||||||
linkingTag: false,
|
|
||||||
opened: false,
|
opened: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadAllTags() {
|
createTag() {
|
||||||
axios.get(tags_path()).then((response) => {
|
if (this.linkingTag || this.searchQuery.trim() === '') {
|
||||||
this.allTags = response.data.data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
linkTag(tag) {
|
|
||||||
if (this.tags.map(t => t[0]).includes(tag[0])) {
|
|
||||||
this.unlinkTag(tag);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.linkingTag) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.linkingTag = true;
|
this.linkingTag = true;
|
||||||
|
|
||||||
axios.post(this.tagResourceUrl, {
|
axios.post(this.createTagUrl, {
|
||||||
tag_id: tag[0],
|
tag: {
|
||||||
|
name: this.searchQuery.trim()
|
||||||
|
}
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
this.tags.push(response.data.tag);
|
this.tags.push(response.data.tag);
|
||||||
|
this.loadAllTags();
|
||||||
this.linkingTag = false;
|
this.linkingTag = false;
|
||||||
|
this.searchQuery = '';
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.linkingTag = false;
|
this.linkingTag = false;
|
||||||
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
openSearch() {
|
||||||
unlinkTag(tag) {
|
this.opened = true;
|
||||||
if (this.linkingTag) {
|
this.$nextTick(() => {
|
||||||
return;
|
this.$refs.tagSearch.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
closeSearch() {
|
||||||
|
this.opened = false;
|
||||||
|
this.searchQuery = '';
|
||||||
|
},
|
||||||
|
handleInputClick() {
|
||||||
|
},
|
||||||
|
handleInputEnter() {
|
||||||
|
if (this.validTagName && this.canManage) {
|
||||||
|
this.createTag();
|
||||||
|
} else if (this.filteredTags.length > 0 && this.canAssign) {
|
||||||
|
this.linkTag(this.filteredTags[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.linkingTag = true;
|
|
||||||
|
|
||||||
axios.post(this.untagResourceUrl, {
|
|
||||||
tag_id: tag[0],
|
|
||||||
}).then((response) => {
|
|
||||||
this.tags = this.tags.filter(t => t[0] !== tag[0]);
|
|
||||||
this.linkingTag = false;
|
|
||||||
}).catch(() => {
|
|
||||||
this.linkingTag = false;
|
|
||||||
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
71
app/javascript/vue/shared/tags_modal.vue
Normal file
71
app/javascript/vue/shared/tags_modal.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<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 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>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.close') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import modalMixin from '../../shared/modal_mixin';
|
||||||
|
import GeneralDropdown from '../../shared/general_dropdown.vue';
|
||||||
|
import TagsMixin from './mixins/tags_mixin.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TagsModal',
|
||||||
|
emits: ['close', 'tagsLoaded', 'tagDeleted'],
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
tagsColors: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
projectTagsUrl: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
projectName: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
'click-outside': vOnClickOutside
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
InlineEdit,
|
||||||
|
GeneralDropdown,
|
||||||
|
ConfirmationModal
|
||||||
|
},
|
||||||
|
mixins: [modalMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: [],
|
||||||
|
allTags: [],
|
||||||
|
linkingTag: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadAllTags();
|
||||||
|
this.tags = this.subject.attributes.tags || [];
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -46,7 +46,9 @@ class MyModuleSerializer < ActiveModel::Serializer
|
||||||
manage_description: can_update_my_module_description?(object),
|
manage_description: can_update_my_module_description?(object),
|
||||||
manage_due_date: can_update_my_module_due_date?(object),
|
manage_due_date: can_update_my_module_due_date?(object),
|
||||||
manage_start_date: can_update_my_module_start_date?(object),
|
manage_start_date: can_update_my_module_start_date?(object),
|
||||||
manage_designated_users: can_manage_my_module_designated_users?(object)
|
manage_designated_users: can_manage_my_module_designated_users?(object),
|
||||||
|
assign_tags: can_manage_my_module_tags?(object),
|
||||||
|
manage_tags: true # TODO: implement
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,7 +57,8 @@ class MyModuleSerializer < ActiveModel::Serializer
|
||||||
show_access: access_permissions_my_module_path(object),
|
show_access: access_permissions_my_module_path(object),
|
||||||
show_user_group_assignments_access: show_user_group_assignments_access_permissions_my_module_path(object),
|
show_user_group_assignments_access: show_user_group_assignments_access_permissions_my_module_path(object),
|
||||||
tag_resource: tag_resource_my_module_path(object),
|
tag_resource: tag_resource_my_module_path(object),
|
||||||
untag_resource: untag_resource_my_module_path(object)
|
untag_resource: untag_resource_my_module_path(object),
|
||||||
|
tag_resource_with_new_tag: tag_resource_with_new_tag_my_module_path(object)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4243,6 +4243,7 @@ en:
|
||||||
tags_input:
|
tags_input:
|
||||||
add_tag: "Add tag"
|
add_tag: "Add tag"
|
||||||
edit_tags: "Edit tags"
|
edit_tags: "Edit tags"
|
||||||
|
create_tag: "Create tag"
|
||||||
user_groups:
|
user_groups:
|
||||||
promo:
|
promo:
|
||||||
head_title: 'User groups'
|
head_title: 'User groups'
|
||||||
|
|
|
@ -529,6 +529,7 @@ Rails.application.routes.draw do
|
||||||
get :assigned_users
|
get :assigned_users
|
||||||
post :tag_resource
|
post :tag_resource
|
||||||
post :untag_resource
|
post :untag_resource
|
||||||
|
post :tag_resource_with_new_tag
|
||||||
end
|
end
|
||||||
resources :user_my_modules, path: '/users', only: %i(index create destroy) do
|
resources :user_my_modules, path: '/users', only: %i(index create destroy) do
|
||||||
collection do
|
collection do
|
||||||
|
|
Loading…
Add table
Reference in a new issue