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,147 @@
"use client";
import type { FC } from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
// plane types
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { ILinkDetails, ModuleLink } from "@plane/types";
// plane ui
import { Input, ModalCore } from "@plane/ui";
type Props = {
createLink: (formData: ModuleLink) => Promise<void>;
data?: ILinkDetails | null;
isOpen: boolean;
handleClose: () => void;
updateLink: (formData: ModuleLink, linkId: string) => Promise<void>;
};
const defaultValues: ModuleLink = {
title: "",
url: "",
};
export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
const { isOpen, handleClose, createLink, updateLink, data } = props;
// form info
const {
formState: { errors, isSubmitting },
handleSubmit,
control,
reset,
} = useForm<ModuleLink>({
defaultValues,
});
const onClose = () => {
handleClose();
};
const handleFormSubmit = async (formData: ModuleLink) => {
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
const payload = {
title: formData.title,
url: parsedUrl,
};
try {
if (!data) {
await createLink(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Module link created successfully.",
});
} else {
await updateLink(payload, data.id);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Module link updated successfully.",
});
}
onClose();
} catch (error: any) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.data?.error ?? "Some error occurred. Please try again.",
});
}
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, isOpen, reset]);
return (
<ModalCore isOpen={isOpen} handleClose={onClose}>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5 p-5">
<h3 className="text-xl font-medium text-custom-text-200">{data ? "Update" : "Add"} link</h3>
<div className="mt-2 space-y-3">
<div>
<label htmlFor="url" className="mb-2 text-custom-text-200">
URL
</label>
<Controller
control={control}
name="url"
rules={{
required: "URL is required",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="url"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.url)}
placeholder="Type or paste a URL"
className="w-full"
/>
)}
/>
</div>
<div>
<label htmlFor="title" className="mb-2 text-custom-text-200">
Display title
<span className="text-[10px] block">Optional</span>
</label>
<Controller
control={control}
name="title"
render={({ field: { value, onChange, ref } }) => (
<Input
id="title"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.title)}
placeholder="What you'd like to see this link as"
className="w-full"
/>
)}
/>
</div>
</div>
</div>
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{data ? (isSubmitting ? "Updating link" : "Update link") : isSubmitting ? "Adding link" : "Add link"}
</Button>
</div>
</form>
</ModalCore>
);
};

View File

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

View File

@@ -0,0 +1,105 @@
import { observer } from "mobx-react";
import { Copy, Pencil, Trash2 } from "lucide-react";
// plane types
import { MODULE_TRACKER_ELEMENTS } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { ILinkDetails } from "@plane/types";
// plane ui
import { getIconForLink, copyTextToClipboard, calculateTimeAgo } from "@plane/utils";
// helpers
//
// hooks
import { useMember } from "@/hooks/store/use-member";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
handleDeleteLink: () => void;
handleEditLink: () => void;
isEditingAllowed: boolean;
link: ILinkDetails;
};
export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
const { handleDeleteLink, handleEditLink, isEditingAllowed, link } = props;
// store hooks
const { getUserDetails } = useMember();
// derived values
const createdByDetails = getUserDetails(link.created_by);
// platform os
const { isMobile } = usePlatformOS();
const Icon = getIconForLink(link.url);
const copyToClipboard = (text: string) => {
copyTextToClipboard(text).then(() =>
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
message: "The URL has been successfully copied to your clipboard",
})
);
};
return (
<div className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<Icon className="size-3 stroke-2 text-custom-text-350 group-hover:text-custom-text-100 flex-shrink-0" />
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<a href={link.url} target="_blank" rel="noopener noreferrer" className="cursor-pointer truncate text-xs">
{link.title && link.title !== "" ? link.title : link.url}
</a>
</Tooltip>
</div>
<div className="z-[1] flex flex-shrink-0 items-center">
{isEditingAllowed && (
<button
type="button"
className="grid place-items-center p-1 hover:bg-custom-background-80"
data-ph-element={MODULE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditLink();
}}
>
<Pencil className="size-3 stroke-[1.5] text-custom-text-200" />
</button>
)}
<span
onClick={() => copyToClipboard(link.url)}
className="grid place-items-center p-1 hover:bg-custom-background-80 cursor-pointer"
>
<Copy className="h-3.5 w-3.5 stroke-[1.5]" />
</span>
{isEditingAllowed && (
<button
type="button"
className="grid place-items-center p-1 hover:bg-custom-background-80"
data-ph-element={MODULE_TRACKER_ELEMENTS.LIST_ITEM}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteLink();
}}
>
<Trash2 className="size-3 stroke-[1.5] text-custom-text-200" />
</button>
)}
</div>
</div>
<div className="px-5">
<p className="flex items-center gap-1.5 mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(link.created_at)}{" "}
{createdByDetails && (
<>by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}</>
)}
</p>
</div>
</div>
);
});

View File

@@ -0,0 +1,44 @@
"use client";
import { useCallback } from "react";
import { observer } from "mobx-react";
// plane types
import type { ILinkDetails } from "@plane/types";
// components
import { ModulesLinksListItem } from "@/components/modules";
// hooks
import { useModule } from "@/hooks/store/use-module";
type Props = {
disabled?: boolean;
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: ILinkDetails) => void;
moduleId: string;
};
export const ModuleLinksList: React.FC<Props> = observer((props) => {
const { moduleId, handleDeleteLink, handleEditLink, disabled } = props;
// store hooks
const { getModuleById } = useModule();
// derived values
const currentModule = getModuleById(moduleId);
const moduleLinks = currentModule?.link_module;
// memoized link handlers
const memoizedDeleteLink = useCallback((id: string) => handleDeleteLink(id), [handleDeleteLink]);
const memoizedEditLink = useCallback((link: ILinkDetails) => handleEditLink(link), [handleEditLink]);
if (!moduleLinks) return null;
return (
<>
{moduleLinks.map((link) => (
<ModulesLinksListItem
key={link.id}
handleDeleteLink={() => memoizedDeleteLink(link.id)}
handleEditLink={() => memoizedEditLink(link)}
isEditingAllowed={!disabled}
link={link}
/>
))}
</>
);
});