Posthog initial integration

This commit is contained in:
srinivaspendem 2023-09-19 13:12:21 +05:30
commit a798707112
86 changed files with 3568 additions and 1275 deletions

View File

@ -293,12 +293,12 @@ class IssueLabelSerializer(BaseSerializer):
class IssueRelationSerializer(BaseSerializer): class IssueRelationSerializer(BaseSerializer):
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
class Meta: class Meta:
model = IssueRelation model = IssueRelation
fields = [ fields = [
"related_issue_detail", "issue_detail",
"relation_type", "relation_type",
"related_issue", "related_issue",
"issue", "issue",

View File

@ -53,6 +53,7 @@ from plane.api.serializers import (
CommentReactionSerializer, CommentReactionSerializer,
IssueVoteSerializer, IssueVoteSerializer,
IssueRelationSerializer, IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer, IssuePublicSerializer,
) )
from plane.api.permissions import ( from plane.api.permissions import (
@ -2085,9 +2086,10 @@ class IssueRelationViewSet(BaseViewSet):
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
try: try:
related_list = request.data.get("related_list", []) related_list = request.data.get("related_list", [])
relation = request.data.get("relation", None)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
issueRelation = IssueRelation.objects.bulk_create( issue_relation = IssueRelation.objects.bulk_create(
[ [
IssueRelation( IssueRelation(
issue_id=related_issue["issue"], issue_id=related_issue["issue"],
@ -2113,8 +2115,14 @@ class IssueRelationViewSet(BaseViewSet):
current_instance=None, current_instance=None,
) )
if relation == "blocking":
return Response( return Response(
IssueRelationSerializer(issueRelation, many=True).data, RelatedIssueSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED,
)
else:
return Response(
IssueRelationSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
except IntegrityError as e: except IntegrityError as e:

View File

@ -1048,6 +1048,25 @@ def create_issue_relation_activity(
) )
if current_instance is None and requested_data.get("related_list") is not None: if current_instance is None and requested_data.get("related_list") is not None:
for issue_relation in requested_data.get("related_list"): for issue_relation in requested_data.get("related_list"):
if issue_relation.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = issue_relation.get("relation_type")
issue = Issue.objects.get(pk=issue_relation.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("related_issue"),
actor=actor,
verb="created",
old_value="",
new_value=f"{project.identifier}-{issue.sequence_id}",
field=relation_type,
project=project,
workspace=project.workspace,
comment=f'added {relation_type} relation',
old_identifier=issue_relation.get("issue"),
)
)
issue = Issue.objects.get(pk=issue_relation.get("related_issue")) issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
@ -1060,7 +1079,7 @@ def create_issue_relation_activity(
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f'added {issue_relation.get("relation_type")} relation', comment=f'added {issue_relation.get("relation_type")} relation',
old_identifier=issue_relation.get("issue"), old_identifier=issue_relation.get("related_issue"),
) )
) )
@ -1073,7 +1092,26 @@ def delete_issue_relation_activity(
json.loads(current_instance) if current_instance is not None else None json.loads(current_instance) if current_instance is not None else None
) )
if current_instance is not None and requested_data.get("related_list") is None: if current_instance is not None and requested_data.get("related_list") is None:
if current_instance.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = current_instance.get("relation_type")
issue = Issue.objects.get(pk=current_instance.get("issue")) issue = Issue.objects.get(pk=current_instance.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("related_issue"),
actor=actor,
verb="deleted",
old_value=f"{project.identifier}-{issue.sequence_id}",
new_value="",
field=relation_type,
project=project,
workspace=project.workspace,
comment=f'deleted {relation_type} relation',
old_identifier=current_instance.get("issue"),
)
)
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
issue_id=current_instance.get("issue"), issue_id=current_instance.get("issue"),
@ -1084,8 +1122,8 @@ def delete_issue_relation_activity(
field=f'{current_instance.get("relation_type")}', field=f'{current_instance.get("relation_type")}',
project=project, project=project,
workspace=project.workspace, workspace=project.workspace,
comment=f'deleted the {current_instance.get("relation_type")} relation', comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("issue"), old_identifier=current_instance.get("related_issue"),
) )
) )

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.3 on 2023-09-15 06:55
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivityModel.objects.all():
if obj.field == "blocks":
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0044_auto_20230913_0709'),
]
operations = [
migrations.RunPython(update_issue_activity),
]

View File

@ -28,7 +28,8 @@
"SLACK_CLIENT_SECRET", "SLACK_CLIENT_SECRET",
"JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST", "JITSU_TRACKER_HOST",
"UNSPLASH_ACCESS_KEY" "UNSPLASH_ACCESS_KEY",
"NODE_ENV"
], ],
"pipeline": { "pipeline": {
"build": { "build": {

View File

@ -103,8 +103,8 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
{projectDetails?.close_in !== 0 && ( {projectDetails?.close_in !== 0 && (
<div className="ml-12"> <div className="ml-12">
<div className="flex flex-col gap-4"> <div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full"> <div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm font-medium"> <div className="w-1/2 text-sm font-medium">
Auto-close issues that are inactive for Auto-close issues that are inactive for
</div> </div>
@ -138,7 +138,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
</div> </div>
</div> </div>
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full"> <div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
<div className="w-1/2 text-sm font-medium">Auto-close Status</div> <div className="w-1/2 text-sm font-medium">Auto-close Status</div>
<div className="w-1/2 "> <div className="w-1/2 ">
<CustomSearchSelect <CustomSearchSelect

View File

@ -90,14 +90,14 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
}, },
archived_at: { archived_at: {
message: (activity) => { message: (activity) => {
if (activity.new_value === "restore") return "restored the issue."; if (activity.new_value === "restore") return "restored the issue.";
else return "archived the issue."; else return "archived the issue.";
}, },
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
}, },
attachment: { attachment: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -136,7 +136,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
}, },
blocking: { blocking: {
message: (activity) => { message: (activity) => {
@ -224,7 +224,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
}, },
description: { description: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -239,7 +239,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
estimate_point: { estimate_point: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -271,14 +271,14 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
}, },
issue: { issue: {
message: (activity) => { message: (activity) => {
if (activity.verb === "created") return "created the issue."; if (activity.verb === "created") return "created the issue.";
else return "deleted an issue."; else return "deleted an issue.";
}, },
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
}, },
labels: { labels: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -327,7 +327,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
}, },
link: { link: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -398,7 +398,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
}, },
modules: { modules: {
message: (activity, showIssue, workspaceSlug) => { message: (activity, showIssue, workspaceSlug) => {
@ -448,7 +448,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
}, },
name: { name: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -463,7 +463,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
parent: { parent: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -496,7 +496,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
}, },
priority: { priority: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -514,7 +514,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
}, },
start_date: { start_date: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -548,7 +548,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
state: { state: {
message: (activity, showIssue) => ( message: (activity, showIssue) => (
@ -564,7 +564,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />, icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
}, },
target_date: { target_date: {
message: (activity, showIssue) => { message: (activity, showIssue) => {
@ -598,7 +598,7 @@ const activityDetails: {
</> </>
); );
}, },
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
}; };

View File

@ -52,10 +52,22 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
}, },
]; ];
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
},
{
type: "kanban",
Icon: GridViewOutlined,
},
];
export const IssuesFilterView: React.FC = () => { export const IssuesFilterView: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname.includes("draft-issues");
const { const {
displayFilters, displayFilters,
@ -75,7 +87,7 @@ export const IssuesFilterView: React.FC = () => {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!isArchivedIssues && ( {!isArchivedIssues && !isDraftIssues && (
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
{issueViewOptions.map((option) => ( {issueViewOptions.map((option) => (
<Tooltip <Tooltip
@ -105,6 +117,36 @@ export const IssuesFilterView: React.FC = () => {
))} ))}
</div> </div>
)} )}
{isDraftIssues && (
<div className="flex items-center gap-x-1">
{issueViewForDraftIssues.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
displayFilters.layout === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setDisplayFilters({ layout: option.type })}
>
<option.Icon
sx={{
fontSize: 16,
}}
className={option.type === "gantt_chart" ? "rotate-90" : ""}
/>
</button>
</Tooltip>
))}
</div>
)}
<SelectFilters <SelectFilters
filters={filters} filters={filters}
onSelect={(option) => { onSelect={(option) => {

View File

@ -1,6 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import NextImage from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// react-dropzone // react-dropzone
@ -12,7 +11,7 @@ import fileServices from "services/file.service";
// hooks // hooks
import useWorkspaceDetails from "hooks/use-workspace-details"; import useWorkspaceDetails from "hooks/use-workspace-details";
// ui // ui
import { PrimaryButton, SecondaryButton } from "components/ui"; import { DangerButton, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { UserCircleIcon } from "components/icons"; import { UserCircleIcon } from "components/icons";
@ -21,6 +20,8 @@ type Props = {
onClose: () => void; onClose: () => void;
isOpen: boolean; isOpen: boolean;
onSuccess: (url: string) => void; onSuccess: (url: string) => void;
isRemoving: boolean;
handleDelete: () => void;
userImage?: boolean; userImage?: boolean;
}; };
@ -29,6 +30,8 @@ export const ImageUploadModal: React.FC<Props> = ({
onSuccess, onSuccess,
isOpen, isOpen,
onClose, onClose,
isRemoving,
handleDelete,
userImage, userImage,
}) => { }) => {
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
@ -148,12 +151,10 @@ export const ImageUploadModal: React.FC<Props> = ({
> >
Edit Edit
</button> </button>
<NextImage <img
layout="fill"
objectFit="cover"
src={image ? URL.createObjectURL(image) : value ? value : ""} src={image ? URL.createObjectURL(image) : value ? value : ""}
alt="image" alt="image"
className="rounded-lg" className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
/> />
</> </>
) : ( ) : (
@ -182,7 +183,13 @@ export const ImageUploadModal: React.FC<Props> = ({
<p className="my-4 text-custom-text-200 text-sm"> <p className="my-4 text-custom-text-200 text-sm">
File formats supported- .jpeg, .jpg, .png, .webp, .svg File formats supported- .jpeg, .jpg, .png, .webp, .svg
</p> </p>
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-between">
<div className="flex items-center">
<DangerButton onClick={handleDelete} outline disabled={!value}>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton <PrimaryButton
onClick={handleSubmit} onClick={handleSubmit}
@ -192,6 +199,7 @@ export const ImageUploadModal: React.FC<Props> = ({
{isImageUploading ? "Uploading..." : "Upload & Save"} {isImageUploading ? "Uploading..." : "Upload & Save"}
</PrimaryButton> </PrimaryButton>
</div> </div>
</div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
</div> </div>

View File

@ -50,6 +50,7 @@ type Props = {
secondaryButton?: React.ReactNode; secondaryButton?: React.ReactNode;
}; };
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleOnDragEnd: (result: DropResult) => Promise<void>; handleOnDragEnd: (result: DropResult) => Promise<void>;
openIssuesListModal: (() => void) | null; openIssuesListModal: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
@ -66,6 +67,7 @@ export const AllViews: React.FC<Props> = ({
dragDisabled = false, dragDisabled = false,
emptyState, emptyState,
handleIssueAction, handleIssueAction,
handleDraftIssueAction,
handleOnDragEnd, handleOnDragEnd,
openIssuesListModal, openIssuesListModal,
removeIssue, removeIssue,
@ -132,6 +134,7 @@ export const AllViews: React.FC<Props> = ({
states={states} states={states}
addIssueToGroup={addIssueToGroup} addIssueToGroup={addIssueToGroup}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null} openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
removeIssue={removeIssue} removeIssue={removeIssue}
myIssueProjectId={myIssueProjectId} myIssueProjectId={myIssueProjectId}
@ -149,6 +152,7 @@ export const AllViews: React.FC<Props> = ({
disableAddIssueOption={disableAddIssueOption} disableAddIssueOption={disableAddIssueOption}
dragDisabled={dragDisabled} dragDisabled={dragDisabled}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null} openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
myIssueProjectId={myIssueProjectId} myIssueProjectId={myIssueProjectId}

View File

@ -20,6 +20,7 @@ type Props = {
disableAddIssueOption?: boolean; disableAddIssueOption?: boolean;
dragDisabled: boolean; dragDisabled: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
@ -37,6 +38,7 @@ export const AllBoards: React.FC<Props> = ({
disableAddIssueOption = false, disableAddIssueOption = false,
dragDisabled, dragDisabled,
handleIssueAction, handleIssueAction,
handleDraftIssueAction,
handleTrashBox, handleTrashBox,
openIssuesListModal, openIssuesListModal,
myIssueProjectId, myIssueProjectId,
@ -94,6 +96,7 @@ export const AllBoards: React.FC<Props> = ({
dragDisabled={dragDisabled} dragDisabled={dragDisabled}
groupTitle={singleGroup} groupTitle={singleGroup}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
openIssuesListModal={openIssuesListModal ?? null} openIssuesListModal={openIssuesListModal ?? null}
handleMyIssueOpen={handleMyIssueOpen} handleMyIssueOpen={handleMyIssueOpen}

View File

@ -24,6 +24,7 @@ type Props = {
dragDisabled: boolean; dragDisabled: boolean;
groupTitle: string; groupTitle: string;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void; handleMyIssueOpen?: (issue: IIssue) => void;
@ -41,6 +42,7 @@ export const SingleBoard: React.FC<Props> = ({
disableAddIssueOption = false, disableAddIssueOption = false,
dragDisabled, dragDisabled,
handleIssueAction, handleIssueAction,
handleDraftIssueAction,
handleTrashBox, handleTrashBox,
openIssuesListModal, openIssuesListModal,
handleMyIssueOpen, handleMyIssueOpen,
@ -136,6 +138,16 @@ export const SingleBoard: React.FC<Props> = ({
editIssue={() => handleIssueAction(issue, "edit")} editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")} makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueEdit={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "edit")
: undefined
}
handleDraftIssueDelete={() =>
handleDraftIssueAction
? handleDraftIssueAction(issue, "delete")
: undefined
}
handleTrashBox={handleTrashBox} handleTrashBox={handleTrashBox}
handleMyIssueOpen={handleMyIssueOpen} handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => { removeIssue={() => {
@ -155,7 +167,7 @@ export const SingleBoard: React.FC<Props> = ({
display: displayFilters?.order_by === "sort_order" ? "inline" : "none", display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
}} }}
> >
{provided.placeholder} <>{provided.placeholder}</>
</span> </span>
</div> </div>
{displayFilters?.group_by !== "created_by" && ( {displayFilters?.group_by !== "created_by" && (

View File

@ -60,6 +60,8 @@ type Props = {
handleMyIssueOpen?: (issue: IIssue) => void; handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue?: (() => void) | null; removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueEdit?: () => void;
handleDraftIssueDelete?: () => void;
handleTrashBox: (isDragging: boolean) => void; handleTrashBox: (isDragging: boolean) => void;
disableUserActions: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
@ -79,6 +81,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
removeIssue, removeIssue,
groupTitle, groupTitle,
handleDeleteIssue, handleDeleteIssue,
handleDraftIssueEdit,
handleDraftIssueDelete,
handleTrashBox, handleTrashBox,
disableUserActions, disableUserActions,
user, user,
@ -99,6 +103,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const isDraftIssue = router.pathname.includes("draft-issues");
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
@ -211,20 +217,37 @@ export const SingleBoardIssue: React.FC<Props> = ({
> >
{!isNotAllowed && ( {!isNotAllowed && (
<> <>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}> <ContextMenu.Item
Icon={PencilIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else editIssue();
}}
>
Edit issue Edit issue
</ContextMenu.Item> </ContextMenu.Item>
{!isDraftIssue && (
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}> <ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy... Make a copy...
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}> )}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
Delete issue Delete issue
</ContextMenu.Item> </ContextMenu.Item>
</> </>
)} )}
{!isDraftIssue && (
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}> <ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link Copy issue link
</ContextMenu.Item> </ContextMenu.Item>
)}
{!isDraftIssue && (
<a <a
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`} href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank" target="_blank"
@ -234,6 +257,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
Open issue in new tab Open issue in new tab
</ContextMenu.Item> </ContextMenu.Item>
</a> </a>
)}
</ContextMenu> </ContextMenu>
<div <div
className={`mb-3 rounded bg-custom-background-100 shadow ${ className={`mb-3 rounded bg-custom-background-100 shadow ${
@ -268,13 +292,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
</button> </button>
} }
> >
<CustomMenu.MenuItem onClick={editIssue}> <CustomMenu.MenuItem
onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else editIssue();
}}
>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" /> <PencilIcon className="h-4 w-4" />
<span>Edit issue</span> <span>Edit issue</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
{type !== "issue" && removeIssue && ( {type !== "issue" && removeIssue && !isDraftIssue && (
<CustomMenu.MenuItem onClick={removeIssue}> <CustomMenu.MenuItem onClick={removeIssue}>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-4 w-4" />
@ -282,18 +311,25 @@ export const SingleBoardIssue: React.FC<Props> = ({
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}> <CustomMenu.MenuItem
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
<span>Delete issue</span> <span>Delete issue</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
{!isDraftIssue && (
<CustomMenu.MenuItem onClick={handleCopyText}> <CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" /> <LinkIcon className="h-4 w-4" />
<span>Copy issue Link</span> <span>Copy issue Link</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
)} )}
</div> </div>
@ -308,7 +344,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
<button <button
type="button" type="button"
className="text-sm text-left break-words line-clamp-2" className="text-sm text-left break-words line-clamp-2"
onClick={openPeekOverview} onClick={() => {
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
else openPeekOverview();
}}
> >
{issue.name} {issue.name}
</button> </button>

View File

@ -19,7 +19,13 @@ import useIssuesProperties from "hooks/use-issue-properties";
import useProjectMembers from "hooks/use-project-members"; import useProjectMembers from "hooks/use-project-members";
// components // components
import { FiltersList, AllViews } from "components/core"; import { FiltersList, AllViews } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues"; import {
CreateUpdateIssueModal,
DeleteIssueModal,
DeleteDraftIssueModal,
IssuePeekOverview,
CreateUpdateDraftIssueModal,
} from "components/issues";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
// ui // ui
import { PrimaryButton, SecondaryButton } from "components/ui"; import { PrimaryButton, SecondaryButton } from "components/ui";
@ -70,8 +76,13 @@ export const IssuesView: React.FC<Props> = ({
// trash box // trash box
const [trashBox, setTrashBox] = useState(false); const [trashBox, setTrashBox] = useState(false);
// selected draft issue
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null);
const [selectedDraftForDelete, setSelectDraftForDelete] = useState<IIssue | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const isDraftIssues = router.asPath.includes("draft-issues");
const { user } = useUserAuth(); const { user } = useUserAuth();
@ -106,6 +117,9 @@ export const IssuesView: React.FC<Props> = ({
[setDeleteIssueModal, setIssueToDelete] [setDeleteIssueModal, setIssueToDelete]
); );
const handleDraftIssueClick = useCallback((issue: any) => setSelectedDraftIssue(issue), []);
const handleDraftIssueDelete = useCallback((issue: any) => setSelectDraftForDelete(issue), []);
const handleOnDragEnd = useCallback( const handleOnDragEnd = useCallback(
async (result: DropResult) => { async (result: DropResult) => {
setTrashBox(false); setTrashBox(false);
@ -343,6 +357,14 @@ export const IssuesView: React.FC<Props> = ({
[makeIssueCopy, handleEditIssue, handleDeleteIssue] [makeIssueCopy, handleEditIssue, handleDeleteIssue]
); );
const handleDraftIssueAction = useCallback(
(issue: IIssue, action: "edit" | "delete") => {
if (action === "edit") handleDraftIssueClick(issue);
else if (action === "delete") handleDraftIssueDelete(issue);
},
[handleDraftIssueClick, handleDraftIssueDelete]
);
const removeIssueFromCycle = useCallback( const removeIssueFromCycle = useCallback(
(bridgeId: string, issueId: string) => { (bridgeId: string, issueId: string) => {
if (!workspaceSlug || !projectId || !cycleId) return; if (!workspaceSlug || !projectId || !cycleId) return;
@ -451,6 +473,27 @@ export const IssuesView: React.FC<Props> = ({
...preloadedData, ...preloadedData,
}} }}
/> />
<CreateUpdateDraftIssueModal
isOpen={selectedDraftIssue !== null}
handleClose={() => setSelectedDraftIssue(null)}
data={
selectedDraftIssue
? {
...selectedDraftIssue,
is_draft: true,
}
: null
}
fieldsToShow={[
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
]}
/>
<CreateUpdateIssueModal <CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"} isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)} handleClose={() => setEditIssueModal(false)}
@ -462,6 +505,11 @@ export const IssuesView: React.FC<Props> = ({
data={issueToDelete} data={issueToDelete}
user={user} user={user}
/> />
<DeleteDraftIssueModal
data={selectedDraftForDelete}
isOpen={selectedDraftForDelete !== null}
handleClose={() => setSelectDraftForDelete(null)}
/>
{areFiltersApplied && ( {areFiltersApplied && (
<> <>
@ -518,14 +566,18 @@ export const IssuesView: React.FC<Props> = ({
displayFilters.group_by === "assignees" displayFilters.group_by === "assignees"
} }
emptyState={{ emptyState={{
title: cycleId title: isDraftIssues
? "Draft issues will appear here"
: cycleId
? "Cycle issues will appear here" ? "Cycle issues will appear here"
: moduleId : moduleId
? "Module issues will appear here" ? "Module issues will appear here"
: "Project issues will appear here", : "Project issues will appear here",
description: description: isDraftIssues
"Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.", ? "Draft issues are issues that are not yet created."
primaryButton: { : "Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.",
primaryButton: !isDraftIssues
? {
icon: <PlusIcon className="h-4 w-4" />, icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue", text: "New Issue",
onClick: () => { onClick: () => {
@ -534,7 +586,8 @@ export const IssuesView: React.FC<Props> = ({
}); });
document.dispatchEvent(e); document.dispatchEvent(e);
}, },
}, }
: undefined,
secondaryButton: secondaryButton:
cycleId || moduleId ? ( cycleId || moduleId ? (
<SecondaryButton <SecondaryButton
@ -548,6 +601,7 @@ export const IssuesView: React.FC<Props> = ({
}} }}
handleOnDragEnd={handleOnDragEnd} handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}
handleDraftIssueAction={handleDraftIssueAction}
openIssuesListModal={openIssuesListModal ?? null} openIssuesListModal={openIssuesListModal ?? null}
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null} removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
trashBox={trashBox} trashBox={trashBox}

View File

@ -15,6 +15,7 @@ type Props = {
states: IState[] | undefined; states: IState[] | undefined;
addIssueToGroup: (groupTitle: string) => void; addIssueToGroup: (groupTitle: string) => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
myIssueProjectId?: string | null; myIssueProjectId?: string | null;
handleMyIssueOpen?: (issue: IIssue) => void; handleMyIssueOpen?: (issue: IIssue) => void;
@ -36,6 +37,7 @@ export const AllLists: React.FC<Props> = ({
myIssueProjectId, myIssueProjectId,
removeIssue, removeIssue,
states, states,
handleDraftIssueAction,
user, user,
userAuth, userAuth,
viewProps, viewProps,
@ -82,6 +84,7 @@ export const AllLists: React.FC<Props> = ({
groupTitle={singleGroup} groupTitle={singleGroup}
currentState={currentState} currentState={currentState}
addIssueToGroup={() => addIssueToGroup(singleGroup)} addIssueToGroup={() => addIssueToGroup(singleGroup)}
handleDraftIssueAction={handleDraftIssueAction}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}
handleMyIssueOpen={handleMyIssueOpen} handleMyIssueOpen={handleMyIssueOpen}
openIssuesListModal={openIssuesListModal} openIssuesListModal={openIssuesListModal}

View File

@ -1,6 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
@ -18,6 +17,7 @@ import {
ViewPrioritySelect, ViewPrioritySelect,
ViewStartDateSelect, ViewStartDateSelect,
ViewStateSelect, ViewStateSelect,
CreateUpdateDraftIssueModal,
} from "components/issues"; } from "components/issues";
// ui // ui
import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
@ -61,6 +61,8 @@ type Props = {
makeIssueCopy: () => void; makeIssueCopy: () => void;
removeIssue?: (() => void) | null; removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueSelect?: (issue: IIssue) => void;
handleDraftIssueDelete?: (issue: IIssue) => void;
handleMyIssueOpen?: (issue: IIssue) => void; handleMyIssueOpen?: (issue: IIssue) => void;
disableUserActions: boolean; disableUserActions: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
@ -76,12 +78,14 @@ export const SingleListIssue: React.FC<Props> = ({
makeIssueCopy, makeIssueCopy,
removeIssue, removeIssue,
groupTitle, groupTitle,
handleDraftIssueDelete,
handleDeleteIssue, handleDeleteIssue,
handleMyIssueOpen, handleMyIssueOpen,
disableUserActions, disableUserActions,
user, user,
userAuth, userAuth,
viewProps, viewProps,
handleDraftIssueSelect,
}) => { }) => {
// context menu // context menu
const [contextMenu, setContextMenu] = useState(false); const [contextMenu, setContextMenu] = useState(false);
@ -90,6 +94,7 @@ export const SingleListIssue: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -178,6 +183,8 @@ export const SingleListIssue: React.FC<Props> = ({
const issuePath = isArchivedIssues const issuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}` ? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: isDraftIssues
? `#`
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`; : `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
const openPeekOverview = (issue: IIssue) => { const openPeekOverview = (issue: IIssue) => {
@ -203,17 +210,33 @@ export const SingleListIssue: React.FC<Props> = ({
> >
{!isNotAllowed && ( {!isNotAllowed && (
<> <>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}> <ContextMenu.Item
Icon={PencilIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
else editIssue();
}}
>
Edit issue Edit issue
</ContextMenu.Item> </ContextMenu.Item>
{!isDraftIssues && (
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}> <ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy... Make a copy...
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}> )}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
Delete issue Delete issue
</ContextMenu.Item> </ContextMenu.Item>
</> </>
)} )}
{!isDraftIssues && (
<>
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}> <ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link Copy issue link
</ContextMenu.Item> </ContextMenu.Item>
@ -222,7 +245,10 @@ export const SingleListIssue: React.FC<Props> = ({
Open issue in new tab Open issue in new tab
</ContextMenu.Item> </ContextMenu.Item>
</a> </a>
</>
)}
</ContextMenu> </ContextMenu>
<div <div
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0" className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
onContextMenu={(e) => { onContextMenu={(e) => {
@ -247,7 +273,10 @@ export const SingleListIssue: React.FC<Props> = ({
<button <button
type="button" type="button"
className="truncate text-[0.825rem] text-custom-text-100" className="truncate text-[0.825rem] text-custom-text-100"
onClick={() => openPeekOverview(issue)} onClick={() => {
if (!isDraftIssues) openPeekOverview(issue);
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
}}
> >
{issue.name} {issue.name}
</button> </button>
@ -345,7 +374,12 @@ export const SingleListIssue: React.FC<Props> = ({
)} )}
{type && !isNotAllowed && ( {type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis> <CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}> <CustomMenu.MenuItem
onClick={() => {
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
else editIssue();
}}
>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" /> <PencilIcon className="h-4 w-4" />
<span>Edit issue</span> <span>Edit issue</span>
@ -359,18 +393,25 @@ export const SingleListIssue: React.FC<Props> = ({
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}> <CustomMenu.MenuItem
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
<span>Delete issue</span> <span>Delete issue</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
{!isDraftIssues && (
<CustomMenu.MenuItem onClick={handleCopyText}> <CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" /> <LinkIcon className="h-4 w-4" />
<span>Copy issue link</span> <span>Copy issue link</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
)} )}
</div> </div>

View File

@ -40,6 +40,7 @@ type Props = {
groupTitle: string; groupTitle: string;
addIssueToGroup: () => void; addIssueToGroup: () => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
openIssuesListModal?: (() => void) | null; openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void; handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null; removeIssue: ((bridgeId: string, issueId: string) => void) | null;
@ -56,6 +57,7 @@ export const SingleList: React.FC<Props> = ({
addIssueToGroup, addIssueToGroup,
handleIssueAction, handleIssueAction,
openIssuesListModal, openIssuesListModal,
handleDraftIssueAction,
handleMyIssueOpen, handleMyIssueOpen,
removeIssue, removeIssue,
disableUserActions, disableUserActions,
@ -253,6 +255,16 @@ export const SingleList: React.FC<Props> = ({
editIssue={() => handleIssueAction(issue, "edit")} editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")} makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueSelect={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "edit")
: undefined
}
handleDraftIssueDelete={
handleDraftIssueAction
? () => handleDraftIssueAction(issue, "delete")
: undefined
}
handleMyIssueOpen={handleMyIssueOpen} handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => { removeIssue={() => {
if (removeIssue !== null && issue.bridge_id) if (removeIssue !== null && issue.bridge_id)

View File

@ -46,15 +46,16 @@ const IntegrationGuide = () => {
return ( return (
<> <>
<div className="h-full space-y-2"> <div className="h-full w-full">
<> <>
<div className="space-y-2"> <div>
{EXPORTERS_LIST.map((service) => ( {EXPORTERS_LIST.map((service) => (
<div <div
key={service.provider} key={service.provider}
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4" className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
> >
<div className="flex items-center gap-4 whitespace-nowrap"> <div className="flex items-start justify-between gap-4 w-full">
<div className="flex item-center gap-2.5">
<div className="relative h-10 w-10 flex-shrink-0"> <div className="relative h-10 w-10 flex-shrink-0">
<Image <Image
src={service.logo} src={service.logo}
@ -63,15 +64,20 @@ const IntegrationGuide = () => {
alt={`${service.title} Logo`} alt={`${service.title} Logo`}
/> />
</div> </div>
<div className="w-full"> <div>
<h3>{service.title}</h3> <h3 className="flex items-center gap-4 text-sm font-medium">
<p className="text-sm text-custom-text-200">{service.description}</p> {service.title}
</h3>
<p className="text-sm text-custom-text-200 tracking-tight">
{service.description}
</p>
</div>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}> <Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
<a> <a>
<PrimaryButton> <PrimaryButton>
<span className="capitalize">{service.type}</span> now <span className="capitalize">{service.type}</span>
</PrimaryButton> </PrimaryButton>
</a> </a>
</Link> </Link>
@ -80,10 +86,11 @@ const IntegrationGuide = () => {
</div> </div>
))} ))}
</div> </div>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <div>
<h3 className="mb-2 flex gap-2 text-lg font-medium justify-between"> <div className="flex items-center justify-between pt-7 pb-3.5 border-b border-custom-border-200">
<div className="flex gap-2"> <div className="flex gap-2 items-center">
<div className="">Previous Exports</div> <h3 className="flex gap-2 text-xl font-medium">Previous Exports</h3>
<button <button
type="button" type="button"
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none" className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
@ -128,10 +135,11 @@ const IntegrationGuide = () => {
<Icon iconName="keyboard_arrow_right" className="!text-lg" /> <Icon iconName="keyboard_arrow_right" className="!text-lg" />
</button> </button>
</div> </div>
</h3> </div>
<div className="flex flex-col">
{exporterServices && exporterServices?.results ? ( {exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? ( exporterServices?.results?.length > 0 ? (
<div className="space-y-2"> <div>
<div className="divide-y divide-custom-border-200"> <div className="divide-y divide-custom-border-200">
{exporterServices?.results.map((service) => ( {exporterServices?.results.map((service) => (
<SingleExport key={service.id} service={service} refreshing={refreshing} /> <SingleExport key={service.id} service={service} refreshing={refreshing} />
@ -150,6 +158,7 @@ const IntegrationGuide = () => {
</Loader> </Loader>
)} )}
</div> </div>
</div>
</> </>
{provider && ( {provider && (
<Exporter <Exporter

View File

@ -23,7 +23,7 @@ export const SingleExport: React.FC<Props> = ({ service, refreshing }) => {
}; };
return ( return (
<div className="flex items-center justify-between gap-2 py-3"> <div className="flex items-center justify-between gap-2 px-4 py-3">
<div> <div>
<h4 className="flex items-center gap-2 text-sm"> <h4 className="flex items-center gap-2 text-sm">
<span> <span>

View File

@ -21,7 +21,6 @@ import {
import { Loader, PrimaryButton } from "components/ui"; import { Loader, PrimaryButton } from "components/ui";
// icons // icons
import { ArrowPathIcon } from "@heroicons/react/24/outline"; import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { ArrowRightIcon } from "components/icons";
// types // types
import { IImporterService } from "types"; import { IImporterService } from "types";
// fetch-keys // fetch-keys
@ -57,10 +56,10 @@ const IntegrationGuide = () => {
data={importToDelete} data={importToDelete}
user={user} user={user}
/> />
<div className="h-full space-y-2"> <div className="h-full">
{(!provider || provider === "csv") && ( {(!provider || provider === "csv") && (
<> <>
<div className="mb-5 flex items-center gap-2"> {/* <div className="mb-5 flex items-center gap-2">
<div className="h-full w-full space-y-1"> <div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div> <div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm"> <div className="text-sm">
@ -78,14 +77,13 @@ const IntegrationGuide = () => {
<ArrowRightIcon width={"18px"} color={"#3F76FF"} /> <ArrowRightIcon width={"18px"} color={"#3F76FF"} />
</div> </div>
</a> </a>
</div> </div> */}
<div className="space-y-2">
{IMPORTERS_EXPORTERS_LIST.map((service) => ( {IMPORTERS_EXPORTERS_LIST.map((service) => (
<div <div
key={service.provider} key={service.provider}
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4" className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
> >
<div className="flex items-center gap-4 whitespace-nowrap"> <div className="flex items-start gap-4">
<div className="relative h-10 w-10 flex-shrink-0"> <div className="relative h-10 w-10 flex-shrink-0">
<Image <Image
src={service.logo} src={service.logo}
@ -94,27 +92,27 @@ const IntegrationGuide = () => {
alt={`${service.title} Logo`} alt={`${service.title} Logo`}
/> />
</div> </div>
<div className="w-full"> <div>
<h3>{service.title}</h3> <h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
<p className="text-sm text-custom-text-200">{service.description}</p> <p className="text-sm text-custom-text-200 tracking-tight">
{service.description}
</p>
</div>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Link <Link href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}>
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
>
<a> <a>
<PrimaryButton> <PrimaryButton>
<span className="capitalize">{service.type}</span> now <span className="capitalize">{service.type}</span>
</PrimaryButton> </PrimaryButton>
</a> </a>
</Link> </Link>
</div> </div>
</div> </div>
</div>
))} ))}
</div> <div>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <div className="flex items-center pt-7 pb-3.5 border-b border-custom-border-200">
<h3 className="mb-2 flex gap-2 text-lg font-medium"> <h3 className="flex gap-2 text-xl font-medium">
Previous Imports Previous Imports
<button <button
type="button" type="button"
@ -130,6 +128,8 @@ const IntegrationGuide = () => {
{refreshing ? "Refreshing..." : "Refresh status"} {refreshing ? "Refreshing..." : "Refresh status"}
</button> </button>
</h3> </h3>
</div>
<div className="flex flex-col px-4 py-6">
{importerServices ? ( {importerServices ? (
importerServices.length > 0 ? ( importerServices.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
@ -158,6 +158,7 @@ const IntegrationGuide = () => {
</Loader> </Loader>
)} )}
</div> </div>
</div>
</> </>
)} )}

View File

@ -16,7 +16,7 @@ type Props = {
}; };
export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => ( export const SingleImport: React.FC<Props> = ({ service, refreshing, handleDelete }) => (
<div className="flex items-center justify-between gap-2 py-3"> <div className="flex items-center justify-between gap-2 px-4 py-3">
<div> <div>
<h4 className="flex items-center gap-2 text-sm"> <h4 className="flex items-center gap-2 text-sm">
<span> <span>

View File

@ -15,6 +15,7 @@ import { DangerButton, Loader, PrimaryButton } from "components/ui";
// icons // icons
import GithubLogo from "public/services/github.png"; import GithubLogo from "public/services/github.png";
import SlackLogo from "public/services/slack.png"; import SlackLogo from "public/services/slack.png";
import { CheckCircle2 } from "lucide-react";
// types // types
import { IAppIntegration, IWorkspaceIntegration } from "types"; import { IAppIntegration, IWorkspaceIntegration } from "types";
// fetch-keys // fetch-keys
@ -27,13 +28,12 @@ type Props = {
const integrationDetails: { [key: string]: any } = { const integrationDetails: { [key: string]: any } = {
github: { github: {
logo: GithubLogo, logo: GithubLogo,
installed: installed: "Activate GitHub on individual projects to sync with specific repositories.",
"Activate GitHub integrations on individual projects to sync with specific repositories.",
notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.", notInstalled: "Connect with GitHub with your Plane workspace to sync project issues.",
}, },
slack: { slack: {
logo: SlackLogo, logo: SlackLogo,
installed: "Activate Slack integrations on individual projects to sync with specific channels.", installed: "Activate Slack on individual projects to sync with specific channels.",
notInstalled: "Connect with Slack with your Plane workspace to sync project issues.", notInstalled: "Connect with Slack with your Plane workspace to sync project issues.",
}, },
}; };
@ -99,31 +99,22 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
); );
return ( return (
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5"> <div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="h-12 w-12 flex-shrink-0"> <div className="h-10 w-10 flex-shrink-0">
<Image <Image
src={integrationDetails[integration.provider].logo} src={integrationDetails[integration.provider].logo}
alt={`${integration.title} Logo`} alt={`${integration.title} Logo`}
/> />
</div> </div>
<div> <div>
<h3 className="flex items-center gap-4 text-xl font-semibold"> <h3 className="flex items-center gap-2 text-sm font-medium">
{integration.title} {integration.title}
{workspaceIntegrations ? ( {workspaceIntegrations
isInstalled ? ( ? isInstalled && <CheckCircle2 className="h-3.5 w-3.5 text-white fill-green-500" />
<span className="flex items-center gap-1 text-sm font-normal text-green-500"> : null}
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" /> Installed
</span>
) : (
<span className="flex items-center gap-1 text-sm font-normal text-custom-text-200">
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-custom-background-80" />{" "}
Not Installed
</span>
)
) : null}
</h3> </h3>
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200 tracking-tight">
{workspaceIntegrations {workspaceIntegrations
? isInstalled ? isInstalled
? integrationDetails[integration.provider].installed ? integrationDetails[integration.provider].installed
@ -135,12 +126,12 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
{workspaceIntegrations ? ( {workspaceIntegrations ? (
isInstalled ? ( isInstalled ? (
<DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration}> <DangerButton onClick={handleRemoveIntegration} loading={deletingIntegration} outline>
{deletingIntegration ? "Removing..." : "Remove installation"} {deletingIntegration ? "Uninstalling..." : "Uninstall"}
</DangerButton> </DangerButton>
) : ( ) : (
<PrimaryButton onClick={startAuth} loading={isInstalling}> <PrimaryButton onClick={startAuth} loading={isInstalling}>
{isInstalling ? "Installing..." : "Add installation"} {isInstalling ? "Installing..." : "Install"}
</PrimaryButton> </PrimaryButton>
) )
) : ( ) : (

View File

@ -0,0 +1,93 @@
import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui
import { SecondaryButton, PrimaryButton } from "components/ui";
type Props = {
isOpen: boolean;
handleClose: () => void;
onDiscard: () => void;
onConfirm: () => Promise<void>;
};
export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
const { isOpen, handleClose, onDiscard, onConfirm } = props;
const [isLoading, setIsLoading] = useState(false);
const onClose = () => {
handleClose();
setIsLoading(false);
};
const handleDeletion = async () => {
setIsLoading(true);
await onConfirm();
setIsLoading(false);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Draft Issue
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Would you like to save this issue in drafts?
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-between gap-2 p-4 sm:px-6">
<div>
<SecondaryButton onClick={onDiscard}>Discard</SecondaryButton>
</div>
<div className="space-x-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton onClick={handleDeletion} loading={isLoading}>
{isLoading ? "Saving..." : "Save Draft"}
</PrimaryButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,145 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import issueServices from "services/issues.service";
// hooks
import useIssuesView from "hooks/use-issues-view";
import useToast from "hooks/use-toast";
// icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui
import { SecondaryButton, DangerButton } from "components/ui";
// types
import type { IIssue, ICurrentUserResponse } from "types";
// fetch-keys
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IIssue | null;
user?: ICurrentUserResponse;
onSubmit?: () => Promise<void> | void;
};
export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
const { isOpen, handleClose, data, user, onSubmit } = props;
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { params } = useIssuesView();
const { setToastAlert } = useToast();
useEffect(() => {
setIsDeleteLoading(false);
}, [isOpen]);
const onClose = () => {
setIsDeleteLoading(false);
handleClose();
};
const handleDeletion = async () => {
if (!workspaceSlug || !data) return;
setIsDeleteLoading(true);
await issueServices
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
.then(() => {
setIsDeleteLoading(false);
handleClose();
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
setToastAlert({
title: "Success",
message: "Draft Issue deleted successfully",
type: "success",
});
})
.catch((error) => {
console.log(error);
handleClose();
setToastAlert({
title: "Error",
message: "Something went wrong",
type: "error",
});
setIsDeleteLoading(false);
});
if (onSubmit) await onSubmit();
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-600"
aria-hidden="true"
/>
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Draft Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail.identifier}-{data?.sequence_id}
</span>
{""}? All of the data related to the draft issue will be permanently removed.
This action cannot be undone.
</p>
</span>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
</DangerButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,580 @@
import React, { FC, useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// services
import aiService from "services/ai.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues";
import {
IssueAssigneeSelect,
IssueDateSelect,
IssueEstimateSelect,
IssueLabelSelect,
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
} from "components/issues/select";
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
// ui
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
import { TipTapEditor } from "components/tiptap";
// icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
const defaultValues: Partial<IIssue> = {
project: "",
name: "",
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
estimate_point: null,
state: "",
parent: null,
priority: "none",
assignees: [],
assignees_list: [],
labels: [],
labels_list: [],
start_date: null,
target_date: null,
};
interface IssueFormProps {
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
data?: Partial<IIssue> | null;
prePopulatedData?: Partial<IIssue> | null;
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
fieldsToShow: (
| "project"
| "name"
| "description"
| "state"
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
| "all"
)[];
}
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit,
data,
prePopulatedData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleClose,
status,
user,
fieldsToShow,
} = props;
const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const editorRef = useRef<any>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
control,
getValues,
setValue,
setFocus,
} = useForm<IIssue>({
defaultValues: prePopulatedData ?? defaultValues,
reValidateMode: "onChange",
});
const issueName = watch("name");
const onClose = () => {
handleClose();
};
const handleCreateUpdateIssue = async (
formData: Partial<IIssue>,
action: "saveDraft" | "createToNewIssue" = "saveDraft"
) => {
await handleFormSubmit({
...formData,
is_draft: action === "saveDraft",
});
setGptAssistantModal(false);
reset({
...defaultValues,
project: projectId,
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
});
editorRef?.current?.clearEditor();
};
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
editorRef.current?.setEditorValue(`${watch("description_html")}`);
};
const handleAutoGenerateDescription = async () => {
if (!workspaceSlug || !projectId) return;
setIAmFeelingLucky(true);
aiService
.createGptTask(
workspaceSlug as string,
projectId as string,
{
prompt: issueName,
task: "Generate a proper description for this issue.",
},
user
)
.then((res) => {
if (res.response === "")
setToastAlert({
type: "error",
title: "Error!",
message:
"Issue title isn't informative enough to generate the description. Please try with a different title.",
});
else handleAiAssistance(res.response_html);
})
.catch((err) => {
const error = err?.data?.error;
if (err.status === 429)
setToastAlert({
type: "error",
title: "Error!",
message:
error ||
"You have reached the maximum number of requests of 50 requests per month per user.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: error || "Some error occurred. Please try again.",
});
})
.finally(() => setIAmFeelingLucky(false));
};
useEffect(() => {
setFocus("name");
reset({
...defaultValues,
...(prePopulatedData ?? {}),
...(data ?? {}),
});
}, [setFocus, prePopulatedData, reset, data]);
// update projectId in form when projectId changes
useEffect(() => {
reset({
...getValues(),
project: projectId,
});
}, [getValues, projectId, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<>
{projectId && (
<>
<CreateStateModal
isOpen={stateModal}
handleClose={() => setStateModal(false)}
projectId={projectId}
user={user}
/>
<CreateLabelModal
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
onSuccess={(response) => {
setValue("labels", [...watch("labels"), response.id]);
setValue("labels_list", [...watch("labels_list"), response.id]);
}}
/>
</>
)}
<form
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
<Controller
control={control}
name="project"
render={({ field: { value, onChange } }) => (
<IssueProjectSelect
value={value}
onChange={(val: string) => {
onChange(val);
setActiveProject(val);
}}
/>
)}
/>
)}
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">
{status ? "Update" : "Create"} Issue
</h3>
</div>
{watch("parent") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
selectedParentIssue && (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: selectedParentIssue.state__color,
}}
/>
<span className="flex-shrink-0 text-custom-text-200">
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
</span>
<span className="truncate font-medium">
{selectedParentIssue.name.substring(0, 50)}
</span>
<XMarkIcon
className="h-3 w-3 cursor-pointer"
onClick={() => {
setValue("parent", null);
setSelectedParentIssue(null);
}}
/>
</div>
</div>
)}
<div className="space-y-3">
<div className="mt-2 space-y-3">
{(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && (
<div>
<Input
id="name"
name="name"
className="resize-none text-xl"
placeholder="Title"
autoComplete="off"
error={errors.name}
register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
{iAmFeelingLucky ? (
"Generating response..."
) : (
<>
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
</>
)}
</button>
)}
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
>
<SparklesIcon className="h-4 w-4" />
AI
</button>
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => {
if (!value && !watch("description_html")) return <></>;
return (
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
customClassName="min-h-[150px]"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
setValue("description", description);
}}
/>
);
}}
/>
<GptAssistantModal
isOpen={gptAssistantModal}
handleClose={() => {
setGptAssistantModal(false);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
inset="top-2 left-0"
content=""
htmlContent={watch("description_html")}
onResponse={(response) => {
handleAiAssistance(response);
}}
projectId={projectId}
/>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
<Controller
control={control}
name="state"
render={({ field: { value, onChange } }) => (
<IssueStateSelect
setIsOpen={setStateModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
<Controller
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
<Controller
control={control}
name="assignees"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect
projectId={projectId}
value={value}
onChange={onChange}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<Controller
control={control}
name="labels"
render={({ field: { value, onChange } }) => (
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Start date"
maxDate={maxDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Due date"
minDate={minDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<div>
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<IssueEstimateSelect value={value} onChange={onChange} />
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<Controller
control={control}
name="parent"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}
handleClose={() => setParentIssueListModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
setSelectedParentIssue(issue);
}}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<CustomMenu ellipsis>
{watch("parent") ? (
<>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setValue("parent", null)}
>
Remove parent issue
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Select Parent Issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
</div>
</div>
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setCreateMore((prevData) => !prevData)}
>
<span className="text-xs">Create more</span>
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
<SecondaryButton
loading={isSubmitting}
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
>
{isSubmitting ? "Saving..." : "Save Draft"}
</SecondaryButton>
{data && (
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Add Issue"}
</PrimaryButton>
)}
</div>
</div>
</form>
</>
);
};

View File

@ -0,0 +1,285 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues";
// components
import { DraftIssueForm } from "components/issues";
// types
import type { IIssue } from "types";
// fetch-keys
import {
PROJECT_ISSUES_DETAILS,
USER_ISSUE,
SUB_ISSUES,
PROJECT_ISSUES_LIST_WITH_PARAMS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
VIEW_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
interface IssuesModalProps {
data?: IIssue | null;
handleClose: () => void;
isOpen: boolean;
isUpdatingSingleIssue?: boolean;
prePopulateData?: Partial<IIssue>;
fieldsToShow?: (
| "project"
| "name"
| "description"
| "state"
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
| "all"
)[];
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
}
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData,
fieldsToShow = ["all"],
onSubmit,
}) => {
// states
const [createMore, setCreateMore] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params;
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
const { user } = useUser();
const { projects } = useProjects();
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
prePopulateData = {
...prePopulateData,
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const onClose = () => {
handleClose();
setActiveProject(null);
};
useEffect(() => {
// if modal is closed, reset active project to null
// and return to avoid activeProject being set to some other project
if (!isOpen) {
setActiveProject(null);
return;
}
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) {
setActiveProject(data.project);
return;
}
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen]);
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
const spreadsheetFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
: viewId
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams);
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
: viewId
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(async () => {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue created successfully.",
});
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
});
if (!createMore) onClose();
};
const updateIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
if (!createMore) onClose();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue updated successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be updated. Please try again.",
});
});
};
const handleFormSubmit = async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject) return;
const payload: Partial<IIssue> = {
...formData,
assignees_list: formData.assignees ?? [],
labels_list: formData.labels ?? [],
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
};
if (!data) await createIssue(payload);
else await updateIssue(payload);
if (onSubmit) await onSubmit(payload);
};
if (!projects || projects.length === 0) return null;
return (
<>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<DraftIssueForm
handleFormSubmit={handleFormSubmit}
prePopulatedData={prePopulateData}
data={data}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
);
};

View File

@ -8,6 +8,7 @@ import { Controller, useForm } from "react-hook-form";
import aiService from "services/ai.service"; import aiService from "services/ai.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// components // components
import { GptAssistantModal } from "components/core"; import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues"; import { ParentIssuesListModal } from "components/issues";
@ -62,8 +63,11 @@ export interface IssueFormProps {
createMore: boolean; createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>; setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void; handleClose: () => void;
handleDiscardClose: () => void;
status: boolean; status: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleFormDirty: (payload: Partial<IIssue> | null) => void;
fieldsToShow: ( fieldsToShow: (
| "project" | "project"
| "name" | "name"
@ -80,18 +84,21 @@ export interface IssueFormProps {
)[]; )[];
} }
export const IssueForm: FC<IssueFormProps> = ({ export const IssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit, handleFormSubmit,
initialData, initialData,
projectId, projectId,
setActiveProject, setActiveProject,
createMore, createMore,
setCreateMore, setCreateMore,
handleClose, handleDiscardClose,
status, status,
user, user,
fieldsToShow, fieldsToShow,
}) => { handleFormDirty,
} = props;
const [stateModal, setStateModal] = useState(false); const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
@ -100,6 +107,8 @@ export const IssueForm: FC<IssueFormProps> = ({
const [gptAssistantModal, setGptAssistantModal] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const router = useRouter(); const router = useRouter();
@ -109,7 +118,7 @@ export const IssueForm: FC<IssueFormProps> = ({
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting, isDirty },
handleSubmit, handleSubmit,
reset, reset,
watch, watch,
@ -124,6 +133,23 @@ export const IssueForm: FC<IssueFormProps> = ({
const issueName = watch("name"); const issueName = watch("name");
const payload: Partial<IIssue> = {
name: getValues("name"),
description: getValues("description"),
state: getValues("state"),
priority: getValues("priority"),
assignees: getValues("assignees"),
target_date: getValues("target_date"),
labels: getValues("labels"),
project: getValues("project"),
};
useEffect(() => {
if (isDirty) handleFormDirty(payload);
else handleFormDirty(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(payload), isDirty]);
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => { const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
await handleFormSubmit(formData); await handleFormSubmit(formData);
@ -543,7 +569,15 @@ export const IssueForm: FC<IssueFormProps> = ({
<ToggleSwitch value={createMore} onChange={() => {}} size="md" /> <ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton> <SecondaryButton
onClick={() => {
const data = JSON.stringify(getValues());
setValueInLocalStorage(data);
handleDiscardClose();
}}
>
Discard
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}> <PrimaryButton type="submit" loading={isSubmitting}>
{status {status
? isSubmitting ? isSubmitting

View File

@ -16,3 +16,9 @@ export * from "./sub-issues-list";
export * from "./label"; export * from "./label";
export * from "./issue-reaction"; export * from "./issue-reaction";
export * from "./peek-overview"; export * from "./peek-overview";
export * from "./confirm-issue-discard";
// draft issue
export * from "./draft-issue-form";
export * from "./draft-issue-modal";
export * from "./delete-draft-issue-modal";

View File

@ -1,6 +1,10 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
import modulesService from "services/modules.service"; import modulesService from "services/modules.service";
@ -16,7 +20,7 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues"; import useMyIssues from "hooks/my-issues/use-my-issues";
// components // components
import { IssueForm } from "components/issues"; import { IssueForm, ConfirmIssueDiscard } from "components/issues";
// types // types
import type { IIssue } from "types"; import type { IIssue } from "types";
// fetch-keys // fetch-keys
@ -31,10 +35,10 @@ import {
MODULE_DETAILS, MODULE_DETAILS,
VIEW_ISSUES, VIEW_ISSUES,
INBOX_ISSUES, INBOX_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// constants // constants
import { INBOX_ISSUE_SOURCE } from "constants/inbox"; import { INBOX_ISSUE_SOURCE } from "constants/inbox";
import { ISSUE_CREATE } from "constants/posthog-events";
export interface IssuesModalProps { export interface IssuesModalProps {
data?: IIssue | null; data?: IIssue | null;
@ -70,6 +74,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}) => { }) => {
// states // states
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);
const [formDirtyState, setFormDirtyState] = useState<any>(null);
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null); const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
@ -77,7 +83,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { displayFilters, params } = useIssuesView(); const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView(); const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params; const { ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView(); const { params: inboxParams } = useInboxView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView(); const { params: spreadsheetParams } = useSpreadsheetIssuesView();
@ -96,10 +102,23 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""], assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
}; };
const onClose = useCallback(() => { const onClose = () => {
if (formDirtyState !== null) {
setShowConfirmDiscard(true);
} else {
handleClose(); handleClose();
setActiveProject(null); setActiveProject(null);
}, [handleClose]); }
};
const onDiscardClose = () => {
handleClose();
setActiveProject(null);
};
const handleFormDirty = (data: any) => {
setFormDirtyState(data);
};
useEffect(() => { useEffect(() => {
// if modal is closed, reset active project to null // if modal is closed, reset active project to null
@ -272,10 +291,50 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}); });
}); });
if (!createMore) onClose(); if (!createMore) onDiscardClose();
};
const createDraftIssue = async () => {
if (!workspaceSlug || !activeProject || !user) return;
const payload: Partial<IIssue> = {
...formDirtyState,
};
await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(() => {
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Draft Issue created successfully.",
});
handleClose();
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
});
}; };
const updateIssue = async (payload: Partial<IIssue>) => { const updateIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService await issuesService
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user) .patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => { .then((res) => {
@ -291,7 +350,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) onClose(); if (!createMore) onDiscardClose();
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -328,6 +387,19 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (!projects || projects.length === 0) return null; if (!projects || projects.length === 0) return null;
return ( return (
<>
<ConfirmIssueDiscard
isOpen={showConfirmDiscard}
handleClose={() => setShowConfirmDiscard(false)}
onConfirm={createDraftIssue}
onDiscard={() => {
handleClose();
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
}}
/>
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}> <Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child <Transition.Child
@ -360,11 +432,14 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
createMore={createMore} createMore={createMore}
setCreateMore={setCreateMore} setCreateMore={setCreateMore}
handleClose={onClose} handleClose={onClose}
handleDiscardClose={onDiscardClose}
setIsConfirmDiscardOpen={setShowConfirmDiscard}
projectId={activeProject ?? ""} projectId={activeProject ?? ""}
setActiveProject={setActiveProject} setActiveProject={setActiveProject}
status={data ? true : false} status={data ? true : false}
user={user} user={user}
fieldsToShow={fieldsToShow} fieldsToShow={fieldsToShow}
handleFormDirty={handleFormDirty}
/> />
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
@ -372,5 +447,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
</>
); );
}; };

View File

@ -205,7 +205,7 @@ export const MyIssuesView: React.FC<Props> = ({
); );
const handleIssueAction = useCallback( const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => { (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
if (action === "copy") makeIssueCopy(issue); if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue); else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue); else if (action === "delete") handleDeleteIssue(issue);

View File

@ -16,8 +16,6 @@ import { Popover, Transition } from "@headlessui/react";
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { Component } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
// fetch-keys // fetch-keys
@ -146,10 +144,10 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
open ? "text-custom-text-100" : "text-custom-text-200" open ? "text-custom-text-100" : "text-custom-text-200"
}`} }`}
> >
<Component <span
className="h-4 w-4 text-custom-text-100 flex-shrink-0" className="h-4 w-4 rounded-full"
style={{ style={{
color: watch("color"), backgroundColor: watch("color"),
}} }}
/> />
</Popover.Button> </Popover.Button>

View File

@ -43,7 +43,7 @@ export const SingleLabel: React.FC<Props> = ({
> >
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}> <CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<RectangleGroupIcon className="h-4 w-4" /> <Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
<span>Convert to group</span> <span>Convert to group</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>

View File

@ -204,7 +204,7 @@ export const ProfileIssuesView = () => {
); );
const handleIssueAction = useCallback( const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => { (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
if (action === "copy") makeIssueCopy(issue); if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue); else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue); else if (action === "delete") handleDeleteIssue(issue);

View File

@ -79,7 +79,7 @@ const ConfirmProjectMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, ha
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2 bg-custom-background-90 p-4 sm:px-6"> <div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}> <DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Removing..." : "Remove"} {isDeleteLoading ? "Removing..." : "Remove"}

View File

@ -49,7 +49,7 @@ export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
{selectedOption ? ( {selectedOption ? (
selectedOption?.display_name selectedOption?.display_name
) : ( ) : (
<span className="text-sm py-0.5 text-custom-text-200">Select</span> <span className="text-sm py-0.5 text-custom-sidebar-text-400">Select</span>
)} )}
</div> </div>
} }

View File

@ -219,7 +219,9 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
} }
</div> </div>
) : ( ) : (
<div>Select co-worker</div> <div className="flex items-center gap-2 py-0.5">
Select co-worker
</div>
)} )}
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button> </button>
@ -249,10 +251,13 @@ const SendProjectInvitationModal: React.FC<Props> = (props) => {
render={({ field }) => ( render={({ field }) => (
<CustomSelect <CustomSelect
{...field} {...field}
label={ customButton={
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-custom-border-200 shadow-sm duration-300 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 focus:outline-none px-3 py-2.5 text-sm text-left">
<span className="capitalize"> <span className="capitalize">
{field.value ? ROLE[field.value] : "Select role"} {field.value ? ROLE[field.value] : "Select role"}
</span> </span>
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</button>
} }
input input
width="w-full" width="w-full"

View File

@ -2,7 +2,11 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
export const SettingsSidebar = () => { type Props = {
profilePage?: boolean;
};
export const SettingsSidebar: React.FC<Props> = ({ profilePage = false }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -43,11 +47,61 @@ export const SettingsSidebar = () => {
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`, href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
}, },
]; ];
const workspaceLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/settings`,
},
{
label: "Members",
href: `/${workspaceSlug}/settings/members`,
},
{
label: "Billing & Plans",
href: `/${workspaceSlug}/settings/billing`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
{
label: "Imports",
href: `/${workspaceSlug}/settings/imports`,
},
{
label: "Exports",
href: `/${workspaceSlug}/settings/exports`,
},
];
const profileLinks: Array<{
label: string;
href: string;
}> = [
{
label: "Profile",
href: `/${workspaceSlug}/me/profile`,
},
{
label: "Activity",
href: `/${workspaceSlug}/me/profile/activity`,
},
{
label: "Preferences",
href: `/${workspaceSlug}/me/profile/preferences`,
},
];
return ( return (
<div className="flex flex-col gap-2 w-80 px-9"> <div className="flex flex-col gap-6 w-80 px-5">
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span> <span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
<div className="flex flex-col gap-1 w-full"> <div className="flex flex-col gap-1 w-full">
{projectLinks.map((link) => ( {(projectId ? projectLinks : workspaceLinks).map((link) => (
<Link key={link.href} href={link.href}> <Link key={link.href} href={link.href}>
<a> <a>
<div <div
@ -68,5 +122,32 @@ export const SettingsSidebar = () => {
))} ))}
</div> </div>
</div> </div>
{!projectId && (
<div className="flex flex-col gap-2">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">My Account</span>
<div className="flex flex-col gap-1 w-full">
{profileLinks.map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`px-4 py-2 text-sm font-medium rounded-md ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
</div>
)}
</div>
); );
}; };

View File

@ -25,6 +25,7 @@ import {
PhotoFilterOutlined, PhotoFilterOutlined,
SettingsOutlined, SettingsOutlined,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { PenSquare } from "lucide-react";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// types // types
@ -288,6 +289,16 @@ export const SingleSidebarProject: React.FC<Props> = observer((props) => {
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
<CustomMenu.MenuItem
onClick={() =>
router.push(`/${workspaceSlug}/projects/${project?.id}/draft-issues`)
}
>
<div className="flex items-center justify-start gap-2">
<PenSquare className="!text-base !leading-4 w-[14px] h-[14px] text-custom-text-300" />
<span>Draft Issues</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)} onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
> >

View File

@ -184,6 +184,8 @@ export const SingleState: React.FC<Props> = ({
<ArrowDownIcon className="h-4 w-4" /> <ArrowDownIcon className="h-4 w-4" />
</button> </button>
)} )}
<div className=" items-center gap-2.5 hidden group-hover:flex">
{state.default ? ( {state.default ? (
<span className="text-xs text-custom-text-200">Default</span> <span className="text-xs text-custom-text-200">Default</span>
) : ( ) : (
@ -196,7 +198,6 @@ export const SingleState: React.FC<Props> = ({
Mark as default Mark as default
</button> </button>
)} )}
<div className=" items-center gap-2.5 hidden group-hover:flex">
<button <button
type="button" type="button"
className="grid place-items-center group-hover:opacity-100 opacity-0" className="grid place-items-center group-hover:opacity-100 opacity-0"
@ -215,14 +216,26 @@ export const SingleState: React.FC<Props> = ({
> >
{state.default ? ( {state.default ? (
<Tooltip tooltipContent="Cannot delete the default state."> <Tooltip tooltipContent="Cannot delete the default state.">
<X className="h-3.5 w-3.5 text-red-500" /> <X
className={`h-4 w-4 ${
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
}`}
/>
</Tooltip> </Tooltip>
) : groupLength === 1 ? ( ) : groupLength === 1 ? (
<Tooltip tooltipContent="Cannot have an empty group."> <Tooltip tooltipContent="Cannot have an empty group.">
<X className="h-3.5 w-3.5 text-red-500" /> <X
className={`h-4 w-4 ${
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
}`}
/>
</Tooltip> </Tooltip>
) : ( ) : (
<X className="h-3.5 w-3.5 text-red-500" /> <X
className={`h-4 w-4 ${
groupLength < 1 ? "text-custom-sidebar-text-400" : "text-red-500"
}`}
/>
)} )}
</button> </button>
</div> </div>

View File

@ -6,7 +6,7 @@ type Props = {
}; };
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => ( export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
<div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200"> <div className="flex items-start gap-3 py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">{bannerName}</h3> <h3 className="text-xl font-medium">{bannerName}</h3>
{description && ( {description && (
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100"> <div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">

View File

@ -73,7 +73,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
}, },
archived_at: { archived_at: {
@ -81,7 +81,7 @@ const activityDetails: {
if (activity.new_value === "restore") return "restored the issue."; if (activity.new_value === "restore") return "restored the issue.";
else return "archived the issue."; else return "archived the issue.";
}, },
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
}, },
attachment: { attachment: {
@ -99,7 +99,7 @@ const activityDetails: {
{showIssue && <IssueLink activity={activity} />} {showIssue && <IssueLink activity={activity} />}
</> </>
), ),
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
}, },
blocking: { blocking: {
@ -156,7 +156,7 @@ const activityDetails: {
</button> </button>
</> </>
), ),
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
}, },
description: { description: {
@ -172,7 +172,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
estimate_point: { estimate_point: {
@ -190,7 +190,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
}, },
issue: { issue: {
@ -198,7 +198,7 @@ const activityDetails: {
if (activity.verb === "created") return "created the issue."; if (activity.verb === "created") return "created the issue.";
else return "deleted an issue."; else return "deleted an issue.";
}, },
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
}, },
labels: { labels: {
@ -225,7 +225,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
}, },
link: { link: {
@ -255,7 +255,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
}, },
modules: { modules: {
@ -279,7 +279,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
}, },
name: { name: {
@ -295,7 +295,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
}, },
parent: { parent: {
@ -314,7 +314,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
}, },
priority: { priority: {
@ -333,7 +333,7 @@ const activityDetails: {
. .
</> </>
), ),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
}, },
start_date: { start_date: {
@ -351,7 +351,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
state: { state: {
@ -389,7 +389,7 @@ const activityDetails: {
)} )}
</> </>
), ),
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
}, },
}; };

View File

@ -79,7 +79,7 @@ const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data,
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2 bg-custom-background-90 p-4 sm:px-6"> <div className="flex justify-end gap-2 p-4 sm:px-6">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}> <DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
{isDeleteLoading ? "Removing..." : "Remove"} {isDeleteLoading ? "Removing..." : "Remove"}

View File

@ -6,7 +6,6 @@ export * from "./help-section";
export * from "./issues-list"; export * from "./issues-list";
export * from "./issues-pie-chart"; export * from "./issues-pie-chart";
export * from "./issues-stats"; export * from "./issues-stats";
export * from "./settings-header";
export * from "./sidebar-dropdown"; export * from "./sidebar-dropdown";
export * from "./sidebar-menu"; export * from "./sidebar-menu";
export * from "./sidebar-quick-action"; export * from "./sidebar-quick-action";

View File

@ -1,13 +0,0 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-8 space-y-6">
<div>
<h3 className="text-2xl font-semibold">Workspace Settings</h3>
<p className="mt-1 text-sm text-custom-text-200">
This information will be displayed to every member of the workspace.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -1,34 +1,131 @@
import React from "react"; import React, { useState } from "react";
// ui // ui
import { Icon } from "components/ui"; import { Icon } from "components/ui";
import { ChevronDown, PenSquare } from "lucide-react";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
import { CreateUpdateDraftIssueModal } from "components/issues";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
export const WorkspaceSidebarQuickAction = () => { export const WorkspaceSidebarQuickAction = () => {
const store: any = useMobxStore(); const store: any = useMobxStore();
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null);
return ( return (
<>
<CreateUpdateDraftIssueModal
isOpen={isDraftIssueModalOpen}
handleClose={() => setIsDraftIssueModalOpen(false)}
prePopulateData={storedValue ? JSON.parse(storedValue) : {}}
onSubmit={() => {
localStorage.removeItem("draftedIssue");
clearValue();
setIsDraftIssueModalOpen(false);
}}
fieldsToShow={[
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
"state",
"startDate",
"project",
]}
/>
<div <div
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${ className={`relative flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2" store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`} }`}
> >
<button <div
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${ className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 ${
store?.theme?.sidebarCollapsed store?.theme?.sidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80" ? "px-2 hover:bg-custom-sidebar-background-80"
: "px-3 shadow border-[0.5px] border-custom-border-300" : "px-3 shadow border-[0.5px] border-custom-border-300"
}`} }`}
>
<button
type="button"
className="flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
onClick={() => { onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" }); const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e); document.dispatchEvent(e);
}} }}
> >
<Icon iconName="edit_square" className="!text-lg !leading-4 text-custom-sidebar-text-300" /> <Icon
{!store?.theme?.sidebarCollapsed && <span className="text-sm font-medium">New Issue</span>} iconName="edit_square"
className="!text-lg !leading-4 text-custom-sidebar-text-300"
/>
{!store?.theme?.sidebarCollapsed && (
<span className="text-sm font-medium">New Issue</span>
)}
</button> </button>
{storedValue && <div className="h-8 w-0.5 bg-custom-sidebar-background-80" />}
{storedValue && (
<div className="relative">
<Menu as={React.Fragment}>
{({ open }) => (
<>
<div>
<Menu.Button
type="button"
className={`flex items-center justify-center rounded flex-shrink-0 p-1.5 ${
open ? "rotate-180 pl-0" : "rotate-0 pr-0"
}`}
>
<ChevronDown
size={16}
className="!text-custom-sidebar-text-300 transform transition-transform duration-300"
/>
</Menu.Button>
</div>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -right-4 mt-1 w-52 bg-custom-background-300">
<div className="px-1 py-1 ">
<Menu.Item>
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
>
<PenSquare
size={16}
className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
/>
Last Drafted Issue
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
)}
</div>
<button <button
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${ className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
store?.theme?.sidebarCollapsed store?.theme?.sidebarCollapsed
@ -43,5 +140,6 @@ export const WorkspaceSidebarQuickAction = () => {
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" /> <Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
</button> </button>
</div> </div>
</>
); );
}; };

View File

@ -140,6 +140,15 @@ export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, para
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`; return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
}; };
export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
if (!params) return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`;
};
export const PROJECT_ISSUES_DETAILS = (issueId: string) => export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`; `PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) => export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>

View File

@ -20,6 +20,7 @@ import {
CYCLE_ISSUES_WITH_PARAMS, CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS, PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST, STATES_LIST,
VIEW_ISSUES, VIEW_ISSUES,
@ -38,6 +39,7 @@ const useIssuesView = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname.includes("draft-issues");
const params: any = { const params: any = {
order_by: displayFilters?.order_by, order_by: displayFilters?.order_by,
@ -72,6 +74,15 @@ const useIssuesView = () => {
: null : null
); );
const { data: draftIssues, mutate: mutateDraftIssues } = useSWR(
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
? PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
: null,
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
? () => issuesService.getDraftIssues(workspaceSlug as string, projectId as string, params)
: null
);
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR( const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
workspaceSlug && projectId && cycleId && params workspaceSlug && projectId && cycleId && params
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params) ? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
@ -151,6 +162,8 @@ const useIssuesView = () => {
? viewIssues ? viewIssues
: isArchivedIssues : isArchivedIssues
? projectArchivedIssues ? projectArchivedIssues
: isDraftIssues
? draftIssues
: projectIssues; : projectIssues;
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup }; if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
@ -169,6 +182,8 @@ const useIssuesView = () => {
moduleId, moduleId,
viewId, viewId,
isArchivedIssues, isArchivedIssues,
isDraftIssues,
draftIssues,
emptyStatesObject, emptyStatesObject,
]); ]);
@ -191,6 +206,8 @@ const useIssuesView = () => {
? mutateViewIssues ? mutateViewIssues
: isArchivedIssues : isArchivedIssues
? mutateProjectArchivedIssues ? mutateProjectArchivedIssues
: isDraftIssues
? mutateDraftIssues
: mutateProjectIssues, : mutateProjectIssues,
filters, filters,
setFilters, setFilters,

View File

@ -1,127 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
type Props = {
profilePage?: boolean;
};
const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const workspaceLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/settings`,
},
{
label: "Members",
href: `/${workspaceSlug}/settings/members`,
},
{
label: "Billing & Plans",
href: `/${workspaceSlug}/settings/billing`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/settings/integrations`,
},
{
label: "Imports",
href: `/${workspaceSlug}/settings/imports`,
},
{
label: "Exports",
href: `/${workspaceSlug}/settings/exports`,
},
];
const projectLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/projects/${projectId}/settings`,
},
{
label: "Control",
href: `/${workspaceSlug}/projects/${projectId}/settings/control`,
},
{
label: "Members",
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
},
{
label: "Features",
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
},
{
label: "States",
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
},
{
label: "Labels",
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
},
{
label: "Estimates",
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
},
{
label: "Automations",
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
},
];
const profileLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/me/profile`,
},
{
label: "Activity",
href: `/${workspaceSlug}/me/profile/activity`,
},
{
label: "Preferences",
href: `/${workspaceSlug}/me/profile/preferences`,
},
];
return (
<div className="flex flex-wrap gap-4">
{(profilePage ? profileLinks : projectId ? projectLinks : workspaceLinks).map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`rounded-full border px-5 py-1.5 text-sm outline-none ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
);
};
export default SettingsNavbar;

View File

@ -7,7 +7,6 @@ import Link from "next/link";
import userService from "services/user.service"; import userService from "services/user.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { ActivityIcon, ActivityMessage } from "components/core"; import { ActivityIcon, ActivityMessage } from "components/core";
import { TipTapEditor } from "components/tiptap"; import { TipTapEditor } from "components/tiptap";
@ -20,6 +19,7 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { USER_ACTIVITY } from "constants/fetch-keys"; import { USER_ACTIVITY } from "constants/fetch-keys";
// helper // helper
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
import { SettingsSidebar } from "components/project";
const ProfileActivity = () => { const ProfileActivity = () => {
const router = useRouter(); const router = useRouter();
@ -38,18 +38,17 @@ const ProfileActivity = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<div className="mb-8 space-y-6"> <div className="w-80 py-8">
<div> <SettingsSidebar />
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-custom-text-200">
This information will be visible to only you.
</p>
</div>
<SettingsNavbar profilePage />
</div> </div>
{userActivity ? ( {userActivity ? (
<div> <section className="pr-9 py-8 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Acitivity</h3>
</div>
<div className={`flex flex-col gap-2 py-4 w-full`}>
<ul role="list" className="-mb-4"> <ul role="list" className="-mb-4">
{userActivity.results.map((activityItem: any, activityIdx: number) => { {userActivity.results.map((activityItem: any, activityIdx: number) => {
if (activityItem.field === "comment") { if (activityItem.field === "comment") {
@ -78,9 +77,9 @@ const ProfileActivity = () => {
</div> </div>
)} )}
<span className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white"> <span className="ring-6 flex h-6 w-6 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
<ChatBubbleLeftEllipsisIcon <ChatBubbleLeftEllipsisIcon
className="h-3.5 w-3.5 text-custom-text-200" className="h-6 w-6 !text-2xl text-custom-text-200"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -143,24 +142,17 @@ const ProfileActivity = () => {
return ( return (
<li key={activityItem.id}> <li key={activityItem.id}>
<div className="relative pb-1"> <div className="relative pb-1">
{userActivity.results.length > 1 && <div className="relative flex items-center space-x-2">
activityIdx !== userActivity.results.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-2">
<> <>
<div> <div>
<div className="relative px-1.5"> <div className="relative px-1.5">
<div className="mt-1.5"> <div className="mt-1.5">
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white"> <div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? ( {activityItem.field ? (
activityItem.new_value === "restore" ? ( activityItem.new_value === "restore" ? (
<Icon <Icon
iconName="history" iconName="history"
className="text-sm text-custom-text-200" className="!text-2xl text-custom-text-200"
/> />
) : ( ) : (
<ActivityIcon activity={activityItem} /> <ActivityIcon activity={activityItem} />
@ -185,8 +177,8 @@ const ProfileActivity = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="min-w-0 flex-1 py-3"> <div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
<div className="text-xs text-custom-text-200 break-words"> <div className="text-sm text-custom-text-200 break-words">
{activityItem.field === "archived_at" && {activityItem.field === "archived_at" &&
activityItem.new_value !== "restore" ? ( activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span> <span className="text-gray font-medium">Plane</span>
@ -218,6 +210,7 @@ const ProfileActivity = () => {
})} })}
</ul> </ul>
</div> </div>
</section>
) : ( ) : (
<Loader className="space-y-5"> <Loader className="space-y-5">
<Loader.Item height="40px" /> <Loader.Item height="40px" />

View File

@ -1,4 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
@ -10,21 +12,15 @@ import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { ImagePickerPopover, ImageUploadModal } from "components/core"; import { ImagePickerPopover, ImageUploadModal } from "components/core";
import { SettingsSidebar } from "components/project";
// ui // ui
import { import { CustomSearchSelect, CustomSelect, Input, PrimaryButton, Spinner } from "components/ui";
CustomSearchSelect,
CustomSelect,
DangerButton,
Input,
SecondaryButton,
Spinner,
} from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { UserIcon } from "@heroicons/react/24/outline"; import { UserIcon } from "@heroicons/react/24/outline";
import { UserCircle } from "lucide-react";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import type { IUser } from "types"; import type { IUser } from "types";
@ -46,6 +42,9 @@ const Profile: NextPage = () => {
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { const {
register, register,
handleSubmit, handleSubmit,
@ -126,6 +125,7 @@ const Profile: NextPage = () => {
if (!prevData) return prevData; if (!prevData) return prevData;
return { ...prevData, avatar: "" }; return { ...prevData, avatar: "" };
}, false); }, false);
setIsRemoving(false);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -155,6 +155,8 @@ const Profile: NextPage = () => {
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)} onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isRemoving}
handleDelete={() => handleDelete(myProfile?.avatar, true)}
onSuccess={(url) => { onSuccess={(url) => {
setValue("avatar", url); setValue("avatar", url);
handleSubmit(onSubmit)(); handleSubmit(onSubmit)();
@ -164,81 +166,49 @@ const Profile: NextPage = () => {
userImage userImage
/> />
{myProfile ? ( {myProfile ? (
<div className="p-8"> <form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-8 space-y-6"> <div className="flex flex-row gap-2">
<div> <div className="w-80 py-8">
<h3 className="text-3xl font-semibold">Profile Settings</h3> <SettingsSidebar />
<p className="mt-1 text-custom-text-200">
This information will be visible to only you.
</p>
</div> </div>
<SettingsNavbar profilePage /> <div className={`flex flex-col gap-8 pr-9 py-9 w-full`}>
</div> <div className="relative h-44 w-full mt-6">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8 sm:space-y-12"> <img
<div className="grid grid-cols-12 gap-4 sm:gap-16"> src={
<div className="col-span-12 sm:col-span-6"> watch("cover_image") ??
<h4 className="text-lg font-semibold text-custom-text-100">Profile Picture</h4> "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
<p className="text-sm text-custom-text-200"> }
Max file size is 5MB. Supported file types are .jpg and .png. className="h-44 w-full rounded-lg object-cover"
</p> alt={myProfile?.name ?? "Cover image"}
</div> />
<div className="col-span-12 sm:col-span-6"> <div className="flex items-end justify-between absolute left-8 -bottom-6">
<div className="flex items-center gap-4"> <div className="flex gap-3">
<div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}> <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? ( {!watch("avatar") || watch("avatar") === "" ? (
<div className="h-12 w-12 rounded-md bg-custom-background-80 p-2"> <div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
<UserIcon className="h-full w-full text-custom-text-200" /> <UserIcon className="h-full w-full text-custom-text-200" />
</div> </div>
) : ( ) : (
<div className="relative h-12 w-12 overflow-hidden"> <div className="relative h-16 w-16 overflow-hidden">
<img <img
src={watch("avatar")} src={watch("avatar")}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
onClick={() => setIsImageUploadModalOpen(true)} onClick={() => setIsImageUploadModalOpen(true)}
alt={myProfile.display_name} alt={myProfile.display_name}
/> />
</div> </div>
)} )}
</button> </button>
<div className="flex items-center gap-2">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
Upload
</SecondaryButton>
{myProfile.avatar && myProfile.avatar !== "" && (
<DangerButton
onClick={() => handleDelete(myProfile.avatar, true)}
loading={isRemoving}
>
{isRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex absolute right-3 bottom-3">
<div className="col-span-12 sm:col-span-6"> <Controller
<h4 className="text-lg font-semibold">Cover Photo</h4> control={control}
<p className="text-sm text-custom-text-200"> name="cover_image"
Select your cover photo from the given library. render={() => (
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<div className="h-32 w-full rounded border border-custom-border-200 p-1">
<div className="relative h-full w-full rounded">
<img
src={
watch("cover_image") ??
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
alt={myProfile?.name ?? "Cover image"}
/>
<div className="absolute bottom-0 flex w-full justify-end">
<ImagePickerPopover <ImagePickerPopover
label={"Change cover"} label={"Change cover"}
onChange={(imageUrl) => { onChange={(imageUrl) => {
@ -249,24 +219,45 @@ const Profile: NextPage = () => {
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab" "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
} }
/> />
)}
/>
</div> </div>
</div> </div>
<div className="flex item-center justify-between px-8 mt-4">
<div className="flex flex-col">
<div className="flex item-center text-lg font-semibold text-custom-text-100">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div> </div>
<span className="text-sm tracking-tight">{watch("email")}</span>
</div> </div>
<Link href={`/${workspaceSlug}/profile/${myProfile.id}`}>
<a className="flex item-center cursor-pointer gap-2 h-4 leading-4 text-sm text-custom-primary-100">
<span className="h-4 w-4">
<UserCircle className="h-4 w-4" />
</span>
View Profile
</a>
</Link>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4> <div className="flex flex-col gap-1">
</div> <h4 className="text-sm">First Name</h4>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input <Input
name="first_name" name="first_name"
id="first_name" id="first_name"
register={register} register={register}
error={errors.first_name} error={errors.first_name}
placeholder="Enter your first name" placeholder="Enter your first name"
className="!px-3 !py-2 rounded-md font-medium"
autoComplete="off" autoComplete="off"
/> />
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Input <Input
name="last_name" name="last_name"
register={register} register={register}
@ -274,18 +265,56 @@ const Profile: NextPage = () => {
id="last_name" id="last_name"
placeholder="Enter your last name" placeholder="Enter your last name"
autoComplete="off" autoComplete="off"
className="!px-3 !py-2 rounded-md font-medium"
/> />
</div> </div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Email</h4>
<Input
id="email"
name="email"
autoComplete="off"
register={register}
className="!px-3 !py-2 rounded-md font-medium"
error={errors.name}
disabled
/>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="flex flex-col gap-1">
<h4 className="text-lg font-semibold text-custom-text-100">Display Name</h4> <h4 className="text-sm">Role</h4>
<p className="text-sm text-custom-text-200"> <Controller
This could be your first name, or a nickname however you{"'"}d like people to name="role"
refer to you in Plane. control={control}
</p> rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
width="w-full"
input
verticalPosition="top"
position="right"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && (
<span className="text-xs text-red-500">Please select a role</span>
)}
</div> </div>
<div className="col-span-12 sm:col-span-6">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Input <Input
id="display_name" id="display_name"
name="display_name" name="display_name"
@ -313,63 +342,10 @@ const Profile: NextPage = () => {
}} }}
/> />
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex flex-col gap-1">
<div className="col-span-12 sm:col-span-6"> <h4 className="text-sm">Timezone </h4>
<h4 className="text-lg font-semibold text-custom-text-100">Email</h4>
<p className="text-sm text-custom-text-200">
The email address that you are using.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="email"
name="email"
autoComplete="off"
register={register}
error={errors.name}
className="w-full"
disabled
/>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Role</h4>
<p className="text-sm text-custom-text-200">Add your role.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
name="role"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500 bg-red-500/10" : ""}
width="w-full"
input
position="right"
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Timezone</h4>
<p className="text-sm text-custom-text-200">Select a timezone</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller <Controller
name="user_timezone" name="user_timezone"
control={control} control={control}
@ -390,16 +366,20 @@ const Profile: NextPage = () => {
/> />
)} )}
/> />
{errors.role && <span className="text-xs text-red-500">Please select a role</span>} {errors.role && (
<span className="text-xs text-red-500">Please select a role</span>
)}
</div>
<div className="flex items-center justify-between py-2">
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating Project..." : "Update Project"}
</PrimaryButton>
</div>
</div> </div>
</div> </div>
<div className="sm:text-right">
<SecondaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update profile"}
</SecondaryButton>
</div> </div>
</form> </form>
</div>
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />

View File

@ -3,7 +3,6 @@ import { useEffect, useState } from "react";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import SettingsNavbar from "layouts/settings-navbar";
// components // components
import { CustomThemeSelector, ThemeSwitch } from "components/core"; import { CustomThemeSelector, ThemeSwitch } from "components/core";
// ui // ui
@ -15,6 +14,7 @@ import { ICustomTheme } from "types";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { SettingsSidebar } from "components/project";
const ProfilePreferences = observer(() => { const ProfilePreferences = observer(() => {
const { user: myProfile } = useUserAuth(); const { user: myProfile } = useUserAuth();
@ -59,18 +59,16 @@ const ProfilePreferences = observer(() => {
} }
> >
{myProfile ? ( {myProfile ? (
<div className="p-8"> <div className="flex flex-row gap-2">
<div className="mb-8 space-y-6"> <div className="w-80 py-8">
<div> <SettingsSidebar />
<h3 className="text-3xl font-semibold">Profile Settings</h3>
<p className="mt-1 text-custom-text-200">
This information will be visible to only you.
</p>
</div> </div>
<SettingsNavbar profilePage />
<div className="pr-9 py-8 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Acitivity</h3>
</div> </div>
<div className="space-y-8 sm:space-y-12"> <div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4> <h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">

View File

@ -0,0 +1,73 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// helper
import { truncateText } from "helpers/string.helper";
// components
import { IssuesFilterView, IssuesView } from "components/core";
// ui
import { Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { X, PenSquare } from "lucide-react";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys";
const ProjectDraftIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
return (
<IssueViewContextProvider>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`}
/>
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView />
</div>
}
>
<div className="h-full w-full flex flex-col">
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-3 w-3 text-custom-text-300" />
<span>Draft Issues</span>
<X className="h-3 w-3" />
</button>
</div>
<IssuesView />
</div>
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);
};
export default ProjectDraftIssues;

View File

@ -19,7 +19,8 @@ import { ToggleSwitch } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { ModuleIcon } from "components/icons"; import { ModuleIcon } from "components/icons";
import { Contrast, FileText, Inbox, Layers } from "lucide-react"; import { FileText, Inbox, Layers } from "lucide-react";
import { ContrastOutlined } from "@mui/icons-material";
// types // types
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -27,13 +28,32 @@ import type { NextPage } from "next";
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
//Posthog
import PosthogService from "services/posthog.service";
import {
TOGGLE_CYCLE_ON,
TOGGLE_CYCLE_OFF,
TOGGLE_MODULE_ON,
TOGGLE_MODULE_OFF,
TOGGLE_VIEW_ON,
TOGGLE_VIEW_OFF,
TOGGLE_PAGES_ON,
TOGGLE_PAGES_OFF,
TOGGLE_INBOX_ON,
TOGGLE_INBOX_OFF,
} from "constants/posthog-events";
const posthogService = new PosthogService();
const featuresList = [ const featuresList = [
{ {
title: "Cycles", title: "Cycles",
description: description:
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.", "Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
icon: <Contrast className="h-4 w-4 text-custom-primary-100 flex-shrink-0" />, icon: (
<ContrastOutlined className="!text-base !leading-4 text-purple-500 flex-shrink-0 rotate-180" />
),
property: "cycle_view", property: "cycle_view",
}, },
{ {
@ -61,7 +81,7 @@ const featuresList = [
title: "Inbox", title: "Inbox",
description: description:
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.", "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
icon: <Inbox className="h-4 w-4 text-cyan-500 flex-shrink-0" />, icon: <Inbox className="h-4 w-4 text-fuchsia-500 flex-shrink-0" />,
property: "inbox_view", property: "inbox_view",
}, },
]; ];
@ -83,6 +103,23 @@ const getEventType = (feature: string, toggle: boolean): MiscellaneousEventType
} }
}; };
const getPosthogEventType = (feature: string, toggle: boolean): MiscellaneousEventType => {
switch (feature) {
case "Cycles":
return toggle ? TOGGLE_CYCLE_ON : TOGGLE_CYCLE_OFF;
case "Modules":
return toggle ? TOGGLE_MODULE_ON : TOGGLE_MODULE_OFF;
case "Views":
return toggle ? TOGGLE_VIEW_ON : TOGGLE_VIEW_OFF;
case "Pages":
return toggle ? TOGGLE_PAGES_ON : TOGGLE_PAGES_OFF;
case "Inbox":
return toggle ? TOGGLE_INBOX_ON : TOGGLE_INBOX_OFF;
default:
throw new Error("Invalid feature");
}
};
const FeaturesSettings: NextPage = () => { const FeaturesSettings: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -191,6 +228,20 @@ const FeaturesSettings: NextPage = () => {
), ),
user user
); );
posthogService.capture(
getPosthogEventType(
feature.title,
!projectDetails?.[feature.property as keyof IProject]
),
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
projectId,
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
user
);
handleSubmit({ handleSubmit({
[feature.property]: !projectDetails?.[feature.property as keyof IProject], [feature.property]: !projectDetails?.[feature.property as keyof IProject],
}); });

View File

@ -25,7 +25,6 @@ import {
TextArea, TextArea,
Loader, Loader,
CustomSelect, CustomSelect,
SecondaryButton,
DangerButton, DangerButton,
Icon, Icon,
PrimaryButton, PrimaryButton,
@ -67,7 +66,7 @@ const GeneralSettings: NextPage = () => {
: null : null
); );
const { data: memberDetails, error } = useSWR( const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
@ -168,6 +167,7 @@ const GeneralSettings: NextPage = () => {
}; };
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network); const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
const isAdmin = memberDetails?.role === 20; const isAdmin = memberDetails?.role === 20;
@ -350,7 +350,7 @@ const GeneralSettings: NextPage = () => {
<CustomSelect <CustomSelect
value={value} value={value}
onChange={onChange} onChange={onChange}
label={currentNetwork?.label ?? "Select network"} label={selectedNetwork?.label ?? "Select network"}
className="!border-custom-border-200 !shadow-none" className="!border-custom-border-200 !shadow-none"
input input
disabled={!isAdmin} disabled={!isAdmin}
@ -388,7 +388,7 @@ const GeneralSettings: NextPage = () => {
)} )}
</div> </div>
</div> </div>
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-400"> <Disclosure as="div" className="border-t border-custom-border-400">
{({ open }) => ( {({ open }) => (
<div className="w-full"> <div className="w-full">
@ -397,8 +397,8 @@ const GeneralSettings: NextPage = () => {
type="button" type="button"
className="flex items-center justify-between w-full py-4" className="flex items-center justify-between w-full py-4"
> >
<span className="text-xl tracking-tight">Danger Zone</span> <span className="text-xl tracking-tight">Delete Project</span>
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" /> <Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button> </Disclosure.Button>
<Transition <Transition
@ -414,9 +414,9 @@ const GeneralSettings: NextPage = () => {
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight"> <span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that The danger zone of the project delete page is a critical area that
requires careful consideration and attention. When deleting a project, all requires careful consideration and attention. When deleting a project,
of the data and resources within that project will be permanently removed all of the data and resources within that project will be permanently
and cannot be recovered. removed and cannot be recovered.
</span> </span>
<div> <div>
{projectDetails ? ( {projectDetails ? (
@ -441,6 +441,7 @@ const GeneralSettings: NextPage = () => {
</div> </div>
)} )}
</Disclosure> </Disclosure>
)}
</div> </div>
</div> </div>
</form> </form>

View File

@ -113,11 +113,11 @@ const LabelsSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2 h-full w-full">
<div className="w-80 py-8"> <div className="w-80 py-8">
<SettingsSidebar /> <SettingsSidebar />
</div> </div>
<section className="pr-9 py-8 gap-10 w-full"> <section className="pr-9 py-8 gap-10 h-full w-full">
<div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200"> <div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Labels</h3> <h3 className="text-xl font-medium">Labels</h3>
@ -129,7 +129,7 @@ const LabelsSettings: NextPage = () => {
Add label Add label
</PrimaryButton> </PrimaryButton>
</div> </div>
<div className="space-y-3 py-6"> <div className="space-y-3 py-6 h-full w-full">
{labelForm && ( {labelForm && (
<CreateUpdateLabelInline <CreateUpdateLabelInline
labelForm={labelForm} labelForm={labelForm}

View File

@ -334,7 +334,7 @@ const MembersSettings: NextPage = () => {
</div> </div>
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200"> <div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium border-b border-custom-border-100">Members</h4> <h4 className="text-xl font-medium">Members</h4>
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton> <PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
</div> </div>
{!projectMembers || !projectInvitations ? ( {!projectMembers || !projectInvitations ? (
@ -386,11 +386,13 @@ const MembersSettings: NextPage = () => {
<h4 className="text-sm">{member.display_name || member.email}</h4> <h4 className="text-sm">{member.display_name || member.email}</h4>
)} )}
{isOwner && ( {isOwner && (
<p className="mt-0.5 text-xs text-custom-text-200">{member.email}</p> <p className="mt-0.5 text-xs text-custom-sidebar-text-300">
{member.email}
</p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-3 text-xs">
{!member.member && ( {!member.member && (
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500"> <div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
Pending Pending

View File

@ -8,7 +8,8 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace"; // component
import { SettingsSidebar } from "components/project";
// ui // ui
import { SecondaryButton } from "components/ui"; import { SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -42,14 +43,17 @@ const BillingSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2 h-full overflow-hidden">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-8"> <SettingsSidebar />
<div>
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3>
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p>
</div> </div>
<div className="space-y-8 md:w-2/3"> <section className="pr-9 py-8 w-full">
<div>
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Billing & Plan</h3>
</div>
</div>
<div className="px-4 py-6">
<div> <div>
<h4 className="text-md mb-1 leading-6">Current plan</h4> <h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200"> <p className="mb-3 text-sm text-custom-text-200">

View File

@ -6,10 +6,9 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import ExportGuide from "components/exporter/guide"; import ExportGuide from "components/exporter/guide";
import { IntegrationAndImportExportBanner } from "components/ui"; import { SettingsSidebar } from "components/project";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
@ -41,11 +40,17 @@ const ImportExport: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8 space-y-4"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<IntegrationAndImportExportBanner bannerName="Export" /> <SettingsSidebar />
</div>
<div className="pr-9 py-8 overflow-y-auto w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Exports</h3>
</div>
<ExportGuide /> <ExportGuide />
</div> </div>
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -6,10 +6,9 @@ import useSWR from "swr";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import IntegrationGuide from "components/integration/guide"; import IntegrationGuide from "components/integration/guide";
import { IntegrationAndImportExportBanner } from "components/ui"; import { SettingsSidebar } from "components/project";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
@ -41,15 +40,19 @@ const ImportExport: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8 space-y-4"> <div className="flex flex-row gap-2 h-full overflow-hidden">
<SettingsHeader /> <div className="w-80 py-8">
<IntegrationAndImportExportBanner <SettingsSidebar />
bannerName="Import/ Export" </div>
description="Integrations and importers are only available on the cloud version. We plan to open-source <section className="pr-9 py-8 w-full">
our SDKs in the near future so that the community can request or contribute integrations as <div className="flex items-center py-3.5 border-b border-custom-border-200">
needed." <h3 className="text-xl font-medium">Imports</h3>
/> </div>
<IntegrationGuide /> <IntegrationGuide />
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Previous Imports</h3>
</div>
</section>
</div> </div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );

View File

@ -16,14 +16,16 @@ import useUserAuth from "hooks/use-user-auth";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components // components
import { ImageUploadModal } from "components/core"; import { ImageUploadModal } from "components/core";
import { DeleteWorkspaceModal, SettingsHeader } from "components/workspace"; import { DeleteWorkspaceModal } from "components/workspace";
import { SettingsSidebar } from "components/project";
// ui // ui
import { Spinner, Input, CustomSelect, SecondaryButton, DangerButton } from "components/ui"; import { Disclosure, Transition } from "@headlessui/react";
import { Spinner, Input, CustomSelect, DangerButton, PrimaryButton, Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { LinkIcon } from "@heroicons/react/24/outline"; import { Pencil } from "lucide-react";
// helpers // helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
import type { IWorkspace } from "types"; import type { IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -135,6 +137,7 @@ const WorkspaceSettings: NextPage = () => {
logo: "", logo: "",
}; };
}); });
setIsImageUploadModalOpen(false);
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -162,6 +165,8 @@ const WorkspaceSettings: NextPage = () => {
<ImageUploadModal <ImageUploadModal
isOpen={isImageUploadModalOpen} isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)} onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isImageRemoving}
handleDelete={() => handleDelete(activeWorkspace?.logo)}
onSuccess={(imageUrl) => { onSuccess={(imageUrl) => {
setIsImageUploading(true); setIsImageUploading(true);
setValue("logo", imageUrl); setValue("logo", imageUrl);
@ -178,26 +183,21 @@ const WorkspaceSettings: NextPage = () => {
data={activeWorkspace ?? null} data={activeWorkspace ?? null}
user={user} user={user}
/> />
<div className="p-8"> <div className="flex flex-row gap-2 h-full w-full">
<SettingsHeader /> <div className="w-80 py-8">
{activeWorkspace ? ( <SettingsSidebar />
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Logo</h4>
<p className="text-sm text-custom-text-200">
Max file size is 5MB. Supported file types are .jpg and .png.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-6"> {activeWorkspace ? (
<div className="flex items-center gap-4"> <div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
<div className="flex flex-col gap-1">
<button <button
type="button" type="button"
onClick={() => setIsImageUploadModalOpen(true)} onClick={() => setIsImageUploadModalOpen(true)}
disabled={!isAdmin} disabled={!isAdmin}
> >
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? ( {watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-12 w-12"> <div className="relative mx-auto flex h-14 w-14">
<img <img
src={watch("logo")!} src={watch("logo")!}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
@ -205,81 +205,41 @@ const WorkspaceSettings: NextPage = () => {
/> />
</div> </div>
) : ( ) : (
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white"> <div className="relative flex h-14 w-14 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white">
{activeWorkspace?.name?.charAt(0) ?? "N"} {activeWorkspace?.name?.charAt(0) ?? "N"}
</div> </div>
)} )}
</button> </button>
{isAdmin && (
<div className="flex gap-4">
<SecondaryButton
onClick={() => {
setIsImageUploadModalOpen(true);
}}
>
{isImageUploading ? "Uploading..." : "Upload"}
</SecondaryButton>
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
<DangerButton
onClick={() => handleDelete(activeWorkspace.logo)}
loading={isImageRemoving}
>
{isImageRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
</div> </div>
)}
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">URL</h4>
<p className="text-sm text-custom-text-200">Your workspace URL.</p>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Input <h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
id="url" <span className="text-sm tracking-tight">{`${
name="url"
autoComplete="off"
register={register}
error={errors.url}
className="w-full"
value={`${
typeof window !== "undefined" && typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "") window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`} }/${activeWorkspace.slug}`}</span>
disabled <div className="flex item-center gap-2.5">
/> <button
</div> className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
<SecondaryButton onClick={() => setIsImageUploadModalOpen(true)}
className="h-min" disabled={!isAdmin}
onClick={() =>
copyTextToClipboard(
`${typeof window !== "undefined" && window.location.origin}/${
activeWorkspace.slug
}`
).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Workspace link copied to clipboard.",
});
})
}
outline
> >
<LinkIcon className="h-[18px] w-[18px]" /> {watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
</SecondaryButton> <>
<Pencil className="h-3 w-3" />
Edit logo
</>
) : (
"Upload logo"
)}
</button>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Name</h4>
<p className="text-sm text-custom-text-200">Give a name to your workspace.</p>
</div> </div>
<div className="col-span-12 sm:col-span-6">
<div className="flex flex-col gap-8 my-10">
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace Name</h4>
<Input <Input
id="name" id="name"
name="name" name="name"
@ -297,13 +257,9 @@ const WorkspaceSettings: NextPage = () => {
disabled={!isAdmin} disabled={!isAdmin}
/> />
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex flex-col gap-1 ">
<div className="col-span-12 sm:col-span-6"> <h4 className="text-sm">Company Size</h4>
<h4 className="text-lg font-semibold">Organization Size</h4>
<p className="text-sm text-custom-text-200">What size is your organization?</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller <Controller
name="organization_size" name="organization_size"
control={control} control={control}
@ -327,40 +283,83 @@ const WorkspaceSettings: NextPage = () => {
)} )}
/> />
</div> </div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace URL</h4>
<Input
id="url"
name="url"
autoComplete="off"
register={register}
error={errors.url}
className="w-full"
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}
disabled
/>
</div>
</div> </div>
{isAdmin && ( <div className="flex items-center justify-between py-2">
<> <PrimaryButton
<div className="sm:text-right">
<SecondaryButton
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}
loading={isSubmitting} loading={isSubmitting}
disabled={!isAdmin} disabled={!isAdmin}
> >
{isSubmitting ? "Updating..." : "Update Workspace"} {isSubmitting ? "Updating..." : "Update Workspace"}
</SecondaryButton> </PrimaryButton>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the workspace delete page is a critical area that requires
careful consideration and attention. When deleting a workspace, all of the
data and resources within that workspace will be permanently removed and
cannot be recovered.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline> <Disclosure as="div" className="border-t border-custom-border-400">
Delete the workspace {({ open }) => (
<div className="w-full">
<Disclosure.Button
as="button"
type="button"
className="flex items-center justify-between w-full py-4"
>
<span className="text-xl tracking-tight">Delete Workspace</span>
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that
requires careful consideration and attention. When deleting a project, all
of the data and resources within that project will be permanently removed
and cannot be recovered.
</span>
<div>
<DangerButton
onClick={() => setIsOpen(true)}
className="!text-sm"
outline
>
Delete my project
</DangerButton> </DangerButton>
</div> </div>
</div> </div>
</> </Disclosure.Panel>
</Transition>
</div>
)} )}
</Disclosure>
</div> </div>
) : ( ) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0"> <div className="flex items-center justify-center h-full w-full px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
)} )}

View File

@ -9,9 +9,9 @@ import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration"; import IntegrationService from "services/integration";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import { SingleIntegrationCard } from "components/integration"; import { SingleIntegrationCard } from "components/integration";
import { SettingsSidebar } from "components/project";
// ui // ui
import { IntegrationAndImportExportBanner, Loader } from "components/ui"; import { IntegrationAndImportExportBanner, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -48,19 +48,21 @@ const WorkspaceIntegrations: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
</div>
<section className="pr-9 py-8 w-full">
<IntegrationAndImportExportBanner bannerName="Integrations" /> <IntegrationAndImportExportBanner bannerName="Integrations" />
<div className="space-y-5"> <div>
{appIntegrations ? ( {appIntegrations ? (
appIntegrations.map((integration) => ( appIntegrations.map((integration) => (
<SingleIntegrationCard key={integration.id} integration={integration} /> <SingleIntegrationCard key={integration.id} integration={integration} />
)) ))
) : ( ) : (
<Loader className="space-y-5"> <Loader className="space-y-1">
<Loader.Item height="60px" /> <Loader.Item height="89px" />
<Loader.Item height="60px" /> <Loader.Item height="89px" />
</Loader> </Loader>
)} )}
</div> </div>

View File

@ -13,15 +13,15 @@ import useUser from "hooks/use-user";
import useWorkspaceMembers from "hooks/use-workspace-members"; import useWorkspaceMembers from "hooks/use-workspace-members";
// layouts // layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { SettingsHeader } from "components/workspace";
// components // components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove"; import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal"; import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
import { SettingsSidebar } from "components/project";
// ui // ui
import { CustomMenu, CustomSelect, Loader } from "components/ui"; import { CustomMenu, CustomSelect, Icon, Loader, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "components/icons";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
@ -143,8 +143,7 @@ const MembersSettings: NextPage = () => {
}); });
}) })
.finally(() => { .finally(() => {
mutateMembers( mutateMembers((prevData: any) =>
(prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedRemoveMember) prevData?.filter((item: any) => item.id !== selectedRemoveMember)
); );
}); });
@ -187,19 +186,14 @@ const MembersSettings: NextPage = () => {
user={user} user={user}
onSuccess={handleInviteModalSuccess} onSuccess={handleInviteModalSuccess}
/> />
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
<div className="flex items-end justify-between gap-4"> </div>
<h3 className="text-2xl font-semibold">Members</h3> <section className="pr-9 py-8 w-full">
<button <div className="flex items-center justify-between gap-4 pt-2 pb-3.5 border-b border-custom-border-200">
type="button" <h4 className="text-xl font-medium">Members</h4>
className="flex items-center gap-2 text-custom-primary outline-none" <PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
Add Member
</button>
</div> </div>
{!workspaceMembers || !workspaceInvitations ? ( {!workspaceMembers || !workspaceInvitations ? (
<Loader className="space-y-5"> <Loader className="space-y-5">
@ -209,23 +203,30 @@ const MembersSettings: NextPage = () => {
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
) : ( ) : (
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 px-6"> <div className="divide-y divide-custom-border-200">
{members.length > 0 {members.length > 0
? members.map((member) => ( ? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6"> <div
key={member.id}
className="group flex items-center justify-between px-3.5 py-[18px]"
>
<div className="flex items-center gap-x-8 gap-y-2"> <div className="flex items-center gap-x-8 gap-y-2">
{member.avatar && member.avatar !== "" ? ( {member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white"> <Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
<img <img
src={member.avatar} src={member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg" className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
alt={member.display_name || member.email} alt={member.display_name || member.email}
/> />
</div> </a>
</Link>
) : member.display_name || member.email ? ( ) : member.display_name || member.email ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white"> <Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
<a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
{(member.display_name || member.email)?.charAt(0)} {(member.display_name || member.email)?.charAt(0)}
</div> </a>
</Link>
) : ( ) : (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white"> <div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
? ?
@ -244,14 +245,18 @@ const MembersSettings: NextPage = () => {
</a> </a>
</Link> </Link>
) : ( ) : (
<h4 className="text-sm">{member.display_name || member.email}</h4> <h4 className="text-sm cursor-default">
{member.display_name || member.email}
</h4>
)} )}
{isOwner && ( {isOwner && (
<p className="text-xs text-custom-text-200">{member.email}</p> <p className="mt-0.5 text-xs text-custom-sidebar-text-300">
{member.email}
</p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-3 text-xs">
{!member?.status && ( {!member?.status && (
<div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500"> <div className="mr-2 flex items-center justify-center rounded-full bg-yellow-500/20 px-2 py-1 text-center text-xs text-yellow-500">
<p>Pending</p> <p>Pending</p>
@ -263,9 +268,22 @@ const MembersSettings: NextPage = () => {
</div> </div>
)} )}
<CustomSelect <CustomSelect
label={ROLE[member.role as keyof typeof ROLE]} customButton={
<button className="flex item-center gap-1">
<span
className={`flex items-center text-sm font-medium ${
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
</span>
{member.memberId !== user?.id && (
<Icon iconName="expand_more" className="text-lg font-medium" />
)}
</button>
}
value={member.role} value={member.role}
onChange={(value: any) => { onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
mutateMembers( mutateMembers(
@ -323,7 +341,14 @@ const MembersSettings: NextPage = () => {
} }
}} }}
> >
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>
{" "}
{user?.id === member.memberId ? "Leave" : "Remove member"} {user?.id === member.memberId ? "Leave" : "Remove member"}
</span>
</span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>

View File

@ -32,13 +32,6 @@ class MyDocument extends Document {
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest.json" /> <link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href="/favicon/favicon.ico" /> <link rel="shortcut icon" href="/favicon/favicon.ico" />
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
<script
defer
data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
src="https://plausible.io/js/script.js"
/>
)}
{isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && ( {isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
<Script id="clarity-tracking"> <Script id="clarity-tracking">
{`(function(c,l,a,r,i,t,y){ {`(function(c,l,a,r,i,t,y){
@ -52,6 +45,19 @@ class MyDocument extends Document {
<body> <body>
<Main /> <Main />
<NextScript /> <NextScript />
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
<script
defer
data-domain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
src="https://plausible.io/js/script.js"
/>
)}
{process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && (
<Script id="posthog-tracking">
{`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${process.env.NEXT_PUBLIC_POSTHOG_KEY}',{api_host:'${process.env.NEXT_PUBLIC_POSTHOG_HOST}'})`}
</Script>
)}
</body> </body>
</Html> </Html>
); );

View File

@ -0,0 +1,54 @@
<svg width="201" height="150" viewBox="0 0 201 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2267_36125" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="101" height="150">
<rect width="101" height="150" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_2267_36125)">
<path d="M1.25 4.00001C1.25 1.92894 2.92893 0.25 5 0.25H225C227.071 0.25 228.75 1.92893 228.75 4V189.75H1.25V4.00001Z" fill="#231035" stroke="#3F2B58" stroke-width="0.5"/>
<path d="M2 4C2 2.34315 3.34315 1 5 1H204C205.657 1 207 2.34315 207 4V10H2V4Z" fill="#32184C"/>
<line x1="51.25" y1="10" x2="51.25" y2="190" stroke="#3F2B58" stroke-width="0.5"/>
<line x1="1" y1="9.75" x2="211" y2="9.75002" stroke="#3F2B58" stroke-width="0.5"/>
<rect x="5" y="14" width="36" height="6" rx="1" fill="#572D81"/>
<rect x="5" y="26" width="39" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="35" width="31" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="53" width="26" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="80" width="34" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="44" width="35" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="62" width="29" height="4" rx="1" fill="#371B52"/>
<rect x="5" y="71" width="38" height="4" rx="1" fill="#371B52"/>
<rect x="43" y="14" width="6" height="6" rx="3" fill="#572D81"/>
<rect x="66" y="44" width="51" height="4" rx="1" fill="#572D81"/>
<rect x="66" y="53" width="132" height="4" rx="1" fill="#371B52"/>
<rect x="66" y="60" width="97" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="76" width="52" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="87" width="78" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="98" width="71" height="4" rx="1" fill="#371B52"/>
<rect x="75" y="109" width="58" height="4" rx="1" fill="#371B52"/>
<rect x="66" y="75" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="86" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="97" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="108" width="6" height="6" rx="3" fill="#401E60"/>
<rect x="66" y="26" width="12" height="12" rx="6" fill="#572D81"/>
<rect x="5" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="9" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="13" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
</g>
<mask id="mask1_2267_36125" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="101" y="0" width="100" height="150">
<rect x="101" width="100" height="150" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask1_2267_36125)">
<path d="M0.5 4.00001C0.5 2.06701 2.067 0.5 4 0.5H224C225.933 0.5 227.5 2.067 227.5 4V189.5H0.5V4.00001Z" fill="#000C1B" stroke="#172534"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="#001936"/>
<line x1="2.18557e-08" y1="9.75" x2="210" y2="9.75002" stroke="#172534" stroke-width="0.5"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#172B52"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#151E3D"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#151E3D"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#151E3D"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#151E3D"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#151E3D"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#151E3D"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#151E3D"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,33 @@
<svg width="462" height="536" viewBox="0 0 462 536" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.827922 13.2468C0.827922 6.38804 6.38802 0.827922 13.2468 0.827922H741.818C748.677 0.827922 754.237 6.38802 754.237 13.2468V628.393H0.827922V13.2468Z" fill="white" stroke="#E5E5E5" stroke-width="1.65584"/>
<path d="M0.827922 13.2468C0.827922 6.38802 6.38802 0.827922 13.2468 0.827922H672.273C677.302 0.827922 681.38 4.90533 681.38 9.93507V32.289H0.827922V13.2468Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="1.65584"/>
<line x1="166.414" y1="33.1162" x2="166.414" y2="629.22" stroke="#E5E5E5" stroke-width="1.65584"/>
<line x1="-7.23793e-08" y1="32.2883" x2="695.455" y2="32.2882" stroke="#E5E5E5" stroke-width="1.65584"/>
<rect x="13.25" y="46.3633" width="119.221" height="19.8701" rx="3.31169" fill="#E5E5E5"/>
<rect x="13.25" y="86.1035" width="129.156" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="115.908" width="102.662" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="175.519" width="86.1039" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="264.935" width="112.597" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="145.714" width="115.909" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="205.324" width="96.039" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="13.25" y="235.129" width="125.844" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="139.094" y="46.3633" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="145.715" width="168.896" height="13.2468" rx="3.31169" fill="#E5E5E5"/>
<rect x="397.406" y="145.715" width="43.0519" height="13.2468" rx="3.31169" fill="#3F76FF"/>
<rect x="215.266" y="175.521" width="437.143" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="215.266" y="198.702" width="321.234" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="251.688" width="172.208" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="288.117" width="258.312" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="324.546" width="235.13" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="245.07" y="360.975" width="192.078" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="427.219" y="251.688" width="19.8701" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="447.078" y="360.975" width="19.8701" height="13.2468" rx="3.31169" fill="#F1F1F1"/>
<rect x="215.266" y="248.378" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="284.806" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="321.234" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="357.663" width="19.8701" height="19.8701" rx="9.93506" fill="#E5E5E5"/>
<rect x="215.266" y="86.1045" width="39.7403" height="39.7403" rx="19.8701" fill="#D4D4D4"/>
<rect x="13.2422" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#EF4444"/>
<rect x="26.4922" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#FCD34D"/>
<rect x="39.7422" y="13.2471" width="9.93506" height="9.93506" rx="4.96753" fill="#4ADE80"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,34 @@
<svg width="201" height="151" viewBox="0 0 201 151" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="#171717" stroke="white" stroke-width="0.5"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="#171717"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="white" stroke-width="0.5"/>
<line x1="2.18557e-08" y1="9.75" x2="210" y2="9.75002" stroke="white" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="white"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#D4D4D4"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#D4D4D4"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#F1F1F1"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#D4D4D4"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#D4D4D4"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#D4D4D4"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#D4D4D4"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="white"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#F1F1F1"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,41 @@
<svg width="201" height="154" viewBox="0 0 201 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2273_36428)">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="#121212" stroke="#262626" stroke-width="0.5"/>
<path d="M0.25 4C0.25 1.92893 1.92893 0.25 4 0.25H203C204.519 0.25 205.75 1.48122 205.75 3V9.75H0.25V4Z" fill="#222222" stroke="#222222" stroke-width="0.5"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="#262626" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="#404040"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#262626"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#262626"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#262626"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#262626"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#262626"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#262626"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#262626"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="#404040"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#404040"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#262626"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#262626"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#262626"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#262626"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#262626"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#262626"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#262626"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#262626"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#262626"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#262626"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="#222222"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#404040"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
<rect x="121" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_2273_36428">
<rect width="201" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,42 @@
<svg width="204" height="154" viewBox="0 0 204 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2273_36429)">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="white" stroke="black" stroke-width="0.5"/>
<path d="M1 4C1 2.34315 2.34315 1 4 1H203C204.657 1 206 2.34315 206 4V10H1V4Z" fill="white"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="black" stroke-width="0.5"/>
<line x1="-2.18557e-08" y1="9.75" x2="210" y2="9.74998" stroke="black" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="#2E2E2E"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#404040"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#404040"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#404040"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#404040"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#404040"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#404040"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#404040"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="#2E2E2E"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#2E2E2E"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#404040"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#404040"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#404040"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#404040"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#404040"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#404040"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#404040"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#404040"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#404040"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#404040"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="#3A3A3A"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#262626"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
<rect x="121" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
</g>
<defs>
<clipPath id="clip0_2273_36429">
<rect width="204" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,42 @@
<svg width="201" height="154" viewBox="0 0 201 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2273_36427)">
<path d="M0.25 4.00001C0.25 1.92894 1.92893 0.25 4 0.25H224C226.071 0.25 227.75 1.92893 227.75 4V189.75H0.25V4.00001Z" fill="white" stroke="#E5E5E5" stroke-width="0.5"/>
<path d="M0.25 4C0.25 1.92893 1.92893 0.25 4 0.25H203C204.519 0.25 205.75 1.48122 205.75 3V9.75H0.25V4Z" fill="#F5F5F5" stroke="#E5E5E5" stroke-width="0.5"/>
<line x1="50.25" y1="10" x2="50.25" y2="190" stroke="#E5E5E5" stroke-width="0.5"/>
<line x1="-2.18557e-08" y1="9.75" x2="210" y2="9.74998" stroke="#E5E5E5" stroke-width="0.5"/>
<rect x="4" y="14" width="36" height="6" rx="1" fill="#E5E5E5"/>
<rect x="4" y="26" width="39" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="35" width="31" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="53" width="26" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="80" width="34" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="44" width="35" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="62" width="29" height="4" rx="1" fill="#F1F1F1"/>
<rect x="4" y="71" width="38" height="4" rx="1" fill="#F1F1F1"/>
<rect x="42" y="14" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="44" width="51" height="4" rx="1" fill="#E5E5E5"/>
<rect x="120" y="44" width="13" height="4" rx="1" fill="#3F76FF"/>
<rect x="65" y="53" width="132" height="4" rx="1" fill="#F1F1F1"/>
<rect x="65" y="60" width="97" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="76" width="52" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="87" width="78" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="98" width="71" height="4" rx="1" fill="#F1F1F1"/>
<rect x="74" y="109" width="58" height="4" rx="1" fill="#F1F1F1"/>
<rect x="129" y="76" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="155" y="87" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="148" y="98" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="135" y="109" width="6" height="4" rx="1" fill="#F1F1F1"/>
<rect x="65" y="75" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="86" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="97" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="108" width="6" height="6" rx="3" fill="#E5E5E5"/>
<rect x="65" y="26" width="12" height="12" rx="6" fill="#D4D4D4"/>
<rect x="4" y="4" width="3" height="3" rx="1.5" fill="#EF4444"/>
<rect x="8" y="4" width="3" height="3" rx="1.5" fill="#FCD34D"/>
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="#4ADE80"/>
</g>
<defs>
<clipPath id="clip0_2273_36427">
<rect width="201" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -4,6 +4,10 @@ import trackEventServices from "services/track-event.service";
import { ICurrentUserResponse, IGptResponse } from "types"; import { ICurrentUserResponse, IGptResponse } from "types";
// helpers // helpers
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "./posthog.service";
import { ASK_GPT } from "constants/posthog-events";
const posthogService = new PosthogService();
class AiServices extends APIService { class AiServices extends APIService {
constructor() { constructor() {
@ -19,6 +23,7 @@ class AiServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user); trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user);
posthogService.capture(ASK_GPT, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -4,6 +4,10 @@ import trackEventServices from "services/track-event.service";
// types // types
import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types"; import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "./posthog.service";
import { CYCLE_CREATE, CYCLE_UPDATE, CYCLE_DELETE } from "constants/posthog-events";
const posthogService = new PosthogService();
class ProjectCycleServices extends APIService { class ProjectCycleServices extends APIService {
constructor() { constructor() {
@ -19,6 +23,8 @@ class ProjectCycleServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user); trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);
posthogService.capture(CYCLE_CREATE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -97,6 +103,7 @@ class ProjectCycleServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user); trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
posthogService.capture(CYCLE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -117,6 +124,7 @@ class ProjectCycleServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user); trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
posthogService.capture(CYCLE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -133,6 +141,7 @@ class ProjectCycleServices extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user); trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user);
posthogService.capture(CYCLE_DELETE, { workspaceSlug, projectId, cycleId }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -4,7 +4,10 @@ import APIService from "services/api.service";
import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types"; import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types";
import trackEventServices from "services/track-event.service"; import trackEventServices from "services/track-event.service";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "./posthog.service";
import { ESTIMATE_CREATE, ESTIMATE_UPDATE, ESTIMATE_DELETE } from "constants/posthog-events";
const posthogService = new PosthogService();
class ProjectEstimateServices extends APIService { class ProjectEstimateServices extends APIService {
constructor() { constructor() {
super(API_BASE_URL); super(API_BASE_URL);
@ -19,6 +22,7 @@ class ProjectEstimateServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user); trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user);
posthogService.capture(ESTIMATE_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -39,6 +43,7 @@ class ProjectEstimateServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user); trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user);
posthogService.capture(ESTIMATE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -79,6 +84,7 @@ class ProjectEstimateServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user); trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user);
posthogService.capture(ESTIMATE_DELETE, { workspaceSlug, projectId, estimateId }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -10,6 +10,14 @@ import type {
ICurrentUserResponse, ICurrentUserResponse,
IInboxQueryParams, IInboxQueryParams,
} from "types"; } from "types";
import PosthogService from "./posthog.service";
import {
INBOX_ISSUE_CREATE,
INBOX_ISSUE_UPDATE,
INBOX_ISSUE_DELETE,
} from "constants/posthog-events";
const posthogService = new PosthogService();
class InboxServices extends APIService { class InboxServices extends APIService {
constructor() { constructor() {
@ -91,6 +99,11 @@ class InboxServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user); trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user);
posthogService.capture(
INBOX_ISSUE_DELETE,
{ workspaceSlug, projectId, inboxId, inboxIssueId },
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -141,6 +154,7 @@ class InboxServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user); trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user);
posthogService.capture(INBOX_ISSUE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -161,6 +175,7 @@ class InboxServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user); trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user);
posthogService.capture(INBOX_ISSUE_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -2,7 +2,10 @@ import APIService from "services/api.service";
import trackEventServices from "services/track-event.service"; import trackEventServices from "services/track-event.service";
import { ICurrentUserResponse } from "types"; import { ICurrentUserResponse } from "types";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "../posthog.service";
import { CSV_EXPORTER_CREATE } from "constants/posthog-events";
const posthogService = new PosthogService();
class CSVIntegrationService extends APIService { class CSVIntegrationService extends APIService {
constructor() { constructor() {
super(API_BASE_URL); super(API_BASE_URL);
@ -25,6 +28,7 @@ class CSVIntegrationService extends APIService {
"CSV_EXPORTER_CREATE", "CSV_EXPORTER_CREATE",
user user
); );
posthogService.capture(CSV_EXPORTER_CREATE, { workspaceSlug, data }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -3,6 +3,10 @@ import trackEventServices from "services/track-event.service";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types"; import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types";
import PosthogService from "../posthog.service";
import { GITHUB_IMPORTER_CREATE } from "constants/posthog-events";
const posthogService = new PosthogService();
const integrationServiceType: string = "github"; const integrationServiceType: string = "github";
class GithubIntegrationService extends APIService { class GithubIntegrationService extends APIService {
@ -44,6 +48,8 @@ class GithubIntegrationService extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user); trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user);
posthogService.capture(GITHUB_IMPORTER_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -3,6 +3,10 @@ import trackEventServices from "services/track-event.service";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
// types // types
import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types"; import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types";
import PosthogService from "../posthog.service";
import { JIRA_IMPORTER_CREATE } from "constants/posthog-events";
const posthogService = new PosthogService();
class JiraImportedService extends APIService { class JiraImportedService extends APIService {
constructor() { constructor() {
@ -27,6 +31,7 @@ class JiraImportedService extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user); trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user);
posthogService.capture(JIRA_IMPORTER_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -12,7 +12,17 @@ import type {
} from "types"; } from "types";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "./posthog.service"; import PosthogService from "./posthog.service";
import { ISSUE_CREATE } from "constants/posthog-events"; import {
ISSUE_CREATE,
ISSUE_UPDATE,
ISSUE_DELETE,
ISSUE_COMMENT_CREATE,
ISSUE_COMMENT_UPDATE,
ISSUE_COMMENT_DELETE,
ISSUE_LABEL_CREATE,
ISSUE_LABEL_UPDATE,
ISSUE_LABEL_DELETE,
} from "constants/posthog-events";
const posthogService = new PosthogService(); const posthogService = new PosthogService();
@ -234,6 +244,7 @@ class ProjectIssuesServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user); trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user);
posthogService.capture(ISSUE_COMMENT_CREATE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -255,6 +266,7 @@ class ProjectIssuesServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user); trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user);
posthogService.capture(ISSUE_COMMENT_UPDATE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -281,6 +293,15 @@ class ProjectIssuesServices extends APIService {
"ISSUE_COMMENT_DELETE", "ISSUE_COMMENT_DELETE",
user user
); );
posthogService.capture(
ISSUE_COMMENT_DELETE,
{
issueId,
commentId,
},
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -326,6 +347,21 @@ class ProjectIssuesServices extends APIService {
"ISSUE_LABEL_CREATE", "ISSUE_LABEL_CREATE",
user user
); );
posthogService.capture(
ISSUE_LABEL_CREATE,
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -359,6 +395,21 @@ class ProjectIssuesServices extends APIService {
"ISSUE_LABEL_UPDATE", "ISSUE_LABEL_UPDATE",
user user
); );
posthogService.capture(
ISSUE_LABEL_UPDATE,
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -384,6 +435,14 @@ class ProjectIssuesServices extends APIService {
"ISSUE_LABEL_DELETE", "ISSUE_LABEL_DELETE",
user user
); );
posthogService.capture(
ISSUE_LABEL_DELETE,
{
workspaceSlug,
projectId,
},
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -404,6 +463,7 @@ class ProjectIssuesServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user); trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user);
posthogService.capture(ISSUE_UPDATE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -420,6 +480,7 @@ class ProjectIssuesServices extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user); trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user);
posthogService.capture(ISSUE_DELETE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -622,6 +683,68 @@ class ProjectIssuesServices extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
params,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async createDraftIssue(
workspaceSlug: string,
projectId: string,
data: any,
user: ICurrentUserResponse
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async updateDraftIssue(
workspaceSlug: string,
projectId: string,
issueId: string,
data: any,
user: ICurrentUserResponse
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
} }
export default new ProjectIssuesServices(); const projectIssuesServices = new ProjectIssuesServices();
export default projectIssuesServices;

View File

@ -4,6 +4,10 @@ import trackEventServices from "./track-event.service";
// types // types
import type { IModule, IIssue, ICurrentUserResponse } from "types"; import type { IModule, IIssue, ICurrentUserResponse } from "types";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "./posthog.service";
import { MODULE_CREATE, MODULE_UPDATE, MODULE_DELETE } from "constants/posthog-events";
const posthogService = new PosthogService();
class ProjectIssuesServices extends APIService { class ProjectIssuesServices extends APIService {
constructor() { constructor() {
@ -27,6 +31,7 @@ class ProjectIssuesServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user); trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user);
posthogService.capture(MODULE_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -47,6 +52,7 @@ class ProjectIssuesServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user); trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
posthogService.capture(MODULE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -79,6 +85,8 @@ class ProjectIssuesServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user); trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
posthogService.capture(MODULE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -97,6 +105,7 @@ class ProjectIssuesServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user); trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user);
posthogService.capture(MODULE_DELETE, { workspaceSlug, projectId, moduleId }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -4,6 +4,18 @@ import APIService from "services/api.service";
import trackEventServices from "services/track-event.service"; import trackEventServices from "services/track-event.service";
// types // types
import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types"; import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types";
import PosthogService from "./posthog.service";
import {
PAGE_CREATE,
PAGE_UPDATE,
PAGE_DELETE,
PAGE_BLOCK_CREATE,
PAGE_BLOCK_UPDATE,
PAGE_BLOCK_DELETE,
PAGE_BLOCK_CONVERTED_TO_ISSUE,
} from "constants/posthog-events";
const posthogService = new PosthogService();
class PageServices extends APIService { class PageServices extends APIService {
constructor() { constructor() {
@ -19,6 +31,7 @@ class PageServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user); trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user);
posthogService.capture(PAGE_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -39,6 +52,7 @@ class PageServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user); trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user);
posthogService.capture(PAGE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -55,6 +69,7 @@ class PageServices extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user); trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user);
posthogService.capture(PAGE_DELETE, { workspaceSlug, projectId, pageId }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -138,6 +153,7 @@ class PageServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user); trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user);
posthogService.capture(PAGE_BLOCK_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -174,6 +190,7 @@ class PageServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user); trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user);
posthogService.capture(PAGE_BLOCK_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -193,6 +210,11 @@ class PageServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user); trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user);
posthogService.capture(
PAGE_BLOCK_DELETE,
{ workspaceSlug, projectId, pageId, pageBlockId },
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -230,6 +252,7 @@ class PageServices extends APIService {
"PAGE_BLOCK_CONVERTED_TO_ISSUE", "PAGE_BLOCK_CONVERTED_TO_ISSUE",
user user
); );
posthogService.capture(PAGE_BLOCK_CONVERTED_TO_ISSUE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -15,6 +15,16 @@ import type {
IProjectViewProps, IProjectViewProps,
TProjectIssuesSearchParams, TProjectIssuesSearchParams,
} from "types"; } from "types";
import PosthogService from "./posthog.service";
import {
CREATE_PROJECT,
UPDATE_PROJECT,
DELETE_PROJECT,
PROJECT_MEMBER_INVITE,
PROJECT_MEMBER_LEAVE,
} from "constants/posthog-events";
const posthogService = new PosthogService();
export class ProjectServices extends APIService { export class ProjectServices extends APIService {
constructor() { constructor() {
@ -29,6 +39,7 @@ export class ProjectServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user); trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user);
posthogService.capture(CREATE_PROJECT, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -80,6 +91,7 @@ export class ProjectServices extends APIService {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user); trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user);
posthogService.capture(UPDATE_PROJECT, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -95,6 +107,7 @@ export class ProjectServices extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user); trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user);
posthogService.capture(DELETE_PROJECT, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -121,6 +134,17 @@ export class ProjectServices extends APIService {
"PROJECT_MEMBER_INVITE", "PROJECT_MEMBER_INVITE",
user user
); );
posthogService.capture(
PROJECT_MEMBER_INVITE,
{
workspaceId: response?.data?.workspace?.id,
workspaceSlug,
projectId,
projectName: response?.data?.project?.name,
memberEmail: response?.data?.member?.email,
},
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -152,6 +176,16 @@ export class ProjectServices extends APIService {
}, },
user user
); );
posthogService.capture(
PROJECT_MEMBER_LEAVE,
{
workspaceSlug,
projectId,
...response?.data,
},
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -10,6 +10,15 @@ import type {
IssueReactionForm, IssueReactionForm,
IssueCommentReactionForm, IssueCommentReactionForm,
} from "types"; } from "types";
import PosthogService from "./posthog.service";
import {
ISSUE_REACTION_CREATE,
ISSUE_COMMENT_REACTION_CREATE,
ISSUE_REACTION_DELETE,
ISSUE_COMMENT_REACTION_DELETE,
} from "constants/posthog-events";
const posthogService = new PosthogService();
class ReactionService extends APIService { class ReactionService extends APIService {
constructor() { constructor() {
@ -29,6 +38,8 @@ class ReactionService extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_CREATE", user); trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_CREATE", user);
posthogService.capture(ISSUE_REACTION_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -62,6 +73,8 @@ class ReactionService extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_DELETE", user); trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_DELETE", user);
posthogService.capture(ISSUE_REACTION_DELETE, { workspaceSlug, projectId, issueId, reaction }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -86,6 +99,7 @@ class ReactionService extends APIService {
"ISSUE_COMMENT_REACTION_CREATE", "ISSUE_COMMENT_REACTION_CREATE",
user user
); );
posthogService.capture(ISSUE_COMMENT_REACTION_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -123,6 +137,12 @@ class ReactionService extends APIService {
"ISSUE_COMMENT_REACTION_DELETE", "ISSUE_COMMENT_REACTION_DELETE",
user user
); );
posthogService.capture(
ISSUE_COMMENT_REACTION_DELETE,
{ workspaceSlug, projectId, commentId, reaction },
user
);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -5,6 +5,10 @@ import trackEventServices from "services/track-event.service";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
// types // types
import type { ICurrentUserResponse, IState, IStateResponse } from "types"; import type { ICurrentUserResponse, IState, IStateResponse } from "types";
import PosthogService from "./posthog.service";
import { STATE_CREATE, STATE_UPDATE, STATE_DELETE } from "constants/posthog-events";
const posthogService = new PosthogService();
class ProjectStateServices extends APIService { class ProjectStateServices extends APIService {
constructor() { constructor() {
@ -20,6 +24,7 @@ class ProjectStateServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackStateEvent(response?.data, "STATE_CREATE", user); trackEventServices.trackStateEvent(response?.data, "STATE_CREATE", user);
posthogService.capture(STATE_CREATE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -65,6 +70,7 @@ class ProjectStateServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user); trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user);
posthogService.capture(STATE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -85,6 +91,7 @@ class ProjectStateServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user); trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user);
posthogService.capture(STATE_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -101,6 +108,7 @@ class ProjectStateServices extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackStateEvent(response?.data, "STATE_DELETE", user); trackEventServices.trackStateEvent(response?.data, "STATE_DELETE", user);
posthogService.capture(STATE_DELETE, { workspaceSlug, projectId, stateId }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -5,6 +5,10 @@ import { IView } from "types/views";
import { ICurrentUserResponse } from "types"; import { ICurrentUserResponse } from "types";
// helpers // helpers
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
import PosthogService from "./posthog.service";
import { VIEW_CREATE, VIEW_UPDATE, VIEW_DELETE } from "constants/posthog-events";
const posthogService = new PosthogService();
class ViewServices extends APIService { class ViewServices extends APIService {
constructor() { constructor() {
@ -20,6 +24,7 @@ class ViewServices extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user); trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user);
posthogService.capture(VIEW_CREATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -37,6 +42,8 @@ class ViewServices extends APIService {
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data) return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user); trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
posthogService.capture(VIEW_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -57,6 +64,8 @@ class ViewServices extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user); trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
posthogService.capture(VIEW_UPDATE, response?.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -73,6 +82,8 @@ class ViewServices extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user); trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user);
posthogService.capture(VIEW_DELETE, { workspaceSlug, projectId, viewId }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {

View File

@ -15,6 +15,18 @@ import {
IWorkspaceBulkInviteFormData, IWorkspaceBulkInviteFormData,
IWorkspaceViewProps, IWorkspaceViewProps,
} from "types"; } from "types";
import PosthogService from "./posthog.service";
import {
CREATE_WORKSPACE,
UPDATE_WORKSPACE,
DELETE_WORKSPACE,
WORKSPACE_USER_INVITE,
WORKSPACE_USER_INVITE_ACCEPT,
WORKSPACE_USER_BULK_INVITE_ACCEPT,
WORKSPACE_MEMBER_LEAVE,
} from "constants/posthog-events";
const posthogService = new PosthogService();
class WorkspaceService extends APIService { class WorkspaceService extends APIService {
constructor() { constructor() {
@ -44,6 +56,7 @@ class WorkspaceService extends APIService {
return this.post("/api/workspaces/", data) return this.post("/api/workspaces/", data)
.then((response) => { .then((response) => {
trackEventServices.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE", user); trackEventServices.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE", user);
posthogService.capture(CREATE_WORKSPACE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -59,6 +72,7 @@ class WorkspaceService extends APIService {
return this.patch(`/api/workspaces/${workspaceSlug}/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE", user); trackEventServices.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE", user);
posthogService.capture(UPDATE_WORKSPACE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -73,6 +87,7 @@ class WorkspaceService extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/`) return this.delete(`/api/workspaces/${workspaceSlug}/`)
.then((response) => { .then((response) => {
trackEventServices.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE", user); trackEventServices.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE", user);
posthogService.capture(DELETE_WORKSPACE, { workspaceSlug }, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -88,6 +103,7 @@ class WorkspaceService extends APIService {
return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data) return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE", user); trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE", user);
posthogService.capture(WORKSPACE_USER_INVITE, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -110,6 +126,7 @@ class WorkspaceService extends APIService {
) )
.then((response) => { .then((response) => {
trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE_ACCEPT", user); trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE_ACCEPT", user);
posthogService.capture(WORKSPACE_USER_INVITE_ACCEPT, response.data, user);
return response?.data; return response?.data;
}) })
.catch((error) => { .catch((error) => {
@ -190,7 +207,10 @@ class WorkspaceService extends APIService {
async deleteWorkspaceMember(workspaceSlug: string, memberId: string): Promise<any> { async deleteWorkspaceMember(workspaceSlug: string, memberId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/members/${memberId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/members/${memberId}/`)
.then((response) => response?.data) .then((response) => {
posthogService.capture(WORKSPACE_MEMBER_LEAVE, { workspaceSlug, memberId });
return response?.data;
})
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });

View File

@ -118,6 +118,7 @@ export interface IIssue {
issue_module: IIssueModule | null; issue_module: IIssueModule | null;
labels: string[]; labels: string[];
label_details: any[]; label_details: any[];
is_draft: boolean;
labels_list: string[]; labels_list: string[];
links_list: IIssueLink[]; links_list: IIssueLink[];
link_count: number; link_count: number;

View File

@ -6647,13 +6647,20 @@ prosemirror-menu@^1.2.1:
prosemirror-history "^1.0.0" prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1:
version "1.18.1" version "1.18.1"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd"
integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw==
dependencies: dependencies:
orderedmap "^2.0.0" orderedmap "^2.0.0"
prosemirror-model@^1.19.0:
version "1.19.3"
resolved "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006"
integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==
dependencies:
orderedmap "^2.0.0"
prosemirror-schema-basic@^1.2.0: prosemirror-schema-basic@^1.2.0:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"