chore: add permission selection dropdown when inviting contacts
This commit is contained in:
@@ -1,15 +1,7 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { DropdownItem } from './DropdownItem'
|
import { DropdownItem } from './DropdownItem'
|
||||||
import { classNames } from '@standardnotes/snjs'
|
import { classNames } from '@standardnotes/snjs'
|
||||||
import {
|
import { Select, SelectItem, SelectLabel, SelectPopover, SelectStoreProps, useSelectStore } from '@ariakit/react'
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectPopover,
|
|
||||||
SelectStoreProps,
|
|
||||||
useSelectStore,
|
|
||||||
VisuallyHidden,
|
|
||||||
} from '@ariakit/react'
|
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
type DropdownProps = {
|
type DropdownProps = {
|
||||||
@@ -25,6 +17,7 @@ type DropdownProps = {
|
|||||||
}
|
}
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
popoverPlacement?: SelectStoreProps['placement']
|
popoverPlacement?: SelectStoreProps['placement']
|
||||||
|
showLabel?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown = ({
|
const Dropdown = ({
|
||||||
@@ -36,6 +29,7 @@ const Dropdown = ({
|
|||||||
fullWidth,
|
fullWidth,
|
||||||
classNameOverride = {},
|
classNameOverride = {},
|
||||||
popoverPlacement,
|
popoverPlacement,
|
||||||
|
showLabel,
|
||||||
}: DropdownProps) => {
|
}: DropdownProps) => {
|
||||||
const select = useSelectStore({
|
const select = useSelectStore({
|
||||||
value,
|
value,
|
||||||
@@ -57,12 +51,12 @@ const Dropdown = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VisuallyHidden>
|
<SelectLabel className={!showLabel ? 'sr-only' : ''} store={select}>
|
||||||
<SelectLabel store={select}>{label}</SelectLabel>
|
{label}
|
||||||
</VisuallyHidden>
|
</SelectLabel>
|
||||||
<Select
|
<Select
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex w-full min-w-55 items-center justify-between rounded border border-passive-3 bg-default md:translucent-ui:bg-transparent px-3.5 py-1.5 text-sm text-foreground',
|
'flex w-full min-w-55 items-center justify-between rounded border border-passive-3 bg-default px-3.5 py-1.5 text-sm text-foreground md:translucent-ui:bg-transparent',
|
||||||
disabled && 'opacity-50',
|
disabled && 'opacity-50',
|
||||||
classNameOverride.button,
|
classNameOverride.button,
|
||||||
!fullWidth && 'md:w-fit',
|
!fullWidth && 'md:w-fit',
|
||||||
@@ -83,7 +77,7 @@ const Dropdown = ({
|
|||||||
<SelectPopover
|
<SelectPopover
|
||||||
store={select}
|
store={select}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] [backdrop-filter:var(--popover-backdrop-filter)] overflow-y-auto rounded border border-passive-3 bg-default py-1',
|
'z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded border border-passive-3 bg-default py-1 [backdrop-filter:var(--popover-backdrop-filter)]',
|
||||||
classNameOverride.popover,
|
classNameOverride.popover,
|
||||||
)}
|
)}
|
||||||
portal={false}
|
portal={false}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const ContactItem = ({ contact }: Props) => {
|
|||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
|
|
||||||
<div className="flex items-start gap-3.5 rounded-lg border border-border px-3.5 py-2.5 shadow-sm">
|
<div className="flex items-start gap-3.5 rounded-lg border border-border px-3.5 py-2.5 shadow-sm">
|
||||||
<div className="grid grid-cols-[1fr,auto] grid-rows-2 place-items-center overflow-hidden [column-gap:0.875rem] [row-gap:0.25rem]">
|
<div className="grid grid-cols-[1fr,auto] grid-rows-2 place-items-center gap-x-3.5 gap-y-1 overflow-hidden">
|
||||||
<Icon type="user" size="custom" className="h-5 w-5 flex-shrink-0" />
|
<Icon type="user" size="custom" className="h-5 w-5 flex-shrink-0" />
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -8,18 +8,25 @@ import {
|
|||||||
classNames,
|
classNames,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import Spinner from '@/Components/Spinner/Spinner'
|
import Spinner from '@/Components/Spinner/Spinner'
|
||||||
|
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
vault: SharedVaultListingInterface
|
vault: SharedVaultListingInterface
|
||||||
onCloseDialog: () => void
|
onCloseDialog: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectedContact = {
|
||||||
|
uuid: string
|
||||||
|
permission: keyof typeof SharedVaultUserPermission.PERMISSIONS
|
||||||
|
}
|
||||||
|
|
||||||
const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog }) => {
|
const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog }) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
|
||||||
const [selectedContacts, setSelectedContacts] = useState<TrustedContactInterface[]>([])
|
const [selectedContacts, setSelectedContacts] = useState<SelectedContact[]>([])
|
||||||
const [isLoadingContacts, setIsLoadingContacts] = useState(false)
|
const [isLoadingContacts, setIsLoadingContacts] = useState(false)
|
||||||
const [contacts, setContacts] = useState<TrustedContactInterface[]>([])
|
const [contacts, setContacts] = useState<TrustedContactInterface[]>([])
|
||||||
|
const [isInvitingContacts, setIsInvitingContacts] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadContacts = async () => {
|
const loadContacts = async () => {
|
||||||
@@ -36,37 +43,48 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
|
|||||||
}, [onCloseDialog])
|
}, [onCloseDialog])
|
||||||
|
|
||||||
const inviteSelectedContacts = useCallback(async () => {
|
const inviteSelectedContacts = useCallback(async () => {
|
||||||
for (const contact of selectedContacts) {
|
setIsInvitingContacts(true)
|
||||||
|
for (const selectedContact of selectedContacts) {
|
||||||
|
const contact = contacts.find((contact) => contact.uuid === selectedContact.uuid)
|
||||||
|
if (!contact) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
await application.vaultInvites.inviteContactToSharedVault(
|
await application.vaultInvites.inviteContactToSharedVault(
|
||||||
vault,
|
vault,
|
||||||
contact,
|
contact,
|
||||||
SharedVaultUserPermission.PERMISSIONS.Write,
|
SharedVaultUserPermission.PERMISSIONS[selectedContact.permission],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
setIsInvitingContacts(false)
|
||||||
handleDialogClose()
|
handleDialogClose()
|
||||||
}, [application.vaultInvites, vault, handleDialogClose, selectedContacts])
|
}, [handleDialogClose, selectedContacts, contacts, application.vaultInvites, vault])
|
||||||
|
|
||||||
const toggleContact = useCallback(
|
const toggleContact = useCallback(
|
||||||
(contact: TrustedContactInterface) => {
|
(contact: TrustedContactInterface) => {
|
||||||
if (selectedContacts.includes(contact)) {
|
const contactWithPermission: SelectedContact = {
|
||||||
const index = selectedContacts.indexOf(contact)
|
uuid: contact.uuid,
|
||||||
const updatedContacts = [...selectedContacts]
|
permission: 'Read',
|
||||||
updatedContacts.splice(index, 1)
|
|
||||||
setSelectedContacts(updatedContacts)
|
|
||||||
} else {
|
|
||||||
setSelectedContacts([...selectedContacts, contact])
|
|
||||||
}
|
}
|
||||||
|
setSelectedContacts((selectedContacts) => {
|
||||||
|
if (selectedContacts.find((c) => c.uuid === contact.uuid)) {
|
||||||
|
return selectedContacts.filter((selectedContact) => selectedContact.uuid !== contact.uuid)
|
||||||
|
} else {
|
||||||
|
return [...selectedContacts, contactWithPermission]
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[selectedContacts, setSelectedContacts],
|
[setSelectedContacts],
|
||||||
)
|
)
|
||||||
|
|
||||||
const modalActions = useMemo(
|
const modalActions = useMemo(
|
||||||
(): ModalAction[] => [
|
(): ModalAction[] => [
|
||||||
{
|
{
|
||||||
label: 'Invite Selected Contacts',
|
label: isInvitingContacts ? <Spinner className="h-5 w-5 border-info-contrast" /> : 'Invite Selected Contacts',
|
||||||
onClick: inviteSelectedContacts,
|
onClick: inviteSelectedContacts,
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
mobileSlot: 'right',
|
mobileSlot: 'right',
|
||||||
|
disabled: isInvitingContacts,
|
||||||
|
hidden: contacts.length === 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Cancel',
|
label: 'Cancel',
|
||||||
@@ -75,7 +93,7 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
|
|||||||
mobileSlot: 'left',
|
mobileSlot: 'left',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[handleDialogClose, inviteSelectedContacts],
|
[contacts.length, handleDialogClose, inviteSelectedContacts, isInvitingContacts],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,17 +103,54 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
|
|||||||
<Spinner className="h-5 w-5" />
|
<Spinner className="h-5 w-5" />
|
||||||
) : contacts.length > 0 ? (
|
) : contacts.length > 0 ? (
|
||||||
contacts.map((contact) => {
|
contacts.map((contact) => {
|
||||||
|
const selectedContact = selectedContacts.find((c) => c.uuid === contact.uuid)
|
||||||
|
const isSelected = !!selectedContact
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5" key={contact.uuid}>
|
<div
|
||||||
|
className={classNames('grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5', isSelected && 'py-0.5')}
|
||||||
|
key={contact.uuid}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
|
id={contact.uuid}
|
||||||
className="h-4 w-4 self-center accent-info"
|
className="h-4 w-4 self-center accent-info"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedContacts.includes(contact)}
|
checked={isSelected}
|
||||||
onChange={() => toggleContact(contact)}
|
onChange={() => toggleContact(contact)}
|
||||||
/>
|
/>
|
||||||
<div className="col-start-2 text-sm font-semibold">{contact.name}</div>
|
<label htmlFor={contact.uuid} className="col-start-2">
|
||||||
<div className="col-start-2">{contact.contactUuid}</div>
|
<div className="text-sm font-semibold">{contact.name}</div>
|
||||||
|
<div className="opacity-90">{contact.contactUuid}</div>
|
||||||
</label>
|
</label>
|
||||||
|
{isSelected && (
|
||||||
|
<Dropdown
|
||||||
|
showLabel
|
||||||
|
label={'Permission:'}
|
||||||
|
classNameOverride={{
|
||||||
|
wrapper: 'col-start-2',
|
||||||
|
}}
|
||||||
|
items={Object.keys(SharedVaultUserPermission.PERMISSIONS).map((key) => ({
|
||||||
|
label: key === 'Write' ? 'Read/Write' : key,
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
value={selectedContact.permission}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedContacts((selectedContacts) =>
|
||||||
|
selectedContacts.map((c) => {
|
||||||
|
if (c.uuid === contact.uuid) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
permission: value as keyof typeof SharedVaultUserPermission.PERMISSIONS,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const KeyStoragePreference = ({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className="grid grid-cols-[auto,1fr] text-base font-medium [column-gap:0.65rem] [row-gap:0.25rem] md:text-sm"
|
className="grid grid-cols-[auto,1fr] gap-x-[0.65rem] gap-y-1 text-base font-medium md:text-sm"
|
||||||
>
|
>
|
||||||
<StyledRadioInput
|
<StyledRadioInput
|
||||||
className="col-start-1 col-end-2 place-self-center"
|
className="col-start-1 col-end-2 place-self-center"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const PasswordTypePreference = ({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className="grid grid-cols-[auto,1fr] text-base font-medium [column-gap:0.65rem] [row-gap:0.25rem] md:text-sm"
|
className="grid grid-cols-[auto,1fr] gap-x-[0.65rem] gap-y-1 text-base font-medium md:text-sm"
|
||||||
>
|
>
|
||||||
<StyledRadioInput
|
<StyledRadioInput
|
||||||
className="col-start-1 col-end-2 place-self-center"
|
className="col-start-1 col-end-2 place-self-center"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const VaultModalInvites = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={invite.uuid}
|
key={invite.uuid}
|
||||||
className="grid grid-cols-[auto,1fr] text-base font-medium [column-gap:0.65rem] [row-gap:0.5rem] md:text-sm"
|
className="grid grid-cols-[auto,1fr] gap-x-[0.65rem] gap-y-2 text-base font-medium md:text-sm"
|
||||||
>
|
>
|
||||||
<Icon type="user" className="col-start-1 col-end-2 place-self-center" />
|
<Icon type="user" className="col-start-1 col-end-2 place-self-center" />
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-ellipsis text-base font-bold">
|
<div className="flex items-center gap-2 overflow-hidden text-ellipsis text-base font-bold">
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const VaultModalMembers = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={contact?.uuid || member.user_uuid}
|
key={contact?.uuid || member.user_uuid}
|
||||||
className="grid grid-cols-[auto,1fr] text-base font-medium [column-gap:0.65rem] [row-gap:0.5rem] md:text-sm"
|
className="grid grid-cols-[auto,1fr] gap-x-[0.65rem] gap-y-2 text-base font-medium md:text-sm"
|
||||||
>
|
>
|
||||||
<Icon type="user" className="col-start-1 col-end-2 place-self-center" />
|
<Icon type="user" className="col-start-1 col-end-2 place-self-center" />
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-ellipsis text-base font-bold">
|
<div className="flex items-center gap-2 overflow-hidden text-ellipsis text-base font-bold">
|
||||||
|
|||||||
Reference in New Issue
Block a user