Compare commits

...

14 Commits

Author SHA1 Message Date
LAKHAN BAHETI
63f5429c18 Merge branch 'develop' of https://github.com/makeplane/plane into style/views_card 2023-11-14 19:08:33 +05:30
Prateek Shourya
1fe09d369f
style: text overflow fix and border color update (#2769)
* style: fix text overflow in:
* Issue activity
* Cycle and Module Select in Create Issue form
* Delete Module modal
* Join Project modal

* style: update assignee select border as per design.
2023-11-14 18:34:51 +05:30
Dakshesh Jain
b7757c6b1a
fix: bugs (#2761)
* fix: semicolon on estimate settings page

* refactor: project settings automations store implementation

* fix: active cycle stuck on infinite loading

* fix: removed delete project option from sidebar

* fix: discloser not opening when navigating to project

* fix: clear filter not working & filter appearing even if nothing is selected

* refactor: select label store implementation

* refactor: select state store implementation
2023-11-14 18:33:01 +05:30
Anmol Singh Bhatia
1a25bacce1
style: create update view modal consistency (#2775) 2023-11-14 18:30:10 +05:30
Anmol Singh Bhatia
6797df239d
chore: no lead option added in lead select dropdown (#2774) 2023-11-14 18:29:39 +05:30
Anmol Singh Bhatia
43e7c10eb7
chore: spreadsheet layout column responsiveness (#2768) 2023-11-14 18:28:49 +05:30
Anmol Singh Bhatia
bdc9c9c2a8
chore: create update issue modal improvement (#2765) 2023-11-14 18:28:15 +05:30
Anmol Singh Bhatia
f0c72bf249
fix: breadcrumb project icon improvement (#2764) 2023-11-14 18:27:47 +05:30
sabith-tu
a8904bfc48
style: ui fixes for pages and views (#2770) 2023-11-14 18:26:50 +05:30
LAKHAN BAHETI
09a6039790 fix: description truncating after 1 line 2023-11-14 17:13:57 +05:30
LAKHAN BAHETI
442bbe41ac style: views card text overflow 2023-11-14 16:53:29 +05:30
Nikhil
b31041726b
dev: create bucket through application (#2720) 2023-11-13 15:57:19 +05:30
Prateek Shourya
e6f947ad90
style: ui improvements and bug fixes (#2758)
* style: add transition to favorite projects dropdown.

* style: update project integration settings borders.

* style: fix text overflow issue in project views.

* fix: issue with non-functional cancel button in leave project modal.
2023-11-13 14:42:45 +05:30
Dakshesh Jain
7963993171
fix: workspace settings bugs (#2743)
* fix: double layout in exports

* fix: typo in jira email address section

* fix: workspace members not mutating

* fix: removed un-used variable

* fix: workspace members can't be filtered using email

* fix: autocomplete in workspace delete

* fix: autocomplete in project delete modal

* fix: update member function in store

* fix: sidebar link not active when in github/jira

* style: margin top & icon inconsistency

* fix: typo in create workspace

* fix: workspace leave flow

* fix: redirection to delete issue

* fix: autocomplete off in jira api token

* refactor: reduced api call, added optional chaining & removed variable with low scope
2023-11-13 13:34:05 +05:30
44 changed files with 417 additions and 288 deletions

View File

@ -0,0 +1,57 @@
import os, sys
import boto3
from botocore.exceptions import ClientError
sys.path.append("/code")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
import django
django.setup()
def create_bucket():
try:
from django.conf import settings
# Create a session using the credentials from Django settings
session = boto3.session.Session(
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
# Create an S3 client using the session
s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL)
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
print("Checking bucket...")
# Check if the bucket exists
s3_client.head_bucket(Bucket=bucket_name)
# If head_bucket does not raise an exception, the bucket exists
print(f"Bucket '{bucket_name}' already exists.")
except ClientError as e:
error_code = int(e.response['Error']['Code'])
bucket_name = settings.AWS_STORAGE_BUCKET_NAME
if error_code == 404:
# Bucket does not exist, create it
print(f"Bucket '{bucket_name}' does not exist. Creating bucket...")
try:
s3_client.create_bucket(Bucket=bucket_name)
print(f"Bucket '{bucket_name}' created successfully.")
except ClientError as create_error:
print(f"Failed to create bucket: {create_error}")
elif error_code == 403:
# Access to the bucket is forbidden
print(f"Access to the bucket '{bucket_name}' is forbidden. Check permissions.")
else:
# Another ClientError occurred
print(f"Failed to check bucket: {e}")
except Exception as ex:
# Handle any other exception
print(f"An error occurred: {ex}")
if __name__ == "__main__":
create_bucket()

View File

@ -5,5 +5,7 @@ python manage.py migrate
# Create a Default User # Create a Default User
python bin/user_script.py python bin/user_script.py
# Create the default bucket
python bin/bucket_script.py
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -

View File

@ -42,13 +42,16 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
return ( return (
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}> <Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
<a <a
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`} aria-disabled={activity.issue === null}
target="_blank" href={`${
rel="noopener noreferrer" activity.issue_detail ? `/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}` : "#"
}`}
target={activity.issue === null ? "_self" : "_blank"}
rel={activity.issue === null ? "" : "noopener noreferrer"}
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"} {activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</Tooltip> </Tooltip>
); );
@ -268,10 +271,10 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="w-full font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.new_value} <span className="truncate">{activity.new_value}</span>
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</> </>
); );
@ -283,10 +286,10 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="w-full font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.new_value} <span className="truncate">{activity.new_value}</span>
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</> </>
); );
@ -298,10 +301,10 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="w-full font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.old_value} <span className="truncate">{activity.old_value}</span>
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</> </>
); );
@ -479,10 +482,10 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="w-full font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.new_value} <span className="truncate">{activity.new_value}</span>
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</> </>
); );
@ -494,10 +497,10 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="w-full font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.new_value} <span className="truncate">{activity.new_value}</span>
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</> </>
); );
@ -509,10 +512,10 @@ const activityDetails: {
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`} href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="w-full font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
> >
{activity.old_value} <span className="truncate">{activity.old_value}</span>
<RocketIcon size={12} color="#6b7280" /> <RocketIcon size={12} color="#6b7280" className="flex-shrink-0" />
</a> </a>
</> </>
); );

View File

@ -80,7 +80,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
); );
const activeCycle = cycleStore.cycles?.[projectId]?.active || null; const activeCycle = cycleStore.cycles?.[projectId]?.current || null;
const cycle = activeCycle ? activeCycle[0] : null; const cycle = activeCycle ? activeCycle[0] : null;
const issues = (cycleStore?.active_cycle_issues as any) || null; const issues = (cycleStore?.active_cycle_issues as any) || null;

View File

@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Circle, ExternalLink, Plus } from "lucide-react"; import { ArrowLeft, Briefcase, Circle, ExternalLink, Plus } from "lucide-react";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
@ -121,17 +121,23 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> currentProjectDetails?.emoji ? (
{renderEmoji(currentProjectDetails.emoji)} <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
</span> {renderEmoji(currentProjectDetails.emoji)}
) : currentProjectDetails?.icon_prop ? ( </span>
<div className="h-7 w-7 flex-shrink-0 grid place-items-center"> ) : currentProjectDetails?.icon_prop ? (
{renderEmoji(currentProjectDetails.icon_prop)} <div className="h-7 w-7 flex-shrink-0 grid place-items-center">
</div> {renderEmoji(currentProjectDetails.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span>
)
) : ( ) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white"> <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{currentProjectDetails?.name.charAt(0)} <Briefcase className="h-4 w-4" />
</span> </span>
) )
} }

View File

@ -163,7 +163,7 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
return ( return (
<form onSubmit={handleSubmit(createGithubImporterService)}> <form onSubmit={handleSubmit(createGithubImporterService)}>
<div className="space-y-2"> <div className="space-y-2 mt-4">
<Link href={`/${workspaceSlug}/settings/imports`}> <Link href={`/${workspaceSlug}/settings/imports`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100"> <div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100">
<ArrowLeft className="h-3 w-3" /> <ArrowLeft className="h-3 w-3" />
@ -191,9 +191,7 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
}`} }`}
> >
<integration.icon <integration.icon
width="18px" className={`w-5 h-5 ${index <= activeIntegrationState() ? "text-white" : "text-custom-text-400"}`}
height="18px"
color={index <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
/> />
</div> </div>
{index < integrationWorkflowData.length - 1 && ( {index < integrationWorkflowData.length - 1 && (

View File

@ -56,6 +56,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
ref={ref} ref={ref}
placeholder="XXXXXXXX" placeholder="XXXXXXXX"
className="w-full" className="w-full"
autoComplete="off"
/> />
)} )}
/> />
@ -94,7 +95,7 @@ export const JiraGetImportDetail: React.FC = observer(() => {
<div className="grid grid-cols-1 gap-10 md:grid-cols-2"> <div className="grid grid-cols-1 gap-10 md:grid-cols-2">
<div className="col-span-1"> <div className="col-span-1">
<h3 className="font-semibold">Jira Email Address</h3> <h3 className="font-semibold">Jira Email Address</h3>
<p className="text-sm text-custom-text-200">Enter the Gmail account that you use in Jira account</p> <p className="text-sm text-custom-text-200">Enter the Email account that you use in Jira account</p>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Controller <Controller

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
// icons // icons
import { ArrowLeft, Check, List, Settings } from "lucide-react"; import { ArrowLeft, Check, List, Settings, Users2 } from "lucide-react";
// services // services
import { JiraImporterService } from "services/integrations"; import { JiraImporterService } from "services/integrations";
// fetch keys // fetch keys
@ -98,7 +98,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
}; };
return ( return (
<div className="flex h-full flex-col space-y-2"> <div className="flex h-full flex-col space-y-2 mt-4">
<Link href={`/${workspaceSlug}/settings/imports`}> <Link href={`/${workspaceSlug}/settings/imports`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100"> <div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-custom-text-200 hover:text-custom-text-100">
<div> <div>
@ -136,9 +136,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
}`} }`}
> >
<integration.icon <integration.icon
width="18px" className={`w-5 h-5 ${index <= activeIntegrationState() ? "text-white" : "text-custom-text-400"}`}
height="18px"
color={index <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
/> />
</button> </button>
{index < integrationWorkflowData.length - 1 && ( {index < integrationWorkflowData.length - 1 && (

View File

@ -542,13 +542,13 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<> <>
<CustomMenu {watch("parent") ? (
customButton={ <CustomMenu
<button customButton={
type="button" <button
className="flex items-center justify-between gap-1 w-full cursor-pointer rounded border-[0.5px] border-custom-border-300 text-custom-text-200 px-2 py-1 text-xs hover:bg-custom-background-80" type="button"
> className="flex items-center justify-between gap-1 w-full cursor-pointer rounded border-[0.5px] border-custom-border-300 text-custom-text-200 px-2 py-1 text-xs hover:bg-custom-background-80"
{watch("parent") ? ( >
<div className="flex items-center gap-1 text-custom-text-200"> <div className="flex items-center gap-1 text-custom-text-200">
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" /> <LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">
@ -557,31 +557,30 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
${selectedParentIssue.sequence_id}`} ${selectedParentIssue.sequence_id}`}
</span> </span>
</div> </div>
) : ( </button>
<div className="flex items-center gap-1 text-custom-text-300"> }
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" /> placement="bottom-start"
<span className="whitespace-nowrap">Add Parent</span> >
</div>
)}
</button>
}
placement="bottom-start"
>
{watch("parent") ? (
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem className="!p-1" onClick={() => setValue("parent", null)}>
Remove parent issue
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}> <CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Select Parent Issue Change parent issue
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} <CustomMenu.MenuItem className="!p-1" onClick={() => setValue("parent", null)}>
</CustomMenu> Remove parent issue
</CustomMenu.MenuItem>
</CustomMenu>
) : (
<button
type="button"
className="flex items-center justify-between gap-1 w-min cursor-pointer rounded border-[0.5px] border-custom-border-300 text-custom-text-200 px-2 py-1 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<div className="flex items-center gap-1 text-custom-text-300">
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add Parent</span>
</div>
</button>
)}
<Controller <Controller
control={control} control={control}
name="parent" name="parent"

View File

@ -49,7 +49,7 @@ export const FilterAssignees: React.FC<Props> = (props) => {
key={`assignees-${member.id}`} key={`assignees-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false} isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)} onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="sm" />} icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name} title={member.display_name}
/> />
))} ))}

View File

@ -49,7 +49,7 @@ export const FilterCreatedBy: React.FC<Props> = (props) => {
key={`created-by-${member.id}`} key={`created-by-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false} isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)} onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} size="sm" />} icon={<Avatar name={member.display_name} src={member.avatar} size="md" />}
title={member.display_name} title={member.display_name}
/> />
))} ))}

View File

@ -49,7 +49,7 @@ export const FilterMentions: React.FC<Props> = (props) => {
key={`mentions-${member.id}`} key={`mentions-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false} isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)} onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} />} icon={<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} size={"md"} />}
title={member.display_name} title={member.display_name}
/> />
))} ))}

View File

@ -117,8 +117,8 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
) : ( ) : (
<span <span
className={`flex items-center justify-between gap-1 h-full w-full text-xs rounded duration-300 focus:outline-none ${ className={`flex items-center justify-between gap-1 h-full w-full text-xs rounded duration-300 focus:outline-none ${
noLabelBorder ? "" : " px-2.5 py-1 border border-custom-border-300" noLabelBorder ? "" : " px-2.5 py-1 border-[0.5px] border-custom-border-300"
}}`} }`}
> >
<User2 className="h-3 w-3" /> <User2 className="h-3 w-3" />
</span> </span>

View File

@ -78,7 +78,7 @@ export const IssueColumn: React.FC<Props> = ({
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-custom-border-100"> <div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-custom-border-100">
{properties.key && ( {properties.key && (
<div <div
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center min-w-[96px]" className="flex gap-1.5 px-4 pr-0 py-2.5 items-center min-w-min"
style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}} style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}}
> >
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100"> <div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">

View File

@ -94,7 +94,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
{displayProperties.key && ( {displayProperties.key && (
<span className="flex items-center px-4 py-2.5 h-full w-24 flex-shrink-0">ID</span> <span className="flex items-center px-4 py-2.5 h-full w-24 flex-shrink-0">ID</span>
)} )}
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">Issue</span> <span className="flex items-center justify-center px-4 py-2.5 h-full w-full flex-grow">Issue</span>
</div> </div>
{issues.map((issue, index) => ( {issues.map((issue, index) => (

View File

@ -55,9 +55,9 @@ export const IssueCycleSelect: React.FC<IssueCycleSelectProps> = observer((props
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const label = selectedCycle ? ( const label = selectedCycle ? (
<div className="flex items-center gap-1 text-custom-text-200"> <div className="flex items-center w-full gap-1 text-custom-text-200">
<ContrastIcon className="h-3 w-3" /> <ContrastIcon className="h-3 w-3 flex-shrink-0" />
<div className="truncate">{selectedCycle.name}</div> <div className="truncate max-w-[160px]">{selectedCycle.name}</div>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-1 text-custom-text-300"> <div className="flex items-center gap-1 text-custom-text-300">

View File

@ -3,16 +3,13 @@ import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { Combobox, Transition } from "@headlessui/react"; import { Combobox, Transition } from "@headlessui/react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
// services import { observer } from "mobx-react-lite";
import { IssueLabelService } from "services/issue"; // store
import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
import { IssueLabelsList } from "components/ui"; import { IssueLabelsList } from "components/ui";
// icons // icons
import { Check, Component, Plus, Search, Tag } from "lucide-react"; import { Check, Component, Plus, Search, Tag } from "lucide-react";
// types
import type { IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -22,15 +19,19 @@ type Props = {
label?: JSX.Element; label?: JSX.Element;
}; };
const issueLabelService = new IssueLabelService(); export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const { setIsOpen, value, onChange, projectId, label } = props;
export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId, label }) => {
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const {
project: { labels, fetchProjectLabels },
} = useMobxStore();
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -38,11 +39,11 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
placement: "bottom-start", placement: "bottom-start",
}); });
const { data: issueLabels } = useSWR<IIssueLabels[]>( const issueLabels = labels?.[projectId] || [];
projectId ? PROJECT_ISSUE_LABELS(projectId) : null,
workspaceSlug && projectId useSWR(
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId) workspaceSlug && projectId ? `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}` : null,
: null workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId) : null
); );
const filteredOptions = const filteredOptions =
@ -202,4 +203,4 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
)} )}
</Combobox> </Combobox>
); );
}; });

View File

@ -55,9 +55,9 @@ export const IssueModuleSelect: React.FC<IssueModuleSelectProps> = observer((pro
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const label = selectedModule ? ( const label = selectedModule ? (
<div className="flex items-center gap-1 text-custom-text-200"> <div className="flex items-center w-full gap-1 text-custom-text-200">
<DiceIcon className="h-3 w-3" /> <DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="truncate">{selectedModule.name}</span> <span className="truncate max-w-[160px]">{selectedModule.name}</span>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-1 text-custom-text-300"> <div className="flex items-center gap-1 text-custom-text-300">

View File

@ -1,14 +1,13 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// services import { observer } from "mobx-react-lite";
import { ProjectStateService } from "services/project"; // store
import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
import { CustomSearchSelect, DoubleCircleIcon, StateGroupIcon } from "@plane/ui"; import { CustomSearchSelect, DoubleCircleIcon, StateGroupIcon } from "@plane/ui";
// icons // icons
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// fetch keys
import { STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -17,19 +16,24 @@ type Props = {
projectId: string; projectId: string;
}; };
// services export const IssueStateSelect: React.FC<Props> = observer((props) => {
const projectStateService = new ProjectStateService(); const { setIsOpen, value, onChange, projectId } = props;
export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => {
// states // states
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: states } = useSWR( const {
workspaceSlug && projectId ? STATES_LIST(projectId) : null, projectState: { states: projectStates, fetchProjectStates },
workspaceSlug && projectId ? () => projectStateService.getStates(workspaceSlug as string, projectId) : null } = useMobxStore();
useSWR(
workspaceSlug && projectId ? `STATES_LIST_${projectId.toUpperCase()}` : null,
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId) : null
); );
const states = projectStates?.[projectId] || [];
const options = states?.map((state) => ({ const options = states?.map((state) => ({
value: state.id, value: state.id,
query: state.name, query: state.name,
@ -74,4 +78,4 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
noChevron noChevron
/> />
); );
}; });

View File

@ -102,7 +102,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">
Are you sure you want to delete module-{" "} Are you sure you want to delete module-{" "}
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the <span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the
data related to the module will be permanently removed. This action cannot be undone. data related to the module will be permanently removed. This action cannot be undone.
</p> </p>
</div> </div>

View File

@ -6,6 +6,7 @@ import { ProjectMemberService } from "services/project";
// ui // ui
import { Avatar, CustomSearchSelect } from "@plane/ui"; import { Avatar, CustomSearchSelect } from "@plane/ui";
// icons // icons
import { Combobox } from "@headlessui/react";
import { UserCircle } from "lucide-react"; import { UserCircle } from "lucide-react";
// fetch-keys // fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_MEMBERS } from "constants/fetch-keys";
@ -59,6 +60,16 @@ export const ModuleLeadSelect: React.FC<Props> = ({ value, onChange }) => {
)} )}
</div> </div>
} }
footerOption={
<Combobox.Option
value=""
className="flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 text-custom-text-200"
>
<span className="flex items-center justify-start gap-1 text-custom-text-200">
<span>No Lead</span>
</span>
</Combobox.Option>
}
onChange={onChange} onChange={onChange}
noChevron noChevron
/> />

View File

@ -58,8 +58,8 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/pages/${page.id}`}>
<a className="block p-4"> <a className="block p-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center overflow-hidden gap-2">
<p className="mr-2 truncate text-sm">{truncateText(page.name, 75)}</p> <p className="mr-2 truncate text-sm">{page.name}</p>
{page.label_details.length > 0 && {page.label_details.length > 0 &&
page.label_details.map((label) => ( page.label_details.map((label) => (
<div <div
@ -188,9 +188,13 @@ export const SinglePageDetailedItem: React.FC<TSingleStatProps> = ({
</CustomMenu> </CustomMenu>
</div> </div>
</div> </div>
<div className="relative mt-2 space-y-2 text-sm text-custom-text-200"> {page.blocks.length > 0 && (
{page.blocks.length > 0 ? page.blocks.slice(0, 3).map((block) => <h4>{block.name}</h4>) : null} <div className="relative mt-2 space-y-2 text-sm text-custom-text-200">
</div> {page.blocks.slice(0, 3).map((block) => (
<h4 className="truncate">{block.name}</h4>
))}
</div>
)}
</a> </a>
</Link> </Link>
</div> </div>

View File

@ -59,9 +59,9 @@ export const SinglePageListItem: React.FC<TSingleStatProps> = ({
<a> <a>
<div className="relative rounded p-4 text-custom-text-200 hover:bg-custom-background-80"> <div className="relative rounded p-4 text-custom-text-200 hover:bg-custom-background-80">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-wrap items-center gap-2"> <div className="flex overflow-hidden items-center gap-2">
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4 shrink-0" />
<p className="mr-2 truncate text-sm text-custom-text-100">{truncateText(page.name, 75)}</p> <p className="mr-2 truncate text-sm text-custom-text-100">{page.name}</p>
{page.label_details.length > 0 && {page.label_details.length > 0 &&
page.label_details.map((label) => ( page.label_details.map((label) => (
<div <div

View File

@ -139,6 +139,7 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
hasError={Boolean(errors.projectName)} hasError={Boolean(errors.projectName)}
placeholder="Project name" placeholder="Project name"
className="mt-2 w-full" className="mt-2 w-full"
autoComplete="off"
/> />
)} )}
/> />
@ -162,6 +163,7 @@ export const DeleteProjectModal: React.FC<DeleteProjectModal> = (props) => {
hasError={Boolean(errors.confirmDelete)} hasError={Boolean(errors.confirmDelete)}
placeholder="Enter 'delete my project'" placeholder="Enter 'delete my project'"
className="mt-2 w-full" className="mt-2 w-full"
autoComplete="off"
/> />
)} )}
/> />

View File

@ -91,7 +91,7 @@ export const IntegrationCard: React.FC<Props> = ({ integration }) => {
return ( return (
<> <>
{integration && ( {integration && (
<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-center justify-between gap-2 border-b border-custom-border-100 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-10 w-10 flex-shrink-0"> <div className="h-10 w-10 flex-shrink-0">
<Image <Image

View File

@ -73,7 +73,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
Join Project? Join Project?
</Dialog.Title> </Dialog.Title>
<p> <p>
Are you sure you want to join the project <span className="font-semibold">{project?.name}</span>? Are you sure you want to join the project <span className="font-semibold break-words">{project?.name}</span>?
Please click the &apos;Join Project&apos; button below to continue. Please click the &apos;Join Project&apos; button below to continue.
</p> </p>
<div className="space-y-3" /> <div className="space-y-3" />

View File

@ -30,7 +30,7 @@ export interface ILeaveProjectModal {
} }
export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => { export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
const { project, isOpen } = props; const { project, isOpen, onClose } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -48,6 +48,7 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
const handleClose = () => { const handleClose = () => {
reset({ ...defaultValues }); reset({ ...defaultValues });
onClose();
}; };
const onSubmit = async (data: any) => { const onSubmit = async (data: any) => {

View File

@ -5,18 +5,7 @@ import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// icons // icons
import { import { MoreVertical, PenSquare, LinkIcon, Star, FileText, Settings, Share2, LogOut, ChevronDown } from "lucide-react";
MoreVertical,
PenSquare,
LinkIcon,
Star,
Trash2,
FileText,
Settings,
Share2,
LogOut,
ChevronDown,
} from "lucide-react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// helpers // helpers
@ -27,7 +16,7 @@ import { IProject } from "types";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui"; import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon } from "@plane/ui";
import { LeaveProjectModal, DeleteProjectModal, PublishProjectModal } from "components/project"; import { LeaveProjectModal, PublishProjectModal } from "components/project";
type Props = { type Props = {
project: IProject; project: IProject;
@ -71,6 +60,7 @@ const navigation = (workspaceSlug: string, projectId: string) => [
]; ];
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => { export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { project, provided, snapshot, handleCopyText, shortContextMenu = false } = props; const { project, provided, snapshot, handleCopyText, shortContextMenu = false } = props;
// store // store
const { project: projectStore, theme: themeStore } = useMobxStore(); const { project: projectStore, theme: themeStore } = useMobxStore();
@ -81,7 +71,6 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// states // states
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false);
const isAdmin = project.member_role === 20; const isAdmin = project.member_role === 20;
@ -121,21 +110,11 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
setLeaveProjectModal(false); setLeaveProjectModal(false);
}; };
const handleDeleteProjectClick = () => {
setDeleteProjectModal(true);
};
const handleDeleteProjectModalClose = () => {
setDeleteProjectModal(false);
router.push(`/${workspaceSlug}/projects`);
};
return ( return (
<> <>
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} /> <PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
<DeleteProjectModal project={project} isOpen={deleteProjectModalOpen} onClose={handleDeleteProjectModalClose} />
<LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} /> <LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} />
<Disclosure key={project.id} defaultOpen={projectId === project.id}> <Disclosure key={`${project.id} ${projectId}`} defaultOpen={projectId === project.id}>
{({ open }) => ( {({ open }) => (
<> <>
<div <div
@ -186,9 +165,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</span> </span>
)} )}
{!isCollapsed && ( {!isCollapsed && <p className={`truncate text-custom-sidebar-text-200`}>{project.name}</p>}
<p className={`truncate text-custom-sidebar-text-200`}>{project.name}</p>
)}
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<ChevronDown <ChevronDown
@ -278,15 +255,6 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} )}
{!shortContextMenu && isAdmin && (
<CustomMenu.MenuItem onClick={handleDeleteProjectClick}>
<span className="flex items-center justify-start gap-2 ">
<Trash2 className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Delete project</span>
</span>
</CustomMenu.MenuItem>
)}
</CustomMenu> </CustomMenu>
)} )}
</div> </div>

View File

@ -149,29 +149,38 @@ export const ProjectSidebarList: FC = observer(() => {
</button> </button>
</div> </div>
)} )}
<Disclosure.Panel as="div" className="space-y-2"> <Transition
{orderedFavProjects.map((project, index) => ( enter="transition duration-100 ease-out"
<Draggable enterFrom="transform scale-95 opacity-0"
key={project.id} enterTo="transform scale-100 opacity-100"
draggableId={project.id} leave="transition duration-75 ease-out"
index={index} leaveFrom="transform scale-100 opacity-100"
isDragDisabled={!project.is_member} leaveTo="transform scale-95 opacity-0"
> >
{(provided, snapshot) => ( <Disclosure.Panel as="div" className="space-y-2">
<div ref={provided.innerRef} {...provided.draggableProps}> {orderedFavProjects.map((project, index) => (
<ProjectSidebarListItem <Draggable
key={project.id} key={project.id}
project={project} draggableId={project.id}
provided={provided} index={index}
snapshot={snapshot} isDragDisabled={!project.is_member}
handleCopyText={() => handleCopyText(project.id)} >
shortContextMenu {(provided, snapshot) => (
/> <div ref={provided.innerRef} {...provided.draggableProps}>
</div> <ProjectSidebarListItem
)} key={project.id}
</Draggable> project={project}
))} provided={provided}
</Disclosure.Panel> snapshot={snapshot}
handleCopyText={() => handleCopyText(project.id)}
shortContextMenu
/>
</div>
)}
</Draggable>
))}
</Disclosure.Panel>
</Transition>
{provided.placeholder} {provided.placeholder}
</> </>
)} )}

View File

@ -104,7 +104,7 @@ export const DeleteProjectViewModal: React.FC<Props> = observer((props) => {
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200">
Are you sure you want to delete view-{" "} Are you sure you want to delete view-{" "}
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the <span className="break-all font-medium text-custom-text-100">{data?.name}</span>? All of the
data related to the view will be permanently removed. This action cannot be undone. data related to the view will be permanently removed. This action cannot be undone.
</p> </p>
</div> </div>

View File

@ -9,7 +9,7 @@ import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components
// ui // ui
import { Button, Input, TextArea } from "@plane/ui"; import { Button, Input, TextArea } from "@plane/ui";
// types // types
import { IProjectView } from "types"; import { IProjectView, IIssueFilterOptions } from "types";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
@ -43,7 +43,34 @@ export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, ha
defaultValues, defaultValues,
}); });
const selectedFilters = watch("query_data"); const selectedFilters: IIssueFilterOptions = {};
Object.entries(watch("query_data") ?? {}).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
selectedFilters[key as keyof IIssueFilterOptions] = value;
});
// for removing filters from a key
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!value) return;
const newValues = selectedFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (newValues.includes(val)) newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (selectedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
}
setValue("query_data", {
...selectedFilters,
[key]: newValues,
});
};
const handleCreateUpdateView = async (formData: IProjectView) => { const handleCreateUpdateView = async (formData: IProjectView) => {
await handleFormSubmit(formData); await handleFormSubmit(formData);
@ -106,7 +133,7 @@ export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, ha
id="description" id="description"
name="description" name="description"
placeholder="Description" placeholder="Description"
className="resize-none text-sm" className="h-24 w-full resize-none text-sm"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -153,10 +180,10 @@ export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, ha
<AppliedFiltersList <AppliedFiltersList
appliedFilters={selectedFilters} appliedFilters={selectedFilters}
handleClearAllFilters={clearAllFilters} handleClearAllFilters={clearAllFilters}
handleRemoveFilter={() => {}} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.projectLabels ?? undefined} labels={projectStore.projectLabels ?? []}
members={projectMembers?.map((m) => m.member) ?? undefined} members={projectMembers?.map((m) => m.member) ?? []}
states={projectStateStore.projectStates ?? undefined} states={projectStateStore.projectStates ?? []}
/> />
</div> </div>
)} )}

View File

@ -60,13 +60,13 @@ export const ProjectViewListItem: React.FC<Props> = observer((props) => {
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}> <Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
<a className="flex items-center justify-between relative rounded p-4 w-full"> <a className="flex items-center justify-between relative rounded p-4 w-full">
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 overflow-hidden">
<div className="grid place-items-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100"> <div className="grid place-items-center flex-shrink-0 h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100">
<PhotoFilterIcon className="h-3.5 w-3.5" /> <PhotoFilterIcon className="h-3.5 w-3.5" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col overflow-hidden ">
<p className="truncate text-sm leading-4 font-medium">{truncateText(view.name, 75)}</p> <p className="text-sm leading-4 font-medium truncate break-all">{view.name}</p>
{view?.description && <p className="text-xs text-custom-text-200">{view.description}</p>} {view?.description && <p className="text-xs text-custom-text-200 break-all">{view.description}</p>}
</div> </div>
</div> </div>
<div className="ml-2 flex flex-shrink-0"> <div className="ml-2 flex flex-shrink-0">

View File

@ -161,7 +161,7 @@ export const CreateWorkspaceForm: FC<Props> = observer((props) => {
}} }}
ref={ref} ref={ref}
hasError={Boolean(errors.slug)} hasError={Boolean(errors.slug)}
placeholder="Enter workspace name..." placeholder="Enter workspace url..."
className="block rounded-md bg-transparent py-2 !px-0 text-sm w-full border-none" className="block rounded-md bg-transparent py-2 !px-0 text-sm w-full border-none"
/> />
)} )}

View File

@ -141,6 +141,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
hasError={Boolean(errors.workspaceName)} hasError={Boolean(errors.workspaceName)}
placeholder="Workspace name" placeholder="Workspace name"
className="mt-2 w-full" className="mt-2 w-full"
autoComplete="off"
/> />
)} )}
/> />
@ -165,6 +166,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
hasError={Boolean(errors.confirmDelete)} hasError={Boolean(errors.confirmDelete)}
placeholder="Enter 'delete my workspace'" placeholder="Enter 'delete my workspace'"
className="mt-2 w-full" className="mt-2 w-full"
autoComplete="off"
/> />
)} )}
/> />

View File

@ -95,27 +95,37 @@ export const WorkspaceMemberSelect: FC<IWorkspaceMemberSelect> = (props) => {
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}> <div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? ( {filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((workspaceMember: IWorkspaceMember) => ( <>
{filteredOptions.map((workspaceMember: IWorkspaceMember) => (
<Listbox.Option
key={workspaceMember.id}
value={workspaceMember}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active && !selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<div className="flex items-center gap-2">
<Avatar name={workspaceMember?.member.display_name} src={workspaceMember?.member.avatar} />
{workspaceMember.member.display_name}
</div>
{selected && <Check className="h-3.5 w-3.5" />}
</>
)}
</Listbox.Option>
))}
<Listbox.Option <Listbox.Option
key={workspaceMember.id} value=""
value={workspaceMember} className="flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 text-custom-text-200"
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active && !selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
> >
{({ selected }) => ( <span className="flex items-center justify-start gap-1 text-custom-text-200">
<> <span>No Lead</span>
<div className="flex items-center gap-2"> </span>
<Avatar name={workspaceMember?.member.display_name} src={workspaceMember?.member.avatar} />
{workspaceMember.member.display_name}
</div>
{selected && <Check className="h-3.5 w-3.5" />}
</>
)}
</Listbox.Option> </Listbox.Option>
)) </>
) : ( ) : (
<span className="flex items-center gap-2 p-1"> <span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p> <p className="text-left text-custom-text-200 ">No matching results</p>

View File

@ -1,6 +1,7 @@
import { useState, FC } from "react"; import { useState, FC } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { mutate } from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
@ -39,7 +40,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
// store // store
const { const {
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation }, workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
user: { currentWorkspaceMemberInfo, currentWorkspaceRole }, user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings },
} = useMobxStore(); } = useMobxStore();
const isAdmin = currentWorkspaceRole === 20; const isAdmin = currentWorkspaceRole === 20;
// states // states
@ -51,14 +52,22 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
if (member.member) if (member.member)
await removeMember(workspaceSlug.toString(), member.id).catch((err) => { await removeMember(workspaceSlug.toString(), member.id)
const error = err?.error; .then(() => {
setToastAlert({ const memberId = member.memberId;
type: "error",
title: "Error", if (memberId === currentUser?.id && currentUserSettings) {
message: error || "Something went wrong", if (currentUserSettings.workspace?.invites > 0) router.push("/invitations");
else router.push("/create-workspace");
}
})
.catch((err) => {
setToastAlert({
type: "error",
title: "Error",
message: err?.error || "Something went wrong",
});
}); });
});
else else
await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id) await deleteWorkspaceInvitation(workspaceSlug.toString(), member.id)
.then(() => { .then(() => {
@ -69,12 +78,17 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
}); });
}) })
.catch((err) => { .catch((err) => {
const error = err?.error;
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error", title: "Error",
message: error || "Something went wrong", message: err?.error || "Something went wrong",
});
})
.finally(() => {
mutate(`WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`, (prevData: any) => {
if (!prevData) return prevData;
return prevData.filter((item: any) => item.id !== member.id);
}); });
}); });
}; };

View File

@ -29,9 +29,15 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea
); );
const searchedMembers = workspaceMembersWithInvitations?.filter((member: any) => { const searchedMembers = workspaceMembersWithInvitations?.filter((member: any) => {
const fullName = `${member.first_name} ${member.last_name}`.toLowerCase(); const email = member.email?.toLowerCase();
const displayName = member.display_name.toLowerCase(); const displayName = member.display_name.toLowerCase();
return displayName.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase()); const fullName = `${member.first_name} ${member.last_name}`.toLowerCase();
return (
displayName.includes(searchQuery.toLowerCase()) ||
fullName.includes(searchQuery.toLowerCase()) ||
email?.includes(searchQuery.toLowerCase())
);
}); });
if ( if (

View File

@ -114,7 +114,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
value={value} value={value}
placeholder="Description" placeholder="Description"
onChange={onChange} onChange={onChange}
className="h-32 resize-none text-sm" className="h-24 w-full resize-none text-sm"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
/> />
)} )}

View File

@ -64,7 +64,7 @@ export const WorkspaceSettingsSidebar = () => {
<a> <a>
<div <div
className={`px-4 py-2 text-sm font-medium rounded-md ${ className={`px-4 py-2 text-sm font-medium rounded-md ${
(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href) router.pathname.split("/")?.[3] === link.href.split("/")?.[3]
? "bg-custom-primary-100/10 text-custom-primary-100" ? "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" : "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`} }`}

View File

@ -1,14 +1,12 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import { observer } from "mobx-react-lite";
// services // store
import { ProjectService, ProjectMemberService } from "services/project"; import { useMobxStore } from "lib/mobx/store-provider";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
import { ProjectSettingLayout } from "layouts/settings-layout"; import { ProjectSettingLayout } from "layouts/settings-layout";
// hooks // hooks
import useUserAuth from "hooks/use-user-auth";
import useProjectDetails from "hooks/use-project-details";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
@ -16,45 +14,32 @@ import { ProjectSettingHeader } from "components/headers";
// types // types
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
import { IProject } from "types"; import { IProject } from "types";
// constant
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
// services const AutomationSettingsPage: NextPageWithLayout = observer(() => {
const projectService = new ProjectService();
const projectMemberService = new ProjectMemberService();
const AutomationSettingsPage: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { projectDetails } = useProjectDetails(); // store
const {
const { data: memberDetails } = useSWR( user: { currentProjectRole },
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, project: { currentProjectDetails: projectDetails, updateProject },
workspaceSlug && projectId } = useMobxStore();
? () => projectMemberService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
: null
);
const handleChange = async (formData: Partial<IProject>) => { const handleChange = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId || !projectDetails) return; if (!workspaceSlug || !projectId || !projectDetails) return;
await projectService await updateProject(workspaceSlug.toString(), projectId.toString(), formData).catch(() => {
.updateProject(workspaceSlug as string, projectId as string, formData, user) setToastAlert({
.then(() => {}) type: "error",
.catch(() => { title: "Error!",
setToastAlert({ message: "Something went wrong. Please try again.",
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
}); });
});
}; };
const isAdmin = memberDetails?.role === 20; const isAdmin = currentProjectRole === 20;
return ( return (
<section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}> <section className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
@ -65,7 +50,7 @@ const AutomationSettingsPage: NextPageWithLayout = () => {
<AutoCloseAutomation handleChange={handleChange} /> <AutoCloseAutomation handleChange={handleChange} />
</section> </section>
); );
}; });
AutomationSettingsPage.getLayout = function getLayout(page: ReactElement) { AutomationSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (

View File

@ -17,7 +17,7 @@ const EstimatesSettingsPage: NextPageWithLayout = () => (
EstimatesSettingsPage.getLayout = function getLayout(page: ReactElement) { EstimatesSettingsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<ProjectSettingHeader title="Estimates Settings" />} withProjectWrapper> <AppLayout header={<ProjectSettingHeader title="Estimates Settings" />} withProjectWrapper>
<ProjectSettingLayout>{page}; </ProjectSettingLayout> <ProjectSettingLayout>{page}</ProjectSettingLayout>
</AppLayout> </AppLayout>
); );
}; };

View File

@ -43,7 +43,7 @@ const ProjectIntegrationsPage: NextPageWithLayout = () => {
return ( return (
<div className={`pr-9 py-8 gap-10 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}> <div className={`pr-9 py-8 gap-10 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-100">
<h3 className="text-xl font-medium">Integrations</h3> <h3 className="text-xl font-medium">Integrations</h3>
</div> </div>
{workspaceIntegrations ? ( {workspaceIntegrations ? (

View File

@ -9,12 +9,12 @@ import ExportGuide from "components/exporter/guide";
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
const ExportsPage: NextPageWithLayout = () => ( const ExportsPage: NextPageWithLayout = () => (
<div className="pr-9 py-8 w-full overflow-y-auto"> <div className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center py-3.5 border-b border-custom-border-100"> <div className="flex items-center py-3.5 border-b border-custom-border-100">
<h3 className="text-xl font-medium">Exports</h3> <h3 className="text-xl font-medium">Exports</h3>
</div> </div>
<ExportGuide /> <ExportGuide />
</div> </div>
); );
ExportsPage.getLayout = function getLayout(page: ReactElement) { ExportsPage.getLayout = function getLayout(page: ReactElement) {

View File

@ -208,29 +208,38 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
* @param data * @param data
*/ */
updateMember = async (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => { updateMember = async (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => {
const members = this.members?.[workspaceSlug]; const originalMembers = [...this.members?.[workspaceSlug]]; // in case of error, we will revert back to original members
members?.map((m) => (m.id === memberId ? { ...m, ...data } : m));
const members = [...this.members?.[workspaceSlug]];
const index = members.findIndex((m) => m.id === memberId);
members[index] = { ...members[index], ...data };
// optimistic update
runInAction(() => {
this.loader = true;
this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
});
try { try {
runInAction(() => {
this.loader = true;
this.error = null;
});
await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data); await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberId, data);
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
}); });
} catch (error) { } catch (error) {
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = error; this.error = error;
this.members = {
...this.members,
[workspaceSlug]: originalMembers,
};
}); });
throw error; throw error;
@ -243,8 +252,20 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
* @param memberId * @param memberId
*/ */
removeMember = async (workspaceSlug: string, memberId: string) => { removeMember = async (workspaceSlug: string, memberId: string) => {
const members = this.members?.[workspaceSlug]; const members = [...this.members?.[workspaceSlug]];
members?.filter((m) => m.id !== memberId); const originalMembers = this.members?.[workspaceSlug]; // in case of error, we will revert back to original members
// removing member from the array
const index = members.findIndex((m) => m.id === memberId);
members.splice(index, 1);
// optimistic update
runInAction(() => {
this.members = {
...this.members,
[workspaceSlug]: members,
};
});
try { try {
runInAction(() => { runInAction(() => {
@ -257,15 +278,15 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
this.members = {
...this.members,
[workspaceSlug]: members,
};
}); });
} catch (error) { } catch (error) {
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = error; this.error = error;
this.members = {
...this.members,
[workspaceSlug]: originalMembers,
};
}); });
throw error; throw error;