mirror of
https://github.com/documize/community.git
synced 2025-07-19 05:09:42 +02:00
Introduce new Tabular editor with CSV import support
Closes #211 and #202 An all-new tabular editor has been added -- this replaces the previous tabular editor. Better formatting options. CSV data can also be imported straight into the table.
This commit is contained in:
parent
ed99b0c9f3
commit
25c247e99b
18 changed files with 2224 additions and 14 deletions
|
@ -39,6 +39,7 @@ type TypeMeta struct {
|
|||
PageType string `json:"pageType"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Retired bool `json:"retired"` // no new inserts of this type, just edits
|
||||
Preview bool `json:"preview"` // coming soon!
|
||||
Callback func(*env.Runtime, *store.Store, http.ResponseWriter, *http.Request) error `json:"-"`
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/documize/community/domain/section/plantuml"
|
||||
"github.com/documize/community/domain/section/provider"
|
||||
"github.com/documize/community/domain/section/table"
|
||||
"github.com/documize/community/domain/section/tabular"
|
||||
"github.com/documize/community/domain/section/trello"
|
||||
"github.com/documize/community/domain/section/wysiwyg"
|
||||
"github.com/documize/community/domain/store"
|
||||
|
@ -38,6 +39,7 @@ func Register(rt *env.Runtime, s *store.Store) {
|
|||
// provider.Register("github", &github.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("markdown", &markdown.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("papertrail", &papertrail.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("tabular", &tabular.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("table", &table.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("code", &code.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("trello", &trello.Provider{Runtime: rt, Store: s})
|
||||
|
|
|
@ -35,6 +35,7 @@ func (*Provider) Meta() provider.TypeMeta {
|
|||
section.ContentType = "table"
|
||||
section.PageType = "section"
|
||||
section.Order = 9996
|
||||
section.Retired = true
|
||||
|
||||
return section
|
||||
}
|
||||
|
|
55
domain/section/tabular/tabular.go
Normal file
55
domain/section/tabular/tabular.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
package tabular
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/domain/section/provider"
|
||||
"github.com/documize/community/domain/store"
|
||||
)
|
||||
|
||||
// Provider represents tabular
|
||||
type Provider struct {
|
||||
Runtime *env.Runtime
|
||||
Store *store.Store
|
||||
}
|
||||
|
||||
// Meta describes us
|
||||
func (*Provider) Meta() provider.TypeMeta {
|
||||
section := provider.TypeMeta{}
|
||||
|
||||
section.ID = "a77d2f73-2cb5-4f6d-bb21-7227a7a097f3"
|
||||
section.Title = "Tabular"
|
||||
section.Description = "Table with rows and columns"
|
||||
section.ContentType = "tabular"
|
||||
section.PageType = "section"
|
||||
section.Order = 9996
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
// Command stub.
|
||||
func (*Provider) Command(ctx *provider.Context, w http.ResponseWriter, r *http.Request) {
|
||||
provider.WriteEmpty(w)
|
||||
}
|
||||
|
||||
// Render returns data as-is (HTML).
|
||||
func (*Provider) Render(ctx *provider.Context, config, data string) string {
|
||||
return data
|
||||
}
|
||||
|
||||
// Refresh just sends back data as-is.
|
||||
func (*Provider) Refresh(ctx *provider.Context, config, data string) string {
|
||||
return data
|
||||
}
|
|
@ -62,6 +62,7 @@ module.exports = {
|
|||
"userLogin": true,
|
||||
"Keycloak": true,
|
||||
"slug": true,
|
||||
"iziToast": true
|
||||
"iziToast": true,
|
||||
"Papa": true,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -24,6 +24,8 @@ export default Component.extend({
|
|||
defaultTable: '<table class="wysiwyg-table" style="width: 100%;"><thead><tr><th><br></th><th><br></th><th><br></th><th><br></th></tr></thead><tbody><tr><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td></tr><tr><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td></tr><tr><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td></tr></tbody></table>',
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.set('pageBody', this.get('meta.rawBody'));
|
||||
|
||||
if (is.empty(this.get('pageBody'))) {
|
||||
|
@ -32,7 +34,10 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
let id = '#' + this.get('editorId');
|
||||
|
||||
$(id).froalaEditor({
|
||||
toolbarButtons: [],
|
||||
toolbarInline: true,
|
||||
|
@ -47,6 +52,8 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$('#' + this.get('editorId')).off('froalaEditor.contentChanged');
|
||||
},
|
||||
|
||||
|
|
245
gui/app/components/section/tabular/type-editor.js
Normal file
245
gui/app/components/section/tabular/type-editor.js
Normal file
|
@ -0,0 +1,245 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import $ from 'jquery';
|
||||
import { computed, set } from '@ember/object';
|
||||
import { schedule } from '@ember/runloop';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Modals from '../../../mixins/modal';
|
||||
import Notifier from '../../../mixins/notifier';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(Modals, Notifier, {
|
||||
appMeta: service(),
|
||||
link: service(),
|
||||
pageBody: '',
|
||||
editorId: computed('page', function () {
|
||||
let page = this.get('page');
|
||||
return `tabular-editor-${page.id}`;
|
||||
}),
|
||||
modalId: computed('page', function () {
|
||||
let page = this.get('page');
|
||||
return `tabular-editor-modal-${page.id}`;
|
||||
}),
|
||||
importData: '',
|
||||
importOption: null,
|
||||
defaultTable: '<table class="wysiwyg-table" style="width: 100%;"><thead><tr><th><br></th><th><br></th><th><br></th><th><br></th></tr></thead><tbody><tr><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td></tr><tr><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td></tr><tr><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td><td style="width: 25.0000%;"><br></td></tr></tbody></table>',
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.set('pageBody', this.get('meta.rawBody'));
|
||||
|
||||
if (is.empty(this.get('pageBody'))) {
|
||||
this.set('pageBody', this.get('defaultTable'));
|
||||
}
|
||||
|
||||
this.set('importOption', {
|
||||
headers: true,
|
||||
parseTypes: false,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.addEditor();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.removeEditor();
|
||||
},
|
||||
|
||||
addEditor() {
|
||||
schedule('afterRender', () => {
|
||||
let options = {
|
||||
cache_suffix: '?v=492',
|
||||
selector: '#' + this.get('editorId'),
|
||||
relative_urls: false,
|
||||
browser_spellcheck: true,
|
||||
gecko_spellcheck: false,
|
||||
statusbar: false,
|
||||
inline: true,
|
||||
paste_data_images: true,
|
||||
images_upload_handler: function (blobInfo, success, failure) { // eslint-disable-line no-unused-vars
|
||||
success("data:" + blobInfo.blob().type + ";base64," + blobInfo.base64());
|
||||
},
|
||||
image_advtab: true,
|
||||
media_live_embeds: true,
|
||||
theme: 'modern',
|
||||
skin: 'lightgray-gradient',
|
||||
entity_encoding: 'raw',
|
||||
extended_valid_elements: 'b,i,b/strong,i/em',
|
||||
fontsize_formats:
|
||||
'8px 10px 12px 14px 15px 16px 18px 20px 22px 24px 26px 28px 30px 32px 34px 36px 38px 40px 42px 44px 46px 48px 50px 52px 54px 56px 58px 60px 70px 80px 90px 100px',
|
||||
formats: {
|
||||
bold: {
|
||||
inline: 'b'
|
||||
},
|
||||
italic: {
|
||||
inline: 'i'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print hr anchor pagebreak',
|
||||
'searchreplace wordcount visualblocks visualchars code codesample fullscreen',
|
||||
'insertdatetime media nonbreaking save table directionality',
|
||||
'template paste textcolor colorpicker textpattern imagetools uploadimage'
|
||||
],
|
||||
menu: {},
|
||||
menubar: false,
|
||||
table_toolbar: '',
|
||||
toolbar1: 'table tabledelete | tableprops tablerowprops tablecellprops | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol',
|
||||
toolbar2: 'fontsizeselect | forecolor backcolor link unlink | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify',
|
||||
save_onsavecallback: function () {
|
||||
Mousetrap.trigger('ctrl+s');
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof tinymce === 'undefined') {
|
||||
$.getScript('/tinymce/tinymce.min.js?v=492', function () {
|
||||
window.tinymce.dom.Event.domLoaded = true;
|
||||
tinymce.baseURL = '//' + window.location.host + '/tinymce';
|
||||
tinymce.suffix = '.min';
|
||||
tinymce.init(options);
|
||||
});
|
||||
} else {
|
||||
tinymce.init(options);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeEditor() {
|
||||
tinymce.EditorManager.execCommand('mceRemoveEditor', true, this.get('editorId'));
|
||||
},
|
||||
|
||||
generateImportTable(results) {
|
||||
// nothing to import?
|
||||
if (is.undefined(results) || results.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let opts = this.get('importOption');
|
||||
|
||||
let table = '<table class="wysiwyg-table" style="width: 100%;"><thead><tr>';
|
||||
|
||||
// Setup the table headers
|
||||
if (opts.headers && is.array(results.meta.fields)) {
|
||||
// use headers from file
|
||||
results.meta.fields.forEach((header) => {
|
||||
table = table + '<th>' + header.trim() + '</th>';
|
||||
});
|
||||
} else {
|
||||
// create dummy headers
|
||||
for (let i = 1; i <= results.data[0].length; i++) {
|
||||
table = table + '<th>Column ' + i + '</th>';
|
||||
}
|
||||
}
|
||||
|
||||
table = table + '</tr></thead>'
|
||||
|
||||
// now convert data rows to table.
|
||||
table = table + '<tbody>'
|
||||
|
||||
results.data.forEach(row => {
|
||||
table = table + '<tr>';
|
||||
|
||||
if (is.array(row)) {
|
||||
row.forEach((cell) => {
|
||||
table = table + '<td>' + cell.trim() + '</td>';
|
||||
});
|
||||
} else {
|
||||
// convert Javascript object to array
|
||||
let cells = Object.values(row);
|
||||
|
||||
cells.forEach((cell) => {
|
||||
table = table + '<td>' + cell.trim() + '</td>';
|
||||
});
|
||||
}
|
||||
|
||||
table = table + '</tr>'
|
||||
});
|
||||
|
||||
table = table + '</tbody>'
|
||||
table = table + '</table>';
|
||||
|
||||
let editor = tinymce.EditorManager.get(this.get('editorId'));
|
||||
editor.setContent(table);
|
||||
},
|
||||
|
||||
actions: {
|
||||
onShowImportModal() {
|
||||
this.modalOpen('#' + this.get('modalId'), {show:true}, "#csv-data");
|
||||
},
|
||||
|
||||
onImport() {
|
||||
let csv = this.get('importData');
|
||||
let opts = this.get('importOption');
|
||||
|
||||
this.modalClose('#' + this.get('modalId'));
|
||||
|
||||
if (is.empty(csv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let results = Papa.parse(csv, {
|
||||
header: opts.headers,
|
||||
dynamicTyping: opts.parseTypes,
|
||||
skipEmptyLines: true,
|
||||
});
|
||||
|
||||
this.generateImportTable(results);
|
||||
},
|
||||
|
||||
onInsertLink(link) {
|
||||
let editor = tinymce.EditorManager.get(this.get('editorId'));
|
||||
let userSelection = editor.selection.getContent();
|
||||
|
||||
if (is.not.empty(userSelection)) {
|
||||
set(link, 'title', userSelection);
|
||||
}
|
||||
|
||||
let linkHTML = this.get('link').buildLink(link);
|
||||
editor.insertContent(linkHTML);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
isDirty() {
|
||||
let editor = tinymce.EditorManager.get(this.get('editorId'));
|
||||
return (
|
||||
is.not.undefined(tinymce) &&
|
||||
is.not.undefined(editor) &&
|
||||
editor.isDirty()
|
||||
);
|
||||
},
|
||||
|
||||
onCancel() {
|
||||
let cb = this.get('onCancel');
|
||||
cb();
|
||||
},
|
||||
|
||||
onAction(title) {
|
||||
let page = this.get('page');
|
||||
let meta = this.get('meta');
|
||||
let editor = tinymce.EditorManager.get(this.get('editorId'));
|
||||
|
||||
page.set('title', title);
|
||||
meta.set('rawBody', editor.getContent());
|
||||
|
||||
let cb = this.get('onAction');
|
||||
cb(page, meta);
|
||||
}
|
||||
}
|
||||
});
|
14
gui/app/components/section/tabular/type-renderer.js
Normal file
14
gui/app/components/section/tabular/type-renderer.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||
//
|
||||
// This software (Documize Community Edition) is licensed under
|
||||
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
//
|
||||
// You can operate outside the AGPL restrictions by purchasing
|
||||
// Documize Enterprise Edition and obtaining a commercial license
|
||||
// by contacting <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({ });
|
|
@ -107,7 +107,7 @@ export default Component.extend({
|
|||
],
|
||||
menu: {},
|
||||
menubar: false,
|
||||
toolbar1: 'formatselect fontsizeselect | bold italic underline strikethrough superscript subscript | forecolor backcolor link unlink',
|
||||
toolbar1: 'formatselect fontsizeselect | bold italic underline strikethrough superscript subscript blockquote | forecolor backcolor link unlink',
|
||||
toolbar2: 'outdent indent bullist numlist | alignleft aligncenter alignright alignjustify | table uploadimage image media codesample',
|
||||
save_onsavecallback: function () {
|
||||
Mousetrap.trigger('ctrl+s');
|
||||
|
@ -129,11 +129,8 @@ export default Component.extend({
|
|||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
tinymce.EditorManager.execCommand(
|
||||
'mceRemoveEditor',
|
||||
true,
|
||||
this.get('editorId')
|
||||
);
|
||||
|
||||
tinymce.EditorManager.execCommand('mceRemoveEditor', true, this.get('editorId'));
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -333,6 +333,7 @@ let constants = EmberObject.extend({
|
|||
Edit: 'Edit',
|
||||
Export: 'Export',
|
||||
File: 'File',
|
||||
Import: 'Import',
|
||||
Insert: 'Insert',
|
||||
Invite: 'Invite',
|
||||
Join: 'Join',
|
||||
|
|
|
@ -21,6 +21,7 @@ export default Model.extend({
|
|||
description: attr('string'),
|
||||
iconFont: attr('string'),
|
||||
iconFile: attr('string'),
|
||||
retired: attr('boolean'),
|
||||
|
||||
hasImage: computed('iconFont', 'iconFile', function () {
|
||||
return this.get('iconFile').length > 0;
|
||||
|
|
|
@ -10,11 +10,15 @@
|
|||
padding: 5px 7px !important;
|
||||
border: 1px solid map-get($gray-shades, 300) !important;
|
||||
background-color: map-get($gray-shades, 100);
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px 7px !important;
|
||||
border: 1px solid map-get($gray-shades, 300) !important;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
|
||||
p {
|
||||
margin: 0 !important;
|
||||
|
|
|
@ -12,12 +12,14 @@
|
|||
|
||||
<ul class="options">
|
||||
{{#each sections as |section|}}
|
||||
<li class="preset-option" {{action "onInsertSection" section}}>
|
||||
<div class="icon">
|
||||
<img class="img" src="/sections/{{section.contentType}}.png" srcset="/sections/{{section.contentType}}@2x.png">
|
||||
</div>
|
||||
<div class="name">{{section.title}}</div>
|
||||
</li>
|
||||
{{#unless section.retired}}
|
||||
<li class="preset-option" {{action "onInsertSection" section}}>
|
||||
<div class="icon">
|
||||
<img class="img" src="/sections/{{section.contentType}}.png" srcset="/sections/{{section.contentType}}@2x.png">
|
||||
</div>
|
||||
<div class="name">{{section.title}}</div>
|
||||
</li>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
{{#if hasBlocks}}
|
||||
<br>
|
||||
|
|
52
gui/app/templates/components/section/tabular/type-editor.hbs
Normal file
52
gui/app/templates/components/section/tabular/type-editor.hbs
Normal file
|
@ -0,0 +1,52 @@
|
|||
{{#section/base-editor-inline
|
||||
document=document
|
||||
folder=folder
|
||||
page=page
|
||||
blockMode=blockMode
|
||||
contentLinkerButton=true
|
||||
onInsertLink=(action "onInsertLink")
|
||||
isDirty=(action "isDirty")
|
||||
onCancel=(action "onCancel")
|
||||
onAction=(action "onAction")}}
|
||||
|
||||
<div class="text-right">
|
||||
{{ui/ui-button
|
||||
color=constants.Color.Yellow
|
||||
light=true
|
||||
label="IMPORT CSV"
|
||||
onClick=(action "onShowImportModal")}}
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=300}}
|
||||
|
||||
<div id={{editorId}} class="mousetrap wysiwyg wysiwyg-editor">
|
||||
{{{pageBody}}}
|
||||
</div>
|
||||
|
||||
<div id={{modalId}} class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">CSV To Table</div>
|
||||
<div class="modal-body">
|
||||
<p class="color-yellow-700">Note: existing table data will be replaced</p>
|
||||
<div class="form-group">
|
||||
<label for="csv-data">Paste CSV Data</label>
|
||||
{{textarea id="csv-data" value=importData class="form-control" rows="10"}}
|
||||
<small class="form-text text-muted">Common delimiters are supported</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>First Row Header</label>
|
||||
{{x-toggle value=importOption.headers size="medium" theme="light" onToggle=(action (mut importOption.headers))}}
|
||||
<small class="form-text text-muted">Enable if first row contains headers</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{ui/ui-button color=constants.Color.Gray light=true label=constants.Label.Cancel dismiss=true}}
|
||||
{{ui/ui-button-gap}}
|
||||
{{ui/ui-button color=constants.Color.Green light=true label=constants.Label.Import onClick=(action "onImport")}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/section/base-editor-inline}}
|
|
@ -0,0 +1 @@
|
|||
{{{page.body}}}
|
BIN
gui/public/sections/tabular.png
Normal file
BIN
gui/public/sections/tabular.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 396 B |
BIN
gui/public/sections/tabular@2x.png
Normal file
BIN
gui/public/sections/tabular@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 487 B |
1826
gui/vendor/papaparse.js
vendored
Executable file
1826
gui/vendor/papaparse.js
vendored
Executable file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue