mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-13 08:38:43 +03:00
InvitePopover: match spec
This commit is contained in:
parent
14b8564b8c
commit
3ba8e7bce1
6
pkg/interface/package-lock.json
generated
6
pkg/interface/package-lock.json
generated
@ -1959,6 +1959,12 @@
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@types/yup": {
|
||||
"version": "0.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.7.tgz",
|
||||
"integrity": "sha512-x3Zeh8/qLZ6fG4S1EztI1S1mLj6N1pSUV1PAj/9finZba48d3Maxtyz4WYNUY0NE76u1KSukfNLkjcRlb+O00g==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz",
|
||||
|
@ -59,6 +59,7 @@
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/styled-components": "^5.1.2",
|
||||
"@types/styled-system": "^5.1.10",
|
||||
"@types/yup": "^0.29.7",
|
||||
"@typescript-eslint/eslint-plugin": "^3.8.0",
|
||||
"@typescript-eslint/parser": "^3.8.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
|
137
pkg/interface/src/views/components/ChipInput.tsx
Normal file
137
pkg/interface/src/views/components/ChipInput.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
ReactNode,
|
||||
SyntheticEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
Box,
|
||||
Label,
|
||||
Row,
|
||||
Col,
|
||||
StatelessTextInput as Input,
|
||||
ErrorLabel
|
||||
} from "@tlon/indigo-react";
|
||||
import { useField } from "formik";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as Yup from "yup";
|
||||
|
||||
function Chip(props: { children: ReactNode }) {
|
||||
return (
|
||||
<Row
|
||||
alignItems="center"
|
||||
height="24px"
|
||||
borderRadius="1"
|
||||
my="1"
|
||||
p="1"
|
||||
bg="blue"
|
||||
color="white"
|
||||
>
|
||||
{props.children}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChipInputProps {
|
||||
id: string;
|
||||
label: string;
|
||||
caption?: string;
|
||||
placeholder: string;
|
||||
breakOnSpace?: boolean;
|
||||
}
|
||||
|
||||
export function ChipInput(props: ChipInputProps) {
|
||||
const { id, label, caption, placeholder } = props;
|
||||
const [{ onBlur, value }, meta, { setError, setValue }] = useField<string[]>(
|
||||
id
|
||||
);
|
||||
const [newChip, setNextChip] = useState("");
|
||||
const onChange = useCallback(
|
||||
(e: any) => {
|
||||
setNextChip(e.target.value);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const addNewChip = useCallback(() => {
|
||||
setValue([...value, newChip]);
|
||||
setNextChip("");
|
||||
}, [setValue, value, newChip, setNextChip]);
|
||||
|
||||
const removeLastChip = useCallback(() => {
|
||||
setValue(value.slice(0, value.length - 1));
|
||||
}, [value, setValue]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) {
|
||||
return () => {};
|
||||
}
|
||||
const mousetrap = Mousetrap(inputRef.current);
|
||||
mousetrap.bind("backspace", (e) => {
|
||||
if (newChip.length === 0) {
|
||||
removeLastChip();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
mousetrap.bind("tab", (e) => {
|
||||
addNewChip();
|
||||
return false;
|
||||
});
|
||||
mousetrap.bind("space", (e) => {
|
||||
if (props.breakOnSpace) {
|
||||
addNewChip();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return () => {
|
||||
mousetrap.unbind("tab");
|
||||
mousetrap.unbind("backspace");
|
||||
mousetrap.unbind("space");
|
||||
};
|
||||
}, [inputRef.current, addNewChip, newChip]);
|
||||
|
||||
return (
|
||||
<Col gapY="2">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
{caption && <Label gray>{caption}</Label>}
|
||||
<Row
|
||||
border="1"
|
||||
borderColor="washedGray"
|
||||
borderRadius="1"
|
||||
pl="2"
|
||||
gapX="2"
|
||||
width="100%"
|
||||
flexWrap="wrap"
|
||||
minHeight="32px"
|
||||
>
|
||||
{value.map((c, idx) => (
|
||||
<Chip key={idx}>{c}</Chip>
|
||||
))}
|
||||
<Input
|
||||
width="auto"
|
||||
height="24px"
|
||||
flexShrink="1"
|
||||
flexGrow="1"
|
||||
pl="0"
|
||||
ref={inputRef}
|
||||
onChange={onChange}
|
||||
value={newChip}
|
||||
onBlur={onBlur}
|
||||
placeholder={placeholder}
|
||||
border="0"
|
||||
my="1"
|
||||
py="1"
|
||||
/>
|
||||
</Row>
|
||||
<ErrorLabel mt="2" hasError={!!(meta.touched && meta.error)}>
|
||||
{meta.error}
|
||||
</ErrorLabel>
|
||||
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -12,10 +12,9 @@ import {
|
||||
Box,
|
||||
Label,
|
||||
ErrorLabel,
|
||||
StatelessTextInput as Input
|
||||
} from "@tlon/indigo-react";
|
||||
import { useDropdown } from "~/logic/lib/useDropdown";
|
||||
import styled from "styled-components";
|
||||
import { space, color, layout, border } from "styled-system";
|
||||
|
||||
interface RenderChoiceProps<C> {
|
||||
candidate: C;
|
||||
@ -47,23 +46,9 @@ interface DropdownSearchProps<C> {
|
||||
caption?: string;
|
||||
disabled?: boolean;
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const TextArea = styled.input`
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
margin-top: ${(p) => p.theme.space[1]}px;
|
||||
padding: ${(p) => p.theme.space[2]}px;
|
||||
font-size: ${(p) => p.theme.fontSizes[0]}px;
|
||||
line-height: 1.2;
|
||||
${space}
|
||||
${color}
|
||||
${layout}
|
||||
${border}
|
||||
`;
|
||||
|
||||
export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
|
||||
const textarea = useRef<HTMLTextAreaElement>();
|
||||
const { candidates, getKey, caption } = props;
|
||||
@ -136,16 +121,12 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
|
||||
<Label htmlFor={props.id}>{props.label}</Label>
|
||||
{caption ? <Label mt="2" gray>{caption}</Label> : null}
|
||||
{!props.disabled && (
|
||||
<TextArea
|
||||
<Input
|
||||
ref={textarea}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
bg="white"
|
||||
color="black"
|
||||
borderRadius={2}
|
||||
onChange={onChange}
|
||||
value={query}
|
||||
autocomplete="off"
|
||||
placeholder={props.placeholder || ""}
|
||||
/>
|
||||
)}
|
||||
{dropdown.length !== 0 && query.length !== 0 && (
|
||||
|
@ -134,7 +134,7 @@ export function ShipSearch(props: InviteSearchProps) {
|
||||
value={undefined}
|
||||
error={error}
|
||||
/>
|
||||
<Row flexWrap="wrap">
|
||||
<Row minHeight="34px" flexWrap="wrap">
|
||||
{value.map((s) => (
|
||||
<Box
|
||||
fontFamily="mono"
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useRef, useMemo } from "react";
|
||||
import { Box, Text, Col, Button, Row } from "@tlon/indigo-react";
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { ShipSearch } from "~/views/components/ShipSearch";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
@ -11,6 +12,7 @@ import { FormError } from "~/views/components/FormError";
|
||||
import { resourceFromPath } from "~/logic/lib/group";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Groups, Rolodex } from "~/types";
|
||||
import { ChipInput } from "~/views/components/ChipInput";
|
||||
|
||||
interface InvitePopoverProps {
|
||||
baseUrl: string;
|
||||
@ -20,11 +22,21 @@ interface InvitePopoverProps {
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
interface FormSchema {
|
||||
emails: string[];
|
||||
ships: string[];
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
emails: Yup.array(Yup.string().email("Invalid email")),
|
||||
ships: Yup.array(Yup.string())
|
||||
});
|
||||
|
||||
export function InvitePopover(props: InvitePopoverProps) {
|
||||
const { baseUrl, api, association } = props;
|
||||
|
||||
const relativePath = (p: string) => baseUrl + p;
|
||||
const { title } = association?.metadata || '';
|
||||
const { title } = association?.metadata || "";
|
||||
const innerRef = useRef(null);
|
||||
const history = useHistory();
|
||||
|
||||
@ -33,7 +45,8 @@ export function InvitePopover(props: InvitePopoverProps) {
|
||||
}, [history.push, props.baseUrl]);
|
||||
useOutsideClick(innerRef, onOutsideClick);
|
||||
|
||||
const onSubmit = async ({ ships }: { ships: string[] }, actions) => {
|
||||
const onSubmit = async ({ ships, emails }: { ships: string[] }, actions) => {
|
||||
// TODO: how to invite via email?
|
||||
try {
|
||||
const resource = resourceFromPath(association["group-path"]);
|
||||
await ships.reduce(
|
||||
@ -48,6 +61,9 @@ export function InvitePopover(props: InvitePopoverProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const initialValues: FormSchema = { ships: [], emails: [] };
|
||||
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={[relativePath("/invites")]}>
|
||||
@ -72,10 +88,14 @@ export function InvitePopover(props: InvitePopoverProps) {
|
||||
width="380px"
|
||||
bg="white"
|
||||
>
|
||||
<Formik initialValues={{ ships: [] }} onSubmit={onSubmit}>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
validationSchema={formSchema}
|
||||
>
|
||||
<Form>
|
||||
<Col p={3}>
|
||||
<Box mb={2}>
|
||||
<Col gapY="3" p={3}>
|
||||
<Box>
|
||||
<Text>Invite to </Text>
|
||||
<Text fontWeight="800">{title}</Text>
|
||||
</Box>
|
||||
@ -86,13 +106,24 @@ export function InvitePopover(props: InvitePopoverProps) {
|
||||
label=""
|
||||
/>
|
||||
<FormError message="Failed to invite" />
|
||||
<ChipInput
|
||||
id="emails"
|
||||
label="Invite via Email"
|
||||
caption="Send an Urbit ID and invite them to this group"
|
||||
placeholder="name@example.com"
|
||||
breakOnSpace
|
||||
/>
|
||||
</Col>
|
||||
<Row
|
||||
borderTop={1}
|
||||
borderTopColor="washedGray"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<AsyncButton border={0} color="blue" loadingText="Inviting...">
|
||||
<AsyncButton
|
||||
border={0}
|
||||
color="blue"
|
||||
loadingText="Inviting..."
|
||||
>
|
||||
Send
|
||||
</AsyncButton>
|
||||
</Row>
|
||||
|
Loading…
Reference in New Issue
Block a user