mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-16 18:14:30 +08:00
Asynchroniously fetch actual smart annotation object data [SCI-11513]
This commit is contained in:
parent
50809dca7d
commit
321733010b
5 changed files with 105 additions and 18 deletions
|
@ -3,4 +3,10 @@
|
||||||
@apply flex m-auto h-[30px] w-[30px] animate-spin;
|
@apply flex m-auto h-[30px] w-[30px] animate-spin;
|
||||||
background: image-url("sn-loader.svg") center center no-repeat;
|
background: image-url("sn-loader.svg") center center no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sci-loader-inline {
|
||||||
|
@apply inline-block h-[1.2em] w-[1.2em] animate-spin;
|
||||||
|
background: image-url("sn-loader.svg") center center no-repeat;
|
||||||
|
background-size: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SmartAnnotationsController < ApplicationController
|
class SmartAnnotationsController < ApplicationController
|
||||||
include InputSanitizeHelper
|
include InputSanitizeHelper
|
||||||
include ActionView::Helpers::TextHelper
|
include ActionView::Helpers::TextHelper
|
||||||
|
@ -14,15 +16,22 @@ class SmartAnnotationsController < ApplicationController
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect
|
def show
|
||||||
|
if params[:data]
|
||||||
|
render json: {
|
||||||
|
name: resource_readable? && resource.name,
|
||||||
|
type: resource_tag
|
||||||
|
}
|
||||||
|
else
|
||||||
redirect_to redirect_path
|
redirect_to redirect_path
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
user_team_assignment = resource.user_assignments.find_by(assignable: current_team)
|
user_team_assignment = resource.user_assignments.find_by(assignable: current_team)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
full_name: resource.full_name,
|
name: resource.name,
|
||||||
email: resource.email,
|
email: resource.email,
|
||||||
avatar_url: user_avatar_absolute_url(resource, :thumb),
|
avatar_url: user_avatar_absolute_url(resource, :thumb),
|
||||||
info: I18n.t(
|
info: I18n.t(
|
||||||
|
@ -36,15 +45,23 @@ class SmartAnnotationsController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def resource
|
def sa_tag
|
||||||
return @resource_class ||= User.find(params[:tag][1..].split('~')[1].base62_decode) if params[:tag][0] == '@'
|
@sa_tag ||= params[:tag][1..].split('~')[1]
|
||||||
|
end
|
||||||
|
|
||||||
_, resource_tag, resource_id = params[:tag][1..].split('~')
|
def resource_tag
|
||||||
|
@resource_tag ||= resource.is_a?(RepositoryRow) ? repository_acronym(resource.repository) : sa_tag
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource
|
||||||
|
return @resource_class ||= User.find(sa_tag.base62_decode) if params[:tag][0] == '@'
|
||||||
|
|
||||||
|
resource_id = params[:tag][1..].split('~').last
|
||||||
|
|
||||||
resource_id = resource_id.base62_decode
|
resource_id = resource_id.base62_decode
|
||||||
|
|
||||||
resource_class =
|
resource_class =
|
||||||
case resource_tag
|
case sa_tag
|
||||||
when 'prj'
|
when 'prj'
|
||||||
Project
|
Project
|
||||||
when 'exp'
|
when 'exp'
|
||||||
|
@ -58,6 +75,16 @@ class SmartAnnotationsController < ApplicationController
|
||||||
@resource ||= resource_class.find(resource_id)
|
@resource ||= resource_class.find(resource_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resource_readable?
|
||||||
|
@resource_readable ||=
|
||||||
|
case resource
|
||||||
|
when RepositoryRow
|
||||||
|
resource.repository.readable_by_user?(current_user)
|
||||||
|
else
|
||||||
|
resource.readable_by_user?(current_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def redirect_path
|
def redirect_path
|
||||||
case resource
|
case resource
|
||||||
when Project
|
when Project
|
||||||
|
@ -70,4 +97,13 @@ class SmartAnnotationsController < ApplicationController
|
||||||
repository_repository_row_path(resource.repository, resource)
|
repository_repository_row_path(resource.repository, resource)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def repository_acronym(repository)
|
||||||
|
words = repository.name.strip.split
|
||||||
|
case words.size
|
||||||
|
when 1 then words[0][0..2]
|
||||||
|
when 2 then words[0][0..1] + words[1][0]
|
||||||
|
else words[0..2].map(&:chr).join
|
||||||
|
end.capitalize
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
/* global I18n */
|
||||||
|
|
||||||
|
const SA_REGEX = /\[@([^~\]]+)~([0-9a-zA-Z]+)\]|\[#(.*?)~(rep_item|prj|exp|tsk)~([0-9a-zA-Z]+)\]/g;
|
||||||
|
|
||||||
const escapeHtml = (unsafe) => (
|
const escapeHtml = (unsafe) => (
|
||||||
unsafe.replaceAll('&', '&')
|
unsafe.replaceAll('&', '&')
|
||||||
.replaceAll('<', '<')
|
.replaceAll('<', '<')
|
||||||
|
@ -6,6 +10,17 @@ const escapeHtml = (unsafe) => (
|
||||||
.replaceAll("'", ''')
|
.replaceAll("'", ''')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isInViewport = (element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
return (
|
||||||
|
rect.top >= 0 && rect.left >= 0
|
||||||
|
&& rect.bottom <= (window.innerHeight || html.clientHeight)
|
||||||
|
&& rect.right <= (window.innerWidth || html.clientWidth)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderUserMention = (tag, userName) => {
|
const renderUserMention = (tag, userName) => {
|
||||||
const safeUserName = escapeHtml(userName);
|
const safeUserName = escapeHtml(userName);
|
||||||
|
|
||||||
|
@ -20,37 +35,63 @@ const renderUserMention = (tag, userName) => {
|
||||||
data-toggle="popover"
|
data-toggle="popover"
|
||||||
data-content=""
|
data-content=""
|
||||||
data-url="/sa/u?tag=${tag}"
|
data-url="/sa/u?tag=${tag}"
|
||||||
>${safeUserName}</a>`;
|
><span class="sa-name"><span class="sci-loader-inline"></span></span></a>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
window.renderSmartAnnotations = (text) => (
|
window.renderSmartAnnotations = (text) => (
|
||||||
text.replace(/\[@([^~\]]+)~([0-9a-zA-Z]+)\]|\[#(.*?)~(rep_item|prj|exp|tsk)~([0-9a-zA-Z]+)\]/g, (match, userName, _userId, label, type) => {
|
text.replace(SA_REGEX, (match, userName, _userId, label, type) => {
|
||||||
|
|
||||||
const tag = encodeURIComponent(match.slice(1, -1));
|
const tag = encodeURIComponent(match.slice(1, -1));
|
||||||
|
|
||||||
if (userName) {
|
if (userName) {
|
||||||
return renderUserMention(tag, userName);
|
return renderUserMention(tag, userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeLabel = escapeHtml(label);
|
|
||||||
const safeType = escapeHtml(type);
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'rep_item':
|
case 'rep_item':
|
||||||
return `<a class="sa-link record-info-link" href="/sa?tag=${tag}"><span class="sa-type">INV</span>${safeLabel}</a>`;
|
return `<a class="sa-link record-info-link" href="/sa?tag=${tag}"><span class="sa-type"></span><span class="sa-name"><span class="sci-loader-inline"></span></span></a>`;
|
||||||
default:
|
default:
|
||||||
return `<a class="sa-link" href="/sa?tag=${tag}" target="_blank"><span class="sa-type">${safeType}</span>${safeLabel}</a>`;
|
return `<a class="sa-link" href="/sa?tag=${tag}" target="_blank"><span class="sa-type"></span><span class="sa-name"><span class="sci-loader-inline"></span></span></a>`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function fetchSmartAnnotationData(element) {
|
||||||
|
const response = await fetch(`${element.getAttribute('href') || element.getAttribute('data-url')}&data=true`);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
element.querySelector('.sa-name').innerHTML = data.name || `(${I18n.t('general.private')})`;
|
||||||
|
|
||||||
|
if (element.querySelector('.sa-type')) {
|
||||||
|
element.querySelector('.sa-type').innerHTML = data.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.renderElementSmartAnnotations = (element) => {
|
window.renderElementSmartAnnotations = (element) => {
|
||||||
|
if (!element.innerHTML.match(SA_REGEX)) return true;
|
||||||
|
|
||||||
element.innerHTML = window.renderSmartAnnotations(element.innerHTML);
|
element.innerHTML = window.renderSmartAnnotations(element.innerHTML);
|
||||||
|
|
||||||
|
// Schedule fetch of data for each annotation element when it scrolls into viewport
|
||||||
|
element.querySelectorAll('.sa-link, .user-tooltip').forEach((el) => {
|
||||||
|
if (isInViewport(el)) {
|
||||||
|
fetchSmartAnnotationData(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchFunction = () => {
|
||||||
|
fetchSmartAnnotationData(el);
|
||||||
|
window.removeEventListener('scroll', fetchFunction);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', fetchFunction);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
$(document).on('focus', '.user-tooltip', function () {
|
// using legacy jQuery style, as we need it for tooltips anyway
|
||||||
|
$(document).on('click', '.user-tooltip', function () {
|
||||||
$.get($(this).data('url'), (data) => {
|
$.get($(this).data('url'), (data) => {
|
||||||
const content = `<div class="user-name-popover-wrapper">
|
const content = `<div class="user-name-popover-wrapper">
|
||||||
<div class='col-xs-3'>
|
<div class='col-xs-3'>
|
||||||
|
@ -62,7 +103,7 @@ $(document).on('focus', '.user-tooltip', function () {
|
||||||
<div class='col-xs-9 pl-3'>
|
<div class='col-xs-9 pl-3'>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-xs-12 text-left font-bold'>
|
<div class='col-xs-12 text-left font-bold'>
|
||||||
${escapeHtml(data.full_name)}
|
${escapeHtml(data.name)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
|
|
|
@ -20,6 +20,10 @@ module PermissionCheckableModel
|
||||||
user_role_permissions.include?(permission)
|
user_role_permissions.include?(permission)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def readable_by_user?(user)
|
||||||
|
permission_granted?(user, "::#{self.class.to_s.split('::').first}Permissions".constantize::READ)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_user_role_permissions(user)
|
def load_user_role_permissions(user)
|
||||||
|
|
|
@ -23,7 +23,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
root 'dashboards#show'
|
root 'dashboards#show'
|
||||||
|
|
||||||
get '/sa', to: 'smart_annotations#redirect'
|
get '/sa', to: 'smart_annotations#show'
|
||||||
get '/sa/u', to: 'smart_annotations#user'
|
get '/sa/u', to: 'smart_annotations#user'
|
||||||
|
|
||||||
resources :navigations, only: [] do
|
resources :navigations, only: [] do
|
||||||
|
|
Loading…
Add table
Reference in a new issue