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)]
|
[%color s+(scot %ux color.contact)]
|
||||||
[%avatar ?~(avatar.contact ~ s+u.avatar.contact)]
|
[%avatar ?~(avatar.contact ~ s+u.avatar.contact)]
|
||||||
[%cover ?~(cover.contact ~ s+u.cover.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)]
|
[%last-updated (time last-updated.contact)]
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
@ -90,8 +90,8 @@
|
|||||||
%color s+(scot %ux color.field)
|
%color s+(scot %ux color.field)
|
||||||
%avatar ?~(avatar.field ~ s+u.avatar.field)
|
%avatar ?~(avatar.field ~ s+u.avatar.field)
|
||||||
%cover ?~(cover.field ~ s+u.cover.field)
|
%cover ?~(cover.field ~ s+u.cover.field)
|
||||||
%add-group (enjs:res resource.field)
|
%add-group s+(enjs-path:res resource.field)
|
||||||
%remove-group (enjs:res resource.field)
|
%remove-group s+(enjs-path:res resource.field)
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ beng
|
++ beng
|
||||||
|
@ -48,11 +48,24 @@ const edit = (json: ContactUpdate, state: S) => {
|
|||||||
data &&
|
data &&
|
||||||
(ship in state.contacts)
|
(ship in state.contacts)
|
||||||
) {
|
) {
|
||||||
const edit = Object.keys(data['edit-field']);
|
console.log(data);
|
||||||
if (edit.length !== 1) {
|
const [field] = Object.keys(data['edit-field']);
|
||||||
|
if (!field) {
|
||||||
return;
|
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 React from "react";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManagedForm as Form,
|
ManagedForm as Form,
|
||||||
@ -65,13 +66,24 @@ export function EditProfile(props: any) {
|
|||||||
api.contacts.setPublic(newValue)
|
api.contacts.setPublic(newValue)
|
||||||
);
|
);
|
||||||
} else if (key === 'groups') {
|
} else if (key === 'groups') {
|
||||||
newValue.map((e) => {
|
const toRemove: string[] = _.difference(contact?.groups || [], newValue);
|
||||||
if (!contact['groups']?.[e]) {
|
console.log(toRemove);
|
||||||
return acc.then(() => {
|
const toAdd: string[] = _.difference(newValue, contact?.groups || []);
|
||||||
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) });
|
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 (
|
} else if (
|
||||||
key !== "last-updated" &&
|
key !== "last-updated" &&
|
||||||
key !== "isPublic"
|
key !== "isPublic"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
ErrorLabel
|
ErrorLabel
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useField } from 'formik';
|
import { useField, useFormikContext, FieldArray } from 'formik';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { roleForShip } from '~/logic/lib/group';
|
import { roleForShip } from '~/logic/lib/group';
|
||||||
@ -18,14 +18,14 @@ import { DropdownSearch } from './DropdownSearch';
|
|||||||
import { Groups } from '~/types';
|
import { Groups } from '~/types';
|
||||||
import { Associations, Association } from '~/types/metadata-update';
|
import { Associations, Association } from '~/types/metadata-update';
|
||||||
|
|
||||||
interface InviteSearchProps {
|
interface GroupSearchProps<I extends string> {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
adminOnly: boolean;
|
adminOnly?: boolean;
|
||||||
groups: Groups;
|
groups: Groups;
|
||||||
associations: Associations;
|
associations: Associations;
|
||||||
label: string;
|
label: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
id: string;
|
id: I;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,9 +63,26 @@ function renderCandidate(
|
|||||||
return <Candidate title={title} selected={selected} onClick={onClick} />;
|
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 { 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(() => {
|
const groups: Association[] = useMemo(() => {
|
||||||
return props.adminOnly
|
return props.adminOnly
|
||||||
? Object.values(
|
? Object.values(
|
||||||
@ -81,74 +98,68 @@ export function GroupSearch(props: InviteSearchProps) {
|
|||||||
: Object.values(props.associations?.groups || {});
|
: Object.values(props.associations?.groups || {});
|
||||||
}, [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 (
|
return (
|
||||||
<Col>
|
<FieldArray
|
||||||
<Label htmlFor={id}>{label}</Label>
|
name={id}
|
||||||
{caption && (
|
render={(arrayHelpers) => {
|
||||||
<Label gray mt="2">
|
const onSelect = (a: Association) => {
|
||||||
{caption}
|
setFieldValue(name, a.group);
|
||||||
</Label>
|
setInputIdx(s => s+1);
|
||||||
)}
|
};
|
||||||
<DropdownSearch<Association>
|
|
||||||
mt="2"
|
const onRemove = (idx: number) => {
|
||||||
candidates={groups}
|
setInputIdx(s => s - 1);
|
||||||
placeholder="Search for groups..."
|
arrayHelpers.remove(idx);
|
||||||
disabled={props.maxLength ? selected.length >= props.maxLength : false}
|
};
|
||||||
renderCandidate={renderCandidate}
|
|
||||||
search={(s: string, a: Association) =>
|
return (
|
||||||
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
|
<Col>
|
||||||
}
|
<Label htmlFor={id}>{label}</Label>
|
||||||
getKey={(a: Association) => a.group}
|
{caption && (
|
||||||
onSelect={onSelect}
|
<Label gray mt="2">
|
||||||
/>
|
{caption}
|
||||||
{value?.length > 0 && (
|
</Label>
|
||||||
value.map((e) => {
|
)}
|
||||||
return (
|
<DropdownSearch<Association>
|
||||||
<Row
|
|
||||||
key={e}
|
|
||||||
borderRadius="1"
|
|
||||||
mt="2"
|
mt="2"
|
||||||
width="fit-content"
|
candidates={groups}
|
||||||
border="1"
|
placeholder="Search for groups..."
|
||||||
borderColor="gray"
|
disabled={props.maxLength ? value.length >= props.maxLength : false}
|
||||||
height="32px"
|
renderCandidate={renderCandidate}
|
||||||
px="2"
|
search={(s: string, a: Association) =>
|
||||||
alignItems="center"
|
a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
|
||||||
>
|
}
|
||||||
<Text mr="2">{groupTitle || e}</Text>
|
getKey={(a: Association) => a.group}
|
||||||
<Icon onClick={onRemove} icon="X" />
|
onSelect={onSelect}
|
||||||
</Row>
|
/>
|
||||||
);
|
{value?.length > 0 && (
|
||||||
})
|
value.map((e, idx: number) => {
|
||||||
)}
|
const { title } =
|
||||||
<ErrorLabel hasError={Boolean(meta.touched && meta.error)}>
|
props.associations.groups?.[e]?.metadata || {};
|
||||||
{meta.error}
|
return (
|
||||||
</ErrorLabel>
|
<Row
|
||||||
</Col>
|
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