Merge pull request #3444 from urbit/lf/publish-fixup

publish: don't timeout, show groupify
This commit is contained in:
matildepark 2020-09-08 12:47:10 -04:00 committed by GitHub
commit 0ddb4a4887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 258 additions and 111 deletions

View File

@ -82,6 +82,17 @@ export default class PublishApi extends BaseApi {
return this.action('publish', 'publish-action', act);
}
groupify(bookId: string, group: Path | null) {
return this.publishAction({
groupify: {
book: bookId,
target: group,
inclusive: false
}
});
}
newBook(bookId: string, title: string, description: string, group?: Path) {
const groupInfo = group ? { 'group-path': group,
invitees: [],

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
export function useWaitForProps<P>(props: P, timeout: number) {
export function useWaitForProps<P>(props: P, timeout: number = 0) {
const [resolve, setResolve] = useState<() => void>(() => () => {});
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
@ -24,9 +24,11 @@ export function useWaitForProps<P>(props: P, timeout: number) {
setReady(() => r);
return new Promise<void>((resolve, reject) => {
setResolve(() => resolve);
setTimeout(() => {
reject(new Error("Timed out"));
}, timeout);
if(timeout > 0) {
setTimeout(() => {
reject(new Error("Timed out"));
}, timeout);
}
});
},
[setResolve, setReady, timeout]

View File

@ -78,6 +78,7 @@ export default class GroupReducer<S extends GroupState> {
this.addGroup(data, state);
this.removeGroup(data, state);
this.changePolicy(data, state);
this.expose(data, state);
}
}
@ -187,6 +188,15 @@ export default class GroupReducer<S extends GroupState> {
}
}
expose(json: GroupUpdate, state: S) {
if( 'expose' in json && state) {
const { resource } = json.expose;
const resourcePath = resourceAsPath(resource);
state.groups[resourcePath].hidden = false;
}
}
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
if ('addInvites' in diff) {
const { addInvites } = diff;

View File

@ -163,6 +163,7 @@ export default function PublishApp(props: PublishAppProps) {
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
associations={associations}
{...props}
/>
);

View File

@ -0,0 +1,77 @@
import React, { useEffect } from "react";
import { Box, Col, Button, InputLabel, InputCaption } from "@tlon/indigo-react";
import * as Yup from "yup";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { MetadataForm } from "./MetadataForm";
import { Groups, Associations } from "~/types";
import { Formik, FormikHelpers, Form } from "formik";
import GroupSearch from "~/views/components/GroupSearch";
import { AsyncButton } from "~/views/components/AsyncButton";
const formSchema = Yup.object({
group: Yup.string().nullable(),
});
interface FormSchema {
group: string | null;
}
interface GroupifyFormProps {
host: string;
book: string;
notebook: Notebook;
groups: Groups;
api: GlobalApi;
associations: Associations;
}
export function GroupifyForm(props: GroupifyFormProps) {
const onGroupify = async (
values: FormSchema,
actions: FormikHelpers<FormSchema>
) => {
try {
await props.api.publish.groupify(props.book, values.group);
actions.setStatus({ success: null });
} catch (e) {
actions.setStatus({ error: e.message });
}
};
const groupPath = props.notebook?.["writers-group-path"];
const isUnmanaged = props.groups?.[groupPath]?.hidden || false;
if (!isUnmanaged) {
return null;
}
const initialValues: FormSchema = {
group: null
};
return (
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onGroupify}
>
<Form style={{ display: "contents" }}>
<GroupSearch
id="group"
label="Group"
caption="What group should this notebook be added to? If blank, a new group will be made for the notebook"
associations={props.associations}
/>
<AsyncButton loadingText="Groupifying..." border>
Groupify
</AsyncButton>
</Form>
</Formik>
);
}
export default GroupifyForm;

View File

@ -17,7 +17,7 @@ export function JoinScreen(props: JoinScreenProps & RouteComponentProps) {
const [error, setError] = useState(false);
const joining = useRef(false);
const waiter = useWaitForProps(props, 10000);
const waiter = useWaitForProps(props);
const onJoin = useCallback(async () => {
joining.current = true;

View File

@ -0,0 +1,104 @@
import React, { useEffect } from "react";
import { AsyncButton } from "../../../../components/AsyncButton";
import * as Yup from "yup";
import {
Box,
Input,
Checkbox,
Col,
InputLabel,
InputCaption,
Button,
Center,
} from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps, useHistory } from "react-router-dom";
interface MetadataFormProps {
host: string;
book: string;
notebook: Notebook;
contacts: Contacts;
api: GlobalApi;
}
interface FormSchema {
name: string;
description: string;
comments: boolean;
}
const formSchema = Yup.object({
name: Yup.string().required("Notebook must have a name"),
description: Yup.string(),
comments: Yup.boolean(),
});
const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => {
const { resetForm } = useFormikContext<FormSchema>();
useEffect(() => {
resetForm({ values: props.init });
}, [props.book]);
return null;
};
export function MetadataForm(props: MetadataFormProps) {
const { host, notebook, api, book } = props;
const initialValues: FormSchema = {
name: notebook?.title,
description: notebook?.about,
comments: notebook?.comments,
};
const onSubmit = async (
values: FormSchema,
actions: FormikHelpers<FormSchema>
) => {
try {
const { name, description, comments } = values;
await api.publish.editBook(book, name, description, comments);
api.publish.fetchNotebook(host, book);
actions.setStatus({ success: null });
} catch (e) {
console.log(e);
actions.setStatus({ error: e.message });
}
};
return (
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form style={{ display: "contents" }}>
<Input
id="name"
label="Rename"
caption="Change the name of this notebook"
/>
<Input
id="description"
label="Change description"
caption="Change the description of this notebook"
/>
<Checkbox
id="comments"
label="Comments"
caption="Subscribers may comment when enabled"
/>
<ResetOnPropsChange init={initialValues} book={book} />
<AsyncButton primary loadingText="Updating.." border>
Save
</AsyncButton>
<FormError message="Failed to update settings" />
</Form>
</Formik>
);
}

View File

@ -20,6 +20,7 @@ import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import styled from "styled-components";
import {Associations} from "~/types";
const TabList = styled(_TabList)`
margin-bottom: ${(p) => p.theme.space[4]}px;
@ -38,6 +39,7 @@ interface NotebookProps {
contacts: Rolodex;
groups: Groups;
hideNicknames: boolean;
associations: Associations;
}
export function Notebook(props: NotebookProps & RouteComponentProps) {
@ -130,6 +132,8 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
api={api}
notebook={notebook}
contacts={notebookContacts}
associations={props.associations}
groups={groups}
/>
</TabPanel>
</TabPanels>

View File

@ -9,7 +9,7 @@ import { Contacts, Rolodex } from "../../../../types/contact-update";
import Notebook from "./Notebook";
import NewPost from "./new-post";
import { NoteRoutes } from './NoteRoutes';
import { LocalUpdateRemoteContentPolicy } from "~/types";
import { LocalUpdateRemoteContentPolicy, Associations } from "~/types";
interface NotebookRoutesProps {
api: GlobalApi;
@ -23,6 +23,7 @@ interface NotebookRoutesProps {
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
associations: Associations;
}
export function NotebookRoutes(

View File

@ -1,128 +1,65 @@
import React, { useEffect } from "react";
import { AsyncButton } from "../../../../components/AsyncButton";
import * as Yup from "yup";
import {
Box,
Input,
Checkbox,
Col,
InputLabel,
InputCaption,
Button,
Center,
} from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import { Box, Col, Button, InputLabel, InputCaption } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps, useHistory } from "react-router-dom";
import { MetadataForm } from "./MetadataForm";
import { Groups, Associations } from "~/types";
import GroupifyForm from "./GroupifyForm";
import { useHistory } from "react-router-dom";
interface SettingsProps {
host: string;
book: string;
notebook: Notebook;
contacts: Contacts;
groups: Groups;
api: GlobalApi;
associations: Associations;
}
interface FormSchema {
name: string;
description: string;
comments: boolean;
}
const formSchema = Yup.object({
name: Yup.string().required("Notebook must have a name"),
description: Yup.string(),
comments: Yup.boolean(),
});
const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => {
const { resetForm } = useFormikContext<FormSchema>();
useEffect(() => {
resetForm({ values: props.init });
}, [props.book]);
return null;
};
const Divider = (props) => (
<Box {...props} mb={4} borderBottom={1} borderBottomColor="lightGray" />
);
export function Settings(props: SettingsProps) {
const { host, notebook, api, book } = props;
const history = useHistory();
const initialValues: FormSchema = {
name: notebook?.title,
description: notebook?.about,
comments: notebook?.comments,
};
const onSubmit = async (
values: FormSchema,
actions: FormikHelpers<FormSchema>
) => {
try {
const { name, description, comments } = values;
await api.publish.editBook(book, name, description, comments);
api.publish.fetchNotebook(host, book);
actions.setStatus({ success: null });
} catch (e) {
console.log(e);
actions.setStatus({ error: e.message });
}
};
const onDelete = async () => {
await api.publish.delBook(book);
await props.api.publish.delBook(props.book);
history.push("/~publish");
};
const groupPath = props.notebook?.["writers-group-path"];
const isUnmanaged = props.groups?.[groupPath]?.hidden || false;
return (
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
<Box
mx="auto"
maxWidth="300px"
mb={4}
gridTemplateColumns="1fr"
gridAutoRows="auto"
display="grid"
>
<Form>
<Box
maxWidth="300px"
mb={4}
gridTemplateColumns="1fr"
gridAutoRows="auto"
display="grid"
>
<Col mb={4}>
<InputLabel>Delete Notebook</InputLabel>
<InputCaption>
Permanently delete this notebook. (All current members will no
longer see this notebook.)
</InputCaption>
<Button onClick={onDelete} mt={1} border error>
Delete this notebook
</Button>
</Col>
<Input
id="name"
label="Rename"
caption="Change the name of this notebook"
/>
<Input
id="description"
label="Change description"
caption="Change the description of this notebook"
/>
<Checkbox
id="comments"
label="Comments"
caption="Subscribers may comment when enabled"
/>
<ResetOnPropsChange init={initialValues} book={book} />
<AsyncButton loadingText="Updating.." border>
Save
</AsyncButton>
<FormError message="Failed to update settings" />
</Box>
</Form>
</Formik>
{isUnmanaged && (
<>
<GroupifyForm {...props} />
<Divider mt={4} />
</>
)}
<MetadataForm {...props} />
<Divider />
<Col mb={4}>
<InputLabel>Delete Notebook</InputLabel>
<InputCaption>
Permanently delete this notebook. (All current members will no longer
see this notebook.)
</InputCaption>
<Button onClick={onDelete} mt={1} border error>
Delete this notebook
</Button>
</Col>
</Box>
);
}

View File

@ -88,7 +88,7 @@ export function GroupSearch(props: InviteSearchProps) {
caption={props.caption}
candidates={groups}
renderCandidate={renderCandidate}
disabled={value.length !== 0}
disabled={value && value.length !== 0}
search={(s: string, a: Association) =>
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
}