mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-16 10:06:57 +08:00
Updating description field [SCI-11824]
This commit is contained in:
parent
e5d447a0be
commit
1d7bfe8517
9 changed files with 338 additions and 46 deletions
|
@ -34,7 +34,8 @@
|
|||
|
||||
<DescriptionModal
|
||||
v-if="descriptionModalObject"
|
||||
:experiment="descriptionModalObject"
|
||||
:object="descriptionModalObject"
|
||||
@update="updateDescription"
|
||||
@close="descriptionModalObject = null"/>
|
||||
<DuplicateModal
|
||||
v-if="duplicateModalObject"
|
||||
|
@ -68,7 +69,7 @@ import DescriptionRenderer from '../shared/datatable/renderers/description.vue';
|
|||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import CompletedTasksRenderer from './renderers/completed_tasks.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import DescriptionModal from './modals/description.vue';
|
||||
import DescriptionModal from '../shared/datatable/modals/description.vue';
|
||||
import DuplicateModal from './modals/duplicate.vue';
|
||||
import MoveModal from './modals/move.vue';
|
||||
import EditModal from './modals/edit.vue';
|
||||
|
@ -281,6 +282,15 @@ export default {
|
|||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
},
|
||||
updateDescription(description) {
|
||||
axios.put(this.descriptionModalObject.urls.update, {
|
||||
experiment: {
|
||||
description
|
||||
}
|
||||
}).then(() => {
|
||||
this.updateTable();
|
||||
});
|
||||
},
|
||||
restore(event, rows) {
|
||||
axios.post(event.path, { experiment_ids: rows.map((row) => row.id) }).then((response) => {
|
||||
this.reloadingTable = true;
|
||||
|
|
|
@ -50,7 +50,8 @@
|
|||
></ConfirmationModal>
|
||||
<DescriptionModal
|
||||
v-if="descriptionModalObject"
|
||||
:project="descriptionModalObject"
|
||||
:object="descriptionModalObject"
|
||||
@update="updateDescription"
|
||||
@close="descriptionModalObject = null"/>
|
||||
<ExportLimitExceededModal v-if="exportLimitExceded" :description="exportDescription" @close="exportLimitExceded = false"/>
|
||||
<EditProjectModal v-if="editProject" :userRolesUrl="userRolesUrl"
|
||||
|
@ -83,7 +84,7 @@ import SuperviserRenderer from './renderers/superviser.vue';
|
|||
import CommentsRenderer from '../shared/datatable/renderers/comments.vue';
|
||||
import DueDateRenderer from '../shared/datatable/renderers/date.vue';
|
||||
import DescriptionRenderer from '../shared/datatable/renderers/description.vue';
|
||||
import DescriptionModal from './modals/description.vue';
|
||||
import DescriptionModal from '../shared/datatable/modals/description.vue';
|
||||
import ProjectCard from './card.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import EditProjectModal from './modals/edit.vue';
|
||||
|
@ -341,6 +342,15 @@ export default {
|
|||
showDescription(_e, project) {
|
||||
[this.descriptionModalObject] = project;
|
||||
},
|
||||
updateDescription(description) {
|
||||
axios.put(this.descriptionModalObject.urls.update, {
|
||||
project: {
|
||||
description
|
||||
}
|
||||
}).then(() => {
|
||||
this.updateTable();
|
||||
});
|
||||
},
|
||||
changeStatus(newStatus, params) {
|
||||
axios.put(params.data.urls.update, {
|
||||
project: {
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<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 class="modal-title truncate !block" id="edit-project-modal-label" :title="project.name">
|
||||
{{ project.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="[&_.atwho-user-container]:!whitespace-normal whitespace-pre-wrap">
|
||||
{{ project.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ i18n.t('general.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'DescriptionModal',
|
||||
props: {
|
||||
project: Object
|
||||
},
|
||||
mixins: [modalMixin]
|
||||
};
|
||||
</script>
|
83
app/javascript/vue/shared/datatable/modals/description.vue
Normal file
83
app/javascript/vue/shared/datatable/modals/description.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<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 class="modal-title truncate !block" :title="object.name">
|
||||
{{ object.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<div ref="description" class="modal-body">
|
||||
<div v-if="editMode">
|
||||
<TinymceEditor
|
||||
v-model="description"
|
||||
textareaId="descriptionModelInput"
|
||||
></TinymceEditor>
|
||||
</div>
|
||||
<span v-else v-html="description"></span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button v-if="editMode" type="button" @click="cancelEdit" class="btn btn-secondary">{{ i18n.t('general.cancel') }}</button>
|
||||
<button v-else type="button" data-dismiss="modal" class="btn btn-secondary">{{ i18n.t('general.cancel') }}</button>
|
||||
<button v-if="object.permissions.manage && !editMode"
|
||||
type="button"
|
||||
@click="editMode = true"
|
||||
class="btn btn-primary">
|
||||
{{ i18n.t('general.edit') }}
|
||||
</button>
|
||||
<button v-if="editMode"
|
||||
type="button"
|
||||
@click="updateDescription"
|
||||
class="btn btn-primary">
|
||||
{{ i18n.t('general.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import modalMixin from '../../modal_mixin';
|
||||
import TinymceEditor from '../../tinymce_editor.vue';
|
||||
|
||||
export default {
|
||||
name: 'DescriptionModal',
|
||||
props: {
|
||||
object: Object
|
||||
},
|
||||
components: {
|
||||
TinymceEditor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
description: this.object.description,
|
||||
editMode: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
window.renderElementSmartAnnotations(this.$refs.description, 'span');
|
||||
});
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
methods: {
|
||||
updateDescription() {
|
||||
this.$emit('update', this.description);
|
||||
this.$emit('close');
|
||||
},
|
||||
cancelEdit() {
|
||||
this.editMode = false;
|
||||
this.description = this.object.description;
|
||||
this.$refs.description.classList.remove('sa-initialized');
|
||||
this.$nextTick(() => {
|
||||
window.renderElementSmartAnnotations(this.$refs.description, 'span');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -3,9 +3,9 @@
|
|||
<template v-if="params.dtComponent.currentViewRender === 'table'">
|
||||
<div class="group relative flex items-center group-hover:marker text-xs h-full w-full leading-[unset]">
|
||||
<div ref="descripitonBox" class="flex gap-2 w-full items-center text-sm leading-[unset]">
|
||||
<span class="cursor-pointer line-clamp-1 leading-[unset]"
|
||||
<span v-if="removeTags(params.data.description).length > 0" class="cursor-pointer line-clamp-1 leading-[unset]"
|
||||
@click.stop="showDescriptionModal">
|
||||
{{ params.data.description }}
|
||||
{{ removeTags(params.data.description) }}
|
||||
</span>
|
||||
<span v-if="!params.data.permissions.manage || (params.data.description && params.data.description.length > 0)"
|
||||
@click.stop="showDescriptionModal"
|
||||
|
@ -24,9 +24,9 @@
|
|||
<span v-if="shouldTruncateText"
|
||||
class="cursor-pointer grow line-clamp-2"
|
||||
@click.stop="showDescriptionModal">
|
||||
{{ params.data.description }}
|
||||
{{ removeTags(params.data.description) }}
|
||||
</span>
|
||||
<span v-else class="grow">{{ params.data.description }}</span>
|
||||
<span v-else class="grow">{{ removeTags(params.data.description) }}</span>
|
||||
<span v-if="shouldTruncateText" @click.stop="showDescriptionModal" class="text-sn-blue cursor-pointer shrink-0 inline-block text-xs">
|
||||
{{ i18n.t('experiments.card.more') }}
|
||||
</span>
|
||||
|
@ -58,6 +58,12 @@ export default {
|
|||
showDescriptionModal() {
|
||||
this.params.dtComponent.$emit('showDescription', null, [this.params.data]);
|
||||
},
|
||||
},
|
||||
removeTags(description) {
|
||||
const itemHtml = $(`<span>${description}</span>`);
|
||||
itemHtml.remove('table, img');
|
||||
const str = itemHtml.text().trim();
|
||||
return str.length > 56 ? `${str.slice(0, 56)}...` : str;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<div class="tinymce-container" :class="{ 'error': error }">
|
||||
<form class="tiny-mce-editor" role="form" :action="updateUrl" accept-charset="UTF-8" data-remote="true" method="post">
|
||||
<input type="hidden" name="_method" value="patch">
|
||||
<input type="hidden" name="format" value="json">
|
||||
<div class="hidden tinymce-cancel-button tox-mbtn" tabindex="-1">
|
||||
<button type="button" tabindex="-1">
|
||||
<span class="sn-icon sn-icon-close"></span>
|
||||
|
|
216
app/javascript/vue/shared/tinymce_editor.vue
Normal file
216
app/javascript/vue/shared/tinymce_editor.vue
Normal file
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<div>
|
||||
<textarea :id="textareaId" ref="inputField"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global SmartAnnotation */
|
||||
|
||||
import tinyMCE from 'tinymce/tinymce';
|
||||
import 'tinymce/models/dom';
|
||||
import 'tinymce/icons/default';
|
||||
import 'tinymce/themes/silver';
|
||||
|
||||
import 'tinymce/plugins/table';
|
||||
import 'tinymce/plugins/autosave';
|
||||
import 'tinymce/plugins/autoresize';
|
||||
import 'tinymce/plugins/link';
|
||||
import 'tinymce/plugins/advlist';
|
||||
import 'tinymce/plugins/codesample';
|
||||
import 'tinymce/plugins/code';
|
||||
import 'tinymce/plugins/autolink';
|
||||
import 'tinymce/plugins/lists';
|
||||
import 'tinymce/plugins/image';
|
||||
import 'tinymce/plugins/charmap';
|
||||
import 'tinymce/plugins/anchor';
|
||||
import 'tinymce/plugins/searchreplace';
|
||||
import 'tinymce/plugins/wordcount';
|
||||
import 'tinymce/plugins/visualblocks';
|
||||
import 'tinymce/plugins/visualchars';
|
||||
import 'tinymce/plugins/insertdatetime';
|
||||
import 'tinymce/plugins/nonbreaking';
|
||||
import 'tinymce/plugins/save';
|
||||
import 'tinymce/plugins/help';
|
||||
import 'tinymce/plugins/help/js/i18n/keynav/en';
|
||||
import 'tinymce/plugins/quickbars';
|
||||
import 'tinymce/plugins/directionality';
|
||||
|
||||
// Content styles, including inline UI like fake cursors
|
||||
// All the above CSS files are loaded on to the page but these two must
|
||||
// be loaded into the editor iframe so they are loaded as strings and passed
|
||||
// to the init function.
|
||||
import 'raw-loader';
|
||||
import contentCss from '!!raw-loader!tinymce/skins/content/default/content.min.css';
|
||||
import contentUiCss from '!!raw-loader!tinymce/skins/ui/tinymce-5/content.min.css';
|
||||
|
||||
const contentPStyle = 'p { margin: 0; padding: 0;}';
|
||||
const contentBodyStyle = 'body { font-family: "SN Inter", "Open Sans", Arial, Helvetica, sans-serif }';
|
||||
const contentStyle = [contentCss, contentUiCss, contentBodyStyle, contentPStyle].map((s) => s.toString()).join('\n');
|
||||
|
||||
export default {
|
||||
name: 'TinemcyEditor',
|
||||
props: {
|
||||
textareaId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
plugins: {
|
||||
default: () => `
|
||||
table autoresize link advlist codesample code autolink lists
|
||||
charmap anchor searchreplace wordcount visualblocks visualchars
|
||||
insertdatetime nonbreaking save directionality help quickbars
|
||||
`
|
||||
},
|
||||
menubar: {
|
||||
default: 'file edit view insert format'
|
||||
},
|
||||
toolbar: {
|
||||
default: 'undo redo | insert | styleselect | bold italic | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent '
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
tinyMCE.init({
|
||||
selector: `#${this.textareaId}`,
|
||||
plugins: this.plugins,
|
||||
menubar: this.menubar,
|
||||
skin: false,
|
||||
content_css: false,
|
||||
content_style: contentStyle,
|
||||
convert_urls: false,
|
||||
toolbar: this.toolbar,
|
||||
contextmenu: '',
|
||||
promotion: false,
|
||||
menu: {
|
||||
insert: { title: 'Insert', items: 'link | charmap hr | nonbreaking anchor | insertdatetime' }
|
||||
},
|
||||
autoresize_bottom_margin: 20,
|
||||
codesample_global_prismjs: true,
|
||||
codesample_languages: [
|
||||
{ text: 'R', value: 'r' },
|
||||
{ text: 'MATLAB', value: 'matlab' },
|
||||
{ text: 'Python', value: 'python' },
|
||||
{ text: 'JSON', value: 'javascript' },
|
||||
{ text: 'HTML/XML', value: 'markup' },
|
||||
{ text: 'JavaScript', value: 'javascript' },
|
||||
{ text: 'CSS', value: 'css' },
|
||||
{ text: 'PHP', value: 'php' },
|
||||
{ text: 'Ruby', value: 'ruby' },
|
||||
{ text: 'Java', value: 'java' },
|
||||
{ text: 'C', value: 'c' },
|
||||
{ text: 'C#', value: 'csharp' },
|
||||
{ text: 'C++', value: 'cpp' }
|
||||
],
|
||||
browser_spellcheck: true,
|
||||
branding: false,
|
||||
object_resizing: true,
|
||||
elementpath: false,
|
||||
quickbars_insert_toolbar: false,
|
||||
toolbar_mode: 'sliding',
|
||||
color_default_background: 'yellow',
|
||||
link_default_target: 'external',
|
||||
mobile: {
|
||||
menubar: 'file edit view insert format table'
|
||||
},
|
||||
link_target_list: [
|
||||
{ title: 'New page', value: 'external' },
|
||||
{ title: 'Same page', value: '_self' }
|
||||
],
|
||||
style_formats: [
|
||||
{
|
||||
title: 'Headers',
|
||||
items: [
|
||||
{ title: 'Header 1', format: 'h1' },
|
||||
{ title: 'Header 2', format: 'h2' },
|
||||
{ title: 'Header 3', format: 'h3' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Inline',
|
||||
items: [
|
||||
{ title: 'Bold', icon: 'bold', format: 'bold' },
|
||||
{ title: 'Italic', icon: 'italic', format: 'italic' },
|
||||
{ title: 'Underline', icon: 'underline', format: 'underline' },
|
||||
{ title: 'Strikethrough', icon: 'strike-through', format: 'strikethrough' },
|
||||
{ title: 'Superscript', icon: 'superscript', format: 'superscript' },
|
||||
{ title: 'Subscript', icon: 'subscript', format: 'subscript' },
|
||||
{ title: 'Code', icon: 'sourcecode', format: 'code' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Blocks',
|
||||
items: [
|
||||
{ title: 'Paragraph', format: 'p' },
|
||||
{ title: 'Blockquote', format: 'blockquote' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Alignment',
|
||||
items: [
|
||||
{ title: 'Left', icon: 'align-left', format: 'alignleft' },
|
||||
{ title: 'Center', icon: 'align-center', format: 'aligncenter' },
|
||||
{ title: 'Right', icon: 'align-right', format: 'alignright' },
|
||||
{ title: 'Justify', icon: 'align-justify', format: 'alignjustify' }
|
||||
]
|
||||
}
|
||||
],
|
||||
init_instance_callback: (editor) => {
|
||||
const editorContainer = editor.getContainer();
|
||||
editorContainer.classList.add('tox-tinymce--loaded');
|
||||
editorContainer.classList.add('!h-[360px]');
|
||||
document.querySelectorAll('.tox-tinymce-aux').forEach((aux) => {
|
||||
aux.style.zIndex = '5000';
|
||||
});
|
||||
|
||||
this.initCssOverrides(editor);
|
||||
|
||||
SmartAnnotation.init($(editor.contentDocument.activeElement), false);
|
||||
},
|
||||
setup: (editor) => {
|
||||
editor.on('init', () => {
|
||||
editor.setContent(this.modelValue);
|
||||
});
|
||||
editor.on('change', () => {
|
||||
this.$emit('update:modelValue', editor.getContent());
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (tinyMCE.activeEditor) {
|
||||
tinyMCE.activeEditor.remove();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initCssOverrides(editor) {
|
||||
const editorIframe = $(`#${editor.id}`).next().find('.tox-edit-area iframe');
|
||||
const primaryColor = '#104da9';
|
||||
editorIframe.contents().find('head').append(`<style type="text/css">
|
||||
img::-moz-selection{background:0 0}
|
||||
img::selection{background:0 0}
|
||||
.mce-content-body img[data-mce-selected]{outline:2px solid ${primaryColor}}
|
||||
.mce-content-body div.mce-resizehandle{background:transparent;border-color:transparent;box-sizing:border-box;height:10px;width:10px; position:absolute}
|
||||
.mce-content-body div.mce-resizehandle:hover{background:transparent}
|
||||
.mce-content-body div#mceResizeHandlenw{border-left: 2px solid ${primaryColor}; border-top: 2px solid ${primaryColor}}
|
||||
.mce-content-body div#mceResizeHandlene{border-right: 2px solid ${primaryColor}; border-top: 2px solid ${primaryColor}}
|
||||
.mce-content-body div#mceResizeHandlesw{border-left: 2px solid ${primaryColor}; border-bottom: 2px solid ${primaryColor}}
|
||||
.mce-content-body div#mceResizeHandlese{border-right: 2px solid ${primaryColor}; border-bottom: 2px solid ${primaryColor}}
|
||||
h1 {font-size: 24px !important }
|
||||
h2 {font-size: 18px !important }
|
||||
h3 {font-size: 16px !important }
|
||||
</style>`);
|
||||
editorIframe.contents().find('head').append($('#font-css-pack').clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -14,5 +14,6 @@
|
|||
:archived="<%= @project.archived?%>"
|
||||
/>
|
||||
</div>
|
||||
<%= render 'shared/tiny_mce_packs' %>
|
||||
<%= javascript_include_tag 'vue_experiments_list' %>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<% provide(:sidebar_url, sidebar_team_path(current_team, project_folder_id: current_folder&.id)) %>
|
||||
<% provide(:container_class, 'no-second-nav-container') %>
|
||||
|
||||
|
||||
<div id="projectsWrapper" class="content-pane flexible projects-index <%= projects_view_mode %>" data-view-mode="<%= projects_view_mode %>" data-e2e="e2e-projects-container">
|
||||
<%= render partial: 'projects/header', locals: { current_folder: current_folder} %>
|
||||
|
||||
|
@ -21,6 +22,7 @@
|
|||
move-to-url="<%= move_to_project_folders_path %>"
|
||||
/>
|
||||
</div>
|
||||
<%= render 'shared/tiny_mce_packs' %>
|
||||
<%= javascript_include_tag 'vue_projects_list' %>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue