Skip to content

Commit

Permalink
Support Canvas Pages content via LMSFilePicker (#5717)
Browse files Browse the repository at this point in the history
Co-authored-by: Alejandro Celaya <[email protected]>
  • Loading branch information
marcospri and acelaya authored Oct 9, 2023
1 parent 3c11664 commit 3bc9f1a
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 237 deletions.
28 changes: 21 additions & 7 deletions lms/static/scripts/frontend_apps/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
import type { APICallInfo } from './config';

/**
* Object representing a file or folder resource in the LMS's file storage.
* Object representing a file, folder or document stored by the LMS
*/
export type File = {
// FIXME - This ought to be present on all file objects.
type?: 'File' | 'Folder';
export type Document = {
type?: 'File' | 'Folder' | 'Page';

/** Identifier for the resource within the LMS's file storage. */
/** Identifier for the document within the LMS. */
id: string;

/** Name of the resource to present in the file picker. */
/** Name of the document to present in the document picker. */
display_name: string;

/** An ISO 8601 date string. */
updated_at?: string;
};

/**
* Object representing a file or folder in the LMS's file storage.
*/
export type File = Document & {
// FIXME - This ought to be present on all file objects.
type?: 'File' | 'Folder';

/** APICallInfo for fetching a folders's content. Only present if `type` is 'Folder'. */
/** APICallInfo for fetching a folder's content. Only present if `type` is 'Folder'. */
contents?: APICallInfo;

/** Only present if `type` is 'Folder'. A folder may have a parent folder. */
Expand All @@ -30,6 +37,13 @@ export type File = {
children?: File[];
};

/**
* Object representing Canvas pages or similar documents
*/
export type Page = Document & {
type: 'Page';
};

/**
* Object representing a set of groups that students can be divided into for
* an assignment.
Expand Down
49 changes: 43 additions & 6 deletions lms/static/scripts/frontend_apps/components/ContentSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OptionButton, SpinnerOverlay } from '@hypothesis/frontend-shared';
import { useMemo, useState } from 'preact/hooks';

import type { Book, File, Chapter } from '../api-types';
import type { Book, File, Chapter, Page } from '../api-types';
import { useConfig } from '../config';
import { PickerCanceledError } from '../errors';
import type { Content } from '../utils/content-item';
Expand All @@ -18,6 +18,7 @@ import YouTubePicker from './YouTubePicker';
type DialogType =
| 'blackboardFile'
| 'canvasFile'
| 'canvasPage'
| 'd2lFile'
| 'jstor'
| 'url'
Expand Down Expand Up @@ -69,6 +70,8 @@ export default function ContentSelector({
enabled: canvasFilesEnabled,
listFiles: listFilesApi,
foldersEnabled: canvasWithFolders,
pagesEnabled: canvasPagesEnabled,
listPages: listPagesApi,
},
google: {
enabled: googleDriveEnabled,
Expand Down Expand Up @@ -109,6 +112,7 @@ export default function ContentSelector({
const selectDialog = (type: DialogType) => {
setActiveDialog(type);
};

// Initialize the Google Picker client if credentials have been provided.
// We do this eagerly to make the picker load faster if the user does click
// on the "Select from Google Drive" button.
Expand Down Expand Up @@ -159,18 +163,27 @@ export default function ContentSelector({
onSelectContent({ type: 'url', url, name });
};

const selectCanvasFile = (file: File) => {
const selectCanvasFile = (file: File | Page) => {
cancelDialog();
onSelectContent({ type: 'file', file });
onSelectContent({ type: 'file', file: file as File });
};

const selectCanvasPage = (page: File | Page) => {
cancelDialog();
onSelectContent({
type: 'url',
url: page.id,
name: `Canvas page: ${page.display_name}`,
});
};

const selectBlackboardFile = (file: File) => {
const selectBlackboardFile = (file: File | Page) => {
cancelDialog();
// file.id is a URL with a `blackboard://` prefix.
onSelectContent({ type: 'url', url: file.id });
};

const selectD2LFile = (file: File) => {
const selectD2LFile = (file: File | Page) => {
cancelDialog();
// file.id is a URL with a `d2l://` prefix.
onSelectContent({ type: 'url', url: file.id });
Expand Down Expand Up @@ -216,6 +229,19 @@ export default function ContentSelector({
/>
);
break;
case 'canvasPage':
dialog = (
<LMSFilePicker
authToken={authToken}
listFilesApi={listPagesApi}
onCancel={cancelDialog}
onSelectFile={selectCanvasPage}
missingFilesHelpLink="TODO"
documentType="page"
/>
);
break;

case 'blackboardFile':
dialog = (
<LMSFilePicker
Expand Down Expand Up @@ -333,9 +359,20 @@ export default function ContentSelector({
onClick={() => selectDialog('canvasFile')}
title="Select PDF from Canvas"
>
Canvas
Canvas File
</OptionButton>
)}
{canvasPagesEnabled && (
<OptionButton
data-testid="canvas-page-button"
details="Page"
onClick={() => selectDialog('canvasPage')}
title="Select a Page from Canvas"
>
Canvas Page
</OptionButton>
)}

{blackboardFilesEnabled && (
<OptionButton
data-testid="blackboard-file-button"
Expand Down
94 changes: 94 additions & 0 deletions lms/static/scripts/frontend_apps/components/DocumentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
DataTable,
FileCodeFilledIcon,
FilePdfFilledIcon,
FolderIcon,
Scroll,
} from '@hypothesis/frontend-shared';
import type { ComponentChildren } from 'preact';

import type { Document } from '../api-types';

export type DocumentListProps<DocumentType extends Document> = {
/** List of document objects returned by the API */
documents: DocumentType[];
/** Whether to show a loading indicator */
isLoading?: boolean;
/** The document within `documents` which is currently selected */
selectedDocument: DocumentType | null;
/** Callback invoked when the user clicks on a document */
onSelectDocument: (doc: DocumentType | null) => void;
/**
* Callback invoked when the user double-clicks a document to indicate that
* they want to use it
*/
onUseDocument: (d: DocumentType | null) => void;
/** Optional message to display if there are no documents */
noDocumentsMessage?: ComponentChildren;
/** Component title for accessibility */
title: string;
};

/**
* List of the documents
*/
export default function DocumentList<DocumentType extends Document>({
documents,
isLoading = false,
selectedDocument,
onSelectDocument,
onUseDocument,
noDocumentsMessage,
title,
}: DocumentListProps<DocumentType>) {
const formatDate = (isoString: string) =>
new Date(isoString).toLocaleDateString();
const columns = [
{
label: 'Name',
field: 'display_name',
},
{
label: 'Last modified',
field: 'updated_at',
classes: 'w-32',
},
];

const renderItem = (document: DocumentType, field: keyof DocumentType) => {
switch (field) {
case 'display_name':
return (
<div className="flex flex-row items-center gap-x-2">
{document.type === 'Folder' ? (
<FolderIcon className="w-5 h-5" />
) : document.type === 'Page' ? (
<FileCodeFilledIcon className="w-5 h-5" />
) : (
<FilePdfFilledIcon className="w-5 h-5" />
)}
{document.display_name}
</div>
);
case 'updated_at':
default:
return document.updated_at ? formatDate(document.updated_at) : '';
}
};

return (
<Scroll>
<DataTable
title={title}
emptyMessage={noDocumentsMessage}
columns={columns}
loading={isLoading}
rows={documents}
selectedRow={selectedDocument}
onSelectRow={onSelectDocument}
onConfirmRow={onUseDocument}
renderItem={renderItem}
/>
</Scroll>
);
}
88 changes: 0 additions & 88 deletions lms/static/scripts/frontend_apps/components/FileList.tsx

This file was deleted.

Loading

0 comments on commit 3bc9f1a

Please sign in to comment.