Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting

This commit is contained in:
gurusainath 2023-09-15 15:40:24 +05:30
commit f6d4ac95ed
58 changed files with 1794 additions and 1173 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,10 +2115,16 @@ class IssueRelationViewSet(BaseViewSet):
current_instance=None, current_instance=None,
) )
return Response( if relation == "blocking":
IssueRelationSerializer(issueRelation, many=True).data, return Response(
status=status.HTTP_201_CREATED, 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,
)
except IntegrityError as e: except IntegrityError as e:
if "already exists" in str(e): if "already exists" in str(e):
return Response( return Response(

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,21 +1092,40 @@ 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:
issue = Issue.objects.get(pk=current_instance.get("issue")) if current_instance.get("relation_type") == "blocked_by":
issue_activities.append( relation_type = "blocking"
IssueActivity( else:
issue_id=current_instance.get("issue"), relation_type = current_instance.get("relation_type")
actor=actor, issue = Issue.objects.get(pk=current_instance.get("issue"))
verb="deleted", issue_activities.append(
old_value=f"{project.identifier}-{issue.sequence_id}", IssueActivity(
new_value="", issue_id=current_instance.get("related_issue"),
field=f'{current_instance.get("relation_type")}', actor=actor,
project=project, verb="deleted",
workspace=project.workspace, old_value=f"{project.identifier}-{issue.sequence_id}",
comment=f'deleted the {current_instance.get("relation_type")} relation', new_value="",
old_identifier=current_instance.get("issue"), 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(
IssueActivity(
issue_id=current_instance.get("issue"),
actor=actor,
verb="deleted",
old_value=f"{project.identifier}-{issue.sequence_id}",
new_value="",
field=f'{current_instance.get("relation_type")}',
project=project,
workspace=project.workspace,
comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("related_issue"),
)
) )
)
def create_draft_issue_activity( def create_draft_issue_activity(

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

@ -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,15 +183,22 @@ 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">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <div className="flex items-center">
<PrimaryButton <DangerButton onClick={handleDelete} outline disabled={!value}>
onClick={handleSubmit} {isRemoving ? "Removing..." : "Remove"}
disabled={!image} </DangerButton>
loading={isImageUploading} </div>
> <div className="flex items-center gap-2">
{isImageUploading ? "Uploading..." : "Upload & Save"} <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
</PrimaryButton> <PrimaryButton
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
</PrimaryButton>
</div>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

@ -49,7 +49,8 @@ type Props = {
}; };
secondaryButton?: React.ReactNode; secondaryButton?: React.ReactNode;
}; };
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => 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

@ -19,7 +19,8 @@ type Props = {
disableUserActions: boolean; disableUserActions: boolean;
disableAddIssueOption?: boolean; disableAddIssueOption?: boolean;
dragDisabled: boolean; dragDisabled: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => 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,
@ -96,6 +98,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,29 +217,47 @@ 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>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}> {!isDraftIssue && (
Make a copy... <ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
</ContextMenu.Item> Make a copy...
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}> </ContextMenu.Item>
)}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
else handleDeleteIssue(issue);
}}
>
Delete issue Delete issue
</ContextMenu.Item> </ContextMenu.Item>
</> </>
)} )}
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}> {!isDraftIssue && (
Copy issue link <ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
</ContextMenu.Item> Copy issue link
<a
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item> </ContextMenu.Item>
</a> )}
{!isDraftIssue && (
<a
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
target="_blank"
rel="noreferrer noopener"
>
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
Open issue in new tab
</ContextMenu.Item>
</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>
<CustomMenu.MenuItem onClick={handleCopyText}> {!isDraftIssue && (
<div className="flex items-center justify-start gap-2"> <CustomMenu.MenuItem onClick={handleCopyText}>
<LinkIcon className="h-4 w-4" /> <div className="flex items-center justify-start gap-2">
<span>Copy issue Link</span> <LinkIcon className="h-4 w-4" />
</div> <span>Copy issue Link</span>
</CustomMenu.MenuItem> </div>
</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

@ -22,6 +22,7 @@ import { FiltersList, AllViews } from "components/core";
import { import {
CreateUpdateIssueModal, CreateUpdateIssueModal,
DeleteIssueModal, DeleteIssueModal,
DeleteDraftIssueModal,
IssuePeekOverview, IssuePeekOverview,
CreateUpdateDraftIssueModal, CreateUpdateDraftIssueModal,
} from "components/issues"; } from "components/issues";
@ -77,9 +78,11 @@ export const IssuesView: React.FC<Props> = ({
// selected draft issue // selected draft issue
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null); 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();
@ -114,7 +117,8 @@ export const IssuesView: React.FC<Props> = ({
[setDeleteIssueModal, setIssueToDelete] [setDeleteIssueModal, setIssueToDelete]
); );
const handleDraftIssueClick = (issue: any) => setSelectedDraftIssue(issue); 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) => {
@ -345,15 +349,22 @@ export const IssuesView: React.FC<Props> = ({
); );
const handleIssueAction = useCallback( const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { (issue: IIssue, action: "copy" | "edit" | "delete") => {
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);
else if (action === "updateDraft") handleDraftIssueClick(issue);
}, },
[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;
@ -494,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 && (
<> <>
@ -550,23 +566,28 @@ 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.",
icon: <PlusIcon className="h-4 w-4" />, primaryButton: !isDraftIssues
text: "New Issue", ? {
onClick: () => { icon: <PlusIcon className="h-4 w-4" />,
const e = new KeyboardEvent("keydown", { text: "New Issue",
key: "c", onClick: () => {
}); const e = new KeyboardEvent("keydown", {
document.dispatchEvent(e); key: "c",
}, });
}, document.dispatchEvent(e);
},
}
: undefined,
secondaryButton: secondaryButton:
cycleId || moduleId ? ( cycleId || moduleId ? (
<SecondaryButton <SecondaryButton
@ -580,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

@ -14,7 +14,8 @@ import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from
type Props = { type Props = {
states: IState[] | undefined; states: IState[] | undefined;
addIssueToGroup: (groupTitle: string) => void; addIssueToGroup: (groupTitle: string) => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => 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";
@ -62,6 +62,7 @@ type Props = {
removeIssue?: (() => void) | null; removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueSelect?: (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;
@ -77,6 +78,7 @@ export const SingleListIssue: React.FC<Props> = ({
makeIssueCopy, makeIssueCopy,
removeIssue, removeIssue,
groupTitle, groupTitle,
handleDraftIssueDelete,
handleDeleteIssue, handleDeleteIssue,
handleMyIssueOpen, handleMyIssueOpen,
disableUserActions, disableUserActions,
@ -210,26 +212,45 @@ 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>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}> {!isDraftIssues && (
Make a copy... <ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
</ContextMenu.Item> Make a copy...
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}> </ContextMenu.Item>
)}
<ContextMenu.Item
Icon={TrashIcon}
onClick={() => {
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
else handleDeleteIssue(issue);
}}
>
Delete issue Delete issue
</ContextMenu.Item> </ContextMenu.Item>
</> </>
)} )}
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}> {!isDraftIssues && (
Copy issue link <>
</ContextMenu.Item> <ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
<a href={issuePath} target="_blank" rel="noreferrer noopener"> Copy issue link
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}> </ContextMenu.Item>
Open issue in new tab <a href={issuePath} target="_blank" rel="noreferrer noopener">
</ContextMenu.Item> <ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
</a> Open issue in new tab
</ContextMenu.Item>
</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) => {
@ -256,8 +277,7 @@ export const SingleListIssue: React.FC<Props> = ({
className="truncate text-[0.825rem] text-custom-text-100" className="truncate text-[0.825rem] text-custom-text-100"
onClick={() => { onClick={() => {
if (!isDraftIssues) openPeekOverview(issue); if (!isDraftIssues) openPeekOverview(issue);
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
if (handleDraftIssueSelect) handleDraftIssueSelect(issue);
}} }}
> >
{issue.name} {issue.name}
@ -356,7 +376,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>
@ -370,18 +395,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>
<CustomMenu.MenuItem onClick={handleCopyText}> {!isDraftIssues && (
<div className="flex items-center justify-start gap-2"> <CustomMenu.MenuItem onClick={handleCopyText}>
<LinkIcon className="h-4 w-4" /> <div className="flex items-center justify-start gap-2">
<span>Copy issue link</span> <LinkIcon className="h-4 w-4" />
</div> <span>Copy issue link</span>
</CustomMenu.MenuItem> </div>
</CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
)} )}
</div> </div>

View File

@ -39,7 +39,8 @@ type Props = {
currentState?: IState | null; currentState?: IState | null;
groupTitle: string; groupTitle: string;
addIssueToGroup: () => void; addIssueToGroup: () => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => 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,7 +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={() => handleIssueAction(issue, "updateDraft")} 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,32 +46,38 @@ 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="relative h-10 w-10 flex-shrink-0"> <div className="flex item-center gap-2.5">
<Image <div className="relative h-10 w-10 flex-shrink-0">
src={service.logo} <Image
layout="fill" src={service.logo}
objectFit="cover" layout="fill"
alt={`${service.title} Logo`} objectFit="cover"
/> alt={`${service.title} Logo`}
</div> />
<div className="w-full"> </div>
<h3>{service.title}</h3> <div>
<p className="text-sm text-custom-text-200">{service.description}</p> <h3 className="flex items-center gap-4 text-sm font-medium">
{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,27 +135,29 @@ const IntegrationGuide = () => {
<Icon iconName="keyboard_arrow_right" className="!text-lg" /> <Icon iconName="keyboard_arrow_right" className="!text-lg" />
</button> </button>
</div> </div>
</h3> </div>
{exporterServices && exporterServices?.results ? ( <div className="flex flex-col">
exporterServices?.results?.length > 0 ? ( {exporterServices && exporterServices?.results ? (
<div className="space-y-2"> exporterServices?.results?.length > 0 ? (
<div className="divide-y divide-custom-border-200"> <div>
{exporterServices?.results.map((service) => ( <div className="divide-y divide-custom-border-200">
<SingleExport key={service.id} service={service} refreshing={refreshing} /> {exporterServices?.results.map((service) => (
))} <SingleExport key={service.id} service={service} refreshing={refreshing} />
))}
</div>
</div> </div>
</div> ) : (
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p>
)
) : ( ) : (
<p className="py-2 text-sm text-custom-text-200">No previous export available.</p> <Loader className="mt-6 grid grid-cols-1 gap-3">
) <Loader.Item height="40px" width="100%" />
) : ( <Loader.Item height="40px" width="100%" />
<Loader className="mt-6 grid grid-cols-1 gap-3"> <Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" /> <Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" /> </Loader>
<Loader.Item height="40px" width="100%" /> )}
<Loader.Item height="40px" width="100%" /> </div>
</Loader>
)}
</div> </div>
</> </>
{provider && ( {provider && (

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,85 +77,87 @@ 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="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6"
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4" >
> <div className="flex items-start gap-4">
<div className="flex items-center gap-4 whitespace-nowrap"> <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} layout="fill"
layout="fill" objectFit="cover"
objectFit="cover" alt={`${service.title} Logo`}
alt={`${service.title} Logo`} />
/> </div>
</div> <div>
<div className="w-full"> <h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
<h3>{service.title}</h3> <p className="text-sm text-custom-text-200 tracking-tight">
<p className="text-sm text-custom-text-200">{service.description}</p> {service.description}
</div> </p>
<div className="flex-shrink-0">
<Link
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span> now
</PrimaryButton>
</a>
</Link>
</div>
</div> </div>
</div> </div>
))} <div className="flex-shrink-0">
</div> <Link href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <a>
<h3 className="mb-2 flex gap-2 text-lg font-medium"> <PrimaryButton>
Previous Imports <span className="capitalize">{service.type}</span>
<button </PrimaryButton>
type="button" </a>
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none" </Link>
onClick={() => { </div>
setRefreshing(true); </div>
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() => ))}
setRefreshing(false) <div>
); <div className="flex items-center pt-7 pb-3.5 border-b border-custom-border-200">
}} <h3 className="flex gap-2 text-xl font-medium">
> Previous Imports
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "} <button
{refreshing ? "Refreshing..." : "Refresh status"} type="button"
</button> className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
</h3> onClick={() => {
{importerServices ? ( setRefreshing(true);
importerServices.length > 0 ? ( mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
<div className="space-y-2"> setRefreshing(false)
<div className="divide-y divide-custom-border-200"> );
{importerServices.map((service) => ( }}
<SingleImport >
key={service.id} <ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
service={service} {refreshing ? "Refreshing..." : "Refresh status"}
refreshing={refreshing} </button>
handleDelete={() => handleDeleteImport(service)} </h3>
/> </div>
))} <div className="flex flex-col px-4 py-6">
{importerServices ? (
importerServices.length > 0 ? (
<div className="space-y-2">
<div className="divide-y divide-custom-border-200">
{importerServices.map((service) => (
<SingleImport
key={service.id}
service={service}
refreshing={refreshing}
handleDelete={() => handleDeleteImport(service)}
/>
))}
</div>
</div> </div>
</div> ) : (
<p className="py-2 text-sm text-custom-text-200">
No previous imports available.
</p>
)
) : ( ) : (
<p className="py-2 text-sm text-custom-text-200"> <Loader className="mt-6 grid grid-cols-1 gap-3">
No previous imports available. <Loader.Item height="40px" width="100%" />
</p> <Loader.Item height="40px" width="100%" />
) <Loader.Item height="40px" width="100%" />
) : ( <Loader.Item height="40px" width="100%" />
<Loader className="mt-6 grid grid-cols-1 gap-3"> </Loader>
<Loader.Item height="40px" width="100%" /> )}
<Loader.Item height="40px" width="100%" /> </div>
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</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,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

@ -133,9 +133,15 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
const issueName = watch("name"); const issueName = watch("name");
const payload = { const payload: Partial<IIssue> = {
name: getValues("name"), name: getValues("name"),
description: getValues("description"), description: getValues("description"),
state: getValues("state"),
priority: getValues("priority"),
assignees: getValues("assignees"),
target_date: getValues("target_date"),
labels: getValues("labels"),
project: getValues("project"),
}; };
useEffect(() => { useEffect(() => {

View File

@ -17,5 +17,8 @@ export * from "./label";
export * from "./issue-reaction"; export * from "./issue-reaction";
export * from "./peek-overview"; export * from "./peek-overview";
export * from "./confirm-issue-discard"; export * from "./confirm-issue-discard";
// draft issue
export * from "./draft-issue-form"; export * from "./draft-issue-form";
export * from "./draft-issue-modal"; export * from "./draft-issue-modal";
export * from "./delete-draft-issue-modal";

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

@ -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={
<span className="capitalize"> <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">
{field.value ? ROLE[field.value] : "Select role"} <span className="capitalize">
</span> {field.value ? ROLE[field.value] : "Select role"}
</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,30 +47,107 @@ 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">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-1 w-full"> <span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
{projectLinks.map((link) => ( <div className="flex flex-col gap-1 w-full">
<Link key={link.href} href={link.href}> {(projectId ? projectLinks : workspaceLinks).map((link) => (
<a> <Link key={link.href} href={link.href}>
<div <a>
className={`px-4 py-2 text-sm font-medium rounded-md ${ <div
( className={`px-4 py-2 text-sm font-medium rounded-md ${
link.label === "Import" (
? router.asPath.includes(link.href) link.label === "Import"
: router.asPath === link.href ? 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" ? "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> {link.label}
</a> </div>
</Link> </a>
))} </Link>
))}
</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> </div>
); );
}; };

View File

@ -184,19 +184,20 @@ export const SingleState: React.FC<Props> = ({
<ArrowDownIcon className="h-4 w-4" /> <ArrowDownIcon className="h-4 w-4" />
</button> </button>
)} )}
{state.default ? (
<span className="text-xs text-custom-text-200">Default</span>
) : (
<button
type="button"
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
onClick={handleMakeDefault}
disabled={isSubmitting}
>
Mark as default
</button>
)}
<div className=" items-center gap-2.5 hidden group-hover:flex"> <div className=" items-center gap-2.5 hidden group-hover:flex">
{state.default ? (
<span className="text-xs text-custom-text-200">Default</span>
) : (
<button
type="button"
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
onClick={handleMakeDefault}
disabled={isSubmitting}
>
Mark as default
</button>
)}
<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

@ -38,6 +38,9 @@ export const WorkspaceSidebarQuickAction = () => {
"priority", "priority",
"dueDate", "dueDate",
"priority", "priority",
"state",
"startDate",
"project",
]} ]}
/> />
@ -47,7 +50,7 @@ export const WorkspaceSidebarQuickAction = () => {
}`} }`}
> >
<div <div
className={`flex items-center justify-between w-full rounded cursor-pointer px-4 gap-1 ${ 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"
@ -80,7 +83,7 @@ export const WorkspaceSidebarQuickAction = () => {
<div> <div>
<Menu.Button <Menu.Button
type="button" type="button"
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${ className={`flex items-center justify-center rounded flex-shrink-0 p-1.5 ${
open ? "rotate-180 pl-0" : "rotate-0 pr-0" open ? "rotate-180 pl-0" : "rotate-0 pr-0"
}`} }`}
> >
@ -108,7 +111,7 @@ export const WorkspaceSidebarQuickAction = () => {
> >
<PenSquare <PenSquare
size={16} size={16}
className="!text-lg !leading-4 text-custom-sidebar-text-300 mx-2" className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
/> />
Last Drafted Issue Last Drafted Issue
</button> </button>

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,186 +38,179 @@ 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 ? (
<div>
<ul role="list" className="-mb-4">
{userActivity.results.map((activityItem: any, activityIdx: number) => {
if (activityItem.field === "comment") {
return (
<div key={activityItem.id} className="mt-2">
<div className="relative flex items-start space-x-3">
<div className="relative px-1">
{activityItem.field ? (
activityItem.new_value === "restore" && (
<Icon iconName="history" className="text-sm text-custom-text-200" />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</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"> {userActivity ? (
<ChatBubbleLeftEllipsisIcon <section className="pr-9 py-8 w-full">
className="h-3.5 w-3.5 text-custom-text-200" <div className="flex items-center py-3.5 border-b border-custom-border-200">
aria-hidden="true" <h3 className="text-xl font-medium">Acitivity</h3>
/> </div>
</span> <div className={`flex flex-col gap-2 py-4 w-full`}>
</div> <ul role="list" className="-mb-4">
<div className="min-w-0 flex-1"> {userActivity.results.map((activityItem: any, activityIdx: number) => {
<div> if (activityItem.field === "comment") {
<div className="text-xs"> return (
{activityItem.actor_detail.is_bot <div key={activityItem.id} className="mt-2">
? activityItem.actor_detail.first_name + " Bot" <div className="relative flex items-start space-x-3">
: activityItem.actor_detail.display_name} <div className="relative px-1">
</div> {activityItem.field ? (
<p className="mt-0.5 text-xs text-custom-text-200"> activityItem.new_value === "restore" && (
Commented {timeAgo(activityItem.created_at)} <Icon iconName="history" className="text-sm text-custom-text-200" />
</p> )
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
<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
className="h-6 w-6 !text-2xl text-custom-text-200"
aria-hidden="true"
/>
</span>
</div> </div>
<div className="issue-comments-section p-0"> <div className="min-w-0 flex-1">
<TipTapEditor <div>
workspaceSlug={workspaceSlug as string} <div className="text-xs">
value={ {activityItem.actor_detail.is_bot
activityItem?.new_value !== "" ? activityItem.actor_detail.first_name + " Bot"
? activityItem.new_value : activityItem.actor_detail.display_name}
: activityItem.old_value </div>
} <p className="mt-0.5 text-xs text-custom-text-200">
customClassName="text-xs border border-custom-border-200 bg-custom-background-100" Commented {timeAgo(activityItem.created_at)}
noBorder </p>
borderOnFocus={false} </div>
editable={false} <div className="issue-comments-section p-0">
/> <TipTapEditor
workspaceSlug={workspaceSlug as string}
value={
activityItem?.new_value !== ""
? activityItem.new_value
: activityItem.old_value
}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
noBorder
borderOnFocus={false}
editable={false}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> );
); }
}
const message = const message =
activityItem.verb === "created" && activityItem.verb === "created" &&
activityItem.field !== "cycles" && activityItem.field !== "cycles" &&
activityItem.field !== "modules" && activityItem.field !== "modules" &&
activityItem.field !== "attachment" && activityItem.field !== "attachment" &&
activityItem.field !== "link" && activityItem.field !== "link" &&
activityItem.field !== "estimate" ? ( activityItem.field !== "estimate" ? (
<span className="text-custom-text-200"> <span className="text-custom-text-200">
created{" "} created{" "}
<Link <Link
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`} href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
> >
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" /> this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a> </a>
</Link> </Link>
</span> </span>
) : activityItem.field ? ( ) : activityItem.field ? (
<ActivityMessage activity={activityItem} showIssue /> <ActivityMessage activity={activityItem} showIssue />
) : ( ) : (
"created the issue." "created the issue."
); );
if ("field" in activityItem && activityItem.field !== "updated_by") { if ("field" in activityItem && activityItem.field !== "updated_by") {
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 <div>
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80" <div className="relative px-1.5">
aria-hidden="true" <div className="mt-1.5">
/> <div className="flex h-6 w-6 items-center justify-center">
) : null} {activityItem.field ? (
<div className="relative flex items-start space-x-2"> activityItem.new_value === "restore" ? (
<> <Icon
<div> iconName="history"
<div className="relative px-1.5"> className="!text-2xl text-custom-text-200"
<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"> ) : (
{activityItem.field ? ( <ActivityIcon activity={activityItem} />
activityItem.new_value === "restore" ? ( )
<Icon ) : activityItem.actor_detail.avatar &&
iconName="history" activityItem.actor_detail.avatar !== "" ? (
className="text-sm text-custom-text-200" <img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
height={24}
width={24}
className="rounded-full"
/> />
) : ( ) : (
<ActivityIcon activity={activityItem} /> <div
) className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
) : activityItem.actor_detail.avatar && >
activityItem.actor_detail.avatar !== "" ? ( {activityItem.actor_detail.display_name?.charAt(0)}
<img </div>
src={activityItem.actor_detail.avatar} )}
alt={activityItem.actor_detail.display_name} </div>
height={24}
width={24}
className="rounded-full"
/>
) : (
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.display_name?.charAt(0)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
</div> <div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
<div className="min-w-0 flex-1 py-3"> <div className="text-sm text-custom-text-200 break-words">
<div className="text-xs 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> ) : activityItem.actor_detail.is_bot ? (
) : activityItem.actor_detail.is_bot ? ( <span className="text-gray font-medium">
<span className="text-gray font-medium"> {activityItem.actor_detail.first_name} Bot
{activityItem.actor_detail.first_name} Bot </span>
) : (
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span> </span>
) : ( </div>
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span>
</div> </div>
</div> </>
</> </div>
</div> </div>
</div> </li>
</li> );
); }
} })}
})} </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">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}> <div className="flex items-center justify-center bg-custom-background-90 h-16 w-16 rounded-lg">
{!watch("avatar") || watch("avatar") === "" ? ( <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
<div className="h-12 w-12 rounded-md bg-custom-background-80 p-2"> {!watch("avatar") || watch("avatar") === "" ? (
<UserIcon className="h-full w-full text-custom-text-200" /> <div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
</div> <UserIcon className="h-full w-full text-custom-text-200" />
) : ( </div>
<div className="relative h-12 w-12 overflow-hidden"> ) : (
<img <div className="relative h-16 w-16 overflow-hidden">
src={watch("avatar")} <img
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" src={watch("avatar")}
onClick={() => setIsImageUploadModalOpen(true)} className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
alt={myProfile.display_name} onClick={() => setIsImageUploadModalOpen(true)}
/> alt={myProfile.display_name}
</div> />
)} </div>
</button> )}
<div className="flex items-center gap-2"> </button>
<SecondaryButton </div>
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 className="flex absolute right-3 bottom-3">
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <Controller
<div className="col-span-12 sm:col-span-6"> control={control}
<h4 className="text-lg font-semibold">Cover Photo</h4> name="cover_image"
<p className="text-sm text-custom-text-200"> render={() => (
Select your cover photo from the given library.
</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,157 +219,167 @@ const Profile: NextPage = () => {
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab" "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
} }
/> />
</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>
<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 className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">First Name</h4>
<Input
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
className="!px-3 !py-2 rounded-md font-medium"
autoComplete="off"
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Input
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
className="!px-3 !py-2 rounded-md font-medium"
/>
</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 className="flex flex-col gap-1">
<h4 className="text-sm">Role</h4>
<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
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 className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Input
id="display_name"
name="display_name"
autoComplete="off"
register={register}
error={errors.display_name}
className="w-full"
placeholder="Enter your display name"
validations={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1)
return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Timezone </h4>
<Controller
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
verticalPosition="top"
optionsClassName="w-full"
input
/>
)}
/>
{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>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> </div>
<div className="col-span-12 sm:col-span-6"> </form>
<h4 className="text-lg font-semibold text-custom-text-100">Full Name</h4>
</div>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6">
<Input
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
autoComplete="off"
/>
<Input
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
/>
</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">Display Name</h4>
<p className="text-sm text-custom-text-200">
This could be your first name, or a nickname however you{"'"}d like people to
refer to you in Plane.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="display_name"
name="display_name"
autoComplete="off"
register={register}
error={errors.display_name}
className="w-full"
placeholder="Enter your display name"
validations={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1)
return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
/>
</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">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
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
verticalPosition="top"
optionsClassName="w-full"
input
/>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
</div>
<div className="sm:text-right">
<SecondaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update profile"}
</SecondaryButton>
</div>
</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>
<SettingsNavbar profilePage />
</div> </div>
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <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 className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<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

@ -55,7 +55,7 @@ const ProjectDraftIssues: NextPage = () => {
<button <button
type="button" type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)} onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs" 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" /> <PenSquare className="h-3 w-3 text-custom-text-300" />
<span>Draft Issues</span> <span>Draft Issues</span>

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";
@ -33,7 +34,10 @@ 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 +65,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",
}, },
]; ];

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,59 +388,60 @@ const GeneralSettings: NextPage = () => {
)} )}
</div> </div>
</div> </div>
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-400">
{({ 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 Project</span>
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button>
<Disclosure as="div" className="border-t border-custom-border-400"> <Transition
{({ open }) => ( show={open}
<div className="w-full"> enter="transition duration-100 ease-out"
<Disclosure.Button enterFrom="transform opacity-0"
as="button" enterTo="transform opacity-100"
type="button" leave="transition duration-75 ease-out"
className="flex items-center justify-between w-full py-4" leaveFrom="transform opacity-100"
> leaveTo="transform opacity-0"
<span className="text-xl tracking-tight">Danger Zone</span> >
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" /> <Disclosure.Panel>
</Disclosure.Button> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
<Transition The danger zone of the project delete page is a critical area that
show={open} requires careful consideration and attention. When deleting a project,
enter="transition duration-100 ease-out" all of the data and resources within that project will be permanently
enterFrom="transform opacity-0" removed and cannot be recovered.
enterTo="transform opacity-100" </span>
leave="transition duration-75 ease-out" <div>
leaveFrom="transform opacity-100" {projectDetails ? (
leaveTo="transform opacity-0" <div>
> <DangerButton
<Disclosure.Panel> onClick={() => setSelectedProject(projectDetails.id ?? null)}
<div className="flex flex-col gap-8"> className="!text-sm"
<span className="text-sm tracking-tight"> outline
The danger zone of the project delete page is a critical area that >
requires careful consideration and attention. When deleting a project, all Delete my project
of the data and resources within that project will be permanently removed </DangerButton>
and cannot be recovered. </div>
</span> ) : (
<div> <Loader className="mt-2 w-full">
{projectDetails ? ( <Loader.Item height="38px" width="144px" />
<div> </Loader>
<DangerButton )}
onClick={() => setSelectedProject(projectDetails.id ?? null)} </div>
className="!text-sm"
outline
>
Delete my project
</DangerButton>
</div>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="38px" width="144px" />
</Loader>
)}
</div> </div>
</div> </Disclosure.Panel>
</Disclosure.Panel> </Transition>
</Transition> </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>
<section className="pr-9 py-8 w-full">
<div> <div>
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p> <h3 className="text-xl font-medium">Billing & Plan</h3>
</div>
</div> </div>
<div className="space-y-8 md:w-2/3"> <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,10 +40,16 @@ 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 />
<ExportGuide /> </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 />
</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,67 +183,109 @@ 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">
<SettingsSidebar />
</div>
{activeWorkspace ? ( {activeWorkspace ? (
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}> <div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
<div className="col-span-12 sm:col-span-6"> <div className="flex flex-col gap-1">
<h4 className="text-lg font-semibold">Logo</h4> <button
<p className="text-sm text-custom-text-200"> type="button"
Max file size is 5MB. Supported file types are .jpg and .png. onClick={() => setIsImageUploadModalOpen(true)}
</p> disabled={!isAdmin}
>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-14 w-14">
<img
src={watch("logo")!}
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
alt="Workspace Logo"
/>
</div>
) : (
<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"}
</div>
)}
</button>
</div> </div>
<div className="col-span-12 sm:col-span-6"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-4"> <h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
<span className="text-sm tracking-tight">{`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}</span>
<div className="flex item-center gap-2.5">
<button <button
type="button" className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
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"> <>
<img <Pencil className="h-3 w-3" />
src={watch("logo")!} Edit logo
className="absolute top-0 left-0 h-full w-full object-cover rounded-md" </>
alt="Workspace Logo"
/>
</div>
) : ( ) : (
<div className="relative flex h-12 w-12 items-center justify-center rounded bg-gray-700 p-4 uppercase text-white"> "Upload logo"
{activeWorkspace?.name?.charAt(0) ?? "N"}
</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> </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-8 my-10">
<h4 className="text-lg font-semibold">URL</h4> <div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
<p className="text-sm text-custom-text-200">Your workspace URL.</p> <div className="flex flex-col gap-1 ">
</div> <h4 className="text-sm">Workspace Name</h4>
<div className="col-span-12 flex items-center gap-2 sm:col-span-6"> <Input
<div className="flex flex-col gap-1"> id="name"
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
required: "Name is required",
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
disabled={!isAdmin}
/>
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Company Size</h4>
<Controller
name="organization_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
}
width="w-full"
input
disabled={!isAdmin}
>
{ORGANIZATION_SIZE?.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace URL</h4>
<Input <Input
id="url" id="url"
name="url" name="url"
@ -253,114 +300,66 @@ const WorkspaceSettings: NextPage = () => {
disabled disabled
/> />
</div> </div>
<SecondaryButton
className="h-min"
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]" />
</SecondaryButton>
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex items-center justify-between py-2">
<div className="col-span-12 sm:col-span-6"> <PrimaryButton
<h4 className="text-lg font-semibold">Name</h4> onClick={handleSubmit(onSubmit)}
<p className="text-sm text-custom-text-200">Give a name to your workspace.</p> loading={isSubmitting}
</div>
<div className="col-span-12 sm:col-span-6">
<Input
id="name"
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
required: "Name is required",
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
disabled={!isAdmin} disabled={!isAdmin}
/> >
</div> {isSubmitting ? "Updating..." : "Update Workspace"}
</div> </PrimaryButton>
<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">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
name="organization_size"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
}
width="w-full"
input
disabled={!isAdmin}
>
{ORGANIZATION_SIZE?.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div> </div>
</div> </div>
{isAdmin && ( <Disclosure as="div" className="border-t border-custom-border-400">
<> {({ open }) => (
<div className="sm:text-right"> <div className="w-full">
<SecondaryButton <Disclosure.Button
onClick={handleSubmit(onSubmit)} as="button"
loading={isSubmitting} type="button"
disabled={!isAdmin} className="flex items-center justify-between w-full py-4"
> >
{isSubmitting ? "Updating..." : "Update Workspace"} <span className="text-xl tracking-tight">Delete Workspace</span>
</SecondaryButton> <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>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> )}
<div className="col-span-12 sm:col-span-6"> </Disclosure>
<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 className="col-span-12 sm:col-span-6">
<DangerButton onClick={() => setIsOpen(true)} outline>
Delete the workspace
</DangerButton>
</div>
</div>
</>
)}
</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,9 +143,8 @@ 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}`}>
<img <a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
src={member.avatar} <img
className="absolute top-0 left-0 h-full w-full object-cover rounded-lg" src={member.avatar}
alt={member.display_name || member.email} className="absolute top-0 left-0 h-full w-full object-cover rounded-lg"
/> 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}`}>
{(member.display_name || member.email)?.charAt(0)} <a className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize bg-gray-700 text-white">
</div> {(member.display_name || member.email)?.charAt(0)}
</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 = () => {
} }
}} }}
> >
{user?.id === member.memberId ? "Leave" : "Remove member"} <span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>
{" "}
{user?.id === member.memberId ? "Leave" : "Remove member"}
</span>
</span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>

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