forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into chore/api_endpoints
This commit is contained in:
commit
c5788d20e7
@ -1,7 +1,7 @@
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
@ -10,7 +10,7 @@ from plane.app.serializers import FileAssetSerializer
|
||||
|
||||
|
||||
class FileAssetEndpoint(BaseAPIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
parser_classes = (MultiPartParser, FormParser, JSONParser,)
|
||||
|
||||
"""
|
||||
A viewset for viewing and editing task instances.
|
||||
@ -25,7 +25,6 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
else:
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def post(self, request, slug):
|
||||
serializer = FileAssetSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
@ -34,12 +33,11 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
def delete(self, request, workspace_id, asset_key):
|
||||
|
||||
def patch(self, request, workspace_id, asset_key):
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||
file_asset.is_deleted = True
|
||||
file_asset.is_deleted = request.data.get("is_deleted", file_asset.is_deleted)
|
||||
file_asset.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
@ -975,7 +975,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
params = {
|
||||
"Bucket": settings.AWS_S3_BUCKET_NAME,
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
}
|
||||
|
||||
@ -987,7 +987,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
files.append(
|
||||
f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
@ -81,13 +81,13 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
)
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_S3_BUCKET_NAME,
|
||||
settings.AWS_STORAGE_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||
Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
# Create the new url with updated domain and protocol
|
||||
@ -105,14 +105,14 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
)
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_S3_BUCKET_NAME,
|
||||
settings.AWS_STORAGE_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||
Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
|
||||
|
@ -42,8 +42,8 @@ def delete_old_s3_link():
|
||||
# Delete object from S3
|
||||
if file_name:
|
||||
if settings.USE_MINIO:
|
||||
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||
s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name)
|
||||
else:
|
||||
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||
s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name)
|
||||
|
||||
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
|
||||
|
@ -40,7 +40,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
# Create an S3 client using the session
|
||||
s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL)
|
||||
bucket_name = settings.AWS_S3_BUCKET_NAME
|
||||
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
|
||||
|
||||
self.stdout.write(self.style.NOTICE("Checking bucket..."))
|
||||
|
||||
@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||
self.set_bucket_public_policy(s3_client, bucket_name)
|
||||
except ClientError as e:
|
||||
error_code = int(e.response['Error']['Code'])
|
||||
bucket_name = settings.AWS_S3_BUCKET_NAME
|
||||
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
|
||||
if error_code == 404:
|
||||
# Bucket does not exist, create it
|
||||
self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket..."))
|
||||
|
@ -224,7 +224,7 @@ STORAGES["default"] = {
|
||||
}
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
AWS_DEFAULT_ACL = "public-read"
|
||||
AWS_QUERYSTRING_AUTH = False
|
||||
@ -234,7 +234,7 @@ AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", None) or os.environ.
|
||||
)
|
||||
if AWS_S3_ENDPOINT_URL:
|
||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_S3_BUCKET_NAME}"
|
||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||
|
||||
|
||||
|
@ -1,33 +1,46 @@
|
||||
import { EditorContainer, EditorContentWrapper } from "@plane/editor-core"
|
||||
import { Editor } from "@tiptap/react"
|
||||
import { DocumentDetails } from "../types/editor-types"
|
||||
import { EditorContainer, EditorContentWrapper } from "@plane/editor-core";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
|
||||
interface IPageRenderer {
|
||||
sidePeakVisible: boolean,
|
||||
documentDetails: DocumentDetails ,
|
||||
editor: Editor,
|
||||
editorClassNames: string,
|
||||
editorContentCustomClassNames?: string
|
||||
sidePeakVisible: boolean;
|
||||
documentDetails: DocumentDetails;
|
||||
editor: Editor;
|
||||
editorClassNames: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
}
|
||||
|
||||
export const PageRenderer = ({ sidePeakVisible, documentDetails, editor, editorClassNames, editorContentCustomClassNames }: IPageRenderer) => {
|
||||
export const PageRenderer = ({
|
||||
sidePeakVisible,
|
||||
documentDetails,
|
||||
editor,
|
||||
editorClassNames,
|
||||
editorContentCustomClassNames,
|
||||
}: IPageRenderer) => {
|
||||
return (
|
||||
<div className={`flex h-[88vh] flex-col w-full max-md:w-full max-md:ml-0 transition-all duration-200 ease-in-out ${sidePeakVisible ? 'ml-[3%] ' : 'ml-0'}`}>
|
||||
<div
|
||||
className={`flex h-[88vh] flex-col w-full max-md:w-full max-md:ml-0 transition-all duration-200 ease-in-out ${
|
||||
sidePeakVisible ? "ml-[3%] " : "ml-0"
|
||||
}`}
|
||||
>
|
||||
<div className="items-start mt-4 h-full flex flex-col w-fit max-md:max-w-full overflow-auto">
|
||||
<div className="flex flex-col py-2 max-md:max-w-full">
|
||||
<h1
|
||||
className="border-none outline-none bg-transparent text-4xl font-bold leading-8 tracking-tight self-center w-[700px] max-w-full"
|
||||
>{documentDetails.title}</h1>
|
||||
<h1 className="border-none outline-none bg-transparent text-4xl font-bold leading-8 tracking-tight self-center w-[700px] max-w-full">
|
||||
{documentDetails.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="border-custom-border border-b border-solid self-stretch w-full h-0.5 mt-3" />
|
||||
<div className="flex flex-col max-md:max-w-full">
|
||||
<div className="border-b border-custom-border-200 self-stretch w-full h-0.5 mt-3" />
|
||||
<div className="flex flex-col h-full w-full max-md:max-w-full">
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer >
|
||||
</EditorContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -11,7 +11,7 @@ import emptyApiTokens from "public/empty-state/api-token.svg";
|
||||
const ApiTokenEmptyState = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className={`flex items-center justify-center mx-auto border bg-custom-background-90 py-10 px-16 w-full`}>
|
||||
<div className={`flex items-center justify-center mx-auto rounded-sm border border-custom-border-200 bg-custom-background-90 py-10 px-16 w-full`}>
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<Image src={emptyApiTokens} className="w-52 sm:w-60" alt="empty" />
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">No API Tokens</h6>
|
||||
|
83
web/components/command-palette/actions/help-actions.tsx
Normal file
83
web/components/command-palette/actions/help-actions.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Command } from "cmdk";
|
||||
import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { DiscordIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteHelpActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
|
||||
const {
|
||||
commandPalette: { toggleShortcutModal },
|
||||
} = useMobxStore();
|
||||
|
||||
return (
|
||||
<Command.Group heading="Help">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleShortcutModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Rocket className="h-3.5 w-3.5" />
|
||||
Open keyboard shortcuts
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://docs.plane.so/", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Open Plane documentation
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
Join our Discord
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<GithubIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
Report a bug
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
(window as any)?.$crisp.push(["do", "chat:open"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
Chat with us
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
);
|
||||
};
|
6
web/components/command-palette/actions/index.ts
Normal file
6
web/components/command-palette/actions/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./issue-actions";
|
||||
export * from "./help-actions";
|
||||
export * from "./project-actions";
|
||||
export * from "./search-results";
|
||||
export * from "./theme-actions";
|
||||
export * from "./workspace-settings-actions";
|
@ -0,0 +1,166 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Command } from "cmdk";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
pages: string[];
|
||||
setPages: (pages: string[]) => void;
|
||||
setPlaceholder: (placeholder: string) => void;
|
||||
setSearchTerm: (searchTerm: string) => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issueDetails, pages, setPages, setPlaceholder, setSearchTerm } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
|
||||
issueDetail: { updateIssue },
|
||||
user: { currentUser },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetails) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issueDetails.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
if (!issueDetails || !assignee) return;
|
||||
|
||||
closePalette();
|
||||
const updatedAssignees = issueDetails.assignees ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
else updatedAssignees.push(assignee);
|
||||
|
||||
handleUpdateIssue({ assignees: updatedAssignees });
|
||||
};
|
||||
|
||||
const deleteIssue = () => {
|
||||
toggleCommandPaletteModal(false);
|
||||
toggleDeleteIssueModal(true);
|
||||
};
|
||||
|
||||
const copyIssueUrlToClipboard = () => {
|
||||
if (!router.query.issueId) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
copyTextToClipboard(url.href)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Command.Group heading="Issue actions">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change state...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-state"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DoubleCircleIcon className="h-3.5 w-3.5" />
|
||||
Change state...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change priority...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-priority"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Signal className="h-3.5 w-3.5" />
|
||||
Change priority...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Assign to...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-assignee"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
||||
Assign to...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
handleIssueAssignees(currentUser?.id ?? "");
|
||||
setSearchTerm("");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
{issueDetails?.assignees.includes(currentUser?.id ?? "") ? (
|
||||
<>
|
||||
<UserMinus2 className="h-3.5 w-3.5" />
|
||||
Un-assign from me
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus2 className="h-3.5 w-3.5" />
|
||||
Assign to me
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Delete issue
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
copyIssueUrlToClipboard();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
Copy issue URL
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
);
|
||||
});
|
@ -0,0 +1,79 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Command } from "cmdk";
|
||||
import { Check } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issue: IIssue;
|
||||
};
|
||||
|
||||
export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store
|
||||
const {
|
||||
issueDetail: { updateIssue },
|
||||
projectMember: { projectMembers },
|
||||
} = useMobxStore();
|
||||
|
||||
const options =
|
||||
projectMembers?.map(({ member }) => ({
|
||||
value: member.id,
|
||||
query: member.display_name,
|
||||
content: (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={member.display_name} src={member.avatar} showTooltip={false} />
|
||||
{member.display_name}
|
||||
</div>
|
||||
{issue.assignees.includes(member.id) && (
|
||||
<div>
|
||||
<Check className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})) ?? [];
|
||||
|
||||
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
const updatedAssignees = issue.assignees ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
else updatedAssignees.push(assignee);
|
||||
|
||||
handleUpdateIssue({ assignees: updatedAssignees });
|
||||
closePalette();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map((option: any) => (
|
||||
<Command.Item
|
||||
key={option.value}
|
||||
onSelect={() => handleIssueAssignees(option.value)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
{option.content}
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Command } from "cmdk";
|
||||
import { Check } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issue: IIssue;
|
||||
};
|
||||
|
||||
export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueDetail: { updateIssue },
|
||||
} = useMobxStore();
|
||||
|
||||
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueState = (priority: TIssuePriorities) => {
|
||||
submitChanges({ priority });
|
||||
closePalette();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<PriorityIcon priority={priority} />
|
||||
<span className="capitalize">{priority ?? "None"}</span>
|
||||
</div>
|
||||
<div>{priority === issue.priority && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,65 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// ui
|
||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||
// icons
|
||||
import { Check } from "lucide-react";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issue: IIssue;
|
||||
};
|
||||
|
||||
export const ChangeIssueState: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
projectState: { projectStates },
|
||||
issueDetail: { updateIssue },
|
||||
} = useMobxStore();
|
||||
|
||||
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueState = (stateId: string) => {
|
||||
submitChanges({ state: stateId });
|
||||
closePalette();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectStates ? (
|
||||
projectStates.length > 0 ? (
|
||||
projectStates.map((state) => (
|
||||
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === issue.state && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No states found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
export * from "./actions-list";
|
||||
export * from "./change-state";
|
||||
export * from "./change-priority";
|
||||
export * from "./change-assignee";
|
83
web/components/command-palette/actions/project-actions.tsx
Normal file
83
web/components/command-palette/actions/project-actions.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Command } from "cmdk";
|
||||
import { ContrastIcon, FileText } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// ui
|
||||
import { DiceIcon, PhotoFilterIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal },
|
||||
} = useMobxStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Command.Group heading="Cycle">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreateCycleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<ContrastIcon className="h-3.5 w-3.5" />
|
||||
Create new cycle
|
||||
</div>
|
||||
<kbd>Q</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Module">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreateModuleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiceIcon className="h-3.5 w-3.5" />
|
||||
Create new module
|
||||
</div>
|
||||
<kbd>M</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="View">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreateViewModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<PhotoFilterIcon className="h-3.5 w-3.5" />
|
||||
Create new view
|
||||
</div>
|
||||
<kbd>V</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Page">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreatePageModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Create new page
|
||||
</div>
|
||||
<kbd>D</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
</>
|
||||
);
|
||||
};
|
49
web/components/command-palette/actions/search-results.tsx
Normal file
49
web/components/command-palette/actions/search-results.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { Command } from "cmdk";
|
||||
// helpers
|
||||
import { commandGroups } from "components/command-palette";
|
||||
// types
|
||||
import { IWorkspaceSearchResults } from "types";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
results: IWorkspaceSearchResults;
|
||||
};
|
||||
|
||||
export const CommandPaletteSearchResults: React.FC<Props> = (props) => {
|
||||
const { closePalette, results } = props;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.keys(results.results).map((key) => {
|
||||
const section = (results.results as any)[key];
|
||||
const currentSection = commandGroups[key];
|
||||
|
||||
if (section.length > 0) {
|
||||
return (
|
||||
<Command.Group key={key} heading={`${currentSection.title} search`}>
|
||||
{section.map((item: any) => (
|
||||
<Command.Item
|
||||
key={item.id}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
router.push(currentSection.path(item));
|
||||
}}
|
||||
value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
||||
{currentSection.icon}
|
||||
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Settings } from "lucide-react";
|
||||
@ -10,22 +10,25 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { THEME_OPTIONS } from "constants/themes";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const ChangeInterfaceTheme: FC<Props> = observer((props) => {
|
||||
const { setIsPaletteOpen } = props;
|
||||
// store
|
||||
const { user: userStore } = useMobxStore();
|
||||
export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||
const { closePalette } = props;
|
||||
// states
|
||||
const [mounted, setMounted] = useState(false);
|
||||
// store
|
||||
const {
|
||||
user: { updateCurrentUserTheme },
|
||||
} = useMobxStore();
|
||||
// hooks
|
||||
const { setTheme } = useTheme();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const updateUserTheme = (newTheme: string) => {
|
||||
const updateUserTheme = async (newTheme: string) => {
|
||||
setTheme(newTheme);
|
||||
return userStore.updateCurrentUserTheme(newTheme).catch(() => {
|
||||
|
||||
return updateCurrentUserTheme(newTheme).catch(() => {
|
||||
setToastAlert({
|
||||
title: "Failed to save user theme settings!",
|
||||
type: "error",
|
||||
@ -47,7 +50,7 @@ export const ChangeInterfaceTheme: FC<Props> = observer((props) => {
|
||||
key={theme.value}
|
||||
onSelect={() => {
|
||||
updateUserTheme(theme.value);
|
||||
setIsPaletteOpen(false);
|
||||
closePalette();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
@ -0,0 +1,61 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { Command } from "cmdk";
|
||||
// icons
|
||||
import { SettingIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const redirect = (path: string) => {
|
||||
closePalette();
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
General
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Members
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Billing and Plans
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Integrations
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Import
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Export
|
||||
</div>
|
||||
</Command.Item>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,22 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
import { Command } from "cmdk";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import {
|
||||
FileText,
|
||||
FolderPlus,
|
||||
LinkIcon,
|
||||
MessageSquare,
|
||||
Rocket,
|
||||
Search,
|
||||
Settings,
|
||||
Signal,
|
||||
Trash2,
|
||||
UserMinus2,
|
||||
UserPlus2,
|
||||
} from "lucide-react";
|
||||
import { FolderPlus, Search, Settings } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
@ -24,47 +12,29 @@ import { WorkspaceService } from "services/workspace.service";
|
||||
import { IssueService } from "services/issue";
|
||||
// hooks
|
||||
import useDebounce from "hooks/use-debounce";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
ChangeInterfaceTheme,
|
||||
CommandPaletteThemeActions,
|
||||
ChangeIssueAssignee,
|
||||
ChangeIssuePriority,
|
||||
ChangeIssueState,
|
||||
commandGroups,
|
||||
CommandPaletteHelpActions,
|
||||
CommandPaletteIssueActions,
|
||||
CommandPaletteProjectActions,
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
CommandPaletteSearchResults,
|
||||
} from "components/command-palette";
|
||||
import {
|
||||
ContrastIcon,
|
||||
DiceIcon,
|
||||
DoubleCircleIcon,
|
||||
LayersIcon,
|
||||
Loader,
|
||||
PhotoFilterIcon,
|
||||
ToggleSwitch,
|
||||
Tooltip,
|
||||
UserGroupIcon,
|
||||
} from "@plane/ui";
|
||||
// icons
|
||||
import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IIssue, IWorkspaceSearchResults } from "types";
|
||||
import { IWorkspaceSearchResults } from "types";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
deleteIssue: () => void;
|
||||
isPaletteOpen: boolean;
|
||||
closePalette: () => void;
|
||||
};
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
const { deleteIssue, isPaletteOpen, closePalette } = props;
|
||||
export const CommandModal: React.FC = observer(() => {
|
||||
// states
|
||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
@ -85,8 +55,14 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
|
||||
const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore();
|
||||
const user = userStore.currentUser ?? undefined;
|
||||
const {
|
||||
commandPalette: {
|
||||
isCommandPaletteOpen,
|
||||
toggleCommandPaletteModal,
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateProjectModal,
|
||||
},
|
||||
} = useMobxStore();
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -96,64 +72,16 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// TODO: update this to mobx store
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const updateIssue = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const payload = { ...formData };
|
||||
await issueService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
mutate(ISSUE_DETAILS(issueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, issueId, projectId]
|
||||
);
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
if (!issueDetails) return;
|
||||
|
||||
closePalette();
|
||||
const updatedAssignees = issueDetails.assignees ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) {
|
||||
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
} else {
|
||||
updatedAssignees.push(assignee);
|
||||
}
|
||||
updateIssue({ assignees: updatedAssignees });
|
||||
};
|
||||
|
||||
const redirect = (path: string) => {
|
||||
closePalette();
|
||||
router.push(path);
|
||||
const closePalette = () => {
|
||||
toggleCommandPaletteModal(false);
|
||||
};
|
||||
|
||||
const createNewWorkspace = () => {
|
||||
@ -161,25 +89,6 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
router.push("/create-workspace");
|
||||
};
|
||||
|
||||
const copyIssueUrlToClipboard = useCallback(() => {
|
||||
if (!router.query.issueId) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
copyTextToClipboard(url.href)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
});
|
||||
}, [router, setToastAlert]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!workspaceSlug) return;
|
||||
@ -189,7 +98,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
if (debouncedSearchTerm) {
|
||||
setIsSearching(true);
|
||||
workspaceService
|
||||
.searchWorkspace(workspaceSlug as string, {
|
||||
.searchWorkspace(workspaceSlug.toString(), {
|
||||
...(projectId ? { project_id: projectId.toString() } : {}),
|
||||
search: debouncedSearchTerm,
|
||||
workspace_search: !projectId ? true : isWorkspaceLevel,
|
||||
@ -225,16 +134,8 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
[debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes
|
||||
);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Transition.Root
|
||||
show={isPaletteOpen}
|
||||
afterLeave={() => {
|
||||
setSearchTerm("");
|
||||
}}
|
||||
as={React.Fragment}
|
||||
>
|
||||
<Transition.Root show={isCommandPaletteOpen} afterLeave={() => setSearchTerm("")} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={() => closePalette()}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
@ -268,9 +169,8 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
onKeyDown={(e) => {
|
||||
// when search is empty and page is undefined
|
||||
// when user tries to close the modal with esc
|
||||
if (e.key === "Escape" && !page && !searchTerm) {
|
||||
closePalette();
|
||||
}
|
||||
if (e.key === "Escape" && !page && !searchTerm) closePalette();
|
||||
|
||||
// Escape goes to previous page
|
||||
// Backspace goes to previous page when search is empty
|
||||
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
|
||||
@ -318,9 +218,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm"
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
onValueChange={(e) => {
|
||||
setSearchTerm(e);
|
||||
}}
|
||||
onValueChange={(e) => setSearchTerm(e)}
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
/>
|
||||
@ -340,7 +238,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||
<div className="my-4 text-center text-custom-text-200">No results found.</div>
|
||||
<div className="my-4 text-center text-custom-text-200 text-sm">No results found.</div>
|
||||
)}
|
||||
|
||||
{(isLoading || isSearching) && (
|
||||
@ -354,125 +252,28 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
</Command.Loading>
|
||||
)}
|
||||
|
||||
{debouncedSearchTerm !== "" &&
|
||||
Object.keys(results.results).map((key) => {
|
||||
const section = (results.results as any)[key];
|
||||
const currentSection = commandGroups[key];
|
||||
|
||||
if (section.length > 0) {
|
||||
return (
|
||||
<Command.Group key={key} heading={currentSection.title}>
|
||||
{section.map((item: any) => (
|
||||
<Command.Item
|
||||
key={item.id}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
router.push(currentSection.path(item));
|
||||
}}
|
||||
value={`${key}-${item?.name}`}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
||||
{currentSection.icon}
|
||||
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{debouncedSearchTerm !== "" && (
|
||||
<CommandPaletteSearchResults closePalette={closePalette} results={results} />
|
||||
)}
|
||||
|
||||
{!page && (
|
||||
<>
|
||||
{/* issue actions */}
|
||||
{issueId && (
|
||||
<Command.Group heading="Issue actions">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setPlaceholder("Change state...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-state"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DoubleCircleIcon className="h-3.5 w-3.5" />
|
||||
Change state...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change priority...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-priority"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Signal className="h-3.5 w-3.5" />
|
||||
Change priority...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Assign to...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-assignee"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
||||
Assign to...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
handleIssueAssignees(user.id);
|
||||
setSearchTerm("");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
{issueDetails?.assignees.includes(user.id) ? (
|
||||
<>
|
||||
<UserMinus2 className="h-3.5 w-3.5" />
|
||||
Un-assign from me
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus2 className="h-3.5 w-3.5" />
|
||||
Assign to me
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Delete issue
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
copyIssueUrlToClipboard();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
Copy issue URL
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<CommandPaletteIssueActions
|
||||
closePalette={closePalette}
|
||||
issueDetails={issueDetails}
|
||||
pages={pages}
|
||||
setPages={(newPages) => setPages(newPages)}
|
||||
setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)}
|
||||
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||
/>
|
||||
)}
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleCreateIssueModal(true);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
className="focus:bg-custom-background-80"
|
||||
>
|
||||
@ -489,7 +290,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
@ -502,70 +303,8 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{projectId && (
|
||||
<>
|
||||
<Command.Group heading="Cycle">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleCreateCycleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<ContrastIcon className="h-3.5 w-3.5" />
|
||||
Create new cycle
|
||||
</div>
|
||||
<kbd>Q</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Module">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleCreateModuleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiceIcon className="h-3.5 w-3.5" />
|
||||
Create new module
|
||||
</div>
|
||||
<kbd>M</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="View">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleCreateViewModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<PhotoFilterIcon className="h-3.5 w-3.5" />
|
||||
Create new view
|
||||
</div>
|
||||
<kbd>V</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Page">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleCreatePageModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Create new page
|
||||
</div>
|
||||
<kbd>D</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
</>
|
||||
)}
|
||||
{/* project actions */}
|
||||
{projectId && <CommandPaletteProjectActions closePalette={closePalette} />}
|
||||
|
||||
<Command.Group heading="Workspace Settings">
|
||||
<Command.Item
|
||||
@ -603,139 +342,37 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Help">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
commandPaletteStore.toggleShortcutModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Rocket className="h-3.5 w-3.5" />
|
||||
Open keyboard shortcuts
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://docs.plane.so/", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Open Plane documentation
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
Join our Discord
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<GithubIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
Report a bug
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
(window as any).$crisp.push(["do", "chat:open"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
Chat with us
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
||||
{/* help options */}
|
||||
<CommandPaletteHelpActions closePalette={closePalette} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* workspace settings actions */}
|
||||
{page === "settings" && workspaceSlug && (
|
||||
<>
|
||||
<Command.Item
|
||||
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
General
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Members
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Billing and Plans
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Integrations
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Import
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Export
|
||||
</div>
|
||||
</Command.Item>
|
||||
</>
|
||||
<CommandPaletteWorkspaceSettingsActions closePalette={closePalette} />
|
||||
)}
|
||||
|
||||
{/* issue details page actions */}
|
||||
{page === "change-issue-state" && issueDetails && (
|
||||
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
|
||||
<ChangeIssueState closePalette={closePalette} issue={issueDetails} />
|
||||
)}
|
||||
{page === "change-issue-priority" && issueDetails && (
|
||||
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
|
||||
<ChangeIssuePriority closePalette={closePalette} issue={issueDetails} />
|
||||
)}
|
||||
{page === "change-issue-assignee" && issueDetails && (
|
||||
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
|
||||
<ChangeIssueAssignee closePalette={closePalette} issue={issueDetails} />
|
||||
)}
|
||||
|
||||
{/* theme actions */}
|
||||
{page === "change-interface-theme" && (
|
||||
<CommandPaletteThemeActions
|
||||
closePalette={() => {
|
||||
closePalette();
|
||||
setPages((pages) => pages.slice(0, -1));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={closePalette} />}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</div>
|
||||
|
@ -32,7 +32,6 @@ export const CommandPalette: FC = observer(() => {
|
||||
// store
|
||||
const { commandPalette, theme: themeStore } = useMobxStore();
|
||||
const {
|
||||
isCommandPaletteOpen,
|
||||
toggleCommandPaletteModal,
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
@ -156,11 +155,6 @@ export const CommandPalette: FC = observer(() => {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const deleteIssue = () => {
|
||||
toggleCommandPaletteModal(false);
|
||||
toggleDeleteIssueModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShortcutsModal
|
||||
@ -231,13 +225,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
}}
|
||||
user={user}
|
||||
/>
|
||||
<CommandModal
|
||||
deleteIssue={deleteIssue}
|
||||
isPaletteOpen={isCommandPaletteOpen}
|
||||
closePalette={() => {
|
||||
toggleCommandPaletteModal(false);
|
||||
}}
|
||||
/>
|
||||
<CommandModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -20,9 +20,7 @@ export const commandGroups: {
|
||||
icon: <ContrastIcon className="h-3 w-3" />,
|
||||
itemName: (cycle: IWorkspaceDefaultSearchResult) => (
|
||||
<h6>
|
||||
<span className="text-custom-text-200 text-xs">{cycle.project__identifier}</span>
|
||||
{"- "}
|
||||
{cycle.name}
|
||||
<span className="text-custom-text-300 text-xs">{cycle.project__identifier}</span> {cycle.name}
|
||||
</h6>
|
||||
),
|
||||
path: (cycle: IWorkspaceDefaultSearchResult) =>
|
||||
@ -33,8 +31,9 @@ export const commandGroups: {
|
||||
icon: <LayersIcon className="h-3 w-3" />,
|
||||
itemName: (issue: IWorkspaceIssueSearchResult) => (
|
||||
<h6>
|
||||
<span className="text-custom-text-200 text-xs">{issue.project__identifier}</span>
|
||||
{"- "}
|
||||
<span className="text-custom-text-300 text-xs">
|
||||
{issue.project__identifier}-{issue.sequence_id}
|
||||
</span>{" "}
|
||||
{issue.name}
|
||||
</h6>
|
||||
),
|
||||
@ -46,9 +45,7 @@ export const commandGroups: {
|
||||
icon: <PhotoFilterIcon className="h-3 w-3" />,
|
||||
itemName: (view: IWorkspaceDefaultSearchResult) => (
|
||||
<h6>
|
||||
<span className="text-custom-text-200 text-xs">{view.project__identifier}</span>
|
||||
{"- "}
|
||||
{view.name}
|
||||
<span className="text-custom-text-300 text-xs">{view.project__identifier}</span> {view.name}
|
||||
</h6>
|
||||
),
|
||||
path: (view: IWorkspaceDefaultSearchResult) =>
|
||||
@ -59,9 +56,7 @@ export const commandGroups: {
|
||||
icon: <DiceIcon className="h-3 w-3" />,
|
||||
itemName: (module: IWorkspaceDefaultSearchResult) => (
|
||||
<h6>
|
||||
<span className="text-custom-text-200 text-xs">{module.project__identifier}</span>
|
||||
{"- "}
|
||||
{module.name}
|
||||
<span className="text-custom-text-300 text-xs">{module.project__identifier}</span> {module.name}
|
||||
</h6>
|
||||
),
|
||||
path: (module: IWorkspaceDefaultSearchResult) =>
|
||||
@ -72,9 +67,7 @@ export const commandGroups: {
|
||||
icon: <FileText className="h-3 w-3" />,
|
||||
itemName: (page: IWorkspaceDefaultSearchResult) => (
|
||||
<h6>
|
||||
<span className="text-custom-text-200 text-xs">{page.project__identifier}</span>
|
||||
{"- "}
|
||||
{page.name}
|
||||
<span className="text-custom-text-300 text-xs">{page.project__identifier}</span> {page.name}
|
||||
</h6>
|
||||
),
|
||||
path: (page: IWorkspaceDefaultSearchResult) =>
|
||||
|
@ -1,5 +1,4 @@
|
||||
export * from "./issue";
|
||||
export * from "./change-interface-theme";
|
||||
export * from "./actions";
|
||||
export * from "./command-modal";
|
||||
export * from "./command-pallette";
|
||||
export * from "./helpers";
|
||||
|
@ -1,111 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useCallback, FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
import { Command } from "cmdk";
|
||||
import { Check } from "lucide-react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// types
|
||||
import { IUser, IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
issue: IIssue;
|
||||
user: IUser | undefined;
|
||||
};
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const ChangeIssueAssignee: FC<Props> = observer((props) => {
|
||||
const { setIsPaletteOpen, issue } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
// store
|
||||
const {
|
||||
projectMember: { projectMembers },
|
||||
} = useMobxStore();
|
||||
|
||||
const options =
|
||||
projectMembers?.map(({ member }) => ({
|
||||
value: member.id,
|
||||
query: member.display_name,
|
||||
content: (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={member.display_name} src={member.avatar} showTooltip={false} />
|
||||
{member.display_name}
|
||||
</div>
|
||||
{issue.assignees.includes(member.id) && (
|
||||
<div>
|
||||
<Check className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})) ?? [];
|
||||
|
||||
const updateIssue = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
async (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const payload = { ...formData };
|
||||
await issueService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, issueId, projectId]
|
||||
);
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
const updatedAssignees = issue.assignees ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) {
|
||||
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
} else {
|
||||
updatedAssignees.push(assignee);
|
||||
}
|
||||
|
||||
updateIssue({ assignees: updatedAssignees });
|
||||
setIsPaletteOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map((option: any) => (
|
||||
<Command.Item
|
||||
key={option.value}
|
||||
onSelect={() => handleIssueAssignees(option.value)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
{option.content}
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// types
|
||||
import { IIssue, IUser, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
import { PRIORITIES } from "constants/project";
|
||||
// icons
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
issue: IIssue;
|
||||
user: IUser;
|
||||
};
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const submitChanges = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
async (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const payload = { ...formData };
|
||||
await issueService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, issueId, projectId]
|
||||
);
|
||||
|
||||
const handleIssueState = (priority: TIssuePriorities) => {
|
||||
submitChanges({ priority });
|
||||
setIsPaletteOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<PriorityIcon priority={priority} />
|
||||
<span className="capitalize">{priority ?? "None"}</span>
|
||||
</div>
|
||||
<div>{priority === issue.priority && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,93 +0,0 @@
|
||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
import { ProjectStateService } from "services/project";
|
||||
// ui
|
||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||
// icons
|
||||
import { Check } from "lucide-react";
|
||||
// types
|
||||
import { IUser, IIssue } from "types";
|
||||
// fetch keys
|
||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||
issue: IIssue;
|
||||
user: IUser | undefined;
|
||||
};
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
const stateService = new ProjectStateService();
|
||||
|
||||
export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: states, mutate: mutateStates } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
const submitChanges = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issueId as string),
|
||||
async (prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const payload = { ...formData };
|
||||
await issueService
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
||||
.then(() => {
|
||||
mutateStates();
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, issueId, projectId, mutateStates]
|
||||
);
|
||||
|
||||
const handleIssueState = (stateId: string) => {
|
||||
submitChanges({ state: stateId });
|
||||
setIsPaletteOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{states ? (
|
||||
states.length > 0 ? (
|
||||
states.map((state) => (
|
||||
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === issue.state && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No states found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
export * from "./change-issue-state";
|
||||
export * from "./change-issue-priority";
|
||||
export * from "./change-issue-assignee";
|
@ -1,4 +1,6 @@
|
||||
import { Fragment, useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
@ -27,6 +29,8 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
const { setToastAlert } = useToast();
|
||||
// states
|
||||
const [loader, setLoader] = useState(false);
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
|
||||
const formSubmit = async () => {
|
||||
setLoader(true);
|
||||
@ -38,6 +42,9 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Cycle deleted successfully.",
|
||||
});
|
||||
|
||||
if (cycleId) router.replace(`/${workspaceSlug}/projects/${projectId}/cycles`);
|
||||
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
|
@ -11,7 +11,7 @@ import { IntegrationService } from "services/integrations";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIntegrationPopup from "hooks/use-integration-popup";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
import { Button, Loader, Tooltip } from "@plane/ui";
|
||||
// icons
|
||||
import GithubLogo from "public/services/github.png";
|
||||
import SlackLogo from "public/services/slack.png";
|
||||
@ -46,8 +46,11 @@ const integrationService = new IntegrationService();
|
||||
export const SingleIntegrationCard: React.FC<Props> = observer(({ integration }) => {
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const isUserAdmin = currentWorkspaceRole === 20;
|
||||
|
||||
const [deletingIntegration, setDeletingIntegration] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@ -127,13 +130,40 @@ export const SingleIntegrationCard: React.FC<Props> = observer(({ integration })
|
||||
|
||||
{workspaceIntegrations ? (
|
||||
isInstalled ? (
|
||||
<Button variant="danger" onClick={handleRemoveIntegration} loading={deletingIntegration}>
|
||||
{deletingIntegration ? "Uninstalling..." : "Uninstall"}
|
||||
</Button>
|
||||
<Tooltip
|
||||
disabled={isUserAdmin}
|
||||
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
|
||||
>
|
||||
<Button
|
||||
className={`${!isUserAdmin ? "hover:cursor-not-allowed" : ""}`}
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (!isUserAdmin) return;
|
||||
handleRemoveIntegration;
|
||||
}}
|
||||
disabled={!isUserAdmin}
|
||||
loading={deletingIntegration}
|
||||
>
|
||||
{deletingIntegration ? "Uninstalling..." : "Uninstall"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant="primary" onClick={startAuth} loading={isInstalling}>
|
||||
{isInstalling ? "Installing..." : "Install"}
|
||||
</Button>
|
||||
<Tooltip
|
||||
disabled={isUserAdmin}
|
||||
tooltipContent={!isUserAdmin ? "You don't have permission to perform this" : null}
|
||||
>
|
||||
<Button
|
||||
className={`${!isUserAdmin ? "hover:cursor-not-allowed" : ""}`}
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (!isUserAdmin) return;
|
||||
startAuth();
|
||||
}}
|
||||
loading={isInstalling}
|
||||
>
|
||||
{isInstalling ? "Installing..." : "Install"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
<Loader>
|
||||
|
@ -38,7 +38,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
const createProjectPage = async (payload: IPage) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
createPage(workspaceSlug.toString(), projectId, payload)
|
||||
await createPage(workspaceSlug.toString(), projectId, payload)
|
||||
.then((res) => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`);
|
||||
onClose();
|
||||
@ -67,7 +67,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
||||
const updateProjectPage = async (payload: IPage) => {
|
||||
if (!data || !workspaceSlug) return;
|
||||
|
||||
return updatePage(workspaceSlug.toString(), projectId, data.id, payload)
|
||||
await updatePage(workspaceSlug.toString(), projectId, data.id, payload)
|
||||
.then((res) => {
|
||||
onClose();
|
||||
setToastAlert({
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Button, Input, Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IPage } from "types";
|
||||
// constants
|
||||
import { PAGE_ACCESS_SPECIFIERS } from "constants/page";
|
||||
|
||||
type Props = {
|
||||
@ -18,31 +18,21 @@ const defaultValues = {
|
||||
access: 0,
|
||||
};
|
||||
|
||||
export const PageForm: React.FC<Props> = ({ handleFormSubmit, handleClose, data }) => {
|
||||
export const PageForm: React.FC<Props> = (props) => {
|
||||
const { handleFormSubmit, handleClose, data } = props;
|
||||
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
} = useForm<IPage>({
|
||||
defaultValues,
|
||||
defaultValues: { ...defaultValues, ...data },
|
||||
});
|
||||
|
||||
const handleCreateUpdatePage = async (formData: IPage) => {
|
||||
await handleFormSubmit(formData);
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...data,
|
||||
});
|
||||
}, [data, reset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
|
||||
<div className="space-y-4">
|
||||
|
@ -38,8 +38,9 @@ export const RecentPagesList: FC = observer(() => {
|
||||
<>
|
||||
{Object.keys(recentProjectPages).map((key) => {
|
||||
if (recentProjectPages[key].length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={key} className="h-full overflow-hidden pb-9">
|
||||
<div key={key} className="overflow-hidden">
|
||||
<h2 className="text-xl font-semibold capitalize mb-2">{replaceUnderscoreIfSnakeCase(key)}</h2>
|
||||
<PagesListView pages={recentProjectPages[key]} />
|
||||
</div>
|
||||
|
@ -21,22 +21,21 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
||||
// states
|
||||
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
|
||||
// store
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const {
|
||||
project: { joinProject },
|
||||
} = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
const handleJoin = () => {
|
||||
setIsJoiningLoading(true);
|
||||
|
||||
projectStore
|
||||
.joinProject(workspaceSlug, [project.id])
|
||||
joinProject(workspaceSlug, [project.id])
|
||||
.then(() => {
|
||||
setIsJoiningLoading(false);
|
||||
|
||||
router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
|
||||
handleClose();
|
||||
})
|
||||
.catch(() => {
|
||||
.finally(() => {
|
||||
setIsJoiningLoading(false);
|
||||
});
|
||||
};
|
||||
@ -73,8 +72,9 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
||||
Join Project?
|
||||
</Dialog.Title>
|
||||
<p>
|
||||
Are you sure you want to join the project <span className="font-semibold break-words">{project?.name}</span>?
|
||||
Please click the 'Join Project' button below to continue.
|
||||
Are you sure you want to join the project{" "}
|
||||
<span className="font-semibold break-words">{project?.name}</span>? Please click the 'Join
|
||||
Project' button below to continue.
|
||||
</p>
|
||||
<div className="space-y-3" />
|
||||
</div>
|
||||
|
@ -1,11 +1,22 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// icons
|
||||
import { MoreVertical, PenSquare, LinkIcon, Star, FileText, Settings, Share2, LogOut, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
MoreVertical,
|
||||
PenSquare,
|
||||
LinkIcon,
|
||||
Star,
|
||||
FileText,
|
||||
Settings,
|
||||
Share2,
|
||||
LogOut,
|
||||
ChevronDown,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// helpers
|
||||
@ -17,6 +28,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui";
|
||||
import { LeaveProjectModal, PublishProjectModal } from "components/project";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
|
||||
type Props = {
|
||||
project: IProject;
|
||||
@ -72,12 +84,15 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
// states
|
||||
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
||||
const [publishModalOpen, setPublishModal] = useState(false);
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
|
||||
const isAdmin = project.member_role === 20;
|
||||
const isViewerOrGuest = project.member_role === 10 || project.member_role === 5;
|
||||
|
||||
const isCollapsed = themeStore.sidebarCollapsed;
|
||||
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
@ -110,6 +125,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
setLeaveProjectModal(false);
|
||||
};
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
|
||||
@ -120,7 +137,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
<div
|
||||
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
|
||||
snapshot?.isDragging ? "opacity-60" : ""
|
||||
}`}
|
||||
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`}
|
||||
>
|
||||
{provided && (
|
||||
<Tooltip
|
||||
@ -131,7 +148,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
type="button"
|
||||
className={`absolute top-1/2 -translate-y-1/2 -left-2.5 hidden rounded p-0.5 text-custom-sidebar-text-400 ${
|
||||
isCollapsed ? "" : "group-hover:!flex"
|
||||
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
|
||||
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""} ${
|
||||
isMenuActive ? "!flex" : ""
|
||||
}`}
|
||||
{...provided?.dragHandleProps}
|
||||
>
|
||||
<MoreVertical className="h-3.5" />
|
||||
@ -169,9 +188,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 flex-shrink-0 ${
|
||||
open ? "rotate-180" : ""
|
||||
} !hidden group-hover:!block text-custom-sidebar-text-400 duration-300`}
|
||||
className={`h-4 w-4 flex-shrink-0 hidden ${open ? "rotate-180" : ""} ${
|
||||
isMenuActive ? "!block" : ""
|
||||
} group-hover:!block mb-0.5 text-custom-sidebar-text-400 duration-300`}
|
||||
/>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
@ -179,7 +198,16 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
{!isCollapsed && (
|
||||
<CustomMenu
|
||||
className="hidden group-hover:block flex-shrink-0"
|
||||
customButton={
|
||||
<div
|
||||
ref={actionSectionRef}
|
||||
className="w-full cursor-pointer text-custom-sidebar-text-400 px-1 mt-1.5 my-auto duration-300"
|
||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||
>
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
}
|
||||
className={`hidden group-hover:block flex-shrink-0 ${isMenuActive ? "!block" : ""}`}
|
||||
buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400"
|
||||
ellipsis
|
||||
placement="bottom-start"
|
||||
|
@ -1,28 +1,32 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@plane/ui";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
import Image from "next/image";
|
||||
import EmptyWebhookLogo from "public/empty-state/issue.svg";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// assets
|
||||
import EmptyWebhook from "public/empty-state/web-hook.svg";
|
||||
|
||||
interface IWebHookLists {
|
||||
workspaceSlug: string;
|
||||
}
|
||||
|
||||
export const EmptyWebhooks: FC<IWebHookLists> = (props) => {
|
||||
const { workspaceSlug } = props;
|
||||
export const EmptyWebhooks = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-center">
|
||||
<div className="flex p-10 flex-col items-center justify-center rounded-[4px] border border-custom-border-200 bg-custom-color-background-90">
|
||||
<Image width="178" height="116" src={EmptyWebhookLogo} alt="empty-webhook image" />
|
||||
|
||||
<div className="mt-4 text-base font-semibold">No Webhooks</div>
|
||||
<p className="text-sm text-neutral-600">Create webhooks to receive real-time updates and automate actions</p>
|
||||
<Link href={`/${workspaceSlug}/settings/webhooks/create`}>
|
||||
<Button variant="primary" className="mt-2">
|
||||
Add Webhook
|
||||
</Button>
|
||||
</Link>
|
||||
<div className={`flex items-center justify-center mx-auto rounded-sm border border-custom-border-200 bg-custom-background-90 py-10 px-16 w-full`}>
|
||||
<div className="text-center flex flex-col items-center w-full">
|
||||
<Image src={EmptyWebhook} className="w-52 sm:w-60" alt="empty" />
|
||||
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">No Webhooks</h6>
|
||||
{
|
||||
<p className="text-custom-text-300 mb-7 sm:mb-8">
|
||||
Create webhooks to receive real-time updates and automate actions
|
||||
</p>
|
||||
}
|
||||
<Button
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => {
|
||||
router.push(`${router.asPath}/create/`);
|
||||
}}
|
||||
>
|
||||
Add Webhook
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -303,7 +303,8 @@ const ProfilePage: NextPageWithLayout = () => {
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={value ? value.toString() : "Select your role"}
|
||||
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
|
||||
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : "border-none"}
|
||||
className="rounded-md border !border-custom-border-200"
|
||||
width="w-full"
|
||||
input
|
||||
>
|
||||
@ -369,6 +370,8 @@ const ProfilePage: NextPageWithLayout = () => {
|
||||
options={timeZoneOptions}
|
||||
onChange={onChange}
|
||||
optionsClassName="w-full"
|
||||
buttonClassName={"border-none"}
|
||||
className="rounded-md border !border-custom-border-200"
|
||||
input
|
||||
/>
|
||||
)}
|
||||
|
@ -235,7 +235,7 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
value={!value || value === "" ? "<p></p>" : value}
|
||||
customClassName="tracking-tight self-center w-full max-w-full px-0"
|
||||
customClassName="tracking-tight self-center px-0 h-full w-full"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
setIsSubmitting("submitting");
|
||||
|
@ -90,7 +90,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-5 p-8 h-full overflow-hidden flex flex-col">
|
||||
<div className="space-y-5 p-6 h-full overflow-hidden flex flex-col">
|
||||
<div className="flex gap-4 justify-between">
|
||||
<h3 className="text-2xl font-semibold text-custom-text-100">Pages</h3>
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@ const WebhooksPage: NextPage = observer(() => {
|
||||
return (
|
||||
<AppLayout header={<WorkspaceSettingHeader title="Webhook Settings" />}>
|
||||
<WorkspaceSettingLayout>
|
||||
<div className="w-full overflow-y-auto py-3 pr-4">
|
||||
<div className="w-full overflow-y-auto py-3 pr-9">
|
||||
{loader ? (
|
||||
<div className="flex h-full w-ful items-center justify-center">
|
||||
<Spinner />
|
||||
@ -41,10 +41,8 @@ const WebhooksPage: NextPage = observer(() => {
|
||||
{Object.keys(webhooks).length > 0 ? (
|
||||
<WebhookLists workspaceSlug={workspaceSlug} />
|
||||
) : (
|
||||
<div className="flex justify-center w-full h-full items-center">
|
||||
<div className="w-auto h-fit">
|
||||
<EmptyWebhooks workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
<div className="py-5 mx-auto">
|
||||
<EmptyWebhooks />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
49
web/public/empty-state/web-hook.svg
Normal file
49
web/public/empty-state/web-hook.svg
Normal file
@ -0,0 +1,49 @@
|
||||
<svg width="128" height="130" viewBox="0 0 128 130" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M60.5893 36.7461C84.3294 36.7461 103.575 55.9912 103.575 79.7313C103.575 103.471 84.3294 122.716 60.5893 122.716C36.8493 122.716 3.99396 99.179 17.6042 79.7313C31.2144 60.2836 36.8493 36.7461 60.5893 36.7461Z" fill="#F2F2F2"/>
|
||||
<path d="M118.122 38.7891C117.736 38.7891 117.423 39.1025 117.423 39.488V61.8543C117.423 62.2398 117.736 62.5532 118.122 62.5532C118.507 62.5532 118.821 62.2398 118.821 61.8543V39.488C118.821 39.1025 118.507 38.7891 118.122 38.7891Z" fill="#525252"/>
|
||||
<path d="M0.698946 22.0195C0.313467 22.0195 0 22.333 0 22.7185V28.31C0 28.6955 0.313467 29.009 0.698946 29.009C1.08443 29.009 1.39789 28.6955 1.39789 28.31V22.7185C1.39789 22.333 1.08443 22.0195 0.698946 22.0195Z" fill="#525252"/>
|
||||
<path d="M0.698946 38.7891C0.313467 38.7891 0 39.1025 0 39.488V50.3217C0 50.7072 0.313467 51.0206 0.698946 51.0206C1.08443 51.0206 1.39789 50.7072 1.39789 50.3217V39.488C1.39789 39.1025 1.08443 38.7891 0.698946 38.7891Z" fill="#525252"/>
|
||||
<path d="M0.698946 54.168C0.313467 54.168 0 54.4814 0 54.8669V65.7006C0 66.0861 0.313467 66.3995 0.698946 66.3995C1.08443 66.3995 1.39789 66.0861 1.39789 65.7006V54.8669C1.39789 54.4814 1.08443 54.168 0.698946 54.168Z" fill="#525252"/>
|
||||
<path d="M63.385 0C62.9995 0 62.686 0.313467 62.686 0.698946V11.5326C62.686 11.9181 62.9995 12.2316 63.385 12.2316C63.7705 12.2316 64.0839 11.9181 64.0839 11.5326V0.698946C64.0839 0.313467 63.7705 0 63.385 0Z" fill="#525252"/>
|
||||
<path d="M41.7948 76.8354H17.604V76.1365H41.7948C43.8962 76.1365 45.6059 74.4267 45.6059 72.325V55.9688H46.3048V72.325C46.3048 74.812 44.2817 76.8354 41.7948 76.8354Z" fill="#396AE6"/>
|
||||
<path d="M87.9532 112.289H32.9336V112.988H87.9532V112.289Z" fill="#396AE6"/>
|
||||
<path d="M122.861 72.1605H93.4733C91.8874 72.1605 90.5972 70.8703 90.5972 69.2844V40.2551C90.5972 38.6691 91.8874 37.3789 93.4733 37.3789H122.861C124.447 37.3789 125.737 38.6691 125.737 40.2551V69.2844C125.737 70.8703 124.447 72.1605 122.861 72.1605Z" fill="#E6E6E6"/>
|
||||
<path d="M98.9462 38.2344C94.8084 38.2344 91.4541 41.5887 91.4541 45.7265V68.667C91.4541 70.1223 92.6339 71.3021 94.0892 71.3021H112.059C119.14 71.3021 124.88 65.5618 124.88 58.4809V40.8695C124.88 39.4142 123.701 38.2344 122.245 38.2344L98.9462 38.2344Z" fill="white"/>
|
||||
<path d="M116.038 46.6717H100.235C99.9333 46.6717 99.688 46.4263 99.688 46.1248C99.688 45.8233 99.9333 45.5781 100.235 45.5781H116.038C116.34 45.5781 116.585 45.8233 116.585 46.1248C116.585 46.4263 116.34 46.6717 116.038 46.6717Z" fill="#E6E6E6"/>
|
||||
<path d="M116.038 58.3318H100.235C99.9333 58.3318 99.688 58.0865 99.688 57.785C99.688 57.4835 99.9333 57.2383 100.235 57.2383H116.038C116.34 57.2383 116.585 57.4835 116.585 57.785C116.585 58.0865 116.34 58.3318 116.038 58.3318Z" fill="#E6E6E6"/>
|
||||
<path d="M122.456 52.5076H93.8173C93.5158 52.5076 93.2705 52.2623 93.2705 51.9608C93.2705 51.6593 93.5158 51.4141 93.8173 51.4141H122.456C122.757 51.4141 123.002 51.6593 123.002 51.9608C123.002 52.2623 122.757 52.5076 122.456 52.5076Z" fill="#E6E6E6"/>
|
||||
<path d="M122.748 63.0701H112.402C112.101 63.0701 111.855 62.8248 111.855 62.5233C111.855 62.2218 112.101 61.9766 112.402 61.9766H122.748C123.049 61.9766 123.295 62.2218 123.295 62.5233C123.295 62.8248 123.049 63.0701 122.748 63.0701Z" fill="#E6E6E6"/>
|
||||
<path d="M61.1334 130.002H37.7975C36.5382 130.002 35.5137 128.977 35.5137 127.718V104.667C35.5137 103.407 36.5382 102.383 37.7975 102.383H61.1334C62.3928 102.383 63.4173 103.407 63.4173 104.667V127.718C63.4173 128.977 62.3928 130.002 61.1334 130.002Z" fill="#E6E6E6"/>
|
||||
<path d="M42.1436 103.062C38.8579 103.062 36.1943 105.726 36.1943 109.012V127.228C36.1943 128.384 37.1312 129.321 38.2868 129.321H52.5561C58.1788 129.321 62.737 124.762 62.737 119.14V105.155C62.737 103.999 61.8002 103.063 60.6445 103.063L42.1436 103.062Z" fill="white"/>
|
||||
<path d="M55.7156 109.759H43.1666C42.9272 109.759 42.7324 109.564 42.7324 109.325C42.7324 109.085 42.9272 108.891 43.1666 108.891H55.7156C55.955 108.891 56.1497 109.085 56.1497 109.325C56.1497 109.564 55.955 109.759 55.7156 109.759Z" fill="#E6E6E6"/>
|
||||
<path d="M55.7156 119.017H43.1666C42.9272 119.017 42.7324 118.822 42.7324 118.583C42.7324 118.343 42.9272 118.148 43.1666 118.148H55.7156C55.955 118.148 56.1497 118.343 56.1497 118.583C56.1497 118.822 55.955 119.017 55.7156 119.017Z" fill="#E6E6E6"/>
|
||||
<path d="M60.8116 114.392H38.0709C37.8315 114.392 37.6367 114.197 37.6367 113.958C37.6367 113.718 37.8315 113.523 38.0709 113.523H60.8116C61.051 113.523 61.2457 113.718 61.2457 113.958C61.2457 114.197 61.051 114.392 60.8116 114.392Z" fill="#E6E6E6"/>
|
||||
<path d="M61.0434 122.786H52.8283C52.5889 122.786 52.394 122.592 52.394 122.352C52.394 122.113 52.5888 121.918 52.8283 121.918H61.0434C61.2828 121.918 61.4775 122.113 61.4775 122.352C61.4775 122.592 61.2828 122.786 61.0434 122.786Z" fill="#E6E6E6"/>
|
||||
<path d="M44.8224 55.7926H27.5645C26.6332 55.7926 25.8755 55.0349 25.8755 54.1036V37.0562C25.8755 36.1249 26.6332 35.3672 27.5645 35.3672H44.8224C45.7538 35.3672 46.5114 36.1249 46.5114 37.0562V54.1036C46.5114 55.0349 45.7538 55.7926 44.8224 55.7926Z" fill="#E6E6E6"/>
|
||||
<path d="M30.7787 35.8711C28.3487 35.8711 26.3789 37.8409 26.3789 40.2708V53.7426C26.3789 54.5972 27.0717 55.29 27.9264 55.29H38.4791C42.6374 55.29 46.0084 51.9191 46.0084 47.7608V37.4186C46.0084 36.5639 45.3155 35.8711 44.4609 35.8711L30.7787 35.8711Z" fill="white"/>
|
||||
<path d="M40.816 40.8219H31.5355C31.3584 40.8219 31.2144 40.6778 31.2144 40.5007C31.2144 40.3237 31.3584 40.1797 31.5355 40.1797H40.816C40.993 40.1797 41.137 40.3237 41.137 40.5007C41.137 40.6778 40.993 40.8219 40.816 40.8219Z" fill="#E6E6E6"/>
|
||||
<path d="M40.816 47.6695H31.5355C31.3584 47.6695 31.2144 47.5254 31.2144 47.3484C31.2144 47.1713 31.3584 47.0273 31.5355 47.0273H40.816C40.993 47.0273 41.137 47.1713 41.137 47.3484C41.137 47.5254 40.993 47.6695 40.816 47.6695Z" fill="#E6E6E6"/>
|
||||
<path d="M44.5847 44.2476H27.7669C27.5899 44.2476 27.4458 44.1036 27.4458 43.9265C27.4458 43.7495 27.5899 43.6055 27.7669 43.6055H44.5847C44.7617 43.6055 44.9057 43.7495 44.9057 43.9265C44.9057 44.1036 44.7617 44.2476 44.5847 44.2476Z" fill="#E6E6E6"/>
|
||||
<path d="M44.756 50.4547H38.6805C38.5034 50.4547 38.3594 50.3106 38.3594 50.1336C38.3594 49.9565 38.5034 49.8125 38.6805 49.8125H44.756C44.933 49.8125 45.077 49.9565 45.077 50.1336C45.077 50.3106 44.933 50.4547 44.756 50.4547Z" fill="#E6E6E6"/>
|
||||
<path d="M95.1871 77.2824C97.1172 77.2824 98.6818 75.7178 98.6818 73.7877C98.6818 71.8576 97.1172 70.293 95.1871 70.293C93.257 70.293 91.6924 71.8576 91.6924 73.7877C91.6924 75.7178 93.257 77.2824 95.1871 77.2824Z" fill="#396AE6"/>
|
||||
<path d="M45.9117 59.4621C47.8418 59.4621 49.4065 57.8975 49.4065 55.9674C49.4065 54.0373 47.8418 52.4727 45.9117 52.4727C43.9816 52.4727 42.417 54.0373 42.417 55.9674C42.417 57.8975 43.9816 59.4621 45.9117 59.4621Z" fill="#396AE6"/>
|
||||
<path d="M62.6861 107.341C64.6162 107.341 66.1809 105.776 66.1809 103.846C66.1809 101.916 64.6162 100.352 62.6861 100.352C60.756 100.352 59.1914 101.916 59.1914 103.846C59.1914 105.776 60.756 107.341 62.6861 107.341Z" fill="#396AE6"/>
|
||||
<path d="M70.235 10.4378C72.6405 10.4378 74.5905 8.52881 74.5905 6.17397C74.5905 3.81913 72.6405 1.91016 70.235 1.91016C67.8295 1.91016 65.8794 3.81913 65.8794 6.17397C65.8794 8.52881 67.8295 10.4378 70.235 10.4378Z" fill="#E5E5E5"/>
|
||||
<path d="M105.152 4.71422H81.7077C80.9034 4.71422 80.249 4.05984 80.249 3.25555C80.249 2.45126 80.9034 1.79688 81.7077 1.79688H105.152C105.956 1.79688 106.611 2.45126 106.611 3.25555C106.611 4.05984 105.956 4.71422 105.152 4.71422Z" fill="#E5E5E5"/>
|
||||
<path d="M123.448 9.65172H81.7077C80.9034 9.65172 80.249 8.99733 80.249 8.19305C80.249 7.38876 80.9034 6.73438 81.7077 6.73438H123.448C124.252 6.73438 124.907 7.38876 124.907 8.19305C124.907 8.99733 124.252 9.65172 123.448 9.65172Z" fill="#E5E5E5"/>
|
||||
<path d="M7.19143 28.3588C8.74824 28.3588 10.0103 27.1233 10.0103 25.5993C10.0103 24.0753 8.74824 22.8398 7.19143 22.8398C5.63461 22.8398 4.37256 24.0753 4.37256 25.5993C4.37256 27.1233 5.63461 28.3588 7.19143 28.3588Z" fill="#E5E5E5"/>
|
||||
<path d="M29.7888 24.6537H14.6159C14.0954 24.6537 13.6719 24.2302 13.6719 23.7097C13.6719 23.1891 14.0954 22.7656 14.6159 22.7656H29.7888C30.3093 22.7656 30.7328 23.1891 30.7328 23.7097C30.7328 24.2302 30.3093 24.6537 29.7888 24.6537Z" fill="#E5E5E5"/>
|
||||
<path d="M41.6297 27.849H14.6159C14.0954 27.849 13.6719 27.4255 13.6719 26.905C13.6719 26.3844 14.0954 25.9609 14.6159 25.9609H41.6297C42.1502 25.9609 42.5737 26.3844 42.5737 26.905C42.5737 27.4255 42.1502 27.849 41.6297 27.849Z" fill="#E5E5E5"/>
|
||||
<path d="M91.7632 126.562C93.32 126.562 94.5821 125.326 94.5821 123.802C94.5821 122.278 93.32 121.043 91.7632 121.043C90.2064 121.043 88.9443 122.278 88.9443 123.802C88.9443 125.326 90.2064 126.562 91.7632 126.562Z" fill="#E5E5E5"/>
|
||||
<path d="M114.361 122.857H99.1882C98.6677 122.857 98.2441 122.433 98.2441 121.913C98.2441 121.392 98.6677 120.969 99.1882 120.969H114.361C114.882 120.969 115.305 121.392 115.305 121.913C115.305 122.433 114.882 122.857 114.361 122.857Z" fill="#E5E5E5"/>
|
||||
<path d="M126.202 126.052H99.1882C98.6677 126.052 98.2441 125.629 98.2441 125.108C98.2441 124.588 98.6677 124.164 99.1882 124.164H126.202C126.722 124.164 127.146 124.588 127.146 125.108C127.146 125.629 126.722 126.052 126.202 126.052Z" fill="#E5E5E5"/>
|
||||
<path d="M57.8811 37.3906H57.1821V70.5906H57.8811V37.3906Z" fill="#E6E6E6"/>
|
||||
<path d="M57.5314 72.6859C58.6895 72.6859 59.6283 71.7471 59.6283 70.589C59.6283 69.431 58.6895 68.4922 57.5314 68.4922C56.3734 68.4922 55.4346 69.431 55.4346 70.589C55.4346 71.7471 56.3734 72.6859 57.5314 72.6859Z" fill="#E6E6E6"/>
|
||||
<path d="M89.3337 96.7991C90.4917 96.7991 91.4305 95.8604 91.4305 94.7023C91.4305 93.5443 90.4917 92.6055 89.3337 92.6055C88.1756 92.6055 87.2368 93.5443 87.2368 94.7023C87.2368 95.8604 88.1756 96.7991 89.3337 96.7991Z" fill="#E6E6E6"/>
|
||||
<path d="M32.7189 68.1429C33.877 68.1429 34.8157 67.2041 34.8157 66.0461C34.8157 64.888 33.877 63.9492 32.7189 63.9492C31.5609 63.9492 30.6221 64.888 30.6221 66.0461C30.6221 67.2041 31.5609 68.1429 32.7189 68.1429Z" fill="#E6E6E6"/>
|
||||
<path d="M31.9438 66.0513L20.4766 66.3984L20.4977 67.0974L31.9649 66.7502L31.9438 66.0513Z" fill="#E6E6E6"/>
|
||||
<path d="M106.392 102.73H89.0034V94.7031H89.7024V102.031H106.392V102.73Z" fill="#E6E6E6"/>
|
||||
<path d="M31.8581 91.0413C33.0161 91.0413 33.9549 90.1025 33.9549 88.9445C33.9549 87.7864 33.0161 86.8477 31.8581 86.8477C30.7 86.8477 29.7612 87.7864 29.7612 88.9445C29.7612 90.1025 30.7 91.0413 31.8581 91.0413Z" fill="#E6E6E6"/>
|
||||
<path d="M48.7091 96.9687H31.3208V88.9414H32.0197V96.2698H48.7091V96.9687Z" fill="#E6E6E6"/>
|
||||
<path d="M101.914 80.9648H18.3965V81.6638H101.914V80.9648Z" fill="#E6E6E6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
@ -69,7 +69,7 @@ export class ProjectService extends APIService {
|
||||
}
|
||||
|
||||
async joinProject(workspaceSlug: string, project_ids: string[]): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/join/`, { project_ids })
|
||||
return this.post(`/api/users/me/workspaces/${workspaceSlug}/projects/invitations/`, { project_ids })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
@ -86,39 +86,67 @@ export class PageStore implements IPageStore {
|
||||
}
|
||||
|
||||
get projectPages() {
|
||||
if (!this.rootStore.project.projectId) return;
|
||||
return this.pages?.[this.rootStore.project.projectId] || [];
|
||||
const projectId = this.rootStore.project.projectId;
|
||||
|
||||
if (!projectId || !this.pages[projectId]) return undefined;
|
||||
|
||||
return this.pages?.[projectId] || [];
|
||||
}
|
||||
|
||||
get recentProjectPages() {
|
||||
if (!this.rootStore.project.projectId) return;
|
||||
const data: IRecentPages = { today: [], yesterday: [], this_week: [] };
|
||||
data["today"] = this.pages[this.rootStore.project.projectId]?.filter((p) => isToday(new Date(p.created_at))) || [];
|
||||
data["yesterday"] =
|
||||
this.pages[this.rootStore.project.projectId]?.filter((p) => isYesterday(new Date(p.created_at))) || [];
|
||||
const projectId = this.rootStore.project.projectId;
|
||||
|
||||
if (!projectId) return undefined;
|
||||
|
||||
if (!this.pages[projectId]) return undefined;
|
||||
|
||||
const data: IRecentPages = { today: [], yesterday: [], this_week: [], older: [] };
|
||||
|
||||
data["today"] = this.pages[projectId]?.filter((p) => isToday(new Date(p.created_at))) || [];
|
||||
data["yesterday"] = this.pages[projectId]?.filter((p) => isYesterday(new Date(p.created_at))) || [];
|
||||
data["this_week"] =
|
||||
this.pages[this.rootStore.project.projectId]?.filter((p) => isThisWeek(new Date(p.created_at))) || [];
|
||||
this.pages[projectId]?.filter(
|
||||
(p) =>
|
||||
isThisWeek(new Date(p.created_at)) && !isToday(new Date(p.created_at)) && !isYesterday(new Date(p.created_at))
|
||||
) || [];
|
||||
data["older"] =
|
||||
this.pages[projectId]?.filter(
|
||||
(p) => !isThisWeek(new Date(p.created_at)) && !isYesterday(new Date(p.created_at))
|
||||
) || [];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
get favoriteProjectPages() {
|
||||
if (!this.rootStore.project.projectId) return;
|
||||
return this.pages[this.rootStore.project.projectId]?.filter((p) => p.is_favorite);
|
||||
const projectId = this.rootStore.project.projectId;
|
||||
|
||||
if (!projectId || !this.pages[projectId]) return undefined;
|
||||
|
||||
return this.pages[projectId]?.filter((p) => p.is_favorite);
|
||||
}
|
||||
|
||||
get privateProjectPages() {
|
||||
if (!this.rootStore.project.projectId) return;
|
||||
return this.pages[this.rootStore.project.projectId]?.filter((p) => p.access === 1);
|
||||
const projectId = this.rootStore.project.projectId;
|
||||
|
||||
if (!projectId || !this.pages[projectId]) return undefined;
|
||||
|
||||
return this.pages[projectId]?.filter((p) => p.access === 1);
|
||||
}
|
||||
|
||||
get sharedProjectPages() {
|
||||
if (!this.rootStore.project.projectId) return;
|
||||
return this.pages[this.rootStore.project.projectId]?.filter((p) => p.access === 0);
|
||||
const projectId = this.rootStore.project.projectId;
|
||||
|
||||
if (!projectId || !this.pages[projectId]) return undefined;
|
||||
|
||||
return this.pages[projectId]?.filter((p) => p.access === 0);
|
||||
}
|
||||
|
||||
get archivedProjectPages() {
|
||||
if (!this.rootStore.project.projectId) return;
|
||||
return this.archivedPages[this.rootStore.project.projectId];
|
||||
const projectId = this.rootStore.project.projectId;
|
||||
|
||||
if (!projectId || !this.archivedPages[projectId]) return undefined;
|
||||
|
||||
return this.archivedPages[projectId];
|
||||
}
|
||||
|
||||
addToFavorites = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
|
1
web/types/pages.d.ts
vendored
1
web/types/pages.d.ts
vendored
@ -30,6 +30,7 @@ export interface IRecentPages {
|
||||
today: IPage[];
|
||||
yesterday: IPage[];
|
||||
this_week: IPage[];
|
||||
older: IPage[];
|
||||
[key: string]: IPage[];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user