Skip to content

Commit

Permalink
feat: Ketcher2 Common Templates, User Templates & User settings (ComP…
Browse files Browse the repository at this point in the history
…lat#2061)

* User is provided with a list of global/common templates to use in the ketcher2.
* Ketcher2 setting are stored for each user as preferences for later effects.
* User templates available to be used within Ketcher2.

---------

Co-authored-by: PiTrem <[email protected]>
  • Loading branch information
haditariq and PiTrem authored Aug 28, 2024
1 parent 98cf444 commit 8557814
Show file tree
Hide file tree
Showing 22 changed files with 1,003 additions and 169 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"username": "postgres",
"password": ""
}
],
]
}
37 changes: 37 additions & 0 deletions READ_ME_Ketcher2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Ketcher2 Integration

This guide will help you integrate Ketcher2 into your project. Follow the steps below to ensure a smooth setup.

## Prerequisites

1. Ensure you have `jq` installed on your system:
```bash
sudo apt install jq
```

## Installation Steps

### 1. Add Ketcher2 Files

- Run the installation script (Choose one script based on release requried):

1- fetching build assets from github release

```bash
✅ bin/chem-ket2-install.sh -s epam/[email protected]
```

2- building from src at v2.22.0-rc.9
```bash
✅ bin/chem-ket2-install.sh -b -s epam/[email protected]
```


### 2. Restart Your Server/Container

- After completing the above steps, restart your server or container to apply the changes.

### 4. Configure User Access

- Ensure that Ketcher2 is allowed for the user(s) by the admin. This step is necessary to provide appropriate access permissions.

115 changes: 112 additions & 3 deletions app/api/chemotion/profile_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ProfileAPI < Grape::API
data = profile.data || {}
layout = {}
layout = Rails.configuration.profile_default&.layout if Rails.configuration.respond_to?(:profile_default)
templates_list = []

layout&.each_key do |ll|
data[ll.to_s] = layout[ll] if layout[ll].present? && data[ll.to_s].nil?
Expand Down Expand Up @@ -64,12 +65,26 @@ class ProfileAPI < Grape::API
data[dt] = sorted_layout
end

folder_path = "user_templates/#{current_user.id}"
user_templates_path = Rails.root.join('uploads', Rails.env, folder_path)
Dir.glob("#{user_templates_path}/*.txt") do |file|
next unless file

if File.file?(file)
content = File.read(file)
content = JSON(content)
content['props']['path'] = file
templates_list.push(content)
end
end

{
data: data,
show_external_name: profile.show_external_name,
show_sample_name: profile.show_sample_name,
show_sample_short_label: profile.show_sample_short_label,
curation: profile.curation,
user_templates: templates_list,
}
end

Expand Down Expand Up @@ -98,7 +113,6 @@ class ProfileAPI < Grape::API
optional :show_sample_name, type: Boolean
optional :show_sample_short_label, type: Boolean
end

put do
declared_params = declared(params, include_missing: false)
available_ements = API::ELEMENTS + Labimotion::ElementKlass.where(is_active: true).pluck(:name)
Expand All @@ -125,17 +139,112 @@ class ProfileAPI < Grape::API
layout = data['layout'].select { |e| available_ements.include?(e) }
data['layout'] = layout.sort_by { |_k, v| v }.to_h
data['default_structure_editor'] = 'ketcher' if data['default_structure_editor'].nil?

new_profile = {
data: data.deep_merge(declared_params[:data] || {}),
show_external_name: declared_params[:show_external_name],
show_sample_name: declared_params[:show_sample_name],
show_sample_short_label: declared_params[:show_sample_short_label],
}

(current_user.profile.update!(**new_profile) &&
new_profile) || error!('profile update failed', 500)
end

desc 'post user template'
params do
requires :content, type: String, desc: 'ketcher file content'
end
post do
error_messages = []

error!({ error_messages: ['Content cannot be blank'] }, 422) if params[:content].blank?

folder_path = "user_templates/#{current_user.id}"
complete_folder_path = Rails.root.join('uploads', Rails.env, folder_path)
file_path = "#{complete_folder_path}/#{SecureRandom.alphanumeric(10)}.txt"
begin
FileUtils.mkdir_p(complete_folder_path) unless File.directory?(complete_folder_path)
File.new(file_path, 'w') unless File.exist?(file_path)
File.write(file_path, params[:content])

template_attachment = Attachment.new(
bucket: 1,
filename: file_path,
key: 'user_template',
created_by: current_user.id,
created_for: current_user.id,
content_type: 'text/html',
file_path: file_path,
)
begin
template_attachment.save
rescue StandardError
error_messages.push(template_attachment.errors.to_h[:attachment]) # rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
end
{ template_details: template_attachment, error_messages: error_messages }
rescue Errno::EACCES
error!('Save files error!', 500)
end
end

desc 'delete user template'
params do
requires :path, type: String, desc: 'file path of user template'
end
delete do
path = params[:path]
error!({ error_messages: ['path cannot be blank'] }, 422) if path.empty? || !File.exist?(path)
FileUtils.rm_f(path)
{ status: true }
end

desc 'get user profile editor ketcher 2 setting options'
get 'editors/ketcher2-options' do
file_path = "ketcher-optns/#{current_user.id}.json"
complete_folder_path = Rails.root.join('uploads', Rails.env, file_path)
error_messages = []

begin
JSON(File.read(complete_folder_path))
rescue StandardError
error_messages.push('Issues with reading settings file')
error!({ status: false, error_messages: error_messages.flatten }, 500)
end
end

desc 'update user profile editor ketcher 2 setting options'
params do
requires :data, type: Hash, desc: 'data structure for ketcher options'
end
put 'editors/ketcher2-options' do
error_messages = []
folder_path = 'ketcher-optns'
complete_folder_path = Rails.root.join('uploads', Rails.env, folder_path)
file_path = "#{complete_folder_path}/#{current_user.id}.json"
begin
FileUtils.mkdir_p(complete_folder_path) unless File.directory?(complete_folder_path)
File.new(file_path, 'w') unless File.exist?(file_path)
File.write(file_path, JSON(params[:data]))

template_attachment = Attachment.new(
bucket: 1,
filename: file_path,
key: 'ketcher-optns',
created_by: current_user.id,
created_for: current_user.id,
content_type: 'application/json',
file_path: file_path,
)
if template_attachment.save
{ status: true, message: 'Ketcher options updated successfully' }
else
error_messages.push('Attachment could not be saved')
error!({ status: false, error_messages: error_messages.flatten }, 422)
end
rescue StandardError => e
error_messages.push(e.message)
error!({ status: false, error_messages: error_messages.flatten }, 500)
end
end
end
end
end
Expand Down
7 changes: 3 additions & 4 deletions app/assets/stylesheets/attachment-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
margin-bottom: 20px;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 4px rgba(0,0,0,0.1);
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
height: 60px;
display: flex;
Expand All @@ -16,7 +16,7 @@

.attachment-dropzone:hover {
border-color: #a0a0a0;
box-shadow: 0px 4px 8px rgba(0,0,0,0.15);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
}

.sorting-row-style {
Expand Down Expand Up @@ -144,5 +144,4 @@
.sort-container {
flex-direction: row;
justify-content: space-between;
}

}
36 changes: 36 additions & 0 deletions app/assets/stylesheets/ketcher.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.ketcher-template-item {
display: flex;
align-items: center;
padding: 5px;
border-radius: 5px;
}

.ketcher-template-item:hover {
background-color: #ebf5ff;
cursor: pointer;
}

.ketcher-select-common-template {
background-color: #fff;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
color: #333;
cursor: default;
display: table;
height: 36px;
width: 100%;
padding: 7px 0 5px 5px;
position: relative;
}

.select-template-badge {
position: absolute;
right: 10px;
top: 5px;
}

.common-template-header {
display: flex;
align-items: center;
}
6 changes: 4 additions & 2 deletions app/assets/stylesheets/overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
overflow: hidden;
white-space: nowrap;
}
.wellplate-overlay:hover{

.wellplate-overlay:hover {
overflow: visible;
white-space: normal;
height: auto;
}

.well-overlay-select {
margin-top: 20px;

.Select-menu-outer {
z-index: 10;
}
}
}
22 changes: 21 additions & 1 deletion app/packs/src/apps/mydb/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import UIStore from 'src/stores/alt/stores/UIStore';
import UserActions from 'src/stores/alt/actions/UserActions';
import Calendar from 'src/components/calendar/Calendar';
import SampleTaskInbox from 'src/components/sampleTaskInbox/SampleTaskInbox';
import OnEventListen from 'src/utilities/UserTemplatesHelpers';

class App extends Component {
constructor(_props) {
Expand All @@ -38,23 +39,42 @@ class App extends Component {
UserActions.fetchOlsChmo();
UserActions.fetchOlsBao();
UserActions.fetchProfile();
UserActions.setUsertemplates();
UserActions.fetchUserLabels();
UserActions.fetchGenericEls();
UserActions.fetchSegmentKlasses();
UserActions.fetchDatasetKlasses();
UserActions.fetchUnitsSystem();
UserActions.fetchEditors();
UserActions.fetchKetcher2Options();
UIActions.initialize.defer();
this.patchExternalLibraries();

document.addEventListener('keydown', this.documentKeyDown);
window.addEventListener('storage', this.handleStorageChange);

this.patchExternalLibraries();
// user templates
this.removeLocalStorageEventListener();
this.storageListener();
}

componentWillUnmount() {
UIStore.unlisten(this.handleUiStoreChange);
document.removeEventListener('keydown', this.documentKeyDown);
}

removeLocalStorageEventListener() {
window.removeEventListener('storage', this.storageListener);
}

storageListener() {
window.addEventListener(
'storage',
OnEventListen,
false
);
}

handleUiStoreChange(state) {
if (this.state.showCollectionManagement !== state.showCollectionManagement) {
this.setState({ showCollectionManagement: state.showCollectionManagement });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export default class LiteratureDetails extends Component {
literature
} = this.state;
const { currentUser } = UserStore.getState();
const label = currentCollection ? currentCollection.label : null
const label = currentCollection ? currentCollection.label : null;
let contentSamples = '';
sampleRefs.forEach((citation) => {
contentSamples = `${contentSamples}\n${literatureContent(citation, true)}`;
Expand Down
22 changes: 22 additions & 0 deletions app/packs/src/components/ketcher-templates/CommonTemplateItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react';

function CommonTemplateItem(props) {
const { item, onClickItem } = props;
return (
<div className="ketcher-template-item" onClick={() => onClickItem(item)}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="css-uwwqev"><path d="M8.75 3.75h-5v5h1.5V6.312L9 10.063l1-1.124L6.312 5.25H8.75v-1.5zM15.25 3.75h5v5h-1.5V6.312L15 10.063l-1-1.124 3.688-3.688H15.25v-1.5zM15.25 20.25h5v-5h-1.5v2.438L15 13.938l-1 1.124 3.688 3.688H15.25v1.5zM8.75 20.25h-5v-5h1.5v2.438L9 13.938l1 1.124-3.688 3.688H8.75v1.5z" fill="currentColor"></path></svg>
<h4 style={{ marginLeft: 15 }}>
{item?.name}
</h4>
</div>
);
}

export default CommonTemplateItem;

CommonTemplateItem.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
item: PropTypes.object.isRequired,
onClickItem: PropTypes.func.isRequired
};
Loading

0 comments on commit 8557814

Please sign in to comment.