mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-07 04:16:31 +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
|
||||
|
||||
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 :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
|
||||
|
||||
def tag_resource
|
||||
|
@ -18,6 +19,20 @@ module TaggableActions
|
|||
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
|
||||
tagging = @taggable_item.taggings.find_by(tag_id: @tag.id)
|
||||
if tagging&.destroy
|
||||
|
@ -29,6 +44,10 @@ module TaggableActions
|
|||
|
||||
private
|
||||
|
||||
def tag_params
|
||||
params.require(:tag).permit(:name, :color)
|
||||
end
|
||||
|
||||
def load_taggable_item
|
||||
@taggable_item = controller_name.singularize.camelize.constantize.find(params[:id])
|
||||
end
|
||||
|
@ -41,4 +60,8 @@ module TaggableActions
|
|||
def check_tag_manage_permissions
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def check_tag_create_permissions
|
||||
true # TODO: implement
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,7 +72,11 @@ module Users
|
|||
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))
|
||||
.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)
|
||||
tags_to_merge.each(&:destroy!)
|
||||
|
|
|
@ -695,6 +695,7 @@ export default {
|
|||
},
|
||||
applyFilters(filters) {
|
||||
this.activeFilters = filters;
|
||||
console.log(this.activeFilters);
|
||||
this.reloadTable();
|
||||
},
|
||||
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>
|
||||
<div>
|
||||
<GeneralDropdown @open="opened = true" @close="opened = false">
|
||||
<GeneralDropdown :canOpen="canAssign" @open="openSearch" @close="closeSearch">
|
||||
<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="{
|
||||
'!border-sn-science-blue': opened,
|
||||
'hover:!border-sn-light-grey !border-transparent': !opened,
|
||||
'!border-sn-science-blue cursor-pointer': opened,
|
||||
'hover:!border-sn-light-grey !border-transparent cursor-pointer': !opened && canAssign,
|
||||
'!border-transparent': !canAssign
|
||||
}">
|
||||
<template v-if="tags.length > 0">
|
||||
<div v-for="tag in tags" :key="tag[0]" class="sci-tag text-white" :style="{ backgroundColor: tag[2] }" >
|
||||
{{ 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>
|
||||
</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') }}
|
||||
</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>
|
||||
</template>
|
||||
<template v-slot:flyout>
|
||||
<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" >
|
||||
<input type="checkbox" :checked="tags.map(t => t[0]).includes(tag[0])" class="sci-checkbox" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
|
@ -41,90 +47,74 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import GeneralDropdown from './general_dropdown.vue';
|
||||
import {
|
||||
tags_path
|
||||
} from '../../routes.js';
|
||||
import TagsMixin from './mixins/tags_mixin.js';
|
||||
|
||||
export default {
|
||||
name: 'TagsInput',
|
||||
props: {
|
||||
subject: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GeneralDropdown,
|
||||
},
|
||||
mixins: [TagsMixin],
|
||||
computed: {
|
||||
tagResourceUrl() {
|
||||
return this.subject.attributes.urls.tag_resource;
|
||||
filteredTags() {
|
||||
if (this.searchQuery.trim() === '') {
|
||||
return this.allTags;
|
||||
}
|
||||
const lowerQuery = this.searchQuery.toLowerCase();
|
||||
return filter(this.allTags, tag => tag[1].toLowerCase().includes(lowerQuery));
|
||||
},
|
||||
untagResourceUrl() {
|
||||
return this.subject.attributes.urls.untag_resource;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadAllTags();
|
||||
this.tags = this.subject.attributes.tags || [];
|
||||
validTagName() {
|
||||
return this.searchQuery.trim().length > GLOBAL_CONSTANTS.NAME_MIN_LENGTH &&
|
||||
!this.allTags.map(t => t[1].toLowerCase()).includes(this.searchQuery.toLowerCase());
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags: [],
|
||||
allTags: [],
|
||||
linkingTag: false,
|
||||
opened: false,
|
||||
};
|
||||
},
|
||||
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) {
|
||||
createTag() {
|
||||
if (this.linkingTag || this.searchQuery.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.linkingTag = true;
|
||||
|
||||
axios.post(this.tagResourceUrl, {
|
||||
tag_id: tag[0],
|
||||
axios.post(this.createTagUrl, {
|
||||
tag: {
|
||||
name: this.searchQuery.trim()
|
||||
}
|
||||
}).then((response) => {
|
||||
this.tags.push(response.data.tag);
|
||||
this.loadAllTags();
|
||||
this.linkingTag = false;
|
||||
this.searchQuery = '';
|
||||
}).catch(() => {
|
||||
this.linkingTag = false;
|
||||
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
unlinkTag(tag) {
|
||||
if (this.linkingTag) {
|
||||
return;
|
||||
openSearch() {
|
||||
this.opened = true;
|
||||
this.$nextTick(() => {
|
||||
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>
|
||||
|
|
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_due_date: can_update_my_module_due_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
|
||||
|
||||
|
@ -55,7 +57,8 @@ class MyModuleSerializer < ActiveModel::Serializer
|
|||
show_access: 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),
|
||||
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
|
||||
|
||||
|
|
|
@ -4243,6 +4243,7 @@ en:
|
|||
tags_input:
|
||||
add_tag: "Add tag"
|
||||
edit_tags: "Edit tags"
|
||||
create_tag: "Create tag"
|
||||
user_groups:
|
||||
promo:
|
||||
head_title: 'User groups'
|
||||
|
|
|
@ -529,6 +529,7 @@ Rails.application.routes.draw do
|
|||
get :assigned_users
|
||||
post :tag_resource
|
||||
post :untag_resource
|
||||
post :tag_resource_with_new_tag
|
||||
end
|
||||
resources :user_my_modules, path: '/users', only: %i(index create destroy) do
|
||||
collection do
|
||||
|
|
Loading…
Add table
Reference in a new issue