From 5a3a19113cd4a41cb54b0b96400027cd458d769c Mon Sep 17 00:00:00 2001 From: Hadi Date: Wed, 21 Aug 2024 14:07:29 +0200 Subject: [PATCH 01/14] feat: show token identifier on app for each attachment --- app/api/chemotion/third_party_app_api.rb | 14 ++++ app/api/helpers/third_party_app_helpers.rb | 38 +++++---- .../ResearchPlanDetailsAttachments.js | 84 ++++++++++++------- .../mydb/elements/list/ThirdPartyAppButton.js | 59 +++++++++++++ .../src/fetchers/ThirdPartyAppFetcher.js | 11 ++- 5 files changed, 157 insertions(+), 49 deletions(-) create mode 100644 app/packs/src/apps/mydb/elements/list/ThirdPartyAppButton.js diff --git a/app/api/chemotion/third_party_app_api.rb b/app/api/chemotion/third_party_app_api.rb index 1f460e6a74..52ccbe6288 100644 --- a/app/api/chemotion/third_party_app_api.rb +++ b/app/api/chemotion/third_party_app_api.rb @@ -106,6 +106,20 @@ class ThirdPartyAppAPI < Grape::API "#{@app.url}?url=#{url}" end + desc 'list of TPA token in a collection' + get 'collection_tpa_tokens' do + token_list = [] + cache_user_keys = cache.read(current_user.id) + cache_user_keys&.each do |token_key| + cached_value = cache.read(token_key) + token_list + .push({ + "#{token_key}": cached_value, + }) + end + { token_list: token_list } + end + desc 'get chemotion handler url' params do requires :attID, type: Integer, desc: 'Attachment ID' diff --git a/app/api/helpers/third_party_app_helpers.rb b/app/api/helpers/third_party_app_helpers.rb index d84ed73711..0ba19755a3 100644 --- a/app/api/helpers/third_party_app_helpers.rb +++ b/app/api/helpers/third_party_app_helpers.rb @@ -20,7 +20,9 @@ def cached_token # desc: define the cache key based on the attachment/user/app ids def cache_key - @cache_key ||= "#{@attachment&.id}/#{@user&.id}/#{@app&.id}" + @user_key ||= @user&.id + @cache_key_attachment_app ||= "#{@app&.id}/#{@attachment&.id}" + [@user_key, @cache_key_attachment_app] end # desc: prepare the token payload from the params @@ -44,14 +46,16 @@ def parse_payload(payload = @payload) # desc: decrement the counters / check if token permission is expired def update_cache(key) - return error!('Invalid token', 403) if cached_token.nil? || cached_token[:token] != params[:token] - - # TODO: expire token when both counters reach 0 - # IDEA: split counters into two caches? - return error!("Token #{key} permission expired", 403) if cached_token[key].negative? - - cached_token[key] -= 1 - cache.write(cache_key, cached_token) + parse_payload + if cached_token.nil? || (@cached_token[:download] < 1 && @cached_token[:upload] < 1) + cache.delete(cache_key[1]) + error!('Invalid token', 403) + elsif @cached_token[key] < 1 + error!("Token #{key} permission expired", 403) + else + @cached_token[key] -= 1 + cache.write(cache_key[1], @cached_token) + end end # desc: return file for download to third party app @@ -90,12 +94,14 @@ def upload_attachment_from_third_party_app { message: 'File uploaded successfully' } end - def encode_and_cache_token(payload = @payload) - @token = JsonWebToken.encode(payload, expiry_time) - cache.write( - cache_key, - { token: @token, download: 3, upload: 10 }, - expires_at: expiry_time, - ) + def encode_and_cache_token + current_state = cache.read(cache_key[0]) + new_state = if current_state + idx = current_state.index(cache_key[1]) + idx.nil? ? current_state.push(cache_key[1]) : current_state + else + [cache_key[1]] + end + cache.write(cache_key[0], new_state) end end diff --git a/app/packs/src/apps/mydb/elements/details/researchPlans/attachmentsTab/ResearchPlanDetailsAttachments.js b/app/packs/src/apps/mydb/elements/details/researchPlans/attachmentsTab/ResearchPlanDetailsAttachments.js index 05652e7b5d..ee3f8491d0 100644 --- a/app/packs/src/apps/mydb/elements/details/researchPlans/attachmentsTab/ResearchPlanDetailsAttachments.js +++ b/app/packs/src/apps/mydb/elements/details/researchPlans/attachmentsTab/ResearchPlanDetailsAttachments.js @@ -1,32 +1,33 @@ /* eslint-disable lines-between-class-members */ /* eslint-disable no-param-reassign */ /* eslint-disable react/destructuring-assignment */ -import { StoreContext } from 'src/stores/mobx/RootStore'; -import EditorFetcher from 'src/fetchers/EditorFetcher'; -import ElementActions from 'src/stores/alt/actions/ElementActions'; -import LoadingActions from 'src/stores/alt/actions/LoadingActions'; -import UIStore from 'src/stores/alt/stores/UIStore'; -import PropTypes from 'prop-types'; +import { findKey, last } from 'lodash'; import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ImageAnnotationModalSVG from 'src/apps/mydb/elements/details/researchPlans/ImageAnnotationModalSVG'; import { Button } from 'react-bootstrap'; -import { last, findKey } from 'lodash'; -import AttachmentFetcher from 'src/fetchers/AttachmentFetcher'; -import ImageAttachmentFilter from 'src/utilities/ImageAttachmentFilter'; +import ImageAnnotationModalSVG from 'src/apps/mydb/elements/details/researchPlans/ImageAnnotationModalSVG'; import SaveEditedImageWarning from 'src/apps/mydb/elements/details/researchPlans/SaveEditedImageWarning'; import { - downloadButton, - removeButton, annotateButton, + attachmentThumbnail, + customDropzone, + downloadButton, editButton, + formatFileSize, importButton, - customDropzone, + removeButton, sortingAndFilteringUI, - formatFileSize, - attachmentThumbnail, - ThirdPartyAppButton, } from 'src/apps/mydb/elements/list/AttachmentList'; +import ThirdPartyAppButton from 'src/apps/mydb/elements/list/ThirdPartyAppButton'; +import AttachmentFetcher from 'src/fetchers/AttachmentFetcher'; +import EditorFetcher from 'src/fetchers/EditorFetcher'; +import ThirdPartyAppFetcher from 'src/fetchers/ThirdPartyAppFetcher'; +import ElementActions from 'src/stores/alt/actions/ElementActions'; +import LoadingActions from 'src/stores/alt/actions/LoadingActions'; +import UIStore from 'src/stores/alt/stores/UIStore'; +import { StoreContext } from 'src/stores/mobx/RootStore'; +import ImageAttachmentFilter from 'src/utilities/ImageAttachmentFilter'; import { formatDate, parseDate } from 'src/utilities/timezoneHelper'; class ResearchPlanDetailsAttachments extends Component { @@ -47,6 +48,7 @@ class ResearchPlanDetailsAttachments extends Component { filterText: '', sortBy: 'name', sortDirection: 'asc', + tokenList: [] }; this.editorInitial = this.editorInitial.bind(this); this.createAttachmentPreviews = this.createAttachmentPreviews.bind(this); @@ -63,16 +65,30 @@ class ResearchPlanDetailsAttachments extends Component { componentDidMount() { this.editorInitial(); this.createAttachmentPreviews(); + this.fetch3paTokenByUserId(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { const { attachments } = this.props; + const { tokenList } = this.state; + if (attachments !== prevProps.attachments) { this.createAttachmentPreviews(); this.setState({ filteredAttachments: [...attachments] }, this.filterAndSortAttachments); } + // Check if tokenList has changed + if (tokenList !== prevState.tokenList) { + this.fetch3paTokenByUserId(); + } } + async fetch3paTokenByUserId() { + const res = await ThirdPartyAppFetcher.fetchCollectionAttachmentTokensByCollectionId(); + if (this.state.tokenList.length !== res.token_list.length) { + this.setState({ tokenList: res?.token_list }); + } + }; + handleEdit(attachment) { const fileType = last(attachment.filename.split('.')); const docType = this.documentType(attachment.filename); @@ -244,15 +260,14 @@ class ResearchPlanDetailsAttachments extends Component { const { researchPlan } = this.props; //Ugly temporary hack to avoid tests failling because the context is not accessable in tests with the enzyme framework - + let combinedAttachments = filteredAttachments; - if(this.context.attachmentNotificationStore ){ - combinedAttachments = this.context.attachmentNotificationStore.getCombinedAttachments(filteredAttachments,"ResearchPlan",researchPlan); + if (this.context.attachmentNotificationStore) { + combinedAttachments = this.context.attachmentNotificationStore.getCombinedAttachments(filteredAttachments, "ResearchPlan", researchPlan); } const { onUndoDelete, attachments } = this.props; - const thirdPartyApps = this.thirdPartyApps; - + return (
{this.renderImageEditModal()} @@ -262,13 +277,13 @@ class ResearchPlanDetailsAttachments extends Component {
{attachments.length > 0 - && sortingAndFilteringUI( - sortDirection, - this.handleSortChange, - this.toggleSortDirection, - this.handleFilterChange, - true - )} + && sortingAndFilteringUI( + sortDirection, + this.handleSortChange, + this.toggleSortDirection, + this.handleFilterChange, + true + )}
{combinedAttachments.length === 0 ? ( @@ -312,15 +327,20 @@ class ResearchPlanDetailsAttachments extends Component { ) : ( <> {downloadButton(attachment)} - + this.fetch3paTokenByUserId()} + /> {editButton( attachment, extension, attachmentEditor, attachment.aasm_state === 'oo_editing' && new Date().getTime() - < (new Date(attachment.updated_at).getTime() + 15 * 60 * 1000), + < (new Date(attachment.updated_at).getTime() + 15 * 60 * 1000), !attachmentEditor || attachment.aasm_state === 'oo_editing' - || attachment.is_new || this.documentType(attachment.filename) === null, + || attachment.is_new || this.documentType(attachment.filename) === null, this.handleEdit )} {annotateButton(attachment, this)} diff --git a/app/packs/src/apps/mydb/elements/list/ThirdPartyAppButton.js b/app/packs/src/apps/mydb/elements/list/ThirdPartyAppButton.js new file mode 100644 index 0000000000..d4c1670b70 --- /dev/null +++ b/app/packs/src/apps/mydb/elements/list/ThirdPartyAppButton.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { + Dropdown, MenuItem +} from 'react-bootstrap'; +import ThirdPartyAppFetcher from 'src/fetchers/ThirdPartyAppFetcher'; +import uuid from 'uuid'; + +const ThirdPartyAppButton = ({ attachment, options, tokenList, onChangeRecall }) => { + + const handleFetchAttachToken = (option) => { + ThirdPartyAppFetcher.fetchAttachmentToken(attachment.id, option.id) + .then((result) => { + onChangeRecall(); + window.open(result, '_blank'); + }); + }; + + const tpaTokenExists = (attachment_id, tpa) => { + let status = false; + tokenList?.map((item) => { + const keySplit = Object.keys(item)[0].split('/'); + const attachment_id_match = keySplit[1] == attachment_id; + const tpa_id = keySplit[0] == tpa.id; + if (tpa_id) { + if (attachment_id_match) { + status = true; + } + } + }); + return status; + }; + + return ( + + +