snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,409 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import type {
Control,
FieldArrayWithId,
UseFieldArrayRemove,
UseFormGetValues,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
import { Controller, useFieldArray, useForm } from "react-hook-form";
// icons
import { usePopper } from "react-popper";
import { AddOutline, ChevronDownOutline, CloseCircleOutline, TickOutline } from "@makeplane/propel/icons";
import { Listbox } from "@headlessui/react";
// plane imports
import { Field } from "@makeplane/propel/components/field";
import { Input, InputGroup } from "@makeplane/propel/components/input";
import type { EUserPermissions } from "@plane/constants";
import { ROLE, ROLE_DETAILS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IWorkspace } from "@plane/types";
// ui
import { Spinner } from "@plane/ui";
// services
import { WorkspaceService } from "@/services/workspace.service";
// components
import { SwitchAccountDropdown } from "./switch-account-dropdown";
type Props = {
finishOnboarding: () => Promise<void>;
totalSteps: number;
user: IUser | undefined;
workspace: IWorkspace | undefined;
};
type EmailRole = {
email: string;
role: EUserPermissions;
role_active: boolean;
};
type FormValues = {
emails: EmailRole[];
};
type InviteMemberFormProps = {
index: number;
remove: UseFieldArrayRemove;
control: Control<FormValues, any>;
setValue: UseFormSetValue<FormValues>;
getValues: UseFormGetValues<FormValues>;
watch: UseFormWatch<FormValues>;
field: FieldArrayWithId<FormValues, "emails", "id">;
fields: FieldArrayWithId<FormValues, "emails", "id">[];
errors: any;
isInvitationDisabled: boolean;
setIsInvitationDisabled: (value: boolean) => void;
};
// services
const workspaceService = new WorkspaceService();
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
const placeholderEmails = [
"charlie.taylor@frstflt.com",
"octave.chanute@frstflt.com",
"george.spratt@frstflt.com",
"frank.coffyn@frstflt.com",
"amos.root@frstflt.com",
"edward.deeds@frstflt.com",
"charles.m.manly@frstflt.com",
"glenn.curtiss@frstflt.com",
"thomas.selfridge@frstflt.com",
"albert.zahm@frstflt.com",
];
const InviteMemberInput = observer(function InviteMemberInput(props: InviteMemberFormProps) {
const {
control,
index,
fields,
remove,
errors,
isInvitationDisabled,
setIsInvitationDisabled,
setValue,
getValues,
watch,
} = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { t } = useTranslation();
const email = watch(`emails.${index}.email`);
const emailOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value === "") {
const validEmail = fields.map((_, i) => emailRegex.test(getValues(`emails.${i}.email`))).includes(true);
if (validEmail) {
setIsInvitationDisabled(false);
} else {
setIsInvitationDisabled(true);
}
if (getValues(`emails.${index}.role_active`)) {
setValue(`emails.${index}.role_active`, false);
}
} else {
if (!getValues(`emails.${index}.role_active`)) {
setValue(`emails.${index}.role_active`, true);
}
if (isInvitationDisabled && emailRegex.test(event.target.value)) {
setIsInvitationDisabled(false);
} else if (!isInvitationDisabled && !emailRegex.test(event.target.value)) {
setIsInvitationDisabled(true);
}
}
};
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-end",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
return (
<div>
<div className="group relative grid grid-cols-10 gap-4">
<div className="col-span-6 ml-8">
<Controller
control={control}
name={`emails.${index}.email`}
rules={{
pattern: {
value: emailRegex,
message: "Invalid Email ID",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Field name="input" invalid={Boolean(errors.emails?.[index]?.email)}>
<InputGroup size="2xl">
<Input
size="2xl"
id={`emails.${index}.email`}
name={`emails.${index}.email`}
type="text"
value={value}
onChange={(event) => {
emailOnChange(event);
onChange(event);
}}
ref={ref}
placeholder={placeholderEmails[index % placeholderEmails.length]}
autoComplete="off"
/>
</InputGroup>
</Field>
)}
/>
</div>
<div className="col-span-4 mr-8">
<Controller
control={control}
name={`emails.${index}.role`}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<Listbox
as="div"
value={value}
onChange={(val) => {
onChange(val);
setValue(`emails.${index}.role_active`, true);
}}
className="w-full flex-shrink-0 text-left"
>
<Listbox.Button
type="button"
ref={setReferenceElement}
className="flex w-full items-center justify-between gap-1 rounded-md border-[0.5px] border-strong px-2.5 py-2 text-13"
>
<span
className={`text-13 ${
!getValues(`emails.${index}.role_active`) ? "text-placeholder" : "text-primary"
} sm:text-13`}
>
{ROLE[value]}
</span>
<ChevronDownOutline
className={`size-3 ${
!getValues(`emails.${index}.role_active`) ? "text-placeholder" : "text-primary"
}`}
/>
</Listbox.Button>
<Listbox.Options as="div">
<div
className="shadow-sm absolute z-10 mt-1 h-fit w-48 space-y-1 rounded-md border border-strong bg-surface-1 p-2 focus:outline-none sm:w-60"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{Object.entries(ROLE_DETAILS).map(([key, value]) => (
<Listbox.Option
as="div"
key={key}
value={parseInt(key)}
className={({ active, selected }) =>
`cursor-pointer truncate rounded-sm px-1 py-1.5 select-none ${
active || selected ? "bg-onboarding-background-400/40" : ""
} ${selected ? "text-primary" : "text-secondary"}`
}
>
{({ selected }) => (
<div className="flex items-center gap-2 p-1 text-wrap">
<div className="flex flex-col">
<div className="text-13 font-medium">{t(value.i18n_title)}</div>
<div className="flex text-11 text-tertiary">{t(value.i18n_description)}</div>
</div>
{selected && <TickOutline className="h-4 w-4 shrink-0" />}
</div>
)}
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Listbox>
)}
/>
</div>
{fields.length > 1 && (
<button
type="button"
className="absolute right-0 hidden place-items-center self-center rounded-sm group-hover:grid"
onClick={() => remove(index)}
>
<CloseCircleOutline className="h-5 w-5 pl-0.5 text-placeholder" />
</button>
)}
</div>
{email && !emailRegex.test(email) && (
<div className="mx-8 my-1">
<span className="text-13">🤥</span>{" "}
<span className="mt-1 text-11 text-danger-primary">That doesn{"'"}t look like an email address.</span>
</div>
)}
</div>
);
});
export function InviteMembers(props: Props) {
const { finishOnboarding, workspace } = props;
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
const {
control,
watch,
getValues,
setValue,
handleSubmit,
formState: { isSubmitting, errors, isValid },
} = useForm<FormValues>();
const { fields, append, remove } = useFieldArray({
control,
name: "emails",
});
const nextStep = async () => {
await finishOnboarding();
};
const onSubmit = async (formData: FormValues) => {
if (!workspace) return;
let payload = { ...formData };
payload = { emails: payload.emails.filter((email) => email.email !== "") };
await workspaceService
.inviteWorkspace(workspace.slug, {
emails: payload.emails.map((email) => ({
email: email.email,
role: email.role,
})),
})
.then(async () => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Invitations sent successfully.",
});
await nextStep();
})
.catch((err) => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error,
});
});
};
const appendField = () => {
append({ email: "", role: 15, role_active: false });
};
useEffect(() => {
if (fields.length === 0) {
append(
[
{ email: "", role: 15, role_active: false },
{ email: "", role: 15, role_active: false },
{ email: "", role: 15, role_active: false },
],
{
focusIndex: 0,
}
);
}
}, [fields, append]);
return (
<div className="flex h-full w-full">
<div className="h-full w-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<div className="mx-auto mt-6 flex w-full flex-col items-center justify-center p-8 md:w-4/5">
<div className="mx-auto w-4/5 space-y-1 py-4 text-center">
<h3 className="text-24 font-bold text-primary">Invite your teammates</h3>
<p className="font-medium text-placeholder">
Work in plane happens best with your team. Invite them now to use Plane to its potential.
</p>
</div>
<form
className="mx-auto mt-2 w-full space-y-4"
onSubmit={handleSubmit(onSubmit)}
onKeyDown={(e) => {
if (e.code === "Enter") e.preventDefault();
}}
>
<div className="w-full py-4 text-13">
<div className="group relative mx-8 grid grid-cols-10 gap-4 py-2">
<div className="col-span-6 px-1 text-13 font-medium text-secondary">Email</div>
<div className="col-span-4 px-1 text-13 font-medium text-secondary">Role</div>
</div>
<div className="mb-3 space-y-3 sm:space-y-4">
{fields.map((field, index) => (
<InviteMemberInput
watch={watch}
getValues={getValues}
setValue={setValue}
isInvitationDisabled={isInvitationDisabled}
setIsInvitationDisabled={(value: boolean) => setIsInvitationDisabled(value)}
control={control}
errors={errors}
field={field}
fields={fields}
index={index}
remove={remove}
key={field.id}
/>
))}
</div>
<button
type="button"
className="mx-8 flex items-center gap-1.5 bg-transparent text-13 font-medium text-accent-primary outline-accent-strong"
onClick={appendField}
>
<AddOutline className="h-4 w-4" />
Add another
</button>
</div>
<div className="mx-auto flex w-full max-w-96 flex-col items-center justify-center gap-4 px-8 sm:px-2">
<Button
variant="primary"
type="submit"
size="xl"
className="w-full"
disabled={isInvitationDisabled || !isValid || isSubmitting}
>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
<Button variant="ghost" size="xl" className="w-full" onClick={nextStep}>
I’ll do it later
</Button>
</div>
</form>
</div>
</div>
<SwitchAccountDropdown />
</div>
);
}