ArticleTextBodyStep UI mostly works

pull/10/head
Jens Pelzetter 2021-05-11 21:04:20 +02:00
parent b526f9eac2
commit 62d98af857
9 changed files with 361 additions and 74 deletions

View File

@ -74,6 +74,7 @@ public class ContentSectionApplication extends Application {
classes.add(DocumentWorkflowController.class); classes.add(DocumentWorkflowController.class);
classes.addAll(getAuthoringSteps()); classes.addAll(getAuthoringSteps());
classes.addAll(getAuthoringStepResources());
classes.add(IsAuthenticatedFilter.class); classes.add(IsAuthenticatedFilter.class);
@ -89,4 +90,12 @@ public class ContentSectionApplication extends Application {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
private Set<Class<?>> getAuthoringStepResources() {
return authoringSteps
.stream()
.map(MvcAuthoringSteps::getResourceClasses)
.flatMap(Set::stream)
.collect(Collectors.toSet());
}
} }

View File

@ -20,6 +20,7 @@ package org.librecms.ui.contentsections.documents;
import org.librecms.ui.contenttypes.MvcArticlePropertiesStep; import org.librecms.ui.contenttypes.MvcArticlePropertiesStep;
import org.librecms.ui.contenttypes.MvcArticleTextBodyStep; import org.librecms.ui.contenttypes.MvcArticleTextBodyStep;
import org.librecms.ui.contenttypes.MvcArticleTextBodyStepResources;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -46,4 +47,12 @@ public class CmsMvcAuthoringSteps implements MvcAuthoringSteps {
return classes; return classes;
} }
@Override
public Set<Class<?>> getResourceClasses() {
final Set<Class<?>> classes = new HashSet<>();
classes.add(MvcArticleTextBodyStepResources.class);
return classes;
}
} }

View File

@ -32,6 +32,12 @@ import javax.ws.rs.Path;
*/ */
public interface MvcAuthoringStep { public interface MvcAuthoringStep {
/**
* Returns the class implementing the step. This method is used by CCM is to
* get the correct class instead of a CDI proxy.
*
* @return The class implementing the step.
*/
Class<? extends MvcAuthoringStep> getStepClass(); Class<? extends MvcAuthoringStep> getStepClass();
ContentSection getContentSection() throws ContentSectionNotFoundException; ContentSection getContentSection() throws ContentSectionNotFoundException;
@ -52,7 +58,9 @@ public interface MvcAuthoringStep {
* admin priviliges for the content section of the item.</li> * admin priviliges for the content section of the item.</li>
* </ul> * </ul>
* *
* @return {@code true} if the current user can edit the document/item, {@false} otherwise. * @return {@code true} if the current user can edit the document/item, {
*
* @false} otherwise.
*/ */
boolean getCanEdit(); boolean getCanEdit();

View File

@ -18,6 +18,7 @@
*/ */
package org.librecms.ui.contentsections.documents; package org.librecms.ui.contentsections.documents;
import java.util.Collections;
import java.util.Set; import java.util.Set;
/** /**
@ -34,8 +35,13 @@ public interface MvcAuthoringSteps {
public static final String DOCUMENT_PATH_PATH_PARAM_NAME = "documentPath"; public static final String DOCUMENT_PATH_PATH_PARAM_NAME = "documentPath";
public static final String DOCUMENT_PATH_PATH_PARAM = DOCUMENT_PATH_PATH_PARAM_NAME + ":(.+)?"; public static final String DOCUMENT_PATH_PATH_PARAM
= DOCUMENT_PATH_PATH_PARAM_NAME + ":(.+)?";
Set<Class<?>> getClasses(); Set<Class<?>> getClasses();
default Set<Class<?>> getResourceClasses() {
return Collections.emptySet();
}
} }

View File

@ -226,38 +226,38 @@ public class MvcArticleTextBodyStep extends AbstractMvcAuthoringStep {
} }
} }
@GET // @GET
// @Path("/{locale}/@view") //// @Path("/{locale}/@view")
@Path("/variants/{locale}") // @Path("/variants/{locale}")
@Transactional(Transactional.TxType.REQUIRED) // @Transactional(Transactional.TxType.REQUIRED)
public String viewTextValue( // public String viewTextValue(
@PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM) // @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
final String sectionIdentifier, // final String sectionIdentifier,
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME) // @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
final String documentPath, // final String documentPath,
@PathParam("locale") final String localeParam // @PathParam("locale") final String localeParam
) { // ) {
try { // try {
init(); // init();
} catch (ContentSectionNotFoundException ex) { // } catch (ContentSectionNotFoundException ex) {
return ex.showErrorMessage(); // return ex.showErrorMessage();
} catch (DocumentNotFoundException ex) { // } catch (DocumentNotFoundException ex) {
return ex.showErrorMessage(); // return ex.showErrorMessage();
} // }
//
if (itemPermissionChecker.canEditItem(getArticle())) { // if (itemPermissionChecker.canEditItem(getArticle())) {
selectedLocale = new Locale(localeParam).toString(); // selectedLocale = new Locale(localeParam).toString();
//
// return "org/librecms/ui/contenttypes/article/article-text/view.xhtml"; //// return "org/librecms/ui/contenttypes/article/article-text/view.xhtml";
return getTextValues().get(localeParam); // return getTextValues().get(localeParam);
} else { // } else {
return documentUi.showAccessDenied( // return documentUi.showAccessDenied(
getContentSection(), // getContentSection(),
getArticle(), // getArticle(),
articleMessageBundle.getMessage("article.edit.denied") // articleMessageBundle.getMessage("article.edit.denied")
); // );
} // }
} // }
@GET @GET

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2021 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.librecms.ui.contenttypes;
import org.librecms.contentsection.ContentItem;
import org.librecms.contentsection.ContentItemRepository;
import org.librecms.contentsection.ContentSection;
import org.librecms.contenttypes.Article;
import org.librecms.ui.contentsections.ContentSectionsUi;
import org.librecms.ui.contentsections.ItemPermissionChecker;
import org.librecms.ui.contentsections.documents.MvcAuthoringSteps;
import java.util.Locale;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Path(MvcAuthoringSteps.PATH_PREFIX + "article-text-resources")
public class MvcArticleTextBodyStepResources {
/**
* Used for retrieving and saving the article.
*/
@Inject
private ContentItemRepository itemRepo;
@Inject
private ContentSectionsUi sectionsUi;
@Inject
private ItemPermissionChecker itemPermissionChecker;
@GET
// @Path("/{locale}/@view")
@Path("/variants/{locale}")
@Transactional(Transactional.TxType.REQUIRED)
public String viewTextValue(
@PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
final String sectionIdentifier,
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
final String documentPathParam,
@PathParam("locale") final String localeParam
) {
// try {
// init();
// } catch (ContentSectionNotFoundException ex) {
// return ex.showErrorMessage();
// } catch (DocumentNotFoundException ex) {
// return ex.showErrorMessage();
// }
final ContentSection contentSection = sectionsUi
.findContentSection(sectionIdentifier)
.orElseThrow(
() -> new NotFoundException()
);
final ContentItem document = itemRepo
.findByPath(contentSection, documentPathParam)
.orElseThrow(
() -> new NotFoundException()
);
if (!(document instanceof Article)) {
throw new NotFoundException();
}
final Article article = (Article) document;
if (itemPermissionChecker.canEditItem(article)) {
return article.getText().getValue(new Locale(localeParam));
} else {
throw new ForbiddenException();
}
}
}

View File

@ -217,7 +217,7 @@
</div> </div>
</template> </template>
<template id="cms-editor-msg-save-successful"> <template id="cms-editor-msg-save-successful">
<div class="alert alert-danger" role="alert"> <div class="alert alert-success" role="alert">
#{cc.attrs.messageSaveSuccessful} #{cc.attrs.messageSaveSuccessful}
</div> </div>
</template> </template>
@ -354,6 +354,7 @@
<button class="btn btn-primary cms-editor-edit-button" <button class="btn btn-primary cms-editor-edit-button"
data-edit-dialog="#{cc.attrs.editorId}-edit-dialog" data-edit-dialog="#{cc.attrs.editorId}-edit-dialog"
data-variant-url="#{cc.attrs.variantUrl}/#{variant.locale}" data-variant-url="#{cc.attrs.variantUrl}/#{variant.locale}"
data-save-url="#{cc.attrs.editMethod}/#{variant.locale}"
type="button"> type="button">
<bootstrap:svgIcon icon="pen" /> <bootstrap:svgIcon icon="pen" />
<span class="sr-only"> <span class="sr-only">
@ -440,34 +441,34 @@
</div> </div>
<div aria-describedby="#{cc.attrs.editorId}-view-dialog-title" <div aria-describedby="#{cc.attrs.editorId}-view-dialog-title"
aria-hidden="true" aria-hidden="true"
class="modal modal-xl fade" class="modal fade"
data-backdrop="static" data-backdrop="static"
id="#{cc.attrs.editorId}-view-dialog" id="#{cc.attrs.editorId}-view-dialog"
tabindex="-1"> tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<c:choose> <c:choose>
<c:when test="#{cc.attrs.headingLevel == 1}"> <c:when test="#{cc.attrs.headingLevel == 1}">
<h2 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.removeDialogTitle}</h2> <h2 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.viewDialogTitle}</h2>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 2}"> <c:when test="#{cc.attrs.headingLevel == 2}">
<h3 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.removeDialogTitle}</h3> <h3 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.viewDialogTitle}</h3>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 3}"> <c:when test="#{cc.attrs.headingLevel == 3}">
<h4 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.removeDialogTitle}</h4> <h4 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.viewDialogTitle}</h4>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 4}"> <c:when test="#{cc.attrs.headingLevel == 4}">
<h5 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.removeDialogTitle}</h5> <h5 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.viewDialogTitle}</h5>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 5}"> <c:when test="#{cc.attrs.headingLevel == 5}">
<h6 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.removeDialogTitle}</h6> <h6 id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.viewDialogTitle}</h6>
</c:when> </c:when>
<c:otherwise> <c:otherwise>
<div id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.removeDialogTitle}</div> <div id="#{cc.attrs.editorId}-view-dialog-title">#{cc.attrs.viewDialogTitle}</div>
</c:otherwise> </c:otherwise>
</c:choose> </c:choose>
<button aria-label="#{cc.attrs.removeDialogCancelLabel}" <button aria-label="#{cc.attrs.viewDialogCancelLabel}"
class="close" class="close"
data-dismiss="modal" data-dismiss="modal"
type="button" > type="button" >
@ -488,42 +489,41 @@
</div> </div>
<div aria-describedby="#{cc.attrs.editorId}-edit-dialog-title" <div aria-describedby="#{cc.attrs.editorId}-edit-dialog-title"
aria-hidden="true" aria-hidden="true"
class="modal modal-xl fade" class="modal fade"
data-backdrop="static" data-backdrop="static"
id="#{cc.attrs.editorId}-edit-dialog" id="#{cc.attrs.editorId}-edit-dialog"
tabindex="-1"> tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<c:choose> <c:choose>
<c:when test="#{cc.attrs.headingLevel == 1}"> <c:when test="#{cc.attrs.headingLevel == 1}">
<h2 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.removeDialogTitle}</h2> <h2 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.editDialogTitle}</h2>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 2}"> <c:when test="#{cc.attrs.headingLevel == 2}">
<h3 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.removeDialogTitle}</h3> <h3 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.editDialogTitle}</h3>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 3}"> <c:when test="#{cc.attrs.headingLevel == 3}">
<h4 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.removeDialogTitle}</h4> <h4 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.editDialogTitle}</h4>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 4}"> <c:when test="#{cc.attrs.headingLevel == 4}">
<h5 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.removeDialogTitle}</h5> <h5 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.editDialogTitle}</h5>
</c:when> </c:when>
<c:when test="#{cc.attrs.headingLevel == 5}"> <c:when test="#{cc.attrs.headingLevel == 5}">
<h6 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.removeDialogTitle}</h6> <h6 id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.editDialogTitle}</h6>
</c:when> </c:when>
<c:otherwise> <c:otherwise>
<div id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.removeDialogTitle}</div> <div id="#{cc.attrs.editorId}-edit-dialog-title">#{cc.attrs.editDialogTitle}</div>
</c:otherwise> </c:otherwise>
</c:choose> </c:choose>
<button aria-label="#{cc.attrs.removeDialogCancelLabel}" <button aria-label="#{cc.attrs.editDialogCancelLabel}"
class="close" class="close"
data-dismiss="modal"
type="button" > type="button" >
<bootstrap:svgIcon icon="x" /> <bootstrap:svgIcon icon="x" />
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="cms-tiptap-editor-buttons"> <div class="cms-tiptap-editor-buttons mb-1">
<button class="btn btn-outline-dark tiptap-emph" <button class="btn btn-outline-dark tiptap-emph"
type="button"> type="button">
<bootstrap:svgIcon icon="type-italic" /> <bootstrap:svgIcon icon="type-italic" />
@ -539,17 +539,17 @@
</span> </span>
</button> </button>
</div> </div>
<div class="cms-tiptap-editor"> <div class="cms-tiptap-editor border">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-warning" <button class="btn btn-warning cms-editor-cancel-button"
data-dismiss="modal"
type="button" > type="button" >
#{cc.attrs.editDialogCancelLabel} #{cc.attrs.editDialogCancelLabel}
</button> </button>
<button type="submit" class="btn btn-success"> <button class="btn btn-success cms-editor-save-button"
type="button" >
#{cc.attrs.editDialogSubmitLabel} #{cc.attrs.editDialogSubmitLabel}
</button> </button>
</div> </div>

View File

@ -35,7 +35,7 @@
removeMethod="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text/remove" removeMethod="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text/remove"
title="#{CmsArticleMessageBundle['text.editor.header']}" title="#{CmsArticleMessageBundle['text.editor.header']}"
unusedLocales="#{CmsArticleTextBodyStep.unusedLocales}" unusedLocales="#{CmsArticleTextBodyStep.unusedLocales}"
variantUrl="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text/variants" variantUrl="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@article-text-resources/variants"
variants="#{CmsArticleTextBodyStep.variants}" variants="#{CmsArticleTextBodyStep.variants}"
/> />

View File

@ -3,6 +3,12 @@ import * as $ from "jquery";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
function showMessage(messageId: string) {
const template = document.querySelector(messageId) as HTMLTemplateElement;
const msg = template.content.cloneNode(true);
document.querySelector(".cms-editor-messages").append(msg);
}
document.addEventListener("DOMContentLoaded", function (event) { document.addEventListener("DOMContentLoaded", function (event) {
document document
.querySelector( .querySelector(
@ -33,25 +39,171 @@ document.addEventListener("DOMContentLoaded", function (event) {
viewDialogBody.textContent = text; viewDialogBody.textContent = text;
$(`#${viewDialogId}`).modal('toggle'); $(`#${viewDialogId}`).modal("toggle");
}) })
.catch(err => { .catch(err => {
const template = document.querySelector( showMessage(
"#cms-editor-msg-variant-load-failed" "#cms-editor-msg-variant-load-failed"
) as HTMLTemplateElement; );
const msg = template.content.cloneNode(true);
document
.querySelector(".cms-editor-messages")
.append(msg);
}); });
} else {
showMessage("#cms-editor-msg-variant-load-failed");
} }
}) })
.catch(err => { .catch(err => {
const template = document.querySelector( showMessage("#cms-editor-msg-variant-load-failed");
});
});
document
.querySelector(
".cms-editor .cms-editor-variants .cms-editor-edit-button"
)
.addEventListener("click", function (event) {
event.preventDefault();
const target = event.currentTarget as Element;
const variantUrl = target.getAttribute("data-variant-url");
const editDialogId = target.getAttribute("data-edit-dialog");
const saveUrl = target.getAttribute("data-save-url");
fetch(variantUrl, {
method: "GET",
credentials: "include"
})
.then(response => {
if (response.ok) {
response
.text()
.then(text => {
const editDialog = document.querySelector(
`#${editDialogId}`
);
const tiptapDiv = editDialog.querySelector(
".modal-body .cms-tiptap-editor"
);
if (!tiptapDiv) {
console.warn("tiptapDiv is null");
}
const editor = new Editor({
element: tiptapDiv,
extensions: [StarterKit],
content: text
});
const buttonsDiv = editDialog.querySelector(
".cms-tiptap-editor-buttons"
);
if (!buttonsDiv) {
console.warn("buttonsDiv is null.");
}
const emphButton = buttonsDiv.querySelector(
".tiptap-emph"
);
if (!emphButton) {
console.warn("emphButton not found.");
}
emphButton.addEventListener("click", event => {
event.preventDefault();
editor.chain().focus().toggleItalic().run();
});
const strongEmphButton = buttonsDiv.querySelector(
".tiptap-strong-emph"
);
if (!strongEmphButton) {
console.warn("strongEmphButton not found.");
}
strongEmphButton.addEventListener(
"click",
event => {
event.preventDefault();
editor
.chain()
.focus()
.toggleBold()
.run();
}
);
const closeButton = editDialog.querySelector(
".modal-header .close"
);
const cancelButton = editDialog.querySelector(
".modal-footer .cms-editor-cancel-button"
);
const saveButton = editDialog.querySelector(
".modal-footer .cms-editor-save-button"
);
closeButton.addEventListener("click", event => {
editor.chain().clearContent();
editor.destroy();
$(`#${editDialogId}`).modal("toggle");
});
cancelButton.addEventListener(
"click",
event => {
editor.chain().clearContent();
editor.destroy();
$(`#${editDialogId}`).modal("toggle");
}
);
saveButton.addEventListener("click", event => {
const html = editor.getHTML();
const formData = new FormData();
formData.append("value", html);
fetch(saveUrl, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: formData
})
.then(saveResponse => {
if (saveResponse.ok) {
showMessage(
"#cms-editor-msg-save-successful"
);
} else {
showMessage(
"#cms-editor-msg-save-failed"
);
}
$(`#${editDialogId}`).modal(
"toggle"
);
})
.catch(err => {
showMessage(
"#cms-editor-msg-save-failed"
);
console.error(err);
$(`#${editDialogId}`).modal(
"toggle"
);
});
});
$(`#${editDialogId}`).modal("toggle");
})
.catch(err => {
showMessage(
"#cms-editor-msg-variant-load-failed" "#cms-editor-msg-variant-load-failed"
) as HTMLTemplateElement; );
const msg = template.content.cloneNode(true); console.error(err);
document.querySelector(".cms-editor-messages").append(msg); });
} else {
showMessage("#cms-editor-msg-variant-load-failed");
}
})
.catch(err => {
showMessage("#cms-editor-msg-variant-load-failed");
console.error(err);
}); });
}); });