Merge pull request #4358 from urbit/lf/fe-things

Contacts: cleanup GroupSearch, allow add/remove of pinned groups from profile
This commit is contained in:
L 2021-02-02 12:04:36 -06:00 committed by GitHub
commit 80959119d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 86 deletions

View File

@ -75,7 +75,7 @@
[%color s+(scot %ux color.contact)]
[%avatar ?~(avatar.contact ~ s+u.avatar.contact)]
[%cover ?~(cover.contact ~ s+u.cover.contact)]
[%groups a+(turn ~(tap in groups.contact) |=(r=resource (enjs:res r)))]
[%groups a+(turn ~(tap in groups.contact) (cork enjs-path:res (lead %s)))]
[%last-updated (time last-updated.contact)]
==
::
@ -90,8 +90,8 @@
%color s+(scot %ux color.field)
%avatar ?~(avatar.field ~ s+u.avatar.field)
%cover ?~(cover.field ~ s+u.cover.field)
%add-group (enjs:res resource.field)
%remove-group (enjs:res resource.field)
%add-group s+(enjs-path:res resource.field)
%remove-group s+(enjs-path:res resource.field)
==
::
++ beng

View File

@ -48,11 +48,24 @@ const edit = (json: ContactUpdate, state: S) => {
data &&
(ship in state.contacts)
) {
const edit = Object.keys(data['edit-field']);
if (edit.length !== 1) {
console.log(data);
const [field] = Object.keys(data['edit-field']);
if (!field) {
return;
}
state.contacts[ship][edit[0]] = data['edit-field'][edit[0]];
const contact = state.contacts?.[ship];
const value = data['edit-field'][field];
if(!contact) {
return;
}
if(field === 'add-group') {
contact.groups.push(value);
} else if (field === 'remove-group') {
contact.groups = contact.groups.filter(g => g !== value);
} else {
contact[field] = value;
}
}
};

View File

@ -1,5 +1,6 @@
import React from "react";
import * as Yup from "yup";
import _ from 'lodash';
import {
ManagedForm as Form,
@ -65,13 +66,24 @@ export function EditProfile(props: any) {
api.contacts.setPublic(newValue)
);
} else if (key === 'groups') {
newValue.map((e) => {
if (!contact['groups']?.[e]) {
return acc.then(() => {
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) });
});
}
})
const toRemove: string[] = _.difference(contact?.groups || [], newValue);
console.log(toRemove);
const toAdd: string[] = _.difference(newValue, contact?.groups || []);
console.log(toAdd);
let promises: Promise<any>[] = [];
promises.concat(
toRemove.map(e =>
api.contacts.edit(ship, {'remove-group': resourceFromPath(e) })
)
);
promises.concat(
toAdd.map(e =>
api.contacts.edit(ship, {'add-group': resourceFromPath(e) })
)
);
return acc.then(() => Promise.all(promises));
} else if (
key !== "last-updated" &&
key !== "isPublic"

View File

@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import React, { useMemo, useState } from 'react';
import {
Box,
Text,
@ -9,7 +9,7 @@ import {
ErrorLabel
} from '@tlon/indigo-react';
import _ from 'lodash';
import { useField } from 'formik';
import { useField, useFormikContext, FieldArray } from 'formik';
import styled from 'styled-components';
import { roleForShip } from '~/logic/lib/group';
@ -18,14 +18,14 @@ import { DropdownSearch } from './DropdownSearch';
import { Groups } from '~/types';
import { Associations, Association } from '~/types/metadata-update';
interface InviteSearchProps {
interface GroupSearchProps<I extends string> {
disabled?: boolean;
adminOnly: boolean;
adminOnly?: boolean;
groups: Groups;
associations: Associations;
label: string;
caption?: string;
id: string;
id: I;
maxLength?: number;
}
@ -63,9 +63,26 @@ function renderCandidate(
return <Candidate title={title} selected={selected} onClick={onClick} />;
}
export function GroupSearch(props: InviteSearchProps) {
type FormValues<I extends string> = {
[id in I]: string[];
};
export function GroupSearch<I extends string, V extends FormValues<I>>(props: GroupSearchProps<I>) {
const { id, caption, label } = props;
const [selected, setSelected] = useState([] as string[]);
const {
values,
touched: touchedFields,
errors,
initialValues,
setFieldValue
} = useFormikContext<V>();
const [inputIdx, setInputIdx] = useState(initialValues[id].length);
const name = `${id}[${inputIdx}]`;
const value: string[] = values[id];
const touched = touchedFields[id] ?? false;
const error = _.compact(errors[id] as string[]);
const groups: Association[] = useMemo(() => {
return props.adminOnly
? Object.values(
@ -81,74 +98,68 @@ export function GroupSearch(props: InviteSearchProps) {
: Object.values(props.associations?.groups || {});
}, [props.associations?.groups]);
const [{ value }, meta, { setValue, setTouched }] = useField(props.id);
useEffect(() => {
setValue(selected);
}, [selected])
const { title: groupTitle } =
props.associations.groups?.[value]?.metadata || {};
const onSelect = useCallback(
(a: Association) => {
setTouched(true);
setSelected(v => _.uniq([...v, a.group]));
},
[setTouched, setSelected]
);
const onRemove = useCallback(
(s: string) => {
setSelected(groups => groups.filter(group => group !== s))
},
[setSelected]
);
return (
<Col>
<Label htmlFor={id}>{label}</Label>
{caption && (
<Label gray mt="2">
{caption}
</Label>
)}
<DropdownSearch<Association>
mt="2"
candidates={groups}
placeholder="Search for groups..."
disabled={props.maxLength ? selected.length >= props.maxLength : false}
renderCandidate={renderCandidate}
search={(s: string, a: Association) =>
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
}
getKey={(a: Association) => a.group}
onSelect={onSelect}
/>
{value?.length > 0 && (
value.map((e) => {
return (
<Row
key={e}
borderRadius="1"
<FieldArray
name={id}
render={(arrayHelpers) => {
const onSelect = (a: Association) => {
setFieldValue(name, a.group);
setInputIdx(s => s+1);
};
const onRemove = (idx: number) => {
setInputIdx(s => s - 1);
arrayHelpers.remove(idx);
};
return (
<Col>
<Label htmlFor={id}>{label}</Label>
{caption && (
<Label gray mt="2">
{caption}
</Label>
)}
<DropdownSearch<Association>
mt="2"
width="fit-content"
border="1"
borderColor="gray"
height="32px"
px="2"
alignItems="center"
>
<Text mr="2">{groupTitle || e}</Text>
<Icon onClick={onRemove} icon="X" />
</Row>
);
})
)}
<ErrorLabel hasError={Boolean(meta.touched && meta.error)}>
{meta.error}
</ErrorLabel>
</Col>
candidates={groups}
placeholder="Search for groups..."
disabled={props.maxLength ? value.length >= props.maxLength : false}
renderCandidate={renderCandidate}
search={(s: string, a: Association) =>
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
}
getKey={(a: Association) => a.group}
onSelect={onSelect}
/>
{value?.length > 0 && (
value.map((e, idx: number) => {
const { title } =
props.associations.groups?.[e]?.metadata || {};
return (
<Row
key={e}
borderRadius="1"
mt="2"
width="fit-content"
border="1"
borderColor="gray"
height="32px"
px="2"
alignItems="center"
>
<Text mr="2">{title || e}</Text>
<Icon onClick={() => onRemove(idx)} icon="X" />
</Row>
);
})
)}
<ErrorLabel hasError={Boolean(touched && error.length > 0)}>
{error.join(', ')}
</ErrorLabel>
</Col>
);
}} />
);
}