Skip to content

Commit

Permalink
WIP - get handlers working
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Jun 1, 2024
1 parent d13781a commit bd26bec
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 35 deletions.
49 changes: 32 additions & 17 deletions public/static/xblock/client-v0.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,12 @@ function toVdom(element, nodeName) {
* e.g. `const { learnerAnswer } = useFields(props);`
*/
function useFields(props) {
var parts = [props['system-fields'], props['content-fields'], props['user-fields']];
return q(function () {
var fields = {};
for (var _i = 0, parts_1 = parts; _i < parts_1.length; _i++) {
var part = parts_1[_i];
var parsed = JSON.parse(part);
for (var _a = 0, _b = Object.entries(parsed); _a < _b.length; _a++) {
var _c = _b[_a], key = _c[0], value = _c[1];
const parts = [props['content-fields'], props['user-fields']];
return q(() => {
const fields = {};
for (const part of parts) {
const parsed = JSON.parse(part);
for (const [key, value] of Object.entries(parsed)) {
fields[key] = value;
}
}
Expand All @@ -193,25 +191,42 @@ function useFields(props) {
* @param usageKey The usage key, available as an attribute/prop on the web component.
*/
function getDomElement(usageKey) {
var result = document.querySelector(".xblock-component.xblock-v2[usage-key=\"".concat(usageKey, "\"]"));
const result = document.querySelector(`.xblock-component.xblock-v2[usage-key="${usageKey}"]`);
if (result === null) {
throw new Error("Unable to find DOM element for ".concat(usageKey));
throw new Error(`Unable to find DOM element for ${usageKey}`);
}
return result;
}
function getRenderContext(usageKey) {
var result = getDomElement(usageKey).closest('xblock-render-context');
const result = getDomElement(usageKey).closest('xblock-render-context');
if (result === null) {
throw new Error("XBlock was rendered outside of an <xblock-render-context>: ".concat(usageKey));
throw new Error(`XBlock was rendered outside of an <xblock-render-context>: ${usageKey}`);
}
return result;
}
function callHandler(usageKey, handlerName, body, method) {
if (method === void 0) { method = 'POST'; }
return getRenderContext(usageKey).callHandler(usageKey, handlerName, body, method);
function callHandlerRaw(usageKey, handlerName, body, method = 'post') {
return getRenderContext(usageKey).callHandlerRaw(usageKey, handlerName, body, method);
}
/** Call a JSON handler using POST. Most handler calls should use this. */
async function callHandler(usageKey, handlerName, body = {}) {
const response = await callHandlerRaw(usageKey, handlerName, new Blob([JSON.stringify(body)]), 'post');
const { data, updated_fields } = await response.json();
const domElement = getDomElement(usageKey);
// Update the actual web component DOM element with any changed field values, if applicable:
for (const fieldScope of ["user", "content"]) {
if (Object.keys(updated_fields[fieldScope]).length > 0) {
const attributeName = `${fieldScope}-fields`;
const scopeFieldValues = JSON.parse(domElement.getAttribute(attributeName));
for (const [key, value] of Object.entries(updated_fields[fieldScope])) {
scopeFieldValues[key] = value;
}
domElement.setAttribute(attributeName, JSON.stringify(scopeFieldValues));
}
}
return data;
}
function registerPreactXBlock(componentClass, blockType, options) {
register(componentClass, "xblock2-".concat(blockType), ['content-fields', 'user-fields'], options);
register(componentClass, `xblock2-${blockType}`, ['content-fields', 'user-fields'], options);
}

export { b$1 as Component, k$1 as Fragment, callHandler, E as cloneElement, G as createContext, _$1 as createElement, m$2 as createRef, getDomElement, _$1 as h, m as html, D$1 as hydrate, t$2 as isValidElement, l$1 as options, registerPreactXBlock, B$1 as render, H as toChildArray, x as useCallback, P as useContext, V as useDebugValue, _ as useEffect, b as useErrorBoundary, useFields, g as useId, T as useImperativeHandle, A as useLayoutEffect, q as useMemo, y as useReducer, F as useRef, p as useState };
export { b$1 as Component, k$1 as Fragment, callHandler, callHandlerRaw, E as cloneElement, G as createContext, _$1 as createElement, m$2 as createRef, getDomElement, _$1 as h, m as html, D$1 as hydrate, t$2 as isValidElement, l$1 as options, registerPreactXBlock, B$1 as render, H as toChildArray, x as useCallback, P as useContext, V as useDebugValue, _ as useEffect, b as useErrorBoundary, useFields, g as useId, T as useImperativeHandle, A as useLayoutEffect, q as useMemo, y as useReducer, F as useRef, p as useState };
1 change: 0 additions & 1 deletion src/courseware/component/XBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const XBlock: React.FC<XBlockDataV2> = ({ id, ...props }) => {
class: 'xblock-component xblock-v2', // For web components in React, the prop is 'class', not 'className'
'usage-key': id,
'content-fields': JSON.stringify(props.contentFields),
'system-fields': JSON.stringify(props.systemFields),
'user-fields': JSON.stringify(props.userFields),
};

Expand Down
36 changes: 20 additions & 16 deletions src/courseware/component/XBlockRenderContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import type { AxiosInstance } from "axios";
import { AxiosHeaders, type AxiosInstance } from "axios";
import { getConfig } from "@edx/frontend-platform";
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

Expand All @@ -9,25 +9,20 @@ if (!globalStatus._loadedBlockTypes) {
}

// TODO: HandlerResponse and CallHandler type definitions should be in a 'xblock-client-api' package on NPM?
export interface HandlerResponse {
data: Record<string, any>;
updatedFields: Record<string, any>;
}

export interface CallHandler {
interface CallHandlerRaw {
(
usageKey: string,
handlerName: string,
body: Record<string, any> | ReadableStream | Blob,
method?: 'GET'|'POST',
): Promise<HandlerResponse>;
body: Blob,
method?: 'get'|'post'|'put'|'delete',
): Promise<Response>;
}

export function ensureBlockScript(blockType: string): void {
if (!globalStatus._loadedBlockTypes.has(blockType)) {
globalStatus._loadedBlockTypes.add(blockType);
// We want the browser to handle this import(), not webpack, so the comment on the next line is essential.
import(/* webpackIgnore: true */ `${getConfig().LMS_BASE_URL}/xblock/resource-v2/${blockType}/public/learner-view-v2.js`).then(() => {
import(/* webpackIgnore: true */ `${getConfig().LMS_BASE_URL}/xblock/resource/${blockType}/public/learner-view-v2.js`).then(() => {
console.log(`Loaded JavaScript for ${blockType} v2 XBlock.`);
}, (err) => {
console.error(`🛑 Unable to Load JavaScript for ${blockType} v2 XBlock: ${err}`);
Expand All @@ -36,11 +31,20 @@ export function ensureBlockScript(blockType: string): void {
}
}

const callHandler: CallHandler = async (usageKey, handlerName, body, method): Promise<HandlerResponse> => {
const callHandlerRaw: CallHandlerRaw = async (usageKey, handlerName, body, method = 'post'): Promise<Response> => {
const client: AxiosInstance = getAuthenticatedHttpClient();
const makeRequest = client[method === "POST" ? 'post' : 'get'];
const response = await makeRequest(`${getConfig().LMS_BASE_URL}/api/xblock/v1/${usageKey}/handler_v2/${handlerName}`, body);
return response.data;
const makeRequest = client[method];
const axiosResponse = await makeRequest(
`${getConfig().LMS_BASE_URL}/xblock/${usageKey}/handler/${handlerName}`,
body,
{ transformResponse: (r) => r }
);
// Convert from the vague axios format to the standard Response format:
const headers: Record<string, any> = (
axiosResponse.headers instanceof AxiosHeaders ? axiosResponse.headers.toJSON(true) : axiosResponse.headers
);
const response = new Response(axiosResponse.data, {headers, status: axiosResponse.status});
return response;
}

/**
Expand All @@ -53,7 +57,7 @@ export const XBlockRenderContextProvider: React.FC<{children: React.ReactNode}>
const ref = React.useRef<HTMLElement>(null);
React.useEffect(() => {
if (ref.current) {
(ref.current as any).callHandler = callHandler;
(ref.current as any).callHandlerRaw = callHandlerRaw;
}
}, [ref.current]);
return (
Expand Down
1 change: 0 additions & 1 deletion src/courseware/component/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export interface XBlockDataV2 {
id: string;
blockType: string;
contentFields: { displayName: string } & Record<string, any>;
systemFields: Record<string, any>;
userFields: Record<string, any>;
}

Expand Down

0 comments on commit bd26bec

Please sign in to comment.