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
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:
157
apps/web/core/components/modules/links/create-update-modal.tsx
Normal file
157
apps/web/core/components/modules/links/create-update-modal.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane types
|
||||
import { Field } from "@makeplane/propel/components/field";
|
||||
import { Input, InputGroup } from "@makeplane/propel/components/input";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { ILinkDetails, ModuleLink } from "@plane/types";
|
||||
// plane ui
|
||||
import { 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 function CreateUpdateModuleLinkModal(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-18 font-medium text-secondary">{data ? "Update" : "Add"} link</h3>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div>
|
||||
<label htmlFor="url" className="mb-2 text-secondary">
|
||||
URL
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
rules={{
|
||||
required: "URL is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Field name="url" invalid={Boolean(errors.url)}>
|
||||
<InputGroup size="2xl">
|
||||
<Input
|
||||
size="2xl"
|
||||
id="url"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
placeholder="Type or paste a URL"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="title" className="mb-2 text-secondary">
|
||||
Display title
|
||||
<span className="block text-10">Optional</span>
|
||||
</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Field name="title" invalid={Boolean(errors.title)}>
|
||||
<InputGroup size="2xl">
|
||||
<Input
|
||||
size="2xl"
|
||||
id="title"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
placeholder="What you'd like to see this link as"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
|
||||
<Button variant="secondary" size="lg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="lg" type="submit" loading={isSubmitting}>
|
||||
{data ? (isSubmitting ? "Updating link" : "Update link") : isSubmitting ? "Adding link" : "Add link"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalCore>
|
||||
);
|
||||
}
|
||||
9
apps/web/core/components/modules/links/index.ts
Normal file
9
apps/web/core/components/modules/links/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./create-update-modal";
|
||||
export * from "./list-item";
|
||||
export * from "./list";
|
||||
110
apps/web/core/components/modules/links/list-item.tsx
Normal file
110
apps/web/core/components/modules/links/list-item.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { CopyOutline, DeleteOutline, EditOutline } from "@makeplane/propel/icons";
|
||||
// plane types
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Tooltip } from "@makeplane/propel/components/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 = observer(function ModulesLinksListItem(props: 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-layer-3 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 shrink-0 stroke-2 text-tertiary group-hover:text-primary" />
|
||||
</span>
|
||||
<Tooltip label={link.title && link.title !== "" ? link.title : link.url} layout="stacked" disabled={isMobile}>
|
||||
<a href={link.url} target="_blank" rel="noopener noreferrer" className="cursor-pointer truncate text-11">
|
||||
{link.title && link.title !== "" ? link.title : link.url}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="z-1 flex shrink-0 items-center">
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center rounded-sm p-1 text-secondary hover:bg-layer-transparent-hover"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleEditLink();
|
||||
}}
|
||||
>
|
||||
<EditOutline className="size-3 stroke-[1.5]" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(link.url)}
|
||||
className="grid place-items-center rounded-sm p-1 text-secondary hover:bg-layer-transparent-hover"
|
||||
>
|
||||
<CopyOutline className="size-3 stroke-[1.5]" />
|
||||
</button>
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center rounded-sm p-1 text-secondary hover:bg-layer-transparent-hover"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDeleteLink();
|
||||
}}
|
||||
>
|
||||
<DeleteOutline className="size-3 stroke-[1.5]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<p className="mt-0.5 flex items-center gap-1.5 stroke-[1.5] text-11 text-tertiary">
|
||||
Added {calculateTimeAgo(link.created_at)}{" "}
|
||||
{createdByDetails && (
|
||||
<>by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
49
apps/web/core/components/modules/links/list.tsx
Normal file
49
apps/web/core/components/modules/links/list.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
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 = observer(function ModuleLinksList(props: 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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user