diff --git a/pkg/arvo/lib/contact-store.hoon b/pkg/arvo/lib/contact-store.hoon index 8356d69aab..18efaa09f9 100644 --- a/pkg/arvo/lib/contact-store.hoon +++ b/pkg/arvo/lib/contact-store.hoon @@ -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 diff --git a/pkg/interface/src/logic/reducers/contact-update.ts b/pkg/interface/src/logic/reducers/contact-update.ts index ac96ae36c1..8d5ba5b1f2 100644 --- a/pkg/interface/src/logic/reducers/contact-update.ts +++ b/pkg/interface/src/logic/reducers/contact-update.ts @@ -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; + } } }; diff --git a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx index 35e3a135f0..c2b653c6dc 100644 --- a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx @@ -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[] = []; + + 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" diff --git a/pkg/interface/src/views/components/GroupSearch.tsx b/pkg/interface/src/views/components/GroupSearch.tsx index 4fc8339835..5994ad78e5 100644 --- a/pkg/interface/src/views/components/GroupSearch.tsx +++ b/pkg/interface/src/views/components/GroupSearch.tsx @@ -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 { 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 ; } -export function GroupSearch(props: InviteSearchProps) { +type FormValues = { + [id in I]: string[]; +}; + +export function GroupSearch>(props: GroupSearchProps) { const { id, caption, label } = props; - const [selected, setSelected] = useState([] as string[]); + const { + values, + touched: touchedFields, + errors, + initialValues, + setFieldValue + } = useFormikContext(); + 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 ( - - - {caption && ( - - )} - - 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 ( - { + const onSelect = (a: Association) => { + setFieldValue(name, a.group); + setInputIdx(s => s+1); + }; + + const onRemove = (idx: number) => { + setInputIdx(s => s - 1); + arrayHelpers.remove(idx); + }; + + return ( + + + {caption && ( + + )} + mt="2" - width="fit-content" - border="1" - borderColor="gray" - height="32px" - px="2" - alignItems="center" - > - {groupTitle || e} - - - ); - }) - )} - - {meta.error} - - + 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 ( + + {title || e} + onRemove(idx)} icon="X" /> + + ); + }) + )} + 0)}> + {error.join(', ')} + + + ); + }} /> ); }