sci-types-project: Managing sponsors.

pull/1/head
Jens Pelzetter 2022-05-14 16:59:56 +02:00
parent aaaa9d8277
commit 236866d93e
9 changed files with 383 additions and 48 deletions

View File

@ -29,7 +29,8 @@ public class SciProjectAuthoringSteps implements MvcAuthoringSteps {
SciProjectDescriptionStepResources.class, SciProjectDescriptionStepResources.class,
SciProjectDescriptionStepService.class, SciProjectDescriptionStepService.class,
SciProjectFundingStepResources.class, SciProjectFundingStepResources.class,
SciProjectFundingStepService.class SciProjectFundingStepService.class,
SciProjectFundingSponsors.class
); );
} }

View File

@ -0,0 +1,102 @@
package org.scientificcms.contenttypes.sciproject.ui;
import org.librecms.contentsection.ContentItem;
import org.librecms.contentsection.ContentItemRepository;
import org.librecms.contentsection.ContentSection;
import org.librecms.ui.contentsections.ContentSectionsUi;
import org.librecms.ui.contentsections.documents.MvcAuthoringSteps;
import org.scientificcms.contenttypes.sciproject.SciProject;
import org.scientificcms.contenttypes.sciproject.Sponsoring;
import org.scientificcms.contenttypes.sciproject.SponsoringRepository;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Path(MvcAuthoringSteps.PATH_PREFIX + "sciproject-funding-sponsors")
public class SciProjectFundingSponsors {
@Inject
private SponsoringRepository sponsoringRepo;
@Inject
private ContentItemRepository itemRepo;
@Inject
private ContentSectionsUi sectionsUi;
@POST
@Path("/save-order")
@Consumes(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response saveOrder(
@PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
final String sectionIdentifier,
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
final String documentPath,
final List<String> order
) {
final ContentSection contentSection = sectionsUi
.findContentSection(sectionIdentifier)
.orElseThrow(
() -> new NotFoundException(
String.format(
"No content identifed by %s found.",
sectionIdentifier
)
)
);
final ContentItem document = itemRepo
.findByPath(contentSection, documentPath)
.orElseThrow(
() -> new NotFoundException(
String.format(
"No document for path %s in section %s.",
documentPath,
contentSection.getLabel()
)
)
);
if (!(document instanceof SciProject)) {
throw new NotFoundException(
String.format(
"No SciProject for path %s in section %s.",
documentPath,
contentSection.getLabel()
)
);
}
final Map<Long, Long> orderMap = new HashMap<>();
for (int i = 0; i < order.size(); i++) {
orderMap.put(Long.parseLong(order.get(i)), (long) i);
}
final SciProject project = (SciProject) document;
for (final Sponsoring sponsoring : project.getSponsoring()) {
sponsoring.setOrder(orderMap.get(sponsoring.getSponsoringId()));
sponsoringRepo.save(sponsoring);
}
return Response.ok().build();
}
}

View File

@ -1,5 +1,7 @@
package org.scientificcms.contenttypes.sciproject.ui; package org.scientificcms.contenttypes.sciproject.ui;
import org.libreccm.api.Identifier;
import org.libreccm.api.IdentifierParser;
import org.libreccm.l10n.GlobalizationHelper; import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.security.AuthorizationRequired; import org.libreccm.security.AuthorizationRequired;
import org.libreccm.ui.BaseUrl; import org.libreccm.ui.BaseUrl;
@ -58,7 +60,7 @@ public class SciProjectFundingStep extends AbstractMvcAuthoringStep {
@Inject @Inject
private AssetRepository assetRepo; private AssetRepository assetRepo;
@Inject @Inject
private BaseUrl baseUrl; private BaseUrl baseUrl;
@ -73,7 +75,10 @@ public class SciProjectFundingStep extends AbstractMvcAuthoringStep {
@Context @Context
private HttpServletRequest request; private HttpServletRequest request;
@Inject
private IdentifierParser identifierParser;
@Inject @Inject
private ItemPermissionChecker itemPermissionChecker; private ItemPermissionChecker itemPermissionChecker;
@ -496,7 +501,7 @@ public class SciProjectFundingStep extends AbstractMvcAuthoringStep {
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME) @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
final String documentPath, final String documentPath,
@FormParam("sponsorUuid") @FormParam("sponsorUuid")
final String sponsorUuid, final String sponsorUuidParam,
@FormParam("fundingCode") @FormParam("fundingCode")
final String fundingCode final String fundingCode
) { ) {
@ -509,11 +514,29 @@ public class SciProjectFundingStep extends AbstractMvcAuthoringStep {
} }
if (itemPermissionChecker.canEditItem(getProject())) { if (itemPermissionChecker.canEditItem(getProject())) {
final Optional<Organization> result = assetRepo final Optional<Organization> result;
.findByUuidAndType(sponsorUuid, Organization.class); final Identifier identifier = identifierParser.parseIdentifier(
sponsorUuidParam
);
switch (identifier.getType()) {
case UUID:
result = assetRepo.findByUuidAndType(
identifier.getIdentifier(), Organization.class
);
break;
case ID:
result = assetRepo.findById(
Long.parseLong(identifier.getIdentifier()),
Organization.class
);
break;
default:
models.put("sponsorNotFound", sponsorUuidParam);
return showStep(sectionIdentifier, documentPath);
}
if (!result.isPresent()) { if (!result.isPresent()) {
models.put("sponsorNotFound", sponsorUuid); models.put("sponsorNotFound", sponsorUuidParam);
return showStep(sectionIdentifier, documentPath); return showStep(sectionIdentifier, documentPath);
} }
@ -582,7 +605,7 @@ public class SciProjectFundingStep extends AbstractMvcAuthoringStep {
} }
@POST @POST
@Path("/sponsoring/remove") @Path("/sponsoring/{sponsoringId}/remove")
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
@AuthorizationRequired @AuthorizationRequired
public String removeSponsoring( public String removeSponsoring(
@ -590,7 +613,7 @@ public class SciProjectFundingStep extends AbstractMvcAuthoringStep {
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,
@FormParam("sponsoringId") @PathParam("sponsoringId")
final String sponsoringId final String sponsoringId
) { ) {
try { try {

View File

@ -13,6 +13,25 @@
<ui:define name="authoringStep"> <ui:define name="authoringStep">
<h2>#{SciProjectMessageBundle['funding_step.header']}</h2> <h2>#{SciProjectMessageBundle['funding_step.header']}</h2>
<c:if test="#{sponsorNotFound != null}">
<div class="alert alert-warning">
#{SciProjectMessageBundle.getMessage('funding.errors.sponsor_not_found', [sponsorNotFound])}
</div>
</c:if>
<template id="sciproject-sponsoring-sort-error-general">
<div class="alert alert-danger mt-3" role="alert">
#{SciProjectMessageBundle['sponsors.sort.errors.general']}
</div>
</template>
<template id="sciproject-sponsoring-sort-error-save">
<div class="alert alert-danger mt-3" role="alert">
#{SciProjectMessageBundle['sponsors.sort.errors.save']}
</div>
</template>
<div id="messages"></div>
<librecms:cmsEditorVariants <librecms:cmsEditorVariants
addButtonLabel="#{SciProjectMessageBundle['funding.editor.add_variant']}" addButtonLabel="#{SciProjectMessageBundle['funding.editor.add_variant']}"
addDialogLocaleSelectHelp="#{SciProjectMessageBundle['funding.editor.add.locale.help']}" addDialogLocaleSelectHelp="#{SciProjectMessageBundle['funding.editor.add.locale.help']}"
@ -72,7 +91,21 @@
name="fundingCode" name="fundingCode"
/> />
</librecms:assetPicker> </librecms:assetPicker>
<table> <button class="btn btn-secondary sponsoring-save-order-button"
disabled="disabled"
type="button">
<span class="save-icon">
<bootstrap:svgIcon icon="save" />
</span>
<span class="save-spinner d-none">
<span aria-hidden="true"
class="spinner-border spinner-border-sm"
role="status"></span>
</span>
<span>#{SciProjectMessageBundle['sponsoring.order.save']}</span>
</button>
<table id="sciproject-sponsoring-table"
data-saveUrl="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@sciproject-funding-sponsors/save-order">
<thead> <thead>
<tr> <tr>
<th scope="col">#{SciProjectMessageBundle['sponsoring.cols.sponsor']}</th> <th scope="col">#{SciProjectMessageBundle['sponsoring.cols.sponsor']}</th>
@ -84,27 +117,40 @@
<tbody> <tbody>
<c:forEach items="#{SciProjectFundingSponsoring.sponsoring}" <c:forEach items="#{SciProjectFundingSponsoring.sponsoring}"
var="sponsoring"> var="sponsoring">
<tr id="#{sponsoring.sponsoringId}"> <tr class="sciproject-sponsor"
<td>#{sponsoring.sponsor}</td> id="#{sponsoring.sponsoringId}"
data-id="#{sponsoring.sponsoringId}">
<td>
<c:if test="#{CmsSelectedDocumentModel.canEdit}">
<button class="btn btn-secondary cms-sort-handle mr-2"
type="button">
<bootstrap:svgIcon icon="arrows-move" />
<span class="sr-only">#{SciProjectMessageBundle['sponsoring.move.button']}</span>
</button>
</c:if>
#{sponsoring.sponsor}
</td>
<td>#{sponsoring.fundingCode}</td> <td>#{sponsoring.fundingCode}</td>
<td> <td>
<button class="btn btn-secondary" <button class="btn btn-secondary"
data-toggle="modal" data-toggle="modal"
data-target="#sponsoring-edit-dialog" data-target="#sponsoring-#{sponsoring.sponsoringId}-edit-dialog"
type="button"> type="button">
<bootstrap:svgIcon icon="pen" /> <bootstrap:svgIcon icon="pen" />
<span>#{SciProjectMessageBundle['sponsoring.edit.label']}</span> <span>#{SciProjectMessageBundle['sponsoring.edit.label']}</span>
</button> </button>
<div aria-hidden="true" <div aria-hidden="true"
aria-labelledby="sponsoring-edit-dialog-title" aria-labelledby="sponsoring-#{sponsoring.sponsoringId}-edit-dialog-title"
class="modal fade" class="modal fade"
id="spoonsoring-edit-dialog" id="sponsoring-#{sponsoring.sponsoringId}-edit-dialog"
tabindex="-1"> tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<form class="modal-content"> <form action="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@sciproject-funding/sponsoring/edit/#{sponsoring.sponsoringId}"
class="modal-content"
method="post">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" <h4 class="modal-title"
id="sponsoring-edit-dialog-title"> id="sponsoring-#{sponsoring.sponsoringId}-edit-dialog-title">
#{SciProjectMessageBundle['sponsoring.edit.title']} #{SciProjectMessageBundle['sponsoring.edit.title']}
</h4> </h4>
<button <button
@ -115,36 +161,37 @@
<bootstrap:svgIcon icon="x"/> <bootstrap:svgIcon icon="x"/>
</button> </button>
</div> </div>
<div class="modal-body">
<bootstrap:formGroupText
help="#{SciProjectMessageBundle['sponsoring.fundingcode.help']}"
inputId="fundingcode"
label="#{SciProjectMessageBundle['sponsoring.fundingcode.label']}"
name="fundingCode"
value="#{sponsoring.fundingCode}"
/>
</div>
<div class="modal-footer">
<button class="btn btn-warning"
data-dismiss="modal"
type="button">
#{SciProjectMessageBundle['sponsoring.edit.cancel']}
</button>
<button class="btn btn-success"
type="submit">
#{SciProjectMessageBundle['sponsoring.edit.submit']}
</button>
</div>
</form> </form>
</div> </div>
<div class="modal-body">
<bootstrap:formGroupText
help="#{SciProjectMessageBundle['sponsoring.fundingcode.help']}"
inputId="fundingcode"
label="#{SciProjectMessageBundle['sponsoring.fundingcode.label']}"
name="fundingCode"
/>
</div>
<div class="modal-footer">
<button class="btn btn-warning"
data-dismiss="modal"
type="button">
#{SciProjectMessageBundle['sponsoring.edit.cancel']}
</button>
<button class="btn btn-success"
type="submit">
#{SciProjectMessageBundle['sponsoring.edit.submit']}
</button>
</div>
</div> </div>
</td> </td>
<td> <td>
<libreccm:deleteDialog <libreccm:deleteDialog
actionTarget="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@sciproject-funding/sponsoring/remove" actionTarget="#{mvc.basePath}/#{ContentSectionModel.sectionName}/documents/#{CmsSelectedDocumentModel.itemPath}/@sciproject-funding/sponsoring/#{sponsoring.sponsoringId}/remove"
buttonText="#{SciProjectMessageBundle['sponsoring.remove.label']}" buttonText="#{SciProjectMessageBundle['sponsoring.remove.label']}"
cancelLabel="#{SciProjectMessageBundle['sponsoring.remove.cancel']}" cancelLabel="#{SciProjectMessageBundle['sponsoring.remove.cancel']}"
confirmLabel="#{SciProjectMessageBundle['sponsoring.remove.confirm']}" confirmLabel="#{SciProjectMessageBundle['sponsoring.remove.confirm']}"
dialogId="sponsoring-remove-dialog" dialogId="sponsoring-#{sponsoring.sponsoringId}-remove-dialog"
dialogTitle="#{SciProjectMessageBundle['sponsoring.remove.title']}" dialogTitle="#{SciProjectMessageBundle['sponsoring.remove.title']}"
message="#{SciProjectMessageBundle.getMessage('sponsoring.remove.message', [sponsoring.sponsor])}" message="#{SciProjectMessageBundle.getMessage('sponsoring.remove.message', [sponsoring.sponsor])}"
/> />
@ -154,10 +201,10 @@
</tbody> </tbody>
</table> </table>
<ui:define name="scripts"> </ui:define>
<script src="#{request.contextPath}/assets/@content-sections/sciproject-sponsoring.js"></script>
</ui:define>
<ui:define name="scripts">
<script src="#{request.contextPath}/assets/@sciproject/sciproject-sponsoring.js"></script>
</ui:define> </ui:define>
</ui:composition> </ui:composition>

View File

@ -181,3 +181,8 @@ contact.add.title=Add contact
memberships.add.title=Add member memberships.add.title=Add member
description_step.errors.person_not_found=Selected person {0} not found. description_step.errors.person_not_found=Selected person {0} not found.
description_step.errors.illegal_member_status_value=The status value {0} is not valid. description_step.errors.illegal_member_status_value=The status value {0} is not valid.
sponsoring.order.save=Save order
sponsoring.move.button=Drag to order sponsors
funding.errors.sponsor_not_found=The selected sponsor {0} was not found.
sponsors.sort.errors.general=Error sorting sponsors
sponsors.sort.errors.save=Failed to save order of sponsors.

View File

@ -181,3 +181,8 @@ contact.add.title=Kontakt hinzuf\u00fcgen
memberships.add.title=Mitglied hinzuf\u00fcgen memberships.add.title=Mitglied hinzuf\u00fcgen
description_step.errors.person_not_found=Die ausgew\u00e4hlte Person {0} wurde nicht gefunden. description_step.errors.person_not_found=Die ausgew\u00e4hlte Person {0} wurde nicht gefunden.
description_step.errors.illegal_member_status_value=Der Status {0} wird nicht unterst\u00fctzt. description_step.errors.illegal_member_status_value=Der Status {0} wird nicht unterst\u00fctzt.
sponsoring.order.save=Sortierung speichern
sponsoring.move.button=Zum sortieren Sponsoren ziehen
funding.errors.sponsor_not_found=Der ausgew\u00e4hlte Sponsor {0} wurde nicht gefunden.
sponsors.sort.errors.general=Fehler beim Sortieren der Sponsoren
sponsors.sort.errors.save=Speichern der Sortierung der Sponsoren fehlgeschlagen.

View File

@ -106,7 +106,7 @@ function saveOrder() {
spinner?.classList.toggle("d-none"); spinner?.classList.toggle("d-none");
} }
throw Error( throw Error(
`Failed to save attachments order. Response status: ${response.status}, statusText: ${response.statusText}` `Failed to save contacts order. Response status: ${response.status}, statusText: ${response.statusText}`
); );
} }
}) })
@ -122,7 +122,7 @@ function saveOrder() {
saveIcon?.classList.toggle("d-none"); saveIcon?.classList.toggle("d-none");
spinner?.classList.toggle("d-none"); spinner?.classList.toggle("d-none");
} }
throw new Error(`Failed to save attachments order: ${error}`); throw new Error(`Failed to save contacts order: ${error}`);
}); });
} }

View File

@ -0,0 +1,155 @@
import Sortable, { SortableEvent } from "sortablejs";
let sponsoringSortable: Sortable;
document.addEventListener("DOMContentLoaded", function (event) {
const sponsoringTable = document.querySelector(
"#sciproject-sponsoring-table tbody"
);
if (sponsoringTable) {
sponsoringSortable = initSponsoringTable(
sponsoringTable as HTMLElement
);
}
const saveOrderButtons = document.querySelectorAll(
".sponsoring-save-order-button"
);
for (let i = 0; i < saveOrderButtons.length; i++) {
saveOrderButtons[i].addEventListener("click", saveOrder);
}
});
function initSponsoringTable(sponsoringTable: HTMLElement): Sortable {
return new Sortable(sponsoringTable, {
animation: 150,
group: "sciproject-sponsor",
handle: ".cms-sort-handle",
onEnd: enableSaveButtons,
});
}
function enableSaveButtons(event: SortableEvent) {
const saveOrderButtons = document.querySelectorAll(
".sponsoring-save-order-button"
);
for (let i = 0; i < saveOrderButtons.length; i++) {
const saveOrderButton: HTMLButtonElement = saveOrderButtons[
i
] as HTMLButtonElement;
saveOrderButton.disabled = false;
}
}
function saveOrder() {
const sponsoringTable = document.querySelector(
"#sciproject-sponsoring-table"
);
if (!sponsoringTable) {
showGeneralError();
throw Error("sciproject-sponsoring-table not found.");
}
const saveUrl = sponsoringTable.getAttribute("data-saveUrl");
if (!saveUrl) {
showGeneralError();
throw Error(
"data-saveUrl on sciproject-sponsoring-table is missing or empty"
);
}
const saveOrderButtons = document.querySelectorAll(
".sponsoring-save-order-button"
);
for (let i = 0; i < saveOrderButtons.length; i++) {
const saveOrderButton: HTMLButtonElement = saveOrderButtons[
i
] as HTMLButtonElement;
saveOrderButton.disabled = true;
const saveIcon = saveOrderButton.querySelector("save-icon");
const spinner = saveOrderButton.querySelector(".save-spinner");
saveIcon?.classList.toggle("d-none");
spinner?.classList.toggle("d-none");
}
const headers = new Headers();
headers.append("Content-Type", "application/json");
fetch(saveUrl, {
credentials: "include",
body: JSON.stringify(sponsoringSortable.toArray()),
headers,
method: "POST",
})
.then((response) => {
if (response.ok) {
for (let i = 0; i < saveOrderButtons.length; i++) {
const saveOrderButton: HTMLButtonElement = saveOrderButtons[
i
] as HTMLButtonElement;
const saveIcon =
saveOrderButton.querySelector(".save-icon");
const spinner =
saveOrderButton.querySelector(".save-spinner");
saveIcon?.classList.toggle("d-none");
spinner?.classList.toggle("d-none");
}
} else {
showSaveError();
for (let i = 0; i < saveOrderButtons.length; i++) {
const saveOrderButton: HTMLButtonElement = saveOrderButtons[
i
] as HTMLButtonElement;
saveOrderButton.disabled = false;
const saveIcon =
saveOrderButton.querySelector(".save-icon");
const spinner =
saveOrderButton.querySelector(".save-spinner");
saveIcon?.classList.toggle("d-none");
spinner?.classList.toggle("d-none");
}
throw Error(
`Failed to save sponsor order. Response status: ${response.status}, statusText: ${response.statusText}`
);
}
})
.catch((error) => {
showSaveError();
for (let i = 0; i < saveOrderButtons.length; i++) {
const saveOrderButton: HTMLButtonElement = saveOrderButtons[
i
] as HTMLButtonElement;
saveOrderButton.disabled = false;
const saveIcon = saveOrderButton.querySelector(".save-icon");
const spinner = saveOrderButton.querySelector(".save-spinner");
saveIcon?.classList.toggle("d-none");
spinner?.classList.toggle("d-none");
}
throw new Error(`Failed to save sponsor order: ${error}`);
});
}
function showGeneralError(): void {
const alertTemplate = document.querySelector(
"#sciproject-sponsoring-sort-error-general"
) as HTMLTemplateElement;
const alert = alertTemplate.content.cloneNode(true) as Element;
const container = document.querySelector("#messages");
if (container) {
container.appendChild(alert);
}
}
function showSaveError(): void {
const alertTemplate = document.querySelector(
"#sciproject-sponsoring-sort-error-save"
) as HTMLTemplateElement;
const alert = alertTemplate.content.cloneNode(true) as Element;
const container = document.querySelector("#messages");
if (container) {
container.appendChild(alert);
}
}

View File

@ -5,11 +5,8 @@ module.exports = {
chunkIds: false chunkIds: false
}, },
entry: { entry: {
// "sciproject-description": "./src/main/typescript/sciproject-description.ts",
"sciproject-contacts": "./src/main/typescript/sciproject-contacts.ts", "sciproject-contacts": "./src/main/typescript/sciproject-contacts.ts",
// "sciproject-fundingtext": "./src/main/typescript/sciproject-fundingtext.ts", "sciproject-sponsoring": "./src/main/typescript/sciproject-sponsoring.ts"
// "sciproject-fundingvolume": "./src/main/typescript/sciproject-fundingvolume.ts",
//"sciproject-sponsoring": "./src/main/typescript/sciproject-sponsoring.ts"
}, },
output: { output: {
filename: "[name].js", filename: "[name].js",