mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-07 20:40:26 +08:00
Add tags input to task page [SCI-12286]
This commit is contained in:
parent
fe17f2256c
commit
1baec4f47d
10 changed files with 216 additions and 113 deletions
|
@ -98,87 +98,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function initTagsSelector() {
|
||||
var myModuleTagsSelector = '#module-tags-selector';
|
||||
|
||||
dropdownSelector.init($(myModuleTagsSelector), {
|
||||
closeOnSelect: true,
|
||||
tagClass: 'my-module-white-tags sci-tag',
|
||||
labelHTML: true,
|
||||
tagStyle: (data) => {
|
||||
return `background: ${data.params.color}`;
|
||||
},
|
||||
customDropdownIcon: () => {
|
||||
return '';
|
||||
},
|
||||
optionLabel: (data) => {
|
||||
if (data.value > 0) {
|
||||
return `<span class="sci-tag max-w-80 truncate text-sn-white "
|
||||
style="background:${data.params.color}">${data.label}</span>`;
|
||||
}
|
||||
return `<span class="my-module-tags-color new"><i class="sn-icon sn-icon-new-task"></i></span>
|
||||
${data.label + ' '}
|
||||
<span class="my-module-tags-create-new"> ${I18n.t('my_modules.details.create_new_tag')}</span>`;
|
||||
},
|
||||
onOpen: function() {
|
||||
$('.select-container .edit-button-container').removeClass('hidden');
|
||||
},
|
||||
onClose: function() {
|
||||
$('.select-container .edit-button-container').addClass('hidden');
|
||||
},
|
||||
onSelect: function() {
|
||||
var selectElement = $(myModuleTagsSelector);
|
||||
var lastTag = selectElement.next().find('.ds-tags').last();
|
||||
var lastTagId = lastTag.find('.tag-label').data('ds-tag-id');
|
||||
var newTag;
|
||||
|
||||
if (lastTagId > 0) {
|
||||
newTag = { my_module_tag: { tag_id: lastTagId } };
|
||||
$.post(selectElement.data('update-module-tags-url'), newTag)
|
||||
.fail(function(response) {
|
||||
dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true);
|
||||
if (response.status === 403) {
|
||||
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
|
||||
}
|
||||
});
|
||||
} else if (lastTag.length > 0) {
|
||||
newTag = {
|
||||
tag: {
|
||||
name: lastTag.find('.tag-label').text(),
|
||||
project_id: selectElement.data('project-id'),
|
||||
color: null
|
||||
},
|
||||
my_module_id: selectElement.data('module-id'),
|
||||
simple_creation: true
|
||||
};
|
||||
|
||||
$.post(selectElement.data('tags-create-url'), newTag, function(result) {
|
||||
dropdownSelector.removeValue(myModuleTagsSelector, 0, '', true);
|
||||
dropdownSelector.addValue(myModuleTagsSelector, {
|
||||
value: result.tag.id,
|
||||
label: result.tag.name,
|
||||
params: {
|
||||
color: result.tag.color
|
||||
}
|
||||
}, true);
|
||||
}).fail(function() {
|
||||
dropdownSelector.removeValue(myModuleTagsSelector, lastTagId, '', true);
|
||||
});
|
||||
}
|
||||
},
|
||||
onUnSelect: (id) => {
|
||||
$.post(`${$(myModuleTagsSelector).data('update-module-tags-url')}/${id}/destroy_by_tag_id`)
|
||||
.done(() => {
|
||||
dropdownSelector.closeDropdown(myModuleTagsSelector);
|
||||
})
|
||||
.fail(function(r) {
|
||||
if (r.status === 403) {
|
||||
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
}).getContainer(myModuleTagsSelector).addClass('my-module-tags-container');
|
||||
}
|
||||
|
||||
|
||||
function initAssignedUsersSelector() {
|
||||
|
@ -258,7 +178,6 @@
|
|||
}
|
||||
|
||||
initTaskCollapseState();
|
||||
initTagsSelector();
|
||||
initStartDatePicker();
|
||||
initDueDatePicker();
|
||||
initAssignedUsersSelector();
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
@layer components {
|
||||
.sci-tag {
|
||||
@apply text-xs !rounded-full !px-2 !py-1 inline-flex items-center gap-1;
|
||||
@apply text-xs !rounded-full !pl-2 !pr-1.5 h-6 !py-0.5 inline-flex items-center gap-1;
|
||||
}
|
||||
|
||||
.sci-tag .sn-icon {
|
||||
@apply -ml-2;
|
||||
@apply leading-4 !text-base cursor-pointer shrink-0;
|
||||
}
|
||||
|
||||
.sci-tag .sn-icon.sn-icon-close {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
|
39
app/controllers/concerns/taggable_actions.rb
Normal file
39
app/controllers/concerns/taggable_actions.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TaggableActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :load_taggable_item, only: %i(link_tag unlink_tag)
|
||||
before_action :load_tag, only: %i(link_tag unlink_tag)
|
||||
end
|
||||
|
||||
def link_tag
|
||||
tagging = @taggable_item.taggings.new(tag: @tag, created_by: current_user)
|
||||
if tagging.save
|
||||
render json: { tag: [@tag.id, @tag.name, @tag.color] }
|
||||
else
|
||||
render json: { status: :error }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def unlink_tag
|
||||
tagging = @taggable_item.taggings.find_by(tag_id: @tag.id)
|
||||
if tagging&.destroy
|
||||
render json: { status: :ok }
|
||||
else
|
||||
render json: { status: :error }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_taggable_item
|
||||
@taggable_item = controller_name.singularize.camelize.constantize.find(params[:id])
|
||||
end
|
||||
|
||||
def load_tag
|
||||
@tag = @taggable_item.team.tags.find_by(id: params[:tag_id])
|
||||
render_404 unless @tag
|
||||
end
|
||||
end
|
|
@ -8,6 +8,7 @@ class MyModulesController < ApplicationController
|
|||
include MyModulesHelper
|
||||
include Breadcrumbs
|
||||
include FavoritesActions
|
||||
include TaggableActions
|
||||
|
||||
before_action :load_vars, except: %i(index restore_group create new save_table_state
|
||||
inventory_assigning_my_module_filter actions_toolbar)
|
||||
|
@ -17,6 +18,7 @@ class MyModulesController < ApplicationController
|
|||
before_action :check_manage_permissions, only: %i(
|
||||
description due_date update_description update_protocol_description update_protocol
|
||||
)
|
||||
before_action :check_tag_manage_permissions, only: %i(link_tag unlink_tag)
|
||||
before_action :check_read_permissions, except: %i(create new update update_description
|
||||
inventory_assigning_my_module_filter
|
||||
update_protocol_description restore_group
|
||||
|
@ -514,6 +516,10 @@ class MyModulesController < ApplicationController
|
|||
render_404 unless @my_module.my_module_status
|
||||
end
|
||||
|
||||
def check_tag_manage_permissions
|
||||
render_403 && return unless can_manage_my_module_tags?(@my_module)
|
||||
end
|
||||
|
||||
def set_inline_name_editing
|
||||
if action_name == 'index'
|
||||
return unless can_manage_experiment?(@experiment)
|
||||
|
|
|
@ -128,7 +128,7 @@
|
|||
{{ i18n.t('my_modules.details.completed_date') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-6 mt-2.5">
|
||||
<div class="flex gap-2 mt-2.5">
|
||||
<span class="sn-icon sn-icon-users"></span>
|
||||
<span class="tw-hidden lg:block shrink-0">
|
||||
{{ i18n.t('my_modules.details.assigned_users') }}
|
||||
|
@ -148,6 +148,15 @@
|
|||
</SelectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-6 mt-2.5">
|
||||
<span class="sn-icon sn-icon-tag"></span>
|
||||
<span class="tw-hidden lg:block shrink-0">
|
||||
{{ i18n.t('my_modules.details.tags') }}
|
||||
</span>
|
||||
<div class="grow -mt-1.5">
|
||||
<TagsInput :subject="myModule" v-if="myModule" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -156,6 +165,7 @@
|
|||
import GeneralDropdown from '../shared/general_dropdown.vue';
|
||||
import DateTimePicker from '../shared/date_time_picker.vue';
|
||||
import SelectDropdown from '../shared/select_dropdown.vue';
|
||||
import TagsInput from '../shared/tags_input.vue';
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import escapeHtml from '../shared/escape_html.js';
|
||||
import {
|
||||
|
@ -175,7 +185,8 @@ export default {
|
|||
components: {
|
||||
GeneralDropdown,
|
||||
DateTimePicker,
|
||||
SelectDropdown
|
||||
SelectDropdown,
|
||||
TagsInput
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
133
app/javascript/vue/shared/tags_input.vue
Normal file
133
app/javascript/vue/shared/tags_input.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div>
|
||||
<GeneralDropdown @open="opened = true" @close="opened = false">
|
||||
<template v-slot:field>
|
||||
<div class="w-full flex flex-wrap rounded gap-2 p-1 border border-solid cursor-pointer"
|
||||
:class="{
|
||||
'!border-sn-science-blue': opened,
|
||||
'hover:!border-sn-light-grey !border-transparent': !opened,
|
||||
}">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="sci-tag bg-sn-super-light-grey">
|
||||
{{ i18n.t('tags.tags_input.add_tag') }}
|
||||
</div>
|
||||
</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 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>
|
||||
</div>
|
||||
<div class="sci-tag text-white" :style="{ backgroundColor: tag[2] }" >
|
||||
{{ tag[1] }}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-0 border-t w-full mb-1">
|
||||
<button class="btn btn-light btn-black w-32">
|
||||
<span class="sn-icon sn-icon-edit"></span>
|
||||
{{ i18n.t('tags.tags_input.edit_tags') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import GeneralDropdown from './general_dropdown.vue';
|
||||
import {
|
||||
list_users_settings_team_tags_path,
|
||||
} from '../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'TagsInput',
|
||||
props: {
|
||||
subject: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GeneralDropdown,
|
||||
},
|
||||
computed: {
|
||||
tagsUrl() {
|
||||
return list_users_settings_team_tags_path({team_id: this.subject.attributes.team_id});
|
||||
},
|
||||
linkTagUrl() {
|
||||
return this.subject.attributes.urls.link_tag;
|
||||
},
|
||||
unlinkTagUrl() {
|
||||
return this.subject.attributes.urls.unlink_tag;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadAllTags();
|
||||
this.tags = this.subject.attributes.tags || [];
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags: [],
|
||||
allTags: [],
|
||||
linkingTag: false,
|
||||
opened: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadAllTags() {
|
||||
axios.get(this.tagsUrl).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.linkTagUrl, {
|
||||
tag_id: tag[0],
|
||||
}).then((response) => {
|
||||
this.tags.push(response.data.tag);
|
||||
this.linkingTag = false;
|
||||
}).catch(() => {
|
||||
this.linkingTag = false;
|
||||
HelperModule.flashAlertMsg(I18n.t('errors.general'), 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
unlinkTag(tag) {
|
||||
if (this.linkingTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.linkingTag = true;
|
||||
|
||||
axios.post(this.unlinkTagUrl, {
|
||||
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>
|
|
@ -6,7 +6,7 @@ class MyModuleSerializer < ActiveModel::Serializer
|
|||
include ApplicationHelper
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
attributes :name, :description, :permissions, :description_view, :urls, :last_modified_by_name, :created_at, :updated_at,
|
||||
attributes :name, :description, :permissions, :description_view, :urls, :last_modified_by_name, :created_at, :updated_at, :tags, :team_id,
|
||||
:project_name, :experiment_name, :created_by_name, :is_creator_current_user, :code, :designated_user_ids, :due_date_cell, :start_date_cell, :completed_on
|
||||
|
||||
def project_name
|
||||
|
@ -17,6 +17,10 @@ class MyModuleSerializer < ActiveModel::Serializer
|
|||
object.experiment.name
|
||||
end
|
||||
|
||||
def team_id
|
||||
object.team.id
|
||||
end
|
||||
|
||||
def created_by_name
|
||||
object.created_by&.full_name
|
||||
end
|
||||
|
@ -53,7 +57,9 @@ class MyModuleSerializer < ActiveModel::Serializer
|
|||
def urls
|
||||
{
|
||||
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),
|
||||
link_tag: link_tag_my_module_path(object),
|
||||
unlink_tag: unlink_tag_my_module_path(object)
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -100,4 +106,10 @@ class MyModuleSerializer < ActiveModel::Serializer
|
|||
def description
|
||||
sanitize_input(object.tinymce_render('description'))
|
||||
end
|
||||
|
||||
def tags
|
||||
object.tags.map do |tag|
|
||||
[tag.id, tag.name, tag.color]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,30 +97,6 @@
|
|||
<%= 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>
|
||||
<div id="accessModalContainer" class="vue-access-modal"
|
||||
data-url="<%= my_module_path(@my_module, format: :json) %>"
|
||||
>
|
||||
|
@ -137,8 +113,6 @@
|
|||
<%= stylesheet_link_tag 'datatables' %>
|
||||
<%= javascript_include_tag "handsontable.full" %>
|
||||
<%= render partial: "shared/formulas_libraries" %>
|
||||
<%= javascript_include_tag("my_modules/protocols") %>
|
||||
<%= javascript_include_tag("my_modules/tags") %>
|
||||
<%= javascript_include_tag 'emoji_button' %>
|
||||
<%= javascript_include_tag("my_modules/repositories") %>
|
||||
<%= javascript_include_tag("my_modules/pwa_mobile_app") %>
|
||||
|
|
|
@ -4236,6 +4236,9 @@ en:
|
|||
merge: "Merge"
|
||||
merge_success: "Tags merged successfully."
|
||||
merge_error: "There was an error merging tags."
|
||||
tags_input:
|
||||
add_tag: "Add tag"
|
||||
edit_tags: "Edit tags"
|
||||
user_groups:
|
||||
promo:
|
||||
head_title: 'User groups'
|
||||
|
|
|
@ -525,6 +525,8 @@ Rails.application.routes.draw do
|
|||
post :favorite
|
||||
post :unfavorite
|
||||
get :assigned_users
|
||||
post :link_tag
|
||||
post :unlink_tag
|
||||
end
|
||||
resources :user_my_modules, path: '/users', only: %i(index create destroy) do
|
||||
collection do
|
||||
|
|
Loading…
Add table
Reference in a new issue