PDF Collaboration permissions using JavaScript
Enforce permissions based on JSON Web Token (JWT) parameters.
import PSPDFKit from "@nutrient-sdk/viewer";import React, { useMemo } from "react";import Select from "react-select";
import styles, { modeSelectorStyles, roleSelectorStyles,} from "./static/styles";
let isInstant = true;let _instance;
export function load(defaultConfiguration) { if (!defaultConfiguration.instant || defaultConfiguration.pdf) { console.log( "Collaboration Permissions is not supported in Nutrient Web SDK in standalone mode or Document Engine without Instant." ); isInstant = false;
return null; }
isInstant = true;
const toolbarItems = PSPDFKit.defaultToolbarItems;
// Enable the comments tool in the main toolbar. // We are placing it as the first tool on the right hand side of the toolbar. toolbarItems.splice( toolbarItems.findIndex((item) => item.type === "spacer") + 1, 0, { type: "comment" } );
PSPDFKit.unload(defaultConfiguration.container);
return PSPDFKit.load({ ...defaultConfiguration, toolbarItems, customRenderers: { Annotation: privateIconRenderer, }, annotationPresets: { ...PSPDFKit.defaultAnnotationPresets, "ink-signature": { ...PSPDFKit.defaultAnnotationPresets["ink-signature"], group: "signature", }, }, }).then((instance) => { _instance = instance;
instance.setAnnotationCreatorName( window.jwtParameters && window.jwtParameters.default_group ); console.log("Nutrient Web SDK successfully loaded!!", instance);
return instance; });}
export const CustomContainer = React.forwardRef((props, ref) => { const params = useMemo(() => { const location = window.location.search;
return new URLSearchParams(location); }, []);
const [page, setPage] = React.useState(1); const [role, setRole] = React.useState(params.get("role")); const [student, setStudent] = React.useState(params.get("student")); const [isPrivate, setIsPrivate] = React.useState(false);
React.useLayoutEffect(() => { if (role && (role === "teacher" || student)) { openPage(); } }, []);
React.useEffect(() => { if (!_instance) return;
_instance.setViewState((s) => s.set("interactionMode", null)); _instance.setSelectedAnnotation(null);
if (isPrivate) { _instance.setGroup( role === "teacher" ? "private_teacher" : "private_students" ); } else { _instance.resetGroup(); } }, [isPrivate]);
const openPage = React.useCallback( (_student = student) => { if (role === "teacher" || (role === "student" && _student)) { window.jwtParameters = getJWTParameters(_student); setPage(2);
// Rerender the document with the new permissions props.onForceReRender(); } }, [student, role, props.onForceReRender] );
const setStudentAndOpenPage = (studentName) => { setStudent(studentName); openPage(studentName); };
if (!isInstant) { return ( <div ref={ref} className="phases__phase"> <style jsx>{styles}</style> <div className="info"> <div className="info-content"> <span className="info-icon"> <InlineSvgComponent src={require("./static/information.js")} /> </span> <h2>Not available in standalone mode</h2> <p> Collaboration Permissions require Instant and a server backend to run. It doesn't work in the standalone mode. </p> </div> </div> </div> ); }
const goBack = () => { setStudent(""); setRole(""); setPage(1); };
return ( <> <style jsx>{styles}</style> {page === 1 && !role && ( <div className="wrapper"> <div className="content"> <div className="info"> <div className="info-content"> <span className="info-icon"> <InlineSvgComponent src={require("./static/collaboration-permissions-welcome.js")} /> </span> <h2>Collaboration Permissions Example</h2> <p> Collaborate on the document as one of the roles below. Please select one to continue. </p>
<div className="button-wrapper"> <button style={{ marginRight: 20, }} onClick={() => setRole("student")} > Student </button> <button onClick={() => setRole("teacher")}>Teacher</button> </div> </div> </div> </div> </div> )}
{page === 1 && (role === "teacher" || (role === "student" && !student)) && ( <div className="wrapper"> <div className="info" style={{ alignItems: "unset", maxWidth: 400, }} > <h2> {role === "teacher" ? "Teacher permissions" : "Student Permissions"} </h2> <ul> {getRules(role).map((rule, i) => ( <li key={i}>{rule}</li> ))} </ul>
{role === "teacher" ? ( <button style={{ width: 180, }} onClick={() => openPage()} > Evaluate Paper </button> ) : ( <> Work on the document as: <div className="button-wrapper bw-mt" style={{ marginTop: 30 }} > <button onClick={() => setStudentAndOpenPage("olivia")}> Olivia </button> <button onClick={() => setStudentAndOpenPage("lucas")}> Lucas </button> </div> <div className="button-wrapper bw-mt"> <button onClick={() => setStudentAndOpenPage("john")}> John </button> <button onClick={() => setStudentAndOpenPage("mary")}> Mary </button> </div> </> )} </div> </div> )} <> <header> <div className="mobile" onClick={goBack}> <InlineSvgComponent src={require("./static/back.js")} /> </div> <button className="desktop" onClick={goBack}> Back </button>
<div className="header-center"> <p className="desktop">Viewing document as:</p> <p className="mobile">Viewing as:</p> <Select value={roleOptions.find( (o) => o.value === (role === "teacher" ? "teacher" : student) )} onChange={(selected) => { const url = selected.value === "teacher" ? getUrl("teacher") : getUrl("student", selected.value);
window.open(url, "_blank"); }} options={roleOptions} styles={roleSelectorStyles} formatOptionLabel={(option) => { if (option.value !== (student || role)) { return ( <> {option.label}{" "} <InlineSvgComponent src={require("./static/external-link.js")} /> </> ); } else { return option.label; } }} /> </div>
<Select value={privacyModeOptions.find((o) => o.value === isPrivate)} onChange={(selected) => { setIsPrivate(selected.value); }} options={privacyModeOptions} styles={modeSelectorStyles} formatOptionLabel={(option, x) => { if (x.context === "value") { return ( <InlineSvgComponent src={require(`./static/${ option.value ? "private" : "public" }.js`)} /> ); }
return ( <> <InlineSvgComponent src={require(`./static/${ option.value ? "private" : "public" }.js`)} /> {option.label} </> ); }} /> </header> <div className="container" ref={ref} style={{ height: "100%", width: "100%", visibility: page === 2 && (role === "teacher" || !!student) ? "visible" : "hidden", }} /> </> </> );});
const InlineSvgComponent = ({ src, ...otherProps }) => { return <span {...otherProps} dangerouslySetInnerHTML={{ __html: src }} />;};
function getUrl(role, student) { const search = new window.URLSearchParams(window.location.search.slice(1));
if (search.has("role")) { search.set("role", role); } else { search.append("role", role); }
if (search.has("student")) { if (!student) { search.delete("student"); } else { search.set("student", student); } } else { if (student) { search.append("student", student); } }
return ( window.location.origin + window.location.pathname + "?" + search.toString() );}
/** * These are the permissions that have been assigned for teacher and student. */function getJWTParameters(studentName) { const teacher = { user_id: "user_teacher", default_group: "teacher", collaboration_permissions: [ "annotations:view:group=students", "annotations:view:self", "annotations:edit:self", "annotations:delete:group=students", "annotations:delete:self", "comments:view:group=students", "comments:view:group=private_teacher", "comments:view:self", "comments:edit:self", "comments:delete:all", "comments:reply:all", "form-fields:view:all", "annotations:view:group=signature", "annotations:set-group:group=private_teacher", "annotations:set-group:group=teacher", "annotations:set-group:group=signature", "comments:set-group:group=private_teacher", "comments:set-group:group=teacher", ], };
const student = { user_id: `user_${studentName}`, default_group: "students", collaboration_permissions: [ "annotations:view:group=students", "annotations:view:group=teacher", "annotations:view:self", "annotations:view:group=signature", "annotations:view:group=private_students", "annotations:edit:self", "annotations:delete:self", "comments:view:group=students", "comments:view:group=private_students", "comments:view:group=teacher", "comments:view:self", "comments:edit:self", "comments:delete:self", "comments:reply:all", "annotations:set-group:group=private_students", "annotations:set-group:group=students", "comments:set-group:group=private_students", "comments:set-group:group=students", "annotations:set-group:group=signature", "form-fields:view:all", "form-fields:fill:all", ], };
return studentName ? student : teacher;}
function getRules(role) { const teacher = [ "You can view public annotations and comments created by all students. You cannot edit them, but you can delete them.", "Students can view public annotations and comments created by you, but they cannot edit or delete them.", "You cannot view annotations or comments created by students in private mode.", "Annotations or comments that you add in private mode won't be visible to students.", ];
const student = [ "You can view the public annotations created by the teacher.", "You can view comments and replies made by the teacher.", "You cannot view annotations or comments created by the teacher in private mode.", "You can view annotations or comments created by other students in private mode.", "You cannot edit or delete comments and annotations created by the teacher or other students. You can only edit comments and annotations that you made.", ];
return role === "teacher" ? teacher : student;}
const roleOptions = [ { value: "teacher", label: "Teacher" }, { value: "olivia", label: "Olivia (Student)" }, { value: "lucas", label: "Lucas (Student)" }, { value: "john", label: "John (Student)" }, { value: "mary", label: "Mary (Student)" },];
const privacyModeOptions = [ { value: true, label: "Private Mode", }, { value: false, label: "Public Mode", },];
const iconNodes = {};
// Renders the private icon in the bottom right corner of a private annotation.function privateIconRenderer({ annotation }) { let node;
if (iconNodes[annotation.id]) { node = iconNodes[annotation.id]; } else { node = document.createElement("img"); node.setAttribute( "src", "/collaboration-permissions/static/private-annotation.svg" ); node.style.position = "absolute"; node.style.right = "-18px"; node.style.bottom = "-14px"; iconNodes[annotation.id] = node; }
return annotation.group && annotation.group.startsWith("private") ? { node, append: true, } : null;}
This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.