diff --git a/pkg/interface/src/views/components/DropdownSearch.tsx b/pkg/interface/src/views/components/DropdownSearch.tsx index 982905891..6a12e5acd 100644 --- a/pkg/interface/src/views/components/DropdownSearch.tsx +++ b/pkg/interface/src/views/components/DropdownSearch.tsx @@ -35,6 +35,8 @@ interface DropdownSearchExtraProps { onSelect: (c: C) => void; disabled?: boolean; placeholder?: string; + onChange?: (e: ChangeEvent) => void; + onBlur?: (e: any) => void; } type DropdownSearchProps = PropFunc & @@ -51,6 +53,8 @@ export function DropdownSearch(props: DropdownSearchProps) { renderCandidate, disabled, placeholder, + onChange = () => {}, + onBlur = () => {}, ...rest } = props; @@ -101,8 +105,9 @@ export function DropdownSearch(props: DropdownSearchProps) { }; }, [textarea.current, next, back, onEnter]); - const onChange = useCallback( + const changeCallback = useCallback( (e: ChangeEvent) => { + onChange(e); search(e.target.value); setQuery(e.target.value); }, @@ -128,11 +133,12 @@ export function DropdownSearch(props: DropdownSearchProps) { {dropdown.length !== 0 && query.length !== 0 && ( ( export function ShipSearch(props: InviteSearchProps) { const { id, label, caption } = props; - const [{ value }, { error }, { setValue, setTouched }] = useField( - props.id + const [{}, meta, { setValue, setTouched, setError: _setError }] = useField({ + name: id, + multiple: true + }); + + const setError = _setError as unknown as (s: string | undefined) => void; + + const { error, touched } = meta; + + const [selected, setSelected] = useState([] as string[]); + const [inputShip, setInputShip] = useState(undefined as string | undefined); + const [inputTouched, setInputTouched] = useState(false); + + const checkInput = useCallback((valid: boolean, ship: string | undefined) => { + if(valid) { + setInputShip(ship); + setError(error === INVALID_SHIP_ERR ? undefined : error); + } else { + setError(INVALID_SHIP_ERR); + setInputTouched(false); + } + }, [setError, error, setInputTouched, setInputShip]); + + const onChange = useCallback( + (e: any) => { + let ship = `~${deSig(e.target.value) || ""}`; + if(ob.isValidPatp(ship)) { + checkInput(true, ship); + } else { + checkInput(ship.length !== 1, undefined) + } + }, + [checkInput] ); + const onBlur = useCallback(() => { + setInputTouched(true); + }, [setInputTouched]); + const onSelect = useCallback( (s: string) => { setTouched(true); - setValue([...value, s]); + checkInput(true, undefined); + s = `~${deSig(s)}`; + setSelected(v => _.uniq([...v, s])) }, - [setValue, value] + [setTouched, checkInput, setSelected] ); const onRemove = useCallback( (s: string) => { - setValue(value.filter((v) => v !== s)); + setSelected(ships => ships.filter(ship => ship !== s)) }, - [setValue, value] + [setSelected] ); + useEffect(() => { + const newValue = inputShip ? [...selected, inputShip] : selected; + setValue(newValue); + }, [inputShip, selected]) + const [peers, nicknames] = useMemo(() => { const peerSet = new Set(); const contacts = new Map(); @@ -125,20 +169,22 @@ export function ShipSearch(props: InviteSearchProps) { isExact={(s) => { const ship = `~${deSig(s)}`; const result = ob.isValidPatp(ship); - return result ? deSig(s) : undefined; + return result ? deSig(s) ?? undefined : undefined; }} placeholder="Search for ships" candidates={peers} renderCandidate={renderCandidate} - disabled={props.maxLength ? value.length >= props.maxLength : false} + disabled={props.maxLength ? selected.length >= props.maxLength : false} search={(s: string, t: string) => t.toLowerCase().startsWith(s.toLowerCase()) } getKey={(s: string) => s} onSelect={onSelect} + onChange={onChange} + onBlur={onBlur} /> - {value.map((s) => ( + {selected.map((s) => ( ))} + + {error} + ); } diff --git a/pkg/interface/src/views/landscape/components/InvitePopover.tsx b/pkg/interface/src/views/landscape/components/InvitePopover.tsx index 2aac6d151..4c6a20acb 100644 --- a/pkg/interface/src/views/landscape/components/InvitePopover.tsx +++ b/pkg/interface/src/views/landscape/components/InvitePopover.tsx @@ -1,18 +1,18 @@ import React, { useCallback, useRef, useMemo } from "react"; -import { Box, Text, Col, Button, Row } from "@tlon/indigo-react"; +import { Switch, Route, useHistory } from "react-router-dom"; +import { Formik, Form } from "formik"; import * as Yup from 'yup'; +import { Box, Text, Col, Button, Row } from "@tlon/indigo-react"; import { ShipSearch } from "~/views/components/ShipSearch"; import { Association } from "~/types/metadata-update"; -import { Switch, Route, useHistory } from "react-router-dom"; -import { Formik, Form } from "formik"; import { AsyncButton } from "~/views/components/AsyncButton"; import { useOutsideClick } from "~/logic/lib/useOutsideClick"; import { FormError } from "~/views/components/FormError"; import { resourceFromPath } from "~/logic/lib/group"; import GlobalApi from "~/logic/api/global"; import { Groups, Rolodex, Workspace } from "~/types"; -import { ChipInput } from "~/views/components/ChipInput"; +import { deSig } from "~/logic/lib/util"; interface InvitePopoverProps { baseUrl: string; @@ -30,7 +30,7 @@ interface FormSchema { const formSchema = Yup.object({ emails: Yup.array(Yup.string().email("Invalid email")), - ships: Yup.array(Yup.string()) + ships: Yup.array(Yup.string()).min(1, "Must invite at least one ship") }); export function InvitePopover(props: InvitePopoverProps) { @@ -48,14 +48,14 @@ export function InvitePopover(props: InvitePopoverProps) { const onSubmit = async ({ ships, emails }: { ships: string[] }, actions) => { if(props.workspace.type === 'home') { - history.push(`/~landscape/dm/${ships[0]}`); + history.push(`/~landscape/dm/${deSig(ships[0])}`); return; } // TODO: how to invite via email? try { const resource = resourceFromPath(association["group-path"]); await ships.reduce( - (acc, s) => acc.then(() => api.contacts.invite(resource, `~${s}`)), + (acc, s) => acc.then(() => api.contacts.invite(resource, `~${deSig(s)}`)), Promise.resolve() ); actions.setStatus({ success: null }); @@ -97,9 +97,10 @@ export function InvitePopover(props: InvitePopoverProps) { initialValues={initialValues} onSubmit={onSubmit} validationSchema={formSchema} + validateOnBlur >
- + Invite to {title || "DM"} @@ -122,13 +123,12 @@ export function InvitePopover(props: InvitePopoverProps) { /> */} Send