mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
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:
commit
80959119d9
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user