-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
improvement: enhance workspace invitation modularity (#6594)
- Loading branch information
1 parent
e071bf4
commit cc9b448
Showing
13 changed files
with
390 additions
and
249 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
web/ce/components/workspace/billing/billing-actions-button.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"use client"; | ||
|
||
import { observer } from "mobx-react"; | ||
|
||
export type TBillingActionsButtonProps = { | ||
canPerformWorkspaceAdminActions: boolean; | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export const BillingActionsButton = observer((props: TBillingActionsButtonProps) => <></>); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./root"; | ||
export * from "./billing-actions-button"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./invite-modal"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
"use client"; | ||
|
||
import React from "react"; | ||
import { observer } from "mobx-react"; | ||
import { useParams } from "next/navigation"; | ||
// plane imports | ||
import { useTranslation } from "@plane/i18n"; | ||
import { IWorkspaceBulkInviteFormData } from "@plane/types"; | ||
// ui | ||
import { EModalWidth, EModalPosition, ModalCore } from "@plane/ui"; | ||
// components | ||
import { InvitationFields, InvitationModalActions } from "@/components/workspace/invite-modal"; | ||
import { InvitationForm } from "@/components/workspace/invite-modal/form"; | ||
// hooks | ||
import { useWorkspaceInvitationActions } from "@/hooks/use-workspace-invitation"; | ||
|
||
export type TSendWorkspaceInvitationModalProps = { | ||
isOpen: boolean; | ||
onClose: () => void; | ||
onSubmit: (data: IWorkspaceBulkInviteFormData) => Promise<void> | undefined; | ||
}; | ||
|
||
export const SendWorkspaceInvitationModal: React.FC<TSendWorkspaceInvitationModalProps> = observer((props) => { | ||
const { isOpen, onClose, onSubmit } = props; | ||
// store hooks | ||
const { t } = useTranslation(); | ||
// router | ||
const { workspaceSlug } = useParams(); | ||
// derived values | ||
const { control, fields, formState, remove, onFormSubmit, handleClose, appendField } = useWorkspaceInvitationActions({ | ||
onSubmit, | ||
onClose, | ||
}); | ||
|
||
return ( | ||
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL}> | ||
<InvitationForm | ||
title={t("workspace_settings.settings.members.modal.title")} | ||
description={t("workspace_settings.settings.members.modal.description")} | ||
onSubmit={onFormSubmit} | ||
actions={ | ||
<InvitationModalActions | ||
isSubmitting={formState.isSubmitting} | ||
handleClose={handleClose} | ||
appendField={appendField} | ||
/> | ||
} | ||
className="p-5" | ||
> | ||
<InvitationFields | ||
workspaceSlug={workspaceSlug.toString()} | ||
fields={fields} | ||
control={control} | ||
formState={formState} | ||
remove={remove} | ||
/> | ||
</InvitationForm> | ||
</ModalCore> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { observer } from "mobx-react"; | ||
import { Plus } from "lucide-react"; | ||
// plane imports | ||
import { useTranslation } from "@plane/i18n"; | ||
import { Button } from "@plane/ui"; | ||
import { cn } from "@plane/utils"; | ||
|
||
type TInvitationModalActionsProps = { | ||
isInviteDisabled?: boolean; | ||
isSubmitting?: boolean; | ||
handleClose: () => void; | ||
appendField: () => void; | ||
addMoreButtonText?: string; | ||
submitButtonText?: { | ||
default: string; | ||
loading: string; | ||
}; | ||
cancelButtonText?: string; | ||
className?: string; | ||
}; | ||
|
||
export const InvitationModalActions: React.FC<TInvitationModalActionsProps> = observer((props) => { | ||
const { | ||
isInviteDisabled = false, | ||
isSubmitting = false, | ||
handleClose, | ||
appendField, | ||
addMoreButtonText, | ||
submitButtonText, | ||
cancelButtonText, | ||
className, | ||
} = props; | ||
// store hooks | ||
const { t } = useTranslation(); | ||
|
||
return ( | ||
<div className={cn("mt-5 flex items-center justify-between gap-2", className)}> | ||
<button | ||
type="button" | ||
className={cn( | ||
"flex items-center gap-1 bg-transparent py-2 pr-3 text-xs font-medium text-custom-primary outline-custom-primary", | ||
{ | ||
"cursor-not-allowed opacity-60": isInviteDisabled, | ||
} | ||
)} | ||
onClick={appendField} | ||
disabled={isInviteDisabled} | ||
> | ||
<Plus className="h-3.5 w-3.5" /> | ||
{addMoreButtonText || t("common.add_more")} | ||
</button> | ||
<div className="flex items-center gap-2"> | ||
<Button variant="neutral-primary" size="sm" onClick={handleClose}> | ||
{cancelButtonText || t("cancel")} | ||
</Button> | ||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} disabled={isInviteDisabled}> | ||
{isSubmitting | ||
? submitButtonText?.loading || t("workspace_settings.settings.members.modal.button_loading") | ||
: submitButtonText?.default || t("workspace_settings.settings.members.modal.button")} | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
"use client"; | ||
|
||
import { observer } from "mobx-react"; | ||
import { Control, Controller, FieldArrayWithId, FormState } from "react-hook-form"; | ||
import { X } from "lucide-react"; | ||
// plane imports | ||
import { ROLE } from "@plane/constants"; | ||
import { useTranslation } from "@plane/i18n"; | ||
import { CustomSelect, Input } from "@plane/ui"; | ||
import { cn } from "@plane/utils"; | ||
// hooks | ||
import { useUserPermissions } from "@/hooks/store"; | ||
import { InvitationFormValues } from "@/hooks/use-workspace-invitation"; | ||
|
||
type TInvitationFieldsProps = { | ||
workspaceSlug: string; | ||
fields: FieldArrayWithId<InvitationFormValues, "emails", "id">[]; | ||
control: Control<InvitationFormValues>; | ||
formState: FormState<InvitationFormValues>; | ||
remove: (index: number) => void; | ||
className?: string; | ||
}; | ||
|
||
export const InvitationFields = observer((props: TInvitationFieldsProps) => { | ||
const { | ||
workspaceSlug, | ||
fields, | ||
control, | ||
formState: { errors }, | ||
remove, | ||
className, | ||
} = props; | ||
// plane hooks | ||
const { t } = useTranslation(); | ||
// store hooks | ||
const { workspaceInfoBySlug } = useUserPermissions(); | ||
// derived values | ||
const currentWorkspaceRole = workspaceInfoBySlug(workspaceSlug.toString())?.role; | ||
|
||
return ( | ||
<div className={cn("mb-3 space-y-4", className)}> | ||
{fields.map((field, index) => ( | ||
<div key={field.id} className="relative group mb-1 flex items-start justify-between gap-x-4 text-sm w-full"> | ||
<div className="w-full"> | ||
<Controller | ||
control={control} | ||
name={`emails.${index}.email`} | ||
rules={{ | ||
required: t("workspace_settings.settings.members.modal.errors.required"), | ||
pattern: { | ||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, | ||
message: t("workspace_settings.settings.members.modal.errors.invalid"), | ||
}, | ||
}} | ||
render={({ field: { value, onChange, ref } }) => ( | ||
<> | ||
<Input | ||
id={`emails.${index}.email`} | ||
name={`emails.${index}.email`} | ||
type="text" | ||
value={value} | ||
onChange={onChange} | ||
ref={ref} | ||
hasError={Boolean(errors.emails?.[index]?.email)} | ||
placeholder={t("workspace_settings.settings.members.modal.placeholder")} | ||
className="w-full text-xs sm:text-sm" | ||
/> | ||
{errors.emails?.[index]?.email && ( | ||
<span className="ml-1 text-xs text-red-500">{errors.emails?.[index]?.email?.message}</span> | ||
)} | ||
</> | ||
)} | ||
/> | ||
</div> | ||
<div className="flex items-center justify-between gap-2 flex-shrink-0 "> | ||
<div className="flex flex-col gap-1"> | ||
<Controller | ||
control={control} | ||
name={`emails.${index}.role`} | ||
rules={{ required: true }} | ||
render={({ field: { value, onChange } }) => ( | ||
<CustomSelect | ||
value={value} | ||
label={<span className="text-xs sm:text-sm">{ROLE[value]}</span>} | ||
onChange={onChange} | ||
optionsClassName="w-full" | ||
className="flex-grow w-24" | ||
input | ||
> | ||
{Object.entries(ROLE).map(([key, value]) => { | ||
if (currentWorkspaceRole && currentWorkspaceRole >= parseInt(key)) | ||
return ( | ||
<CustomSelect.Option key={key} value={parseInt(key)}> | ||
{value} | ||
</CustomSelect.Option> | ||
); | ||
})} | ||
</CustomSelect> | ||
)} | ||
/> | ||
</div> | ||
{fields.length > 1 && ( | ||
<div className="flex-item flex w-6"> | ||
<button type="button" className="place-items-center self-center rounded" onClick={() => remove(index)}> | ||
<X className="h-4 w-4 text-custom-text-200" /> | ||
</button> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
"use client"; | ||
|
||
import { observer } from "mobx-react"; | ||
import { Dialog } from "@headlessui/react"; | ||
|
||
type TInvitationFormProps = { | ||
title: string; | ||
description: React.ReactNode; | ||
children: React.ReactNode; | ||
onSubmit: () => void; | ||
actions: React.ReactNode; | ||
className?: string; | ||
}; | ||
|
||
export const InvitationForm = observer((props: TInvitationFormProps) => { | ||
const { title, description, children, actions, onSubmit, className } = props; | ||
|
||
return ( | ||
<form | ||
onSubmit={onSubmit} | ||
onKeyDown={(e) => { | ||
if (e.code === "Enter") e.preventDefault(); | ||
}} | ||
className={className} | ||
> | ||
<div className="space-y-4"> | ||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100"> | ||
{title} | ||
</Dialog.Title> | ||
<div className="text-sm text-custom-text-200">{description}</div> | ||
{children} | ||
</div> | ||
{actions} | ||
</form> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./actions"; | ||
export * from "./fields"; | ||
export * from "./form"; |
Oops, something went wrong.