fix: Allow selection of embeds (#1562)
* feat: Support importing .docx or .html files as new documents (#1551) * Support importing .docx as new documents * Add html file support, build types and interface for easily adding file types to importer * fix: Upload embedded images in docx to storage * refactor: Bulk of logic to command * refactor: Do all importing on server, so we're not splitting logic for import into two places * test: Add documentImporter tests Co-authored-by: Lance Whatley <whatl3y@gmail.com> * fix: Accessibility audit * fix: Quick fix, non editable title closes #1560 * fix: Embed selection Co-authored-by: Lance Whatley <whatl3y@gmail.com>
This commit is contained in:
@@ -8,7 +8,6 @@ import { withRouter, type RouterHistory, type Match } from "react-router-dom";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import importFile from "utils/importFile";
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
let importingLock = false;
|
||||
@@ -61,12 +60,12 @@ class DropToImport extends React.Component<Props> {
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const doc = await importFile({
|
||||
documents: this.props.documents,
|
||||
const doc = await this.props.documents.import(
|
||||
file,
|
||||
documentId,
|
||||
collectionId,
|
||||
});
|
||||
{ publish: true }
|
||||
);
|
||||
|
||||
if (redirect) {
|
||||
this.props.history.push(doc.url);
|
||||
@@ -95,7 +94,7 @@ class DropToImport extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
accept="text/markdown, text/plain"
|
||||
accept={documents.importFileTypes.join(", ")}
|
||||
onDropAccepted={this.onDropAccepted}
|
||||
style={EMPTY_OBJECT}
|
||||
disableClick
|
||||
|
||||
@@ -177,6 +177,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
{label || (
|
||||
<NudeButton
|
||||
id={`${this.id}button`}
|
||||
aria-label="More options"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen ? "true" : "false"}
|
||||
aria-controls={this.id}
|
||||
|
||||
@@ -21,7 +21,7 @@ function HeaderBlock({
|
||||
}: Props) {
|
||||
return (
|
||||
<Header justify="flex-start" align="center" {...rest}>
|
||||
<TeamLogo alt={`${teamName} logo`} src={logoUrl} />
|
||||
<TeamLogo alt={`${teamName} logo`} src={logoUrl} size="38px" />
|
||||
<Flex align="flex-start" column>
|
||||
<TeamName showDisclosure>
|
||||
{teamName}{" "}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const TeamLogo = styled.img`
|
||||
width: auto;
|
||||
height: 38px;
|
||||
width: ${(props) => props.size || "auto"};
|
||||
height: ${(props) => props.size || "38px"};
|
||||
border-radius: 4px;
|
||||
background: ${(props) => props.theme.background};
|
||||
outline: 1px solid ${(props) => props.theme.divider};
|
||||
border: 1px solid ${(props) => props.theme.divider};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export default TeamLogo;
|
||||
|
||||
@@ -21,6 +21,7 @@ export default class Abstract extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://app.goabstract.com/embed/${shareId}`}
|
||||
title={`Abstract (${shareId})`}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ export default class Airtable extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://airtable.com/embed/${shareId}`}
|
||||
title={`Airtable (${shareId})`}
|
||||
border
|
||||
|
||||
@@ -17,6 +17,12 @@ export default class ClickUp extends React.Component<Props> {
|
||||
static ENABLED = [URL_REGEX];
|
||||
|
||||
render() {
|
||||
return <Frame src={this.props.attrs.href} title="ClickUp Embed" />;
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href}
|
||||
title="ClickUp Embed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ export default class Codepen extends React.Component<Props> {
|
||||
render() {
|
||||
const normalizedUrl = this.props.attrs.href.replace(/\/pen\//, "/embed/");
|
||||
|
||||
return <Frame src={normalizedUrl} title="Codepen Embed" />;
|
||||
return <Frame {...this.props} src={normalizedUrl} title="Codepen Embed" />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export default class Figma extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://www.figma.com/embed?embed_host=outline&url=${this.props.attrs.href}`}
|
||||
title="Figma Embed"
|
||||
border
|
||||
|
||||
@@ -15,6 +15,13 @@ export default class Framer extends React.Component<Props> {
|
||||
static ENABLED = [URL_REGEX];
|
||||
|
||||
render() {
|
||||
return <Frame src={this.props.attrs.href} title="Framer Embed" border />;
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href}
|
||||
title="Framer Embed"
|
||||
border
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const URL_REGEX = new RegExp(
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
isSelected: boolean,
|
||||
attrs: {|
|
||||
href: string,
|
||||
matches: string[],
|
||||
@@ -48,6 +49,7 @@ class Gist extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className={this.props.isSelected ? "ProseMirror-selectednode" : ""}
|
||||
ref={this.updateIframeContent}
|
||||
type="text/html"
|
||||
frameBorder="0"
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class GoogleDocs extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href.replace("/edit", "/preview")}
|
||||
icon={
|
||||
<img
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class GoogleSlides extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href.replace("/edit", "/preview")}
|
||||
icon={
|
||||
<img
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class GoogleSlides extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href
|
||||
.replace("/edit", "/preview")
|
||||
.replace("/pub", "/embed")}
|
||||
|
||||
@@ -12,6 +12,7 @@ const IMAGE_REGEX = new RegExp(
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
isSelected: boolean,
|
||||
attrs: {|
|
||||
href: string,
|
||||
matches: string[],
|
||||
@@ -25,6 +26,7 @@ export default class InVision extends React.Component<Props> {
|
||||
if (IMAGE_REGEX.test(this.props.attrs.href)) {
|
||||
return (
|
||||
<ImageZoom
|
||||
className={this.props.isSelected ? "ProseMirror-selectednode" : ""}
|
||||
image={{
|
||||
src: this.props.attrs.href,
|
||||
alt: "InVision Embed",
|
||||
@@ -37,6 +39,12 @@ export default class InVision extends React.Component<Props> {
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Frame src={this.props.attrs.href} title="InVision Embed" />;
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href}
|
||||
title="InVision Embed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ export default class Loom extends React.Component<Props> {
|
||||
render() {
|
||||
const normalizedUrl = this.props.attrs.href.replace("share", "embed");
|
||||
|
||||
return <Frame src={normalizedUrl} title="Loom Embed" />;
|
||||
return <Frame {...this.props} src={normalizedUrl} title="Loom Embed" />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export default class Lucidchart extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://lucidchart.com/documents/embeddedchart/${chartId}`}
|
||||
title="Lucidchart Embed"
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,13 @@ export default class Marvel extends React.Component<Props> {
|
||||
static ENABLED = [URL_REGEX];
|
||||
|
||||
render() {
|
||||
return <Frame src={this.props.attrs.href} title="Marvel Embed" border />;
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href}
|
||||
title="Marvel Embed"
|
||||
border
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export default class Mindmeister extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://www.mindmeister.com/maps/public_map_shell/${chartId}`}
|
||||
title="Mindmeister Embed"
|
||||
border
|
||||
|
||||
@@ -20,6 +20,7 @@ export default class RealtimeBoard extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://realtimeboard.com/app/embed/${boardId}`}
|
||||
title={`RealtimeBoard (${boardId})`}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,11 @@ export default class ModeAnalytics extends React.Component<Props> {
|
||||
const normalizedUrl = this.props.attrs.href.replace(/\/embed$/, "");
|
||||
|
||||
return (
|
||||
<Frame src={`${normalizedUrl}/embed`} title="Mode Analytics Embed" />
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`${normalizedUrl}/embed`}
|
||||
title="Mode Analytics Embed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export default class Prezi extends React.Component<Props> {
|
||||
render() {
|
||||
const url = this.props.attrs.href.replace(/\/embed$/, "");
|
||||
|
||||
return <Frame src={`${url}/embed`} title="Prezi Embed" border />;
|
||||
return (
|
||||
<Frame {...this.props} src={`${url}/embed`} title="Prezi Embed" border />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export default class Spotify extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
width="300px"
|
||||
height="380px"
|
||||
src={`https://open.spotify.com/embed${normalizedPath}`}
|
||||
|
||||
@@ -31,6 +31,7 @@ export default class Trello extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
width="248px"
|
||||
height="185px"
|
||||
src={`https://trello.com/embed/board?id=${objectId}`}
|
||||
|
||||
@@ -17,6 +17,12 @@ export default class Typeform extends React.Component<Props> {
|
||||
static ENABLED = [URL_REGEX];
|
||||
|
||||
render() {
|
||||
return <Frame src={this.props.attrs.href} title="Typeform Embed" />;
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={this.props.attrs.href}
|
||||
title="Typeform Embed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export default class Vimeo extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://player.vimeo.com/video/${videoId}?byline=0`}
|
||||
title={`Vimeo Embed (${videoId})`}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Frame from "./components/Frame";
|
||||
const URL_REGEX = /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})$/i;
|
||||
|
||||
type Props = {|
|
||||
isSelected: boolean,
|
||||
attrs: {|
|
||||
href: string,
|
||||
matches: string[],
|
||||
@@ -20,6 +21,7 @@ export default class YouTube extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<Frame
|
||||
{...this.props}
|
||||
src={`https://www.youtube.com/embed/${videoId}?modestbranding=1`}
|
||||
title={`YouTube (${videoId})`}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ type Props = {
|
||||
title?: string,
|
||||
icon?: React.Node,
|
||||
canonicalUrl?: string,
|
||||
isSelected?: boolean,
|
||||
width?: string,
|
||||
height?: string,
|
||||
};
|
||||
@@ -48,13 +49,19 @@ class Frame extends React.Component<PropsWithRef> {
|
||||
icon,
|
||||
title,
|
||||
canonicalUrl,
|
||||
...rest
|
||||
isSelected,
|
||||
src,
|
||||
} = this.props;
|
||||
const Component = border ? StyledIframe : "iframe";
|
||||
const withBar = !!(icon || canonicalUrl);
|
||||
|
||||
return (
|
||||
<Rounded width={width} height={height} withBar={withBar}>
|
||||
<Rounded
|
||||
width={width}
|
||||
height={height}
|
||||
withBar={withBar}
|
||||
className={isSelected ? "ProseMirror-selectednode" : ""}
|
||||
>
|
||||
{this.isLoaded && (
|
||||
<Component
|
||||
ref={forwardedRef}
|
||||
@@ -66,8 +73,8 @@ class Frame extends React.Component<PropsWithRef> {
|
||||
frameBorder="0"
|
||||
title="embed"
|
||||
loading="lazy"
|
||||
src={src}
|
||||
allowFullScreen
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
{withBar && (
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import Modal from "components/Modal";
|
||||
import VisuallyHidden from "components/VisuallyHidden";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
import importFile from "utils/importFile";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
@@ -55,11 +54,13 @@ class CollectionMenu extends React.Component<Props> {
|
||||
const files = getDataTransferFiles(ev);
|
||||
|
||||
try {
|
||||
const document = await importFile({
|
||||
file: files[0],
|
||||
documents: this.props.documents,
|
||||
collectionId: this.props.collection.id,
|
||||
});
|
||||
const file = files[0];
|
||||
const document = await this.props.documents.import(
|
||||
file,
|
||||
null,
|
||||
this.props.collection.id,
|
||||
{ publish: true }
|
||||
);
|
||||
this.props.history.push(document.url);
|
||||
} catch (err) {
|
||||
this.props.ui.showToast(err.message);
|
||||
@@ -103,7 +104,14 @@ class CollectionMenu extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { policies, collection, position, onOpen, onClose } = this.props;
|
||||
const {
|
||||
policies,
|
||||
documents,
|
||||
collection,
|
||||
position,
|
||||
onOpen,
|
||||
onClose,
|
||||
} = this.props;
|
||||
const can = policies.abilities(collection.id);
|
||||
|
||||
return (
|
||||
@@ -114,7 +122,7 @@ class CollectionMenu extends React.Component<Props> {
|
||||
ref={(ref) => (this.file = ref)}
|
||||
onChange={this.onFilePicked}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
accept="text/markdown, text/plain"
|
||||
accept={documents.importFileTypes.join(", ")}
|
||||
/>
|
||||
</VisuallyHidden>
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ class DocumentScene extends React.Component<Props> {
|
||||
|
||||
if (this.props.readOnly) {
|
||||
this.lastRevision = document.revision;
|
||||
|
||||
if (document.title !== this.title) {
|
||||
this.title = document.title;
|
||||
}
|
||||
} else if (prevProps.document.revision !== this.lastRevision) {
|
||||
if (auth.user && document.updatedBy.id !== auth.user.id) {
|
||||
this.props.ui.showToast(
|
||||
@@ -106,12 +110,9 @@ class DocumentScene extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isDirty && document.title !== this.title) {
|
||||
this.title = document.title;
|
||||
}
|
||||
|
||||
if (document.injectTemplate) {
|
||||
document.injectTemplate = false;
|
||||
this.title = document.title;
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,23 @@ import Document from "models/Document";
|
||||
import type { FetchOptions, PaginationParams, SearchResult } from "types";
|
||||
import { client } from "utils/ApiClient";
|
||||
|
||||
type ImportOptions = {
|
||||
publish?: boolean,
|
||||
};
|
||||
|
||||
export default class DocumentsStore extends BaseStore<Document> {
|
||||
@observable recentlyViewedIds: string[] = [];
|
||||
@observable searchCache: Map<string, SearchResult[]> = new Map();
|
||||
@observable starredIds: Map<string, boolean> = new Map();
|
||||
@observable backlinks: Map<string, string[]> = new Map();
|
||||
|
||||
importFileTypes: string[] = [
|
||||
"text/markdown",
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
];
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, Document);
|
||||
}
|
||||
@@ -455,6 +466,41 @@ export default class DocumentsStore extends BaseStore<Document> {
|
||||
return this.add(res.data);
|
||||
};
|
||||
|
||||
@action
|
||||
import = async (
|
||||
file: File,
|
||||
parentDocumentId: string,
|
||||
collectionId: string,
|
||||
options: ImportOptions
|
||||
) => {
|
||||
const title = file.name.replace(/\.[^/.]+$/, "");
|
||||
const formData = new FormData();
|
||||
|
||||
[
|
||||
{ key: "parentDocumentId", value: parentDocumentId },
|
||||
{ key: "collectionId", value: collectionId },
|
||||
{ key: "title", value: title },
|
||||
{ key: "publish", value: options.publish },
|
||||
{ key: "file", value: file },
|
||||
].map((info) => {
|
||||
if (typeof info.value === "string" && info.value) {
|
||||
formData.append(info.key, info.value);
|
||||
}
|
||||
if (typeof info.value === "boolean") {
|
||||
formData.append(info.key, info.value.toString());
|
||||
}
|
||||
if (info.value instanceof File) {
|
||||
formData.append(info.key, info.value);
|
||||
}
|
||||
});
|
||||
|
||||
const res = await client.post("/documents.import", formData);
|
||||
invariant(res && res.data, "Data should be available");
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
return this.add(res.data);
|
||||
};
|
||||
|
||||
_add = this.add;
|
||||
|
||||
@action
|
||||
|
||||
@@ -28,12 +28,13 @@ class ApiClient {
|
||||
fetch = async (
|
||||
path: string,
|
||||
method: string,
|
||||
data: ?Object,
|
||||
data: ?Object | FormData | void,
|
||||
options: Object = {}
|
||||
) => {
|
||||
let body;
|
||||
let modifiedPath;
|
||||
let urlToFetch;
|
||||
let isJson;
|
||||
|
||||
if (method === "GET") {
|
||||
if (data) {
|
||||
@@ -42,7 +43,18 @@ class ApiClient {
|
||||
modifiedPath = path;
|
||||
}
|
||||
} else if (method === "POST" || method === "PUT") {
|
||||
body = data ? JSON.stringify(data) : undefined;
|
||||
body = data || undefined;
|
||||
|
||||
// Only stringify data if its a normal object and
|
||||
// not if it's [object FormData], in addition to
|
||||
// toggling Content-Type to application/json
|
||||
if (
|
||||
typeof data === "object" &&
|
||||
(data || "").toString() === "[object Object]"
|
||||
) {
|
||||
isJson = true;
|
||||
body = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.match(/^http/)) {
|
||||
@@ -51,14 +63,20 @@ class ApiClient {
|
||||
urlToFetch = this.baseUrl + (modifiedPath || path);
|
||||
}
|
||||
|
||||
// Construct headers
|
||||
const headers = new Headers({
|
||||
let headerOptions: any = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"cache-control": "no-cache",
|
||||
"x-editor-version": EDITOR_VERSION,
|
||||
pragma: "no-cache",
|
||||
});
|
||||
};
|
||||
// for multipart forms or other non JSON requests fetch
|
||||
// populates the Content-Type without needing to explicitly
|
||||
// set it.
|
||||
if (isJson) {
|
||||
headerOptions["Content-Type"] = "application/json";
|
||||
}
|
||||
const headers = new Headers(headerOptions);
|
||||
|
||||
if (stores.auth.authenticated) {
|
||||
invariant(stores.auth.token, "JWT token not set properly");
|
||||
headers.set("Authorization", `Bearer ${stores.auth.token}`);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// @flow
|
||||
import parseTitle from "shared/utils/parseTitle";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import Document from "models/Document";
|
||||
|
||||
type Options = {
|
||||
file: File,
|
||||
documents: DocumentsStore,
|
||||
collectionId: string,
|
||||
documentId?: string,
|
||||
};
|
||||
|
||||
const importFile = async ({
|
||||
documents,
|
||||
file,
|
||||
documentId,
|
||||
collectionId,
|
||||
}: Options): Promise<Document> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async (ev) => {
|
||||
let text = ev.target.result;
|
||||
let title;
|
||||
|
||||
// If the first line of the imported file looks like a markdown heading
|
||||
// then we can use this as the document title
|
||||
if (text.trim().startsWith("# ")) {
|
||||
const result = parseTitle(text);
|
||||
title = result.title;
|
||||
text = text.replace(`# ${title}\n`, "");
|
||||
|
||||
// otherwise, just use the filename without the extension as our best guess
|
||||
} else {
|
||||
title = file.name.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
|
||||
let document = new Document(
|
||||
{
|
||||
parentDocumentId: documentId,
|
||||
collectionId,
|
||||
text,
|
||||
title,
|
||||
},
|
||||
documents
|
||||
);
|
||||
try {
|
||||
document = await document.save({ publish: true });
|
||||
resolve(document);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
export default importFile;
|
||||
Reference in New Issue
Block a user