Question about adding footnotes to Wagtail - wagtail

I'm making a project on wagtail and want to add possibility to add footnotes to a text in Draftjs editor. I've read that this is possible here, but then I tried to repeat this success i've got only errors.
Here my code of wagtail_hooks.py:
#hooks.register('register_rich_text_features')
def register_footnote_feature(features):
features.default_features.append('footnote')
"""
Registering the `footnote` feature, which uses the `FOOTNOTE` Draft.js entity type,
and is stored as HTML with a `<span data-footnote>` tag.
"""
feature_name = 'footnote'
type_ = 'FOOTNOTE'
control = {
'type': type_,
'label': '$',
'description': 'footnote',
}
features.register_editor_plugin(
'draftail', feature_name, draftail_features.EntityFeature(
control,
js=['js/footnote.js'],
css={'all': []}
)
)
features.register_converter_rule('contentstate', feature_name, {
# Note here that the conversion is more complicated than for blocks and inline styles.
'from_database_format': {'span[data-footnote]': FootnoteEntityElementHandler(type_)},
'to_database_format': {'entity_decorators': {type_: footnote_entity_decorator}},
})
def footnote_entity_decorator(props):
"""
Draft.js ContentState to database HTML.
Converts the FOOTNOTE entities into a span tag.
"""
return DOM.create_element('span', {
'data-footnote': props['footnote'],
}, props['children'])
class FootnoteEntityElementHandler(InlineEntityElementHandler):
"""
Database HTML to Draft.js ContentState.
Converts the span tag into a FOOTNOTE entity, with the right data.
"""
mutability = 'MUTABLE'
def get_attribute_data(self, attrs):
"""
Take the ``footnote`` value from the ``data-footnote`` HTML attribute.
"""
return {
'footnote': attrs['data-footnote'],
Here is the code of footnote.js:
const React = window.React;
const Modifier = window.DraftJS.Modifier;
const EditorState = window.DraftJS.EditorState;
class FootnoteSource extends React.Component {
componentDidMount() {
const { editorState, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent();
// // Uses the Draft.js API to create a new entity with the right data.
const contentWithEntity = content.createEntity(entityType.type, 'MUTABLE', {});
const entityKey = contentWithEntity.getLastCreatedEntityKey();
const editorStateWithEntity = Modifier.applyEntity(
editorState.getCurrentContent(),
editorState.getSelection(),
entityKey
)
const nextState = EditorState.push(editorState, editorStateWithEntity, 'apply-entity');
onComplete(nextState);
}
render() {
return null;
}
}
//import PropTypes from 'prop-types';
const Footnote = (props) => {
const { children, entityKey, contentState } = props;
const data = contentState.getEntity(entityKey).getData();
return (
<span
role="button"
className="FootnoteEntity"
>
<span class="FootnoteEntity__label" aria-hidden="true">[Footnote]</span>
{children}
</span>
);
};
Footnote.propTypes = {
entityKey: PropTypes.string.isRequired,
contentState: PropTypes.object.isRequired,
};
window.draftail.registerPlugin({
type: 'FOOTNOTE',
source: FootnoteSource,
decorator: Footnote,
});
I've got this code from this question. Unfortunately, I've not found any docs or references about system of JS objects of Wagtail admin panel. So, now I just don't know what to do, cause I've got nothing to work with.
Now in the richtext editor I just get error:
Invariant Violation: Minified React error #130; visit https://reactjs.org/docs/error-decoder.html?invariant=130&args[]=undefined&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
Invariant Violation: Minified React error #130; visit https://reactjs.org/docs/error-decoder.html?invariant=130&args[]=undefined&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at r (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:102534)
at i (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:102803)
at It (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:127629)
at c (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:133308)
at h (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:134676)
at v (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:135240)
at http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:137125
at En (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:142062)
at Dn (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:149997)
at xr (http://localhost:8000/static/wagtailadmin/js/vendor.js?v=a92d317e:1:169294)
in div
in e
in e
May be anyone know how to extend Draftail in Wagtail admin panel and could pick me where to dig?

Related

Uppy/Shrine: How to retrieve presigned url for video after successful upload (using AWS S3)

I'm using Uppy for file uploads in React, with a Rails API using Shrine.
I'm trying to show a preview for an uploaded video before submitting a form. It's important to emphasize that this is specifically for a video upload, not an image. So the 'thumbnail:generated' event will not apply here.
I can't seem to find any events that uppy provides that returns a cached video preview (like thumbnail:generated does) or anything that passes back a presigned url for the uploaded file (less expected, obviously), so the only option I see is constructing the url manually. Here's what I'm currently trying for that (irrelevant code removed for brevity):
import React, { useEffect, useState } from 'react'
import AwsS3 from '#uppy/aws-s3'
import Uppy from '#uppy/core'
import axios from 'axios'
import { DragDrop } from '#uppy/react'
import { API_BASE } from '../../../api'
const constructParams = (metadata) => ([
`?X-Amz-Algorithm=${metadata['x-amz-algorithm']}`,
`&X-Amz-Credential=${metadata['x-amz-credential']}`,
`&X-Amz-Date=${metadata['x-amz-date']}`,
'&X-Amz-Expires=900',
'&X-Amz-SignedHeaders=host',
`&X-Amz-Signature=${metadata['x-amz-signature']}`,
].join('').replaceAll('/', '%2F'))
const MediaUploader = () => {
const [videoSrc, setVideoSrc] = useState('')
const uppy = new Uppy({
meta: { type: 'content' },
restrictions: {
maxNumberOfFiles: 1
},
autoProceed: true,
})
const getPresigned = async (id, type) => {
const response = await axios.get(`${API_BASE}/s3/params?filename=${id}&type=${type}`)
const { fields, url } = response.data
const params = constructParams(fields)
const presignedUrl = `${url}/${fields.key}${params}`
console.log('presignedUrl from Shrine request data: ', presignedUrl)
setVideoSrc(presignedUrl)
}
useEffect(() => {
uppy
.use(AwsS3, {
id: `AwsS3:${Math.random()}`,
companionUrl: API_BASE,
})
uppy.on('upload-success', (file, _response) => {
const { type, meta } = file
// First attempt to construct presigned URL here
const url = 'https://my-s3-bucket.s3.us-west-1.amazonaws.com'
const params = constructParams(meta)
const presignedUrl = `${url}/${meta.key}${params}`
console.log('presignedUrl from upload-success data: ', presignedUrl)
// Second attempt to construct presigned URL here
const id = meta.key.split(`${process.env.REACT_APP_ENV}/cache/`)[1]
getPresigned(id, type)
})
}, [uppy])
return (
<div className="MediaUploader">
<div className="Uppy__preview__wrapper">
<video
src={videoSrc || ''}
className="Uppy__preview"
controls
/>
</div>
{(!videoSrc || videoSrc === '') && (
<DragDrop
uppy={uppy}
className="UploadForm"
locale={{
strings: {
dropHereOr: 'Drop here or %{browse}',
browse: 'browse',
},
}}
/>
)}
</div>
)
}
export default MediaUploader
Both urls here come back with a SignatureDoesNotMatch error from AWS.
The manual construction of the url comes mainly from constructParams. I have two different implementations of this, the first of which takes the metadata directly from the uploaded file data in the 'upload-success' event, and then just concatenates a string to build the url. The second one uses getPresigned, which makes a request to my API, which points to a generated Shrine path that should return data for a presigned URL. API_BASE simply points to my Rails API. More info on the generated Shrine route here.
It's worth noting that everything works perfectly with the upload process that passes through Shrine, and after submitting the form, I'm able to get a presigned url for the video and play it without issue on the site. So I have no reason to believe Shrine is returning incorrectly signed urls.
I've compared the two presigned urls I'm manually generating in the form, with the url returned from Shrine after uploading. All 3 are identical in structure, but have different signatures. Here are those three urls:
presignedUrl from upload-success data:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/41b229fb17cbf21925d2cd907a59be25.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132613Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=97aefd1ac7f3d42abd2c48fe3ad50b542742ad0717a51528c35f1159bfb15609
presignedUrl from Shrine request data:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/023592fb14c63a45f02c1ad89a49e5fd.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132619Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=7171ac72f7db2b8871668f76d96d275aa6c53f71b683bcb6766ac972e549c2b3
presigned url displayed on site after form submission:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/41b229fb17cbf21925d2cd907a59be25.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132734Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=9ecc98501866f9c5bd460369a7c2ce93901f94c19afa28144e0f99137cdc2aaf
The first two urls come back with SignatureDoesNotMatch, while the third url properly plays the video.
I'm aware the first and third urls have the same file name, while the second url does not. I'm not sure what to make of that, though, but the relevance of this is secondary to me, since that solution was more of a last ditch effort anyway.
I'm not at all attached to the current way I'm doing things. It's just the only solution I could come up with, due to lack of options. If there's a better way of going about this, I'm very open to suggestions.

MDX loader only working on top level directory

In the "Create Next App" section about rendering markdown they have us make posts.js that uses remark to parse the .md files.
In the guide for adding .mdx support they initialize it in next.config.js but don't address the changes needed in posts.js. I think that's why mdx support is working in pages > my-page.mdx but not posts > my-post.mdx which uses posts.js to render.
relevant function:
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.mdx`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Use remark to convert markdown into HTML string
const processedContent = await remark()
.use(html)
.process(matterResult.content);
const contentHtml = processedContent.toString();
// Combine the data with the id and contentHtml
return {
id,
contentHtml,
...matterResult.data,
};
How can I update the getPostData to parse mdx?
(MDX page in the docs using next.config.js)

Querying persisted React WYSIWYG data from MongoDB

thanks in advance!
In summary, I am using React's WYSIWYG rich text editor, and saving the text written in the editor to a MongoDB, data is sent to a server which does the insertion. My issue is that I am unable, after following recommended code, to retrieve the stored data back successfully to display it on my page. This is for a prospective blog post site.
Below I've provided all relevant code:
My Component which sends the data to the server to insert it into MongoDB, (not in order, only relevant code):
<Editor
editorState={editorState}
onEditorStateChange={handleEditorChange}
wrapperClassName="wrapper-class"
editorClassName="editor-class"
toolbarClassName="toolbar-class"
/>
const Practice = () => {
const [editorState, setEditorState] = useState(
() => EditorState.createEmpty(),
);
const [convertedContent, setConvertedContent] = useState(null);
const handleEditorChange = (state) => {
setEditorState(state);
convertContentToRaw();
}
const convertContentToRaw = () => {
const contentState = editorState.getCurrentContent();
setEditorState(editorState: {convertToRaw(contentState)});
}
const stateToSend = JSON.stringify(editorState);
try {
const response = await axios.post('http://localhost:8080/api/insert', {
content: stateToSend
})
} catch(error) {
}
In MongoDB, I've initialized 1 column for storing the WYSIWYG data, I've initialized as an empty JS object:
const wysiwygtest = new mongoose.Schema({
content: {
type: {}
}
});
As a result, my data is inserted into MongoDB as such, with everything desired clearly in data type such as RGBA etc. correct me if I'm wrong but I believe Mongo uses BSON, a form of binary based JSON, so this looks doable for retrieval:
Lastly, the code which is not working correctly, the retrieval. For this, I have no interest just yet in placing the data back into the text editor. Rather, I'd like to display it on the page like a typical blog post. However, I'm unable to even log to the console as of yet.
I am parsing the data back to JSON using JSON.parse, converting JSON to JS object using createFromRaw and using EdiorState (even though I don't have the text editor in this component but this seems to be needed to convert the data fully..) to convert fully:
useEffect( async () => {
try {
const response = await axios.get('http://localhost:8080/api/query', {
_id: '60da9673b996f54d507dbfc5'
});
const content = response;
if(content) {
const convertedContent =
EditorState.createWithContent(convertFromRaw(JSON.parse(content)));
console.log('convertedContent - ', convertedContent);
}
console.log('response - ', content);
} catch(error) {
console.log('error!', error);
}
}, [])
My result for the past day and last night has been the following:
"SyntaxError: Unexpected token o in JSON at position 1" and so I'm unsure what I'm doing wrong in the data retrieval, and possibly even the insertion.
Any ideas? Thanks again!
Edit: For more reference, here is what the data looks like when output to the console without a JSON.stringify, this is the full tree of data. I can see all of the relevant data is there, but how do I convert this data and display it into a div or paragraph tag, for example?
More or less figured this out, see my solution below given the aforementioned implementation:
Firstly, I think my biggest mistake was using JSON.parse(); I did away with this with success. My guess as to why this does not work (even though I inserted into MongoDB as JSON) is because we ultimately need the draft-js.Editor Object to convert the data from the DB into an object type it can understand, in order to subsequently convert into HTML successfully, with all properties.
Below is the code with captions/descriptions:
Retrieve data (in useEffect before React component is rendered:
useEffect( async () => {
console.log('useeffect');
try {
const response = await axios.get('http://localhost:8080/api/query', {
_id: '60da9673b996f54d507dbfc5' //hard-coded id from DB for testing
});
const content = response.data; //get JSON data from MongoDB
if(content) {
const rawContent = convertFromRaw(content); //convert from JSON to contentstate understood by DraftJS, for EditorState obj to use
setEditorState(EditorState.createWithContent(rawContent)); //create EditorState based on JSON data from DB and set into component state
let currentContentAsHTML = draftToHtml(convertToRaw(editorState.getCurrentContent())); //create object which converts contentstate understood by DraftJS into a regular vanilla JS object, then take THAT and convert into HTML with "draftToHtml" function. Save that into our 2nd state titled "convertedContent" to be displayed on page for blog post
setConvertedContent(currentContentAsHTML);
}
} catch(error) {
console.log('error retrieving!', error);
} },[convertedContent]) //ensure dependency with with convertedContent state, DB/server calls take time...
In component render, return HTML which sets the innerHTML in the DOM using/passing the convertedContent state which we converted to proper HTML format in step 1.
return (
<div className="blog-container" dangerouslySetInnerHTML={createMarkup(convertedContent)}></div>
</div>
);
In step 2, we called a function entitled, "createMarkup"; here is that method. It essentially returns HTML object using the HTML converted data originally from our database. This is a bit vulnerable it terms of malicious users being able to intercept that HTML in the DOM, however, so we use a method, "purify" from "DOMPurify" class from 'isomorphic-dompurify" library. I'm using this instead of regular DOMPurify because I am using Next JS and NEXT runs on the server side as well, and DOMPurify only expects client side:
const createMarkup = (html) => {
return {
__html: DOMPurify.sanitize(html)
}
}

How can I access a library through a script in a Typescript React app?

I am fairly new to React, and have not done any extensive web development in years, so am struggling with a (probably) basic web issue:
I am implementing a Stripe based payment flow in a React web app (written in Typescript), and have hit a roadblock on step 2 (adding a redirect to checkout client-side).
The quickstart guide instructs me to insert the following script tag on my website, which I have done through inserting the tag inside the <head> tag:
Checkout relies on Stripe.js. To get started, include the following
script tag on your website—it should always be loaded directly from
https://js.stripe.com:
<script src="https://js.stripe.com/v3/"></script>
The next step is where I am having a problem (using the ESNext syntax since this is in a Typescript project):
Next, create an instance of the Stripe object by providing your publishable API key as the first parameter:
const stripe = Stripe('pk_test_sdjxyNjHWmRefdkUNYuS53MA00Ot1f9HOu');
I would like to access Stripe through a service worker, rather than a component directly. However, trying to initialise the stripe instance is not working. I have tried:
importing the Stripe module in various ways, which hasn't worked
adding a dependency on #types/stripe, which seems to prevent the compiler complaining
Currently, my StripeService.ts file has the following code:
const stripe = Stripe("SOME_KEY");
export const redirectToCheckout = (sessionId: string) => {
return stripe.redirectToCheckout(
{
sessionId: sessionId,
});
};
Localhost instance is giving this error:
/src/services/stripe/StripeService.ts
Line 12: 'Stripe' is not defined no-undef
Any suggestions on how I can resolve this issue? I have looked into the react-stripe-elements wrapper, but that is geared towards providing UI components, whereas I only want the Stripe checkout API call behaviour.
Bare Minimum
Minimum implementation is to declare Stripe using any:
declare class Stripe {
constructor(...args: any[]);
redirectToCheckout(...args: any[]): any;
}
const stripe = new Stripe("pk_test_sdjxyNjHWmRefdkUNYuS53MA00Ot1f9HOu");
stripe.redirectToCheckout({
sessionId: sessionId
})
Stronger Typings
You can of course expand this by more explicitly typing the parts that you need:
declare class Stripe {
constructor(publicKey: string);
redirectToCheckout({
sessionId
}: {
sessionId: string;
}): Promise<{ error: Error }>;
}
const stripe = new Stripe("pk_test_sdjxyNjHWmRefdkUNYuS53MA00Ot1f9HOu");
stripe.redirectToCheckout({
sessionId
}).then(function (result) {
// If `redirectToCheckout` fails due to a browser or network
// error, display the localized error message to your customer
// using `result.error.message`.
});
Try using the windows object instead:
var stripe = window.Stripe("pk_test_h4naRpZD1t2edp2HQKG2NrZi00rzz5TQJk");
For a service file, you would just add stripe to package.json, then in the file would do:
import Stripe from "stripe";
const stripe = Stripe("SOME_KEY");
export const redirectToCheckout = (sessionId: string) => {
return stripe.redirectToCheckout(
{
sessionId: sessionId,
});
};
You would use the public key in the client side, and the secret key in the server side. You should keep stripe object (Stripe('pk_test_sdjxyNjHWmRefdkUNYuS53MA00Ot1f9HOu')) in your state somehow to be able to retrieve it later.
An example call could be like this:
client side
const {paymentMethod, error} = await this.state.stripe.createPaymentMethod('card', cardElement, {
billing_details: {
name: 'Jenny Rosen',
},
});
StripeService.makePayment(paymentMethod);
server side
import Stripe as "stripe";
const stripe = Stripe("SOME_KEY");
export const makePayment = (paymentMethod: object) => {
...
};

Property 'calendar' does not exist on type 'typeof client'

I'm trying to connect my Google Calender to my React website. I've got a component called Calendar. I've used the JS tutorial from Google and I've changed it to work in Typescript. I've got the authentication and authorization already working, however fetching data from the calendar is not working. I'm getting the following error when compiling/editing.
[ts] Property 'calendar' does not exist on type 'typeof client'. Did you mean 'calendars'?
I've already downloaded the types for the gapi.client.calendar and as you can see in the image below, they are also found in the #types folder. I'm kind of stuck and I don't know how I can fix this issue..
Here is my code from my Calendar.tsx
import * as React from 'react';
import { Button } from 'semantic-ui-react'
import googleApiKey from '../googleapi-key.json';
const CLIENT_ID = googleApiKey.CLIENT_ID;
const API_KEY = googleApiKey.API_KEY;
const DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"];
const SCOPES = "https://www.googleapis.com/auth/calendar.readonly";
class Calendar extends React.Component {
constructor(props: any) {
super(props);
console.log(CLIENT_ID);
console.log(API_KEY);
this.handleClientLoad = this.handleClientLoad.bind(this);
this.handleAuthClick = this.handleAuthClick.bind(this);
this.handleSignoutClick = this.handleSignoutClick.bind(this);
this.initClient = this.initClient.bind(this);
this.updateSigninStatus = this.updateSigninStatus.bind(this);
this.listUpcomingEvents = this.listUpcomingEvents.bind(this);
}
componentDidMount() {
this.initClient();
}
public render() {
return (
<div>
<Button onClick={this.handleAuthClick}>
authorizeButton
</Button>
<Button onClick={this.handleSignoutClick}>
signoutButton
</Button>
</div>
);
}
/**
* On load, called to load the auth2 library and API client library.
*/
public handleClientLoad() {
gapi.load('client:auth2', this.initClient);
}
/**
* Sign in the user upon button click.
*/
public handleAuthClick(event: any) {
gapi.auth2.getAuthInstance().signIn();
}
/**
* Sign out the user upon button click.
*/
public handleSignoutClick(event: any) {
gapi.auth2.getAuthInstance().signOut();
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
public async initClient() {
await gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
})
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(this.updateSigninStatus);
// Handle the initial sign-in state.
this.updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
}
/**
* Called when the signed in status changes, to update the UI
* appropriately. After a sign-in, the API is called.
*/
public updateSigninStatus(isSignedIn: any) {
if (isSignedIn) {
this.listUpcomingEvents();
}
}
/**
* Print the summary and start datetime/date of the next ten events in
* the authorized user's calendar. If no events are found an
* appropriate message is printed.
*/
public listUpcomingEvents() {
console.log(gapi.client.calendar); // <--- Compile error: Does not recognize calendar
}
}
export default Calendar;
EDIT
When performing console.log(gapi.client) I can see that the client contains a calendar object (see image). But why can't I reach it in my own code?
I managed to fix my own problem. After performing console.log(gapi.client) I noticed that calender was already there, so I tried the following gapi.client['calendar'] and it worked as it should. I don't know why Typescript does not recognize the calendar in the first place, so if anybody has an idea feel free to leave a comment.
Try the following
Install types npm i #types/gapi.client.calendar
Include https://apis.google.com/js/api.js & https://apis.google.com/js/platform.js in index.html
Add the following inside types in tsconfig.app.json
"types": [
"gapi",
"gapi.auth2",
"gapi.client",
"gapi.client.calendar"
]
You have to add:
"types": ["gapi", "gapi.auth2", "gapi.client", "gapi.client.calendar"]
in tsconfig.app.js and in tsconfig.json.

Resources