[28167] Allow embedded table macros in WP description

This PR renders macros within WP descriptions. This has one drastic side
effect when embedded tables are rendered:

1. If the work package (lets asumme that ID #1) is returned in the
embedded table results, it is updated in the state/cache service.

2. The updated entry in the cache service results in refreshing the display fields

3. The display field will re-render the embedded table

4. Results are being re-fetched. This results in recursion into step 1

We can avoid that by assumming identical sources returned from the
backend to be identical work packages and in turn not updating those.
It's a hack, however.

https://community.openproject.com/wp/28167
pull/6505/head
Oliver Günther 6 years ago
parent f4301a398e
commit 2c98eda4f2
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 2
      app/assets/javascripts/vendor/ckeditor/ckeditor.js
  2. 2
      app/assets/javascripts/vendor/ckeditor/ckeditor.js.map
  3. 3
      app/assets/stylesheets/content/work_packages/inplace_editing/_edit_fields.sass
  4. 72
      frontend/src/app/components/ckeditor/ckeditor-setup.service.ts
  5. 16
      frontend/src/app/components/work-packages/work-package-cache.service.ts
  6. 8
      frontend/src/app/components/wp-edit/wp-edit-field/wp-edit-field.component.ts
  7. 7
      frontend/src/app/modules/fields/display/field-types/wp-display-formattable-field.module.ts
  8. 1
      frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.ts
  9. 45
      spec/features/wysiwyg/macros/embedded_tables_spec.rb

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -41,9 +41,6 @@
&:last-of-type
margin-bottom: 0
*
font-size: 14px
.read-value--html
*
// Adjust size of all members in the html

@ -4,7 +4,9 @@ import {Injectable} from "@angular/core";
export interface ICKEditorInstance {
getData():string;
setData(content:string):void;
model:any;
editing:any;
config:any;
@ -12,6 +14,7 @@ export interface ICKEditorInstance {
export interface ICKEditorStatic {
create(el:HTMLElement, config?:any):Promise<ICKEditorInstance>;
createCustomized(el:HTMLElement, config?:any):Promise<ICKEditorInstance>;
}
@ -20,7 +23,7 @@ export interface ICKEditorContext {
// Specific removing of plugins
removePlugins?:string[];
// Set of enabled macro plugins or false to disable all
macros?:false|string[];
macros?:'none'|'wp'|'full'|boolean|string[];
// context link to append on preview requests
previewContext?:string;
}
@ -34,8 +37,8 @@ declare global {
@Injectable()
export class CKEditorSetupService {
constructor(private PathHelper:PathHelperService) {
}
constructor(private PathHelper:PathHelperService) {
}
/**
* Create a CKEditor instance of the given type on the wrapper element.
@ -47,29 +50,46 @@ export class CKEditorSetupService {
* @param {ICKEditorContext} context
* @returns {Promise<ICKEditorInstance>}
*/
public create(type:'full'|'constrained', wrapper:HTMLElement, context:ICKEditorContext) {
const editor = type === 'constrained' ? window.OPConstrainedEditor : window.OPClassicEditor;
wrapper.classList.add(`ckeditor-type-${type}`);
public create(type:'full' | 'constrained', wrapper:HTMLElement, context:ICKEditorContext) {
const editor = type === 'constrained' ? window.OPConstrainedEditor : window.OPClassicEditor;
wrapper.classList.add(`ckeditor-type-${type}`);
return editor
.createCustomized(wrapper, {
openProject: {
context: context,
helpURL: this.PathHelper.textFormattingHelp(),
pluginContext: window.OpenProject.pluginContext.value
}
})
.then((editor) => {
// Allow custom events on wrapper to set/get data for debugging
jQuery(wrapper)
.on('op:ckeditor:setData', (event:any, data:string) => editor.setData(data))
.on('op:ckeditor:clear', (event:any) => editor.setData(' '))
.on('op:ckeditor:getData', (event:any, cb:any) => cb(editor.getData()));
return editor
.createCustomized(wrapper, {
openProject: this.createConfig(context)
})
.then((editor) => {
// Allow custom events on wrapper to set/get data for debugging
jQuery(wrapper)
.on('op:ckeditor:setData', (event:any, data:string) => editor.setData(data))
.on('op:ckeditor:clear', (event:any) => editor.setData(' '))
.on('op:ckeditor:getData', (event:any, cb:any) => cb(editor.getData()));
return editor;
})
.catch((error:any) => {
console.error(`Failed to setup CKEditor instance: ${error}`);
});
}
return editor;
})
.catch((error:any) => {
console.error(`Failed to setup CKEditor instance: ${error}`);
});
}
private createConfig(context:ICKEditorContext):any {
if (context.macros === 'none') {
context.macros = false;
}
else if (context.macros === 'wp') {
context.macros = [
'OPMacroToc',
'OPMacroEmbeddedTable',
'OPMacroWpButton'
];
} else {
context.macros = true;
}
return {
context: context,
helpURL: this.PathHelper.textFormattingHelp(),
pluginContext: window.OpenProject.pluginContext.value
};
}
}

@ -36,6 +36,7 @@ import {WorkPackageCollectionResource} from 'core-app/modules/hal/resources/wp-c
import {SchemaResource} from 'core-app/modules/hal/resources/schema-resource';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {Injectable} from '@angular/core';
import {debugLog} from "core-app/helpers/debug_output";
function getWorkPackageId(id:number | string):string {
return (id || '__new_work_package__').toString();
@ -53,21 +54,28 @@ export class WorkPackageCacheService extends StateCacheService<WorkPackageResour
}
public updateValue(id:string, val:WorkPackageResource) {
this.updateWorkPackageList([val]);
this.updateWorkPackageList([val], false);
}
updateWorkPackage(wp:WorkPackageResource) {
this.updateWorkPackageList([wp]);
this.updateWorkPackageList([wp], false);
}
updateWorkPackageList(list:WorkPackageResource[]) {
updateWorkPackageList(list:WorkPackageResource[], skipOnIdentical = true) {
for (var i of list) {
const wp = i;
const workPackageId = getWorkPackageId(wp.id);
const state = this.multiState.get(workPackageId);
// If the work package is new, ignore the schema
if (wp.isNew) {
this.multiState.get(workPackageId).putValue(wp);
state.putValue(wp);
continue;
}
// Check if the work package has changed
if (skipOnIdentical && state.hasValue() && _.isEqual(state.value!.$source, wp.$source)) {
debugLog('Skipping identical work package from updating');
continue;
}

@ -66,6 +66,7 @@ export class WorkPackageEditFieldComponent implements OnInit {
public fieldRenderer = new DisplayFieldRenderer(this.injector, 'single-view');
public editFieldContainerClass = editFieldContainerClass;
public active = false;
public rendered = false;
private $element:JQuery;
constructor(protected states:States,
@ -103,6 +104,7 @@ export class WorkPackageEditFieldComponent implements OnInit {
const el = this.fieldRenderer.render(this.resource, this.fieldName, null, this.displayPlaceholder);
this.displayContainer.nativeElement.innerHTML = '';
this.displayContainer.nativeElement.appendChild(el);
this.rendered = true;
}
public deactivate(focus:boolean = false) {
@ -118,7 +120,7 @@ export class WorkPackageEditFieldComponent implements OnInit {
public get resource() {
return this.wpEditing
.temporaryEditResource(this.workPackageId)
.getValueOr(this.wpEditFieldGroup.workPackage);
.getValueOr(this.workPackage);
}
public get isEditable() {
@ -133,9 +135,9 @@ export class WorkPackageEditFieldComponent implements OnInit {
return true;
}
// Skip activation if the user clicked on a link
// Skip activation if the user clicked on a link or within a macro
const target = jQuery(event.target);
if (target.closest('a', this.displayContainer.nativeElement).length > 0) {
if (target.closest('a,macro', this.displayContainer.nativeElement).length > 0) {
return true;
}

@ -28,9 +28,13 @@
import {DisplayField} from "core-app/modules/fields/display/display-field.module";
import {ExpressionService} from "../../../../../../common/expression.service";
import {ApplicationRef} from "@angular/core";
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
export class FormattableDisplayField extends DisplayField {
private readonly appRef = this.$injector.get(ApplicationRef);
public render(element:HTMLElement, displayText:string):void {
element.classList.add('read-value--html', 'wiki', 'highlight', '-multiline');
@ -39,6 +43,9 @@ export class FormattableDisplayField extends DisplayField {
element.innerHTML = '';
element.appendChild(span);
// Allow embeddable rendered content
DynamicBootstrapper.bootstrapOptionalEmbeddable(this.appRef, span);
}
public get isFormattable():boolean {

@ -71,6 +71,7 @@ export class FormattableEditField extends EditField {
const element = container.querySelector('.op-ckeditor-source-element') as HTMLElement;
const context = { resource: this.resource,
macros: 'wp' as 'wp',
previewContext: this.previewContext };
this.ckEditorSetup

@ -43,6 +43,51 @@ describe 'Wysiwyg embedded work package tables',
login_as(user)
end
describe 'in a work package description' do
let(:wp_page) { ::Pages::FullWorkPackage.new(work_package, project) }
let(:embedded_table) { ::Pages::EmbeddedWorkPackagesTable.new page.find('.work-packages--details--description') }
let(:editor) { ::Components::WysiwygEditor.new '.work-packages--details--description' }
before do
wp_page.visit!
wp_page.ensure_page_loaded
end
it 'can embed a table with the same work package listed' do
description_field = wp_page.work_package_field :description
description_field.activate!
editor.in_editor do |_container, editable|
editor.insert_macro 'Embed work package table'
# Keep all open as filter
modal.expect_open
modal.save
description_field.submit_by_click
wp_page.expect_and_dismiss_notification message: 'Successful update'
# Expect work package page in container
embedded_table.expect_work_package_listed work_package
embedded_subject = embedded_table.edit_field work_package, :subject
embedded_subject.update 'My new subject!'
wp_page.expect_and_dismiss_notification message: 'Successful update'
# Updates the outer same WP
wp_page.edit_field(:subject).expect_state_text 'My new subject!'
work_package.reload
expect(work_package.subject).to eq 'My new subject!'
# Updates the wp table
embedded_table.expect_work_package_listed work_package
embedded_subject = embedded_table.edit_field work_package, :subject
embedded_subject.expect_state_text 'My new subject!'
end
end
end
describe 'in wikis' do
describe 'creating a wiki page' do
before do

Loading…
Cancel
Save