feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { STATE_TRACKER_EVENTS, STATE_GROUPS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types";
// components
import { StateForm } from "@/components/project-states";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type TStateCreate = {
groupKey: TStateGroups;
shouldTrackEvents: boolean;
createStateCallback: TStateOperationsCallbacks["createState"];
handleClose: () => void;
};
export const StateCreate: FC<TStateCreate> = observer((props) => {
const { groupKey, shouldTrackEvents, createStateCallback, handleClose } = props;
// states
const [loader, setLoader] = useState(false);
const onCancel = () => {
setLoader(false);
handleClose();
};
const onSubmit = async (formData: Partial<IState>) => {
if (!groupKey) return { status: "error" };
try {
const response = await createStateCallback({ ...formData, group: groupKey });
if (shouldTrackEvents)
captureSuccess({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
state_group: groupKey,
id: response.id,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "State created successfully.",
});
handleClose();
return { status: "success" };
} catch (error) {
const errorStatus = error as unknown as { status: number; data: { error: string } };
if (shouldTrackEvents)
captureError({
eventName: STATE_TRACKER_EVENTS.create,
payload: {
state_group: groupKey,
},
});
if (errorStatus?.status === 400) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "State with that name already exists. Please try again with another name.",
});
return { status: "already_exists" };
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorStatus.data.error ?? "State could not be created. Please try again.",
});
return { status: "error" };
}
}
};
return (
<StateForm
data={{ name: "", description: "", color: STATE_GROUPS[groupKey].color }}
onSubmit={onSubmit}
onCancel={onCancel}
buttonDisabled={loader}
buttonTitle={loader ? `Creating` : `Create`}
/>
);
});

View File

@@ -0,0 +1,109 @@
"use client";
import type { FC } from "react";
import { useEffect, useState, useMemo } from "react";
import { TwitterPicker } from "react-color";
import { Button } from "@plane/propel/button";
import type { IState } from "@plane/types";
import { Popover, Input, TextArea } from "@plane/ui";
type TStateForm = {
data: Partial<IState>;
onSubmit: (formData: Partial<IState>) => Promise<{ status: string }>;
onCancel: () => void;
buttonDisabled: boolean;
buttonTitle: string;
};
export const StateForm: FC<TStateForm> = (props) => {
const { data, onSubmit, onCancel, buttonDisabled, buttonTitle } = props;
// states
const [formData, setFromData] = useState<Partial<IState> | undefined>(undefined);
const [errors, setErrors] = useState<Partial<Record<keyof IState, string>> | undefined>(undefined);
useEffect(() => {
if (data && !formData) setFromData(data);
}, [data, formData]);
const handleFormData = <T extends keyof IState>(key: T, value: IState[T]) => {
setFromData((prev) => ({ ...prev, [key]: value }));
setErrors((prev) => ({ ...prev, [key]: "" }));
};
const formSubmit = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
const name = formData?.name || undefined;
if (!formData || !name) {
let currentErrors: Partial<Record<keyof IState, string>> = {};
if (!name) currentErrors = { ...currentErrors, name: "Name is required" };
setErrors(currentErrors);
return;
}
try {
await onSubmit(formData);
} catch (error) {
console.log("error", error);
}
};
const PopoverButton = useMemo(
() => (
<div
className="group inline-flex items-center text-base font-medium focus:outline-none h-5 w-5 rounded transition-all"
style={{
backgroundColor: formData?.color ?? "black",
}}
/>
),
[formData?.color]
);
return (
<div className="relative flex space-x-2 bg-custom-background-100 p-3 rounded">
{/* color */}
<div className="flex-shrink-0 h-full mt-2">
<Popover button={PopoverButton} panelClassName="mt-4 -ml-3">
<TwitterPicker color={formData?.color} onChange={(value) => handleFormData("color", value.hex)} />
</Popover>
</div>
<div className="w-full space-y-2">
{/* title */}
<Input
id="name"
type="text"
name="name"
placeholder="Name"
value={formData?.name}
onChange={(e) => handleFormData("name", e.target.value)}
hasError={(errors && Boolean(errors.name)) || false}
className="w-full"
maxLength={100}
autoFocus
/>
{/* description */}
<TextArea
id="description"
name="description"
placeholder="Describe this state for your members."
value={formData?.description}
onChange={(e) => handleFormData("description", e.target.value)}
hasError={(errors && Boolean(errors.description)) || false}
className="w-full text-sm min-h-14 resize-none"
/>
<div className="flex space-x-2 items-center">
<Button onClick={formSubmit} variant="primary" size="sm" disabled={buttonDisabled}>
{buttonTitle}
</Button>
<Button type="button" variant="neutral-primary" size="sm" disabled={buttonDisabled} onClick={onCancel}>
Cancel
</Button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./create";
export * from "./update";
export * from "./form";

View File

@@ -0,0 +1,90 @@
"use client";
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { STATE_TRACKER_EVENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IState, TStateOperationsCallbacks } from "@plane/types";
// components
import { StateForm } from "@/components/project-states";
// hooks
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
type TStateUpdate = {
state: IState;
updateStateCallback: TStateOperationsCallbacks["updateState"];
shouldTrackEvents: boolean;
handleClose: () => void;
};
export const StateUpdate: FC<TStateUpdate> = observer((props) => {
const { state, updateStateCallback, shouldTrackEvents, handleClose } = props;
// states
const [loader, setLoader] = useState(false);
const onCancel = () => {
setLoader(false);
handleClose();
};
const onSubmit = async (formData: Partial<IState>) => {
if (!state.id) return { status: "error" };
try {
await updateStateCallback(state.id, formData);
if (shouldTrackEvents) {
captureSuccess({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
state_group: state.group,
id: state.id,
},
});
}
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "State updated successfully.",
});
handleClose();
return { status: "success" };
} catch (error) {
const errorStatus = error as unknown as { status: number };
if (errorStatus?.status === 400) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Another state exists with the same name. Please try again with another name.",
});
return { status: "already_exists" };
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "State could not be updated. Please try again.",
});
if (shouldTrackEvents) {
captureError({
eventName: STATE_TRACKER_EVENTS.update,
payload: {
state_group: state.group,
id: state.id,
},
});
}
return { status: "error" };
}
}
};
return (
<StateForm
data={state}
onSubmit={onSubmit}
onCancel={onCancel}
buttonDisabled={loader}
buttonTitle={loader ? `Updating` : `Update`}
/>
);
});