fix: merge conflicts resolved

This commit is contained in:
Aaryan Khandelwal 2023-12-14 17:49:19 +05:30
commit 2f27d2a772
103 changed files with 929 additions and 2000 deletions

View File

@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
1. Clone the repo
```bash
git clone https://github.com/makeplane/plane
cd plane
git clone https://github.com/makeplane/plane.git [folder-name]
cd [folder-name]
chmod +x setup.sh
```
@ -44,33 +44,12 @@ chmod +x setup.sh
./setup.sh
```
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
3. Start the containers
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
docker compose -f docker-compose-local.yml up
```
```bash
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
```
4. Run Docker compose up
```bash
docker compose up -d
```
5. Install dependencies
```bash
yarn install
```
6. Run the web app in development mode
```bash
yarn dev
```
## Missing a Feature?

View File

@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
## ⚡️ Contributors Quick Start
@ -63,7 +63,7 @@ Thats it!
## 🍙 Self Hosting
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
## 🚀 Features

View File

@ -49,5 +49,5 @@ USER captain
# Expose container port and run entry point script
EXPOSE 8000
# CMD [ "./bin/takeoff" ]
CMD [ "./bin/takeoff.local" ]

31
apiserver/bin/takeoff.local Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
set -e
python manage.py wait_for_db
python manage.py migrate
# Create the default bucket
#!/bin/bash
# Collect system information
HOSTNAME=$(hostname)
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
CPU_INFO=$(cat /proc/cpuinfo)
MEMORY_INFO=$(free -h)
DISK_INFO=$(df -h)
# Concatenate information and compute SHA-256 hash
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
# Export the variables
export MACHINE_SIGNATURE=$SIGNATURE
# Register instance
python manage.py register_instance $MACHINE_SIGNATURE
# Load the configuration variable
python manage.py configure_instance
# Create the default bucket
python manage.py create_bucket
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local

View File

@ -145,6 +145,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
)
)
)
.prefetch_related(
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=self.kwargs.get("slug"),
is_active=True,
).select_related("member"),
to_attr="members_list",
)
)
.distinct()
)
@ -160,16 +170,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects = (
self.get_queryset()
.annotate(sort_order=Subquery(sort_order_query))
.prefetch_related(
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug,
is_active=True,
).select_related("member"),
to_attr="members_list",
)
)
.order_by("sort_order", "name")
)
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@ -677,6 +677,25 @@ class ProjectMemberViewSet(BaseViewSet):
)
)
# Check if the user is already a member of the project and is inactive
if ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member_id=member.get("member_id"),
is_active=False,
).exists():
member_detail = ProjectMember.objects.get(
workspace__slug=slug,
project_id=project_id,
member_id=member.get("member_id"),
is_active=False,
)
# Check if the user has not deactivated the account
user = User.objects.filter(pk=member.get("member_id")).first()
if user.is_active:
member_detail.is_active = True
member_detail.save(update_fields=["is_active"])
project_members = ProjectMember.objects.bulk_create(
bulk_project_members,
batch_size=10,

View File

@ -70,6 +70,7 @@ from plane.app.permissions import (
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
WorkspaceViewerPermission,
WorkspaceUserPermission,
)
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters
@ -501,6 +502,18 @@ class WorkSpaceMemberViewSet(BaseViewSet):
WorkspaceEntityPermission,
]
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [
WorkspaceUserPermission,
]
else:
self.permission_classes = [
WorkspaceEntityPermission,
]
return super(WorkSpaceMemberViewSet, self).get_permissions()
search_fields = [
"member__display_name",
"member__first_name",

View File

@ -65,7 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS),
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(

View File

@ -51,7 +51,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS),
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(

View File

@ -41,7 +41,7 @@ def magic_link(email, key, token, current_site):
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS),
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(

View File

@ -60,7 +60,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS),
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(

View File

@ -70,7 +70,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=bool(EMAIL_USE_TLS),
use_tls=EMAIL_USE_TLS == "1",
)
msg = EmailMultiAlternatives(

View File

@ -12,7 +12,6 @@ volumes:
services:
plane-redis:
container_name: plane-redis
image: redis:6.2.7-alpine
restart: unless-stopped
networks:
@ -21,7 +20,6 @@ services:
- redisdata:/data
plane-minio:
container_name: plane-minio
image: minio/minio
restart: unless-stopped
networks:
@ -36,7 +34,6 @@ services:
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
plane-db:
container_name: plane-db
image: postgres:15.2-alpine
restart: unless-stopped
networks:
@ -53,7 +50,6 @@ services:
PGDATA: /var/lib/postgresql/data
web:
container_name: web
build:
context: .
dockerfile: ./web/Dockerfile.dev
@ -61,8 +57,7 @@ services:
networks:
- dev_env
volumes:
- .:/app
command: yarn dev --filter=web
- ./web:/app/web
env_file:
- ./web/.env
depends_on:
@ -73,22 +68,17 @@ services:
build:
context: .
dockerfile: ./space/Dockerfile.dev
container_name: space
restart: unless-stopped
networks:
- dev_env
volumes:
- .:/app
command: yarn dev --filter=space
env_file:
- ./space/.env
- ./space:/app/space
depends_on:
- api
- worker
- web
api:
container_name: api
build:
context: ./apiserver
dockerfile: Dockerfile.dev
@ -99,7 +89,7 @@ services:
- dev_env
volumes:
- ./apiserver:/code
command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
# command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
env_file:
- ./apiserver/.env
depends_on:
@ -107,7 +97,6 @@ services:
- plane-redis
worker:
container_name: bgworker
build:
context: ./apiserver
dockerfile: Dockerfile.dev
@ -127,7 +116,6 @@ services:
- plane-redis
beat-worker:
container_name: beatworker
build:
context: ./apiserver
dockerfile: Dockerfile.dev
@ -147,10 +135,9 @@ services:
- plane-redis
proxy:
container_name: proxy
build:
context: ./nginx
dockerfile: Dockerfile
dockerfile: Dockerfile.dev
restart: unless-stopped
networks:
- dev_env

10
nginx/Dockerfile.dev Normal file
View File

@ -0,0 +1,10 @@
FROM nginx:1.25.0-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf.dev /etc/nginx/nginx.conf.template
COPY ./env.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Update all environment variables
CMD ["/docker-entrypoint.sh"]

View File

@ -18,7 +18,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /space/ {
location /spaces/ {
proxy_pass http://localhost:4000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

36
nginx/nginx.conf.dev Normal file
View File

@ -0,0 +1,36 @@
events {
}
http {
sendfile on;
server {
listen 80;
root /www/data/;
access_log /var/log/nginx/access.log;
client_max_body_size ${FILE_SIZE_LIMIT};
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://web:3000/;
}
location /api/ {
proxy_pass http://api:8000/api/;
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_pass http://space:4000/spaces/;
}
location /${BUCKET_NAME}/ {
proxy_pass http://plane-minio:9000/uploads/;
}
}
}

View File

@ -6,7 +6,6 @@ export LC_ALL=C
export LC_CTYPE=C
cp ./web/.env.example ./web/.env
cp ./space/.env.example ./space/.env
cp ./apiserver/.env.example ./apiserver/.env
# Generate the SECRET_KEY that will be used by django

View File

@ -7,5 +7,8 @@ WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
EXPOSE 3000
EXPOSE 4000
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
CMD ["yarn","dev", "--filter=space"]

View File

@ -8,4 +8,5 @@ COPY . .
RUN yarn global add turbo
RUN yarn install
EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/web/node_modules" ]
CMD ["yarn", "dev", "--filter=web"]

View File

@ -78,7 +78,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// toast alert
const { setToastAlert } = useToast();
useSWR(
const { isLoading } = useSWR(
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
);
@ -97,7 +97,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
// : null
// ) as { data: IIssue[] | undefined };
if (!cycle)
if (!cycle && isLoading)
return (
<Loader>
<Loader.Item height="250px" />

View File

@ -30,6 +30,8 @@ import {
} from "helpers/date-time.helper";
// types
import { ICycle } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// fetch-keys
import { CYCLE_STATUS } from "constants/cycle";
@ -53,6 +55,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const {
cycle: cycleDetailsStore,
trackEvent: { setTrackElement },
user: { currentProjectRole },
} = useMobxStore();
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
@ -270,10 +273,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader>
);
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? "");
const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? "");
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const areYearsEqual =
startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear());
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
@ -286,6 +290,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
: `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
{cycleDetails && workspaceSlug && projectId && (
@ -312,7 +318,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<button onClick={handleCopyText}>
<LinkIcon className="h-3 w-3 text-custom-text-300" />
</button>
{!isCompleted && (
{!isCompleted && isEditingAllowed && (
<CustomMenu width="lg" placement="bottom-end" ellipsis>
<CustomMenu.MenuItem
onClick={() => {
@ -349,8 +355,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="relative flex h-full w-52 items-center gap-2.5">
<Popover className="flex h-full items-center justify-center rounded-lg">
<Popover.Button
disabled={isCompleted ?? false}
className="cursor-default text-sm font-medium text-custom-text-300"
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
</Popover.Button>
@ -373,10 +381,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart
selectsStart={watch("end_date") ? true : false}
/>
</Popover.Panel>
</Transition>
@ -385,8 +393,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<Popover className="flex h-full items-center justify-center rounded-lg">
<>
<Popover.Button
disabled={isCompleted ?? false}
className="cursor-default text-sm font-medium text-custom-text-300"
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={isCompleted || !isEditingAllowed}
>
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</Popover.Button>
@ -409,10 +419,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
startDate={watch("start_date") ?? watch("end_date") ?? null}
endDate={watch("end_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>

View File

@ -15,7 +15,6 @@ import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { IIssue } from "types";
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
@ -33,7 +32,6 @@ type Props = {
export const IssueGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
title,
blockUpdateHandler,
blocks,
enableReorder,

View File

@ -156,7 +156,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
key={cycle.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
>
{truncateText(cycle.name, 40)}
<div className="flex items-center gap-1.5">
<ContrastIcon className="h-3 w-3" />
{truncateText(cycle.name, 40)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@ -193,20 +196,23 @@ export const CycleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("CYCLE_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
setTrackElement("CYCLE_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)}
<button
type="button"

View File

@ -16,6 +16,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EFilterType } from "store_legacy/issues/types";
import { EUserWorkspaceRoles } from "constants/workspace";
const GLOBAL_VIEW_LAYOUTS = [
{ key: "list", title: "List", link: "/workspace-views", icon: List },
@ -38,7 +39,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
workspace: { workspaceLabels },
workspaceMember: { workspaceMembers },
project: { workspaceProjects },
user: { currentWorkspaceRole },
workspaceGlobalIssuesFilter: { issueFilters, updateFilters },
} = useMobxStore();
@ -77,6 +78,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
[workspaceSlug, updateFilters]
);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
@ -142,10 +145,11 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
</FiltersDropdown>
</>
)}
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
New View
</Button>
{isAuthorizedUser && (
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
New View
</Button>
)}
</div>
</div>
</>

View File

@ -11,7 +11,7 @@ import { ProjectAnalyticsModal } from "components/analytics";
// ui
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
// icons
import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
import { ArrowRight, Plus } from "lucide-react";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
@ -144,7 +144,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
<CustomMenu
label={
<>
<ContrastIcon className="h-3 w-3" />
<DiceIcon className="h-3 w-3" />
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
</>
}
@ -157,7 +157,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
key={module.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
>
{truncateText(module.name, 40)}
<div className="flex items-center gap-1.5">
<DiceIcon className="h-3 w-3" />
{truncateText(module.name, 40)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@ -194,20 +197,23 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("MODULE_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
setTrackElement("MODULE_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)}
<button
type="button"

View File

@ -202,20 +202,23 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</span>
</Link>
)}
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("PROJECT_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
<>
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
setTrackElement("PROJECT_PAGE_HEADER");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</>
)}
</div>
</div>

View File

@ -139,7 +139,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
key={view.id}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${view.id}`)}
>
{truncateText(view.name, 40)}
<div className="flex items-center gap-1.5">
<PhotoFilterIcon height={12} width={12} />
{truncateText(view.name, 40)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@ -153,7 +156,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end">
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@ -176,7 +180,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
{
{canUserCreateIssue && (
<Button
onClick={() => {
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
@ -187,7 +191,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
>
Add Issue
</Button>
}
)}
</div>
</div>
);

View File

@ -2,11 +2,13 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
// components
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectViewsHeader: React.FC = observer(() => {
// router
@ -16,8 +18,14 @@ export const ProjectViewsHeader: React.FC = observer(() => {
const {
commandPalette: { toggleCreateViewModal },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const canUserCreateIssue =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return (
<>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
@ -52,18 +60,20 @@ export const ProjectViewsHeader: React.FC = observer(() => {
</Breadcrumbs>
</div>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<div>
<Button
variant="primary"
size="sm"
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
onClick={() => toggleCreateViewModal(true)}
>
Create View
</Button>
{canUserCreateIssue && (
<div className="flex flex-shrink-0 items-center gap-2">
<div>
<Button
variant="primary"
size="sm"
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
onClick={() => toggleCreateViewModal(true)}
>
Create View
</Button>
</div>
</div>
</div>
)}
</div>
</>
);

View File

@ -1,9 +1,11 @@
import { observer } from "mobx-react-lite";
import { Search, Plus, Briefcase } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectsHeader = observer(() => {
// store hooks
@ -11,8 +13,13 @@ export const ProjectsHeader = observer(() => {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { workspaceProjects, searchQuery, setSearchQuery } = useProject();
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@ -38,17 +45,18 @@ export const ProjectsHeader = observer(() => {
/>
</div>
)}
<Button
prependIcon={<Plus />}
size="sm"
onClick={() => {
setTrackElement("PROJECTS_PAGE_HEADER");
commandPaletteStore.toggleCreateProjectModal(true);
}}
>
Add Project
</Button>
{isAuthorizedUser && (
<Button
prependIcon={<Plus />}
size="sm"
onClick={() => {
setTrackElement("PROJECTS_PAGE_HEADER");
commandPaletteStore.toggleCreateProjectModal(true);
}}
>
Add Project
</Button>
)}
</div>
</div>
);

View File

@ -165,16 +165,16 @@ export const InboxMainContent: React.FC = observer(() => {
issueStatus === -2
? "border-yellow-500 bg-yellow-500/10 text-yellow-500"
: issueStatus === -1
? "border-red-500 bg-red-500/10 text-red-500"
: issueStatus === 0
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
? "border-red-500 bg-red-500/10 text-red-500"
: issueStatus === 0
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
? "border-red-500 bg-red-500/10 text-red-500"
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
: issueStatus === 1
? "border-green-500 bg-green-500/10 text-green-500"
: issueStatus === 2
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
: ""
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
: issueStatus === 1
? "border-green-500 bg-green-500/10 text-green-500"
: issueStatus === 2
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
: ""
}`}
>
{issueStatus === -2 ? (
@ -225,7 +225,7 @@ export const InboxMainContent: React.FC = observer(() => {
</>
) : null}
</div>
<div className="mb-5 flex items-center">
<div className="mb-2.5 flex items-center">
{currentIssueState && (
<StateGroupIcon
className="mr-3 h-4 w-4"

View File

@ -135,7 +135,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
debouncedFormSave();
}}
required
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
hasError={Boolean(errors?.description)}
role="textbox"
disabled={!isAllowed}

View File

@ -1,3 +1,4 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { Droppable } from "@hello-pangea/dnd";
// components
@ -48,11 +49,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
quickAddCallback,
viewId,
} = props;
const [showAllIssues, setShowAllIssues] = useState(false);
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
const issueIdList = groupedIssueIds ? groupedIssueIds[renderDateFormat(date.date)] : null;
const totalIssues = issueIdList?.length ?? 0;
return (
<>
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
@ -87,7 +89,13 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
{...provided.droppableProps}
ref={provided.innerRef}
>
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
<CalendarIssueBlocks
issues={issues}
issueIdList={issueIdList}
quickActions={quickActions}
showAllIssues={showAllIssues}
/>
{enableQuickIssueCreate && !disableIssueCreation && (
<div className="px-2 py-1">
<CalendarQuickAddIssueForm
@ -98,9 +106,23 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
}}
quickAddCallback={quickAddCallback}
viewId={viewId}
onOpen={() => setShowAllIssues(true)}
/>
</div>
)}
{totalIssues > 4 && (
<div className="flex items-center px-2.5 py-1">
<button
type="button"
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
onClick={() => setShowAllIssues((prevData) => !prevData)}
>
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
</button>
</div>
)}
{provided.placeholder}
</div>
)}

View File

@ -15,10 +15,11 @@ type Props = {
issues: IIssueResponse | undefined;
issueIdList: string[] | null;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
showAllIssues?: boolean;
};
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const { issues, issueIdList, quickActions } = props;
const { issues, issueIdList, quickActions, showAllIssues = false } = props;
// router
const router = useRouter();
@ -52,7 +53,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
return (
<>
{issueIdList?.map((issueId, index) => {
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
if (!issues?.[issueId]) return null;
const issue = issues?.[issueId];

View File

@ -26,6 +26,7 @@ type Props = {
viewId?: string
) => Promise<IIssue | undefined>;
viewId?: string;
onOpen?: () => void;
};
const defaultValues: Partial<IIssue> = {
@ -56,7 +57,8 @@ const Inputs = (props: any) => {
};
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -142,6 +144,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
}
};
const handleOpen = () => {
setIsOpen(true);
if (onOpen) onOpen();
};
return (
<>
{isOpen && (
@ -165,7 +172,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
<button
type="button"
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100"
onClick={() => setIsOpen(true)}
onClick={handleOpen}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>

View File

@ -15,6 +15,8 @@ import emptyIssue from "public/empty-state/issue.svg";
// types
import { ISearchIssueResponse } from "types";
import { EProjectStore } from "store_legacy/command-palette.store";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
workspaceSlug: string | undefined;
@ -31,6 +33,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
cycleIssues: cycleIssueStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
user: { currentProjectRole: userRole },
} = useMobxStore();
const { setToastAlert } = useToast();
@ -49,6 +52,8 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
});
};
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<ExistingIssuesListModal
@ -75,10 +80,12 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setCycleIssuesListModal(true)}
disabled={!isEditingAllowed}
>
Add an existing issue
</Button>
}
disabled={!isEditingAllowed}
/>
</div>
</>

View File

@ -10,6 +10,8 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { ISearchIssueResponse } from "types";
import useToast from "hooks/use-toast";
import { useState } from "react";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
workspaceSlug: string | undefined;
@ -26,6 +28,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
moduleIssues: moduleIssueStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
user: { currentProjectRole: userRole },
} = useMobxStore();
const { setToastAlert } = useToast();
@ -44,6 +47,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
);
};
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<ExistingIssuesListModal
@ -70,10 +75,12 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setModuleIssuesListModal(true)}
disabled={!isEditingAllowed}
>
Add an existing issue
</Button>
}
disabled={!isEditingAllowed}
/>
</div>
</>

View File

@ -1,9 +1,11 @@
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// components
import { NewEmptyState } from "components/common/new-empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// assets
import emptyIssue from "public/empty-state/empty_issues.webp";
import { EProjectStore } from "store_legacy/command-palette.store";
@ -14,6 +16,11 @@ export const ProjectEmptyState: React.FC = observer(() => {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="grid h-full w-full place-items-center">
@ -35,6 +42,7 @@ export const ProjectEmptyState: React.FC = observer(() => {
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
},
}}
disabled={!isEditingAllowed}
/>
</div>
);

View File

@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
AppliedDateFilters,
@ -16,6 +16,8 @@ import { X } from "lucide-react";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
appliedFilters: IIssueFilterOptions;
@ -33,10 +35,16 @@ const dateFilters = ["start_date", "target_date"];
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props;
const {
user: { currentProjectRole },
} = useMobxStore();
if (!appliedFilters) return null;
if (Object.keys(appliedFilters).length === 0) return null;
const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{Object.entries(appliedFilters).map(([key, value]) => {
@ -53,6 +61,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
<div className="flex flex-wrap items-center gap-1">
{membersFilters.includes(filterKey) && (
<AppliedMembersFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
members={members}
values={value}
@ -63,16 +72,22 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
)}
{filterKey === "labels" && (
<AppliedLabelsFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("labels", val)}
labels={labels}
values={value}
/>
)}
{filterKey === "priority" && (
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} />
<AppliedPriorityFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("priority", val)}
values={value}
/>
)}
{filterKey === "state" && states && (
<AppliedStateFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("state", val)}
states={states}
values={value}
@ -86,30 +101,35 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
)}
{filterKey === "project" && (
<AppliedProjectFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("project", val)}
projects={projects}
values={value}
/>
)}
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemoveFilter(filterKey, null)}
>
<X size={12} strokeWidth={2} />
</button>
{isEditingAllowed && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemoveFilter(filterKey, null)}
>
<X size={12} strokeWidth={2} />
</button>
)}
</div>
</div>
);
})}
<button
type="button"
onClick={handleClearAllFilters}
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
>
Clear all
<X size={12} strokeWidth={2} />
</button>
{isEditingAllowed && (
<button
type="button"
onClick={handleClearAllFilters}
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
>
Clear all
<X size={12} strokeWidth={2} />
</button>
)}
</div>
);
});

View File

@ -9,10 +9,11 @@ type Props = {
handleRemove: (val: string) => void;
labels: IIssueLabel[] | undefined;
values: string[];
editable: boolean | undefined;
};
export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
const { handleRemove, labels, values } = props;
const { handleRemove, labels, values, editable } = props;
return (
<>
@ -30,13 +31,15 @@ export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
}}
/>
<span className="normal-case">{labelDetails.name}</span>
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(labelId)}
>
<X size={10} strokeWidth={2} />
</button>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(labelId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}

View File

@ -9,10 +9,11 @@ type Props = {
handleRemove: (val: string) => void;
members: IUserLite[] | undefined;
values: string[];
editable: boolean | undefined;
};
export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
const { handleRemove, members, values } = props;
const { handleRemove, members, values, editable } = props;
return (
<>
@ -25,13 +26,15 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
<div key={memberId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
<span className="normal-case">{memberDetails.display_name}</span>
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(memberId)}
>
<X size={10} strokeWidth={2} />
</button>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(memberId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}

View File

@ -9,10 +9,11 @@ import { TIssuePriorities } from "types";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values } = props;
const { handleRemove, values, editable } = props;
return (
<>
@ -20,13 +21,15 @@ export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
<div key={priority} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<PriorityIcon priority={priority as TIssuePriorities} className={`h-3 w-3`} />
{priority}
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(priority)}
>
<X size={10} strokeWidth={2} />
</button>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(priority)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
))}
</>

View File

@ -10,10 +10,11 @@ type Props = {
handleRemove: (val: string) => void;
projects: IProject[] | undefined;
values: string[];
editable: boolean | undefined;
};
export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
const { handleRemove, projects, values } = props;
const { handleRemove, projects, values, editable } = props;
return (
<>
@ -34,13 +35,15 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
</span>
)}
<span className="normal-case">{projectDetails.name}</span>
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(projectId)}
>
<X size={10} strokeWidth={2} />
</button>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(projectId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}

View File

@ -10,10 +10,11 @@ type Props = {
handleRemove: (val: string) => void;
states: IState[];
values: string[];
editable: boolean | undefined;
};
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
const { handleRemove, states, values } = props;
const { handleRemove, states, values, editable } = props;
return (
<>
@ -26,13 +27,15 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
<div key={stateId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
{stateDetails.name}
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(stateId)}
>
<X size={10} strokeWidth={2} />
</button>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(stateId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}

View File

@ -11,10 +11,11 @@ type Props = {
children: React.ReactNode;
title?: string;
placement?: Placement;
disabled?: boolean;
};
export const FiltersDropdown: React.FC<Props> = (props) => {
const { children, title = "Dropdown", placement } = props;
const { children, title = "Dropdown", placement, disabled = false } = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -32,6 +33,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
<>
<Popover.Button as={React.Fragment}>
<Button
disabled={disabled}
ref={setReferenceElement}
variant="neutral-primary"
size="sm"

View File

@ -100,7 +100,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
const label = (
<Tooltip tooltipHeading="Assignee" tooltipContent={getTooltipContent()} position="top">
<div className="flex h-full w-full cursor-pointer items-center gap-2 text-custom-text-200">
<div className="flex h-full w-full items-center gap-2 text-custom-text-200">
{value && value.length > 0 && Array.isArray(value) ? (
<AvatarGroup showTooltip={false}>
{value.map((assigneeId) => {

View File

@ -10,7 +10,7 @@ import { IIssue, IUserLite } from "types";
type Props = {
issue: IIssue;
members: IUserLite[] | undefined;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -18,7 +18,7 @@ type Props = {
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
@ -26,8 +26,13 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
projectId={issue.project_detail?.id ?? null}
value={issue.assignees}
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
onChange={(data) => onChange({ assignees: data })}
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
onChange={(data) => {
onChange(issue, { assignees: data });
if (issue.parent) {
mutateSubIssues(issue, { assignees: data });
}
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
noLabelBorder
hideDropdownArrow

View File

@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
return (
<>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
</div>

View File

@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
return (
<>
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200">
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{renderLongDetailDateFormat(issue.created_at)}
</div>

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
<ViewDueDateSelect
issue={issue}
onChange={(val) => onChange({ target_date: val })}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
onChange={(val) => {
onChange(issue, { target_date: val });
if (issue.parent) {
mutateSubIssues(issue, { target_date: val });
}
}}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
noBorder
disabled={disabled}
/>

View File

@ -7,7 +7,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
onChange: (formData: Partial<IIssue>) => void;
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -17,15 +17,20 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
<IssuePropertyEstimates
projectId={issue.project_detail?.id ?? null}
value={issue.estimate_point}
onChange={(data) => onChange({ estimate_point: data })}
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
onChange={(data) => {
onChange(issue, { estimate_point: data });
if (issue.parent) {
mutateSubIssues(issue, { estimate_point: data });
}
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
hideDropdownArrow
disabled={disabled}

View File

@ -9,7 +9,7 @@ import { IIssue, IIssueLabel } from "types";
type Props = {
issue: IIssue;
onChange: (formData: Partial<IIssue>) => void;
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
labels: IIssueLabel[] | undefined;
expandedIssues: string[];
disabled: boolean;
@ -20,7 +20,7 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
@ -28,8 +28,13 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
projectId={issue.project_detail?.id ?? null}
value={issue.labels}
defaultOptions={issue?.label_details ? issue.label_details : []}
onChange={(data) => onChange({ labels: data })}
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
onChange={(data) => {
onChange(issue, { labels: data });
if (issue.parent) {
mutateSubIssues(issue, { assignees: data });
}
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="px-2.5 h-full"
hideDropdownArrow
maxRender={1}

View File

@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
return (
<>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issue.link_count} {issue.link_count === 1 ? "link" : "links"}
</div>

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
<PrioritySelect
value={issue.priority}
onChange={(data) => onChange({ priority: data })}
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
onChange={(data) => {
onChange(issue, { priority: data });
if (issue.parent) {
mutateSubIssues(issue, { priority: data });
}
}}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1"
showTitle
highlightUrgentPriority={false}

View File

@ -9,7 +9,7 @@ import { IIssue } from "types";
type Props = {
issue: IIssue;
onChange: (formData: Partial<IIssue>) => void;
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
expandedIssues: string[];
disabled: boolean;
};
@ -17,14 +17,19 @@ type Props = {
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
<ViewStartDateSelect
issue={issue}
onChange={(val) => onChange({ start_date: val })}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
onChange={(val) => {
onChange(issue, { start_date: val });
if (issue.parent) {
mutateSubIssues(issue, { start_date: val });
}
}}
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
noBorder
disabled={disabled}
/>

View File

@ -6,10 +6,12 @@ import { IssuePropertyState } from "../../properties";
import useSubIssue from "hooks/use-sub-issue";
// types
import { IIssue, IState } from "types";
import { mutate } from "swr";
import { SUB_ISSUES } from "constants/fetch-keys";
type Props = {
issue: IIssue;
onChange: (data: Partial<IIssue>) => void;
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
states: IState[] | undefined;
expandedIssues: string[];
disabled: boolean;
@ -20,7 +22,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
return (
<>
@ -28,7 +30,12 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
projectId={issue.project_detail?.id ?? null}
value={issue.state}
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
onChange={(data) => onChange({ state: data.id, state_detail: data })}
onChange={(data) => {
onChange(issue, { state: data.id, state_detail: data });
if (issue.parent) {
mutateSubIssues(issue, { state: data.id, state_detail: data });
}
}}
className="w-full !h-11 border-b-[0.5px] border-custom-border-200"
buttonClassName="!shadow-none !border-0 h-full w-full"
hideDropdownArrow

View File

@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
return (
<>
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>

View File

@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
return (
<>
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200">
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
{renderLongDetailDateFormat(issue.updated_at)}
</div>

View File

@ -163,18 +163,13 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
{issues?.map((issue) => {
const disableUserActions = !canEditProperties(issue.project);
return (
<div
key={`${property}-${issue.id}`}
className={`h-fit ${
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
}`}
>
<div key={`${property}-${issue.id}`} className={`h-fit ${disableUserActions ? "" : "cursor-pointer"}`}>
{property === "state" ? (
<SpreadsheetStateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
states={states}
/>
) : property === "priority" ? (
@ -182,14 +177,14 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
disabled={disableUserActions}
expandedIssues={expandedIssues}
issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "estimate" ? (
<SpreadsheetEstimateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "assignee" ? (
<SpreadsheetAssigneeColumn
@ -197,7 +192,7 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
expandedIssues={expandedIssues}
issue={issue}
members={members}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "labels" ? (
<SpreadsheetLabelColumn
@ -205,21 +200,21 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
expandedIssues={expandedIssues}
issue={issue}
labels={labels}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "start_date" ? (
<SpreadsheetStartDateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "due_date" ? (
<SpreadsheetDueDateColumn
disabled={disableUserActions}
expandedIssues={expandedIssues}
issue={issue}
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
/>
) : property === "created_on" ? (
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issue={issue} />

View File

@ -216,7 +216,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
</CustomMenu>
</div>
) : null}
<div className="mb-5 flex items-center">
<div className="mb-2.5 flex items-center">
{currentIssueState && (
<StateGroupIcon
className="mr-3 h-4 w-4"

View File

@ -60,7 +60,9 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabl
) : (
<button
type="button"
className="rounded bg-custom-background-80 px-2.5 py-0.5 text-xs text-custom-text-200"
className={`rounded bg-custom-background-80 px-2.5 py-0.5 text-xs text-custom-text-200 ${
disabled ? "cursor-not-allowed" : ""
}`}
>
No assignees
</button>

View File

@ -4,6 +4,8 @@ import { useRouter } from "next/router";
// components
import { ParentIssuesListModal } from "components/issues";
// icons
import { X } from "lucide-react";
// types
import { IIssue, ISearchIssueResponse } from "types";
@ -32,12 +34,20 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
issueId={issueId as string}
projectId={projectId as string}
/>
<button
type="button"
className={`rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
className={`flex items-center gap-2 rounded bg-custom-background-80 px-2.5 py-0.5 text-xs max-w-max" ${
disabled ? "cursor-not-allowed" : "cursor-pointer "
}`}
onClick={() => setIsParentModalOpen(true)}
onClick={() => {
if (issueDetails?.parent) {
onChange("");
setSelectedParentIssue(null);
} else {
setIsParentModalOpen(true);
}
}}
disabled={disabled}
>
{selectedParentIssue && issueDetails?.parent ? (
@ -47,6 +57,7 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
) : (
<span className="text-custom-text-200">Select issue</span>
)}
{issueDetails?.parent && <X className="h-2.5 w-2.5" />}
</button>
</>
);

View File

@ -572,7 +572,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
labelList={issueDetail?.labels ?? []}
submitChanges={submitChanges}
isNotAllowed={!isAllowed}
uneditable={uneditable ?? false}
uneditable={uneditable || !isAllowed}
/>
</div>
</div>

View File

@ -78,7 +78,7 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
projectId={issue?.project_detail?.id || null}
value={issue?.state || null}
onChange={(data) => handleStateChange(data)}
disabled={false}
disabled={!editable}
hideDropdownArrow
/>
</div>
@ -89,7 +89,7 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
value={issue?.assignees || null}
hideDropdownArrow
onChange={(val) => handleAssigneeChange(val)}
disabled={false}
disabled={!editable}
/>
</div>
</div>

View File

@ -2,12 +2,14 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useApplication, useModule } from "hooks/store";
import { useApplication, useModule, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// components
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules";
// ui
import { Loader } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// assets
import emptyModule from "public/empty-state/empty_modules.webp";
import { NewEmptyState } from "components/common/new-empty-state";
@ -18,10 +20,15 @@ export const ModulesListView: React.FC = observer(() => {
const { workspaceSlug, projectId, peekModule } = router.query;
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { projectModules } = useModule();
const { storedValue: modulesView } = useLocalStorage("modules_view", "grid");
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!projectModules)
return (
<Loader className="grid grid-cols-3 gap-4 p-8">
@ -92,6 +99,7 @@ export const ModulesListView: React.FC = observer(() => {
text: "Build your first module",
onClick: () => commandPaletteStore.toggleCreateModuleModal(true),
}}
disabled={!isEditingAllowed}
/>
)}
</>

View File

@ -13,12 +13,13 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
value: string | null | undefined;
onChange: (val: string) => void;
disabled?: boolean;
};
const projectMemberService = new ProjectMemberService();
export const SidebarLeadSelect: FC<Props> = (props) => {
const { value, onChange } = props;
const { value, onChange, disabled = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -51,6 +52,7 @@ export const SidebarLeadSelect: FC<Props> = (props) => {
</div>
<div className="flex w-1/2 items-center rounded-sm">
<CustomSearchSelect
disabled={disabled}
className="w-full rounded-sm"
value={value}
customButtonClassName="rounded-sm"
@ -63,7 +65,7 @@ export const SidebarLeadSelect: FC<Props> = (props) => {
) : (
<div className="group flex w-full items-center justify-between gap-2 p-1 text-sm text-custom-text-400">
<span>No lead</span>
<ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" />
{!disabled && <ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" />}
</div>
)
}

View File

@ -13,12 +13,13 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
value: string[] | undefined;
onChange: (val: string[]) => void;
disabled?: boolean;
};
// services
const projectMemberService = new ProjectMemberService();
export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -48,6 +49,7 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
</div>
<div className="flex w-1/2 items-center rounded-sm ">
<CustomSearchSelect
disabled={disabled}
className="w-full rounded-sm"
value={value ?? []}
customButtonClassName="rounded-sm"
@ -67,7 +69,7 @@ export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
) : (
<div className="group flex w-full items-center justify-between gap-2 p-1 text-sm text-custom-text-400">
<span>No members</span>
<ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" />
{!disabled && <ChevronDown className="hidden h-3.5 w-3.5 group-hover:flex" />}
</div>
)
}

View File

@ -137,7 +137,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
};
const handleCopyText = () => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`)
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`)
.then(() => {
setToastAlert({
type: "success",
@ -238,10 +238,11 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
</Loader>
);
const startDate = new Date(moduleDetails.start_date ?? "");
const endDate = new Date(moduleDetails.target_date ?? "");
const startDate = new Date(watch("start_date") ?? moduleDetails.start_date ?? "");
const endDate = new Date(watch("target_date") ?? moduleDetails.target_date ?? "");
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const areYearsEqual =
startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear());
const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);
@ -254,6 +255,8 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
: `${moduleDetails.total_issues}`
: `${moduleDetails.completed_issues}/${moduleDetails.total_issues}`;
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<LinkModal
@ -283,14 +286,16 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<button onClick={handleCopyText}>
<LinkIcon className="h-3 w-3 text-custom-text-300" />
</button>
<CustomMenu width="lg" placement="bottom-end" ellipsis>
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
{isEditingAllowed && (
<CustomMenu width="lg" placement="bottom-end" ellipsis>
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
</div>
@ -304,7 +309,9 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<CustomSelect
customButton={
<span
className="flex h-6 w-20 cursor-default items-center justify-center rounded-sm text-center text-xs"
className={`flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
style={{
color: moduleStatus ? moduleStatus.color : "#a3a3a2",
backgroundColor: moduleStatus ? `${moduleStatus.color}20` : "#a3a3a220",
@ -317,6 +324,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
onChange={(value: any) => {
submitChanges({ status: value });
}}
disabled={!isEditingAllowed}
>
{MODULE_STATUS.map((status) => (
<CustomSelect.Option key={status.value} value={status.value}>
@ -332,7 +340,12 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="relative flex h-full w-52 items-center gap-2.5">
<Popover className="flex h-full items-center justify-center rounded-lg">
<Popover.Button className="cursor-default text-sm font-medium text-custom-text-300">
<Popover.Button
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
</Popover.Button>
@ -353,10 +366,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("target_date") ? `${watch("target_date")}` : null}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
maxDate={new Date(`${watch("target_date")}`)}
selectsStart
selectsStart={watch("target_date") ? true : false}
/>
</Popover.Panel>
</Transition>
@ -364,7 +377,12 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<MoveRight className="h-4 w-4 text-custom-text-300" />
<Popover className="flex h-full items-center justify-center rounded-lg">
<>
<Popover.Button className="cursor-default text-sm font-medium text-custom-text-300">
<Popover.Button
className={`text-sm font-medium text-custom-text-300 ${
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
}`}
disabled={!isEditingAllowed}
>
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</Popover.Button>
@ -385,10 +403,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("target_date") ? `${watch("target_date")}` : null}
startDate={watch("start_date") ?? watch("target_date") ?? null}
endDate={watch("target_date") ?? watch("start_date") ?? null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd
selectsEnd={watch("start_date") ? true : false}
/>
</Popover.Panel>
</Transition>
@ -410,6 +428,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
name="lead"
render={({ field: { value } }) => (
<SidebarLeadSelect
disabled={!isEditingAllowed}
value={value}
onChange={(val: string) => {
submitChanges({ lead: val });
@ -422,6 +441,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
name="members"
render={({ field: { value } }) => (
<SidebarMembersSelect
disabled={!isEditingAllowed}
value={value}
onChange={(val: string[]) => {
submitChanges({ members: val });
@ -546,15 +566,17 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="mt-2 flex h-72 w-full flex-col space-y-3 overflow-y-auto">
{currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? (
<>
<div className="flex w-full items-center justify-end">
<button
className="flex items-center gap-1.5 text-sm font-medium text-custom-primary-100"
onClick={() => setModuleLinkModal(true)}
>
<Plus className="h-3 w-3" />
Add link
</button>
</div>
{isEditingAllowed && (
<div className="flex w-full items-center justify-end">
<button
className="flex items-center gap-1.5 text-sm font-medium text-custom-primary-100"
onClick={() => setModuleLinkModal(true)}
>
<Plus className="h-3 w-3" />
Add link
</button>
</div>
)}
<LinksList
links={moduleDetails.link_module}

View File

@ -14,7 +14,7 @@ import { IUser } from "types";
// services
import { FileService } from "services/file.service";
// assets
import IssuesSvg from "public/onboarding/onboarding-issues.svg";
import IssuesSvg from "public/onboarding/onboarding-issues.webp";
const defaultValues: Partial<IUser> = {
first_name: "",

View File

@ -8,6 +8,8 @@ import { useApplication, useProject, useUser } from "hooks/store";
import { TourRoot } from "components/onboarding";
import { UserGreetingsView } from "components/user";
import { CompletedIssuesGraph, IssuesList, IssuesPieChart, IssuesStats } from "components/workspace";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
// images
import { NewEmptyState } from "components/common/new-empty-state";
import emptyProject from "public/empty-state/dashboard_empty_project.webp";
@ -31,6 +33,8 @@ export const WorkspaceDashboardView = observer(() => {
workspaceSlug ? () => fetchUserDashboardInfo(workspaceSlug.toString(), month) : null
);
const isEditingAllowed = !!userStore.currentProjectRole && userStore.currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
@ -90,6 +94,7 @@ export const WorkspaceDashboardView = observer(() => {
commandPaletteStore.toggleCreateProjectModal(true);
},
}}
disabled={!isEditingAllowed}
/>
)
) : null}

View File

@ -31,18 +31,7 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const canUserCreatePage =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
const emptyStatePrimaryButton = canUserCreatePage
? {
primaryButton: {
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
},
}
: {};
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
@ -70,7 +59,12 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
"We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.",
direction: "right",
}}
{...emptyStatePrimaryButton}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
}}
disabled={!isEditingAllowed}
/>
)}
</div>

View File

@ -2,7 +2,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useApplication, usePage } from "hooks/store";
import { useApplication, usePage, useUser } from "hooks/store";
// components
import { PagesListView } from "components/pages/pages-list";
import { NewEmptyState } from "components/common/new-empty-state";
@ -12,14 +12,21 @@ import { Loader } from "@plane/ui";
import emptyPage from "public/empty-state/empty_page.png";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const RecentPagesList: FC = observer(() => {
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { recentProjectPages } = usePage();
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value) => value.length === 0);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!recentProjectPages) {
return (
<Loader className="space-y-4">
@ -64,6 +71,7 @@ export const RecentPagesList: FC = observer(() => {
text: "Create your first page",
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}}
disabled={!isEditingAllowed}
/>
</>
)}

View File

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
// components
import { ProjectCard } from "components/project";
import { Loader } from "@plane/ui";
@ -8,6 +8,8 @@ import { Loader } from "@plane/ui";
import emptyProject from "public/empty-state/empty_project.webp";
// icons
import { NewEmptyState } from "components/common/new-empty-state";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectCardList = observer(() => {
// store hooks
@ -15,9 +17,14 @@ export const ProjectCardList = observer(() => {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { workspaceProjects, searchedProjects, getProjectById } = useProject();
if (!workspaceProjects) {
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!workspaceProjects)
return (
<Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" />
@ -28,7 +35,6 @@ export const ProjectCardList = observer(() => {
<Loader.Item height="100px" />
</Loader>
);
}
return (
<>
@ -65,6 +71,7 @@ export const ProjectCardList = observer(() => {
commandPaletteStore.toggleCreateProjectModal(true);
},
}}
disabled={!isEditingAllowed}
/>
)}
</>

View File

@ -5,7 +5,7 @@ import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CreateProjectModal, ProjectSidebarListItem } from "components/project";
@ -14,6 +14,8 @@ import { copyUrlToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import { IProject } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectSidebarList: FC = observer(() => {
// states
@ -28,6 +30,9 @@ export const ProjectSidebarList: FC = observer(() => {
commandPalette: { toggleCreateProjectModal },
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
// const { joinedProjects, favoriteProjects, orderProjectsWithSortOrder, updateProjectView } = useProject();
// router
const router = useRouter();
@ -36,6 +41,8 @@ export const ProjectSidebarList: FC = observer(() => {
// toast
const { setToastAlert } = useToast();
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
? orderArrayBy(joinedProjects, "sort_order", "ascending")
: undefined;
@ -135,16 +142,18 @@ export const ProjectSidebarList: FC = observer(() => {
<ChevronRight className="h-3 w-3 opacity-0 group-hover:opacity-100" />
)}
</Disclosure.Button>
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
setIsFavoriteProjectCreate(true);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
{isAuthorizedUser && (
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
setIsFavoriteProjectCreate(true);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
)}
</div>
)}
<Transition
@ -210,15 +219,17 @@ export const ProjectSidebarList: FC = observer(() => {
<ChevronRight className="h-3 w-3 opacity-0 group-hover:opacity-100" />
)}
</Disclosure.Button>
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setIsFavoriteProjectCreate(false);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
{isAuthorizedUser && (
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setIsFavoriteProjectCreate(false);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
)}
</div>
)}
<Transition
@ -257,7 +268,7 @@ export const ProjectSidebarList: FC = observer(() => {
</Droppable>
</DragDropContext>
{joinedProjects && joinedProjects.length === 0 && (
{isAuthorizedUser && joinedProjects && joinedProjects.length === 0 && (
<button
type="button"
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"

View File

@ -56,11 +56,11 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
await projectViewsStore
.updateView(workspaceSlug, projectId, data?.id as string, payload)
.then(() => handleClose())
.catch(() =>
.catch((err) =>
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
message: err.detail ?? "Something went wrong. Please try again.",
})
);
};

View File

@ -2,17 +2,22 @@ import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { PencilIcon, StarIcon, TrashIcon } from "lucide-react";
import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// components
import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views";
// ui
import { CustomMenu, PhotoFilterIcon } from "@plane/ui";
// helpers
import { calculateTotalFilters } from "helpers/filter.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IProjectView } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
view: IProjectView;
@ -27,7 +32,12 @@ export const ProjectViewListItem: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { projectViews: projectViewsStore } = useMobxStore();
const { setToastAlert } = useToast();
const {
projectViews: projectViewsStore,
user: { currentProjectRole },
} = useMobxStore();
const handleAddToFavorites = () => {
if (!workspaceSlug || !projectId) return;
@ -41,8 +51,22 @@ export const ProjectViewListItem: React.FC<Props> = observer((props) => {
projectViewsStore.removeViewFromFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
};
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/views/${view.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "View link copied to clipboard.",
});
});
};
const totalFilters = calculateTotalFilters(view.query_data ?? {});
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
{workspaceSlug && projectId && view && (
@ -73,55 +97,66 @@ export const ProjectViewListItem: React.FC<Props> = observer((props) => {
<p className="hidden rounded bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200 group-hover:block">
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
</p>
{isEditingAllowed &&
(view.is_favorite ? (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromFavorites();
}}
className="grid place-items-center"
>
<StarIcon className="h-3.5 w-3.5 fill-orange-400 text-orange-400" strokeWidth={2} />
</button>
) : (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleAddToFavorites();
}}
className="grid place-items-center"
>
<StarIcon size={14} strokeWidth={2} />
</button>
))}
{view.is_favorite ? (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemoveFromFavorites();
}}
className="grid place-items-center"
>
<StarIcon className="h-3.5 w-3.5 fill-orange-400 text-orange-400" strokeWidth={2} />
</button>
) : (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleAddToFavorites();
}}
className="grid place-items-center"
>
<StarIcon size={14} strokeWidth={2} />
</button>
)}
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateViewModal(true);
}}
>
{isEditingAllowed && (
<>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdateViewModal(true);
}}
>
<span className="flex items-center justify-start gap-2">
<PencilIcon size={14} strokeWidth={2} />
<span>Edit View</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteViewModal(true);
}}
>
<span className="flex items-center justify-start gap-2">
<TrashIcon size={14} strokeWidth={2} />
<span>Delete View</span>
</span>
</CustomMenu.MenuItem>
</>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<PencilIcon size={14} strokeWidth={2} />
<span>Edit View</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteViewModal(true);
}}
>
<span className="flex items-center justify-start gap-2">
<TrashIcon size={14} strokeWidth={2} />
<span>Delete View</span>
<LinkIcon className="h-3 w-3" />
<span>Copy view link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>

View File

@ -13,6 +13,8 @@ import { Input, Loader } from "@plane/ui";
import emptyView from "public/empty-state/empty_view.webp";
// icons
import { Plus, Search } from "lucide-react";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectViewsList = observer(() => {
const [query, setQuery] = useState("");
@ -20,10 +22,16 @@ export const ProjectViewsList = observer(() => {
const router = useRouter();
const { projectId } = router.query;
const { projectViews: projectViewsStore, commandPalette: commandPaletteStore } = useMobxStore();
const {
projectViews: projectViewsStore,
commandPalette: commandPaletteStore,
user: { currentProjectRole },
} = useMobxStore();
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!viewsList)
return (
<Loader className="space-y-4 p-4">
@ -73,6 +81,7 @@ export const ProjectViewsList = observer(() => {
text: "Build your first view",
onClick: () => commandPaletteStore.toggleCreateViewModal(true),
}}
disabled={!isEditingAllowed}
/>
)}
</>

View File

@ -4,11 +4,13 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { BarChart2, Briefcase, CheckCircle, LayoutGrid } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// components
import { NotificationPopover } from "components/notifications";
// ui
import { Tooltip } from "@plane/ui";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
const workspaceLinks = (workspaceSlug: string) => [
{
@ -36,15 +38,20 @@ const workspaceLinks = (workspaceSlug: string) => [
export const WorkspaceSidebarMenu = observer(() => {
// store hooks
const { theme: themeStore } = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="w-full cursor-pointer space-y-1 p-4">
{workspaceLinks(workspaceSlug as string).map((link, index) => {
const isActive = link.name === "Settings" ? router.asPath.includes(link.href) : router.asPath === link.href;
if (!isAuthorizedUser && link.name === "Analytics") return;
return (
<Link key={index} href={link.href}>
<span className="block w-full">

View File

@ -1,14 +1,14 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import { ChevronUp, PenSquare, Search } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// components
import { CreateUpdateDraftIssueModal } from "components/issues";
// ui
import { ChevronUp, PenSquare, Search } from "lucide-react";
// constants
import { EProjectStore } from "store_legacy/command-palette.store";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const WorkspaceSidebarQuickAction = observer(() => {
// states
@ -19,11 +19,16 @@ export const WorkspaceSidebarQuickAction = observer(() => {
commandPalette: commandPaletteStore,
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({}));
const isSidebarCollapsed = themeStore.sidebarCollapsed;
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<CreateUpdateDraftIssueModal
@ -41,61 +46,67 @@ export const WorkspaceSidebarQuickAction = observer(() => {
isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`}
>
<div
className={`group relative flex w-full cursor-pointer items-center justify-between gap-1 rounded px-2 ${
isSidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "border-[0.5px] border-custom-border-200 px-3 shadow-custom-sidebar-shadow-2xs"
}`}
>
<button
type="button"
className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${
isSidebarCollapsed ? "justify-center" : ""
{isAuthorizedUser && (
<div
className={`group relative flex w-full cursor-pointer items-center justify-between gap-1 rounded px-2 ${
isSidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "border-[0.5px] border-custom-border-200 px-3 shadow-custom-sidebar-shadow-2xs"
}`}
onClick={() => {
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
}}
>
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
</button>
<button
type="button"
className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${
isSidebarCollapsed ? "justify-center" : ""
}`}
onClick={() => {
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
}}
>
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
</button>
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
<>
<div className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`} />
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
<>
<div
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`}
/>
<button
type="button"
className={`ml-1.5 flex flex-shrink-0 items-center justify-center rounded py-1.5 ${
isSidebarCollapsed ? "hidden" : "block"
}`}
>
<ChevronUp className="h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-0" />
</button>
<button
type="button"
className={`ml-1.5 flex flex-shrink-0 items-center justify-center rounded py-1.5 ${
isSidebarCollapsed ? "hidden" : "block"
}`}
>
<ChevronUp className="h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 group-hover:rotate-0" />
</button>
<div
className={`pointer-events-none fixed left-4 mt-0 h-10 w-[203px] pt-2 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ${
isSidebarCollapsed ? "top-[5.5rem]" : "top-24"
}`}
>
<div className="h-full w-full">
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="flex w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-3 py-[10px] text-sm text-custom-text-300 shadow"
>
<PenSquare size={16} className="mr-2 !text-lg !leading-4 text-custom-sidebar-text-300" />
Last Drafted Issue
</button>
<div
className={`pointer-events-none fixed left-4 mt-0 h-10 w-[203px] pt-2 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ${
isSidebarCollapsed ? "top-[5.5rem]" : "top-24"
}`}
>
<div className="h-full w-full">
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="flex w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-3 py-[10px] text-sm text-custom-text-300 shadow"
>
<PenSquare size={16} className="mr-2 !text-lg !leading-4 text-custom-sidebar-text-300" />
Last Drafted Issue
</button>
</div>
</div>
</div>
</>
)}
</div>
</>
)}
</div>
)}
<button
className={`flex flex-shrink-0 items-center justify-center rounded p-2 outline-none ${
className={`flex flex-shrink-0 items-center rounded p-2 gap-2 outline-none ${
isAuthorizedUser ? "justify-center" : "w-full"
} ${
isSidebarCollapsed
? "hover:bg-custom-sidebar-background-80"
: "border-[0.5px] border-custom-border-200 shadow-custom-sidebar-shadow-2xs"
@ -103,6 +114,7 @@ export const WorkspaceSidebarQuickAction = observer(() => {
onClick={() => commandPaletteStore.toggleCommandPaletteModal(true)}
>
<Search className="h-4 w-4 text-custom-sidebar-text-300" />
{!isAuthorizedUser && !isSidebarCollapsed && <span className="text-xs font-medium">Open command menu</span>}
</button>
</div>
</>

View File

@ -3,7 +3,7 @@ import Link from "next/link";
import { observer } from "mobx-react-lite";
// icons
import { Sparkles } from "lucide-react";
import { PhotoFilterIcon } from "@plane/ui";
// helpers
import { truncateText } from "helpers/string.helper";
@ -22,7 +22,7 @@ export const GlobalDefaultViewListItem: React.FC<Props> = observer((props) => {
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-4">
<div className="grid h-10 w-10 place-items-center rounded bg-custom-background-90 group-hover:bg-custom-background-100">
<Sparkles size={14} strokeWidth={2} />
<PhotoFilterIcon className="h-3.5 w-3.5" />
</div>
<div className="flex flex-col">
<p className="truncate text-sm font-medium leading-4">{truncateText(view.label, 75)}</p>

View File

@ -51,6 +51,9 @@ export const PROJECT_AUTOMATION_MONTHS = [
{ label: "12 Months", value: 12 },
];
export const STATE_GROUP_KEYS = ["backlog", "unstarted", "started", "completed", "cancelled"];
export const PROJECT_UNSPLASH_COVERS = [
"https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
"https://images.unsplash.com/photo-1693027407934-e3aa8a54c7ae?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",

View File

@ -1,7 +1,19 @@
// types
import { IStateResponse } from "types";
import { STATE_GROUP_KEYS } from "constants/project";
import { IState, IStateResponse } from "types";
export const orderStateGroups = (unorderedStateGroups: IStateResponse | undefined): IStateResponse | undefined => {
if (!unorderedStateGroups) return undefined;
return Object.assign({ backlog: [], unstarted: [], started: [], completed: [], cancelled: [] }, unorderedStateGroups);
};
export const sortStates = (states: IState[]) => {
if (!states || states.length === 0) return null;
return states.sort((stateA, stateB) => {
if (stateA.group === stateB.group) {
return stateA.sequence - stateB.sequence;
}
return STATE_GROUP_KEYS.indexOf(stateA.group) - STATE_GROUP_KEYS.indexOf(stateB.group);
});
};

View File

@ -1,11 +1,11 @@
import { useRouter } from "next/router";
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// services
import { IssueService } from "services/issue";
// types
import { ISubIssueResponse } from "types";
import { IIssue, ISubIssueResponse } from "types";
// fetch-keys
import { SUB_ISSUES } from "constants/fetch-keys";
@ -22,9 +22,33 @@ const useSubIssue = (projectId: string, issueId: string, isExpanded: boolean) =>
shouldFetch ? () => issueService.subIssues(workspaceSlug as string, projectId as string, issueId as string) : null
);
const mutateSubIssues = (issue: IIssue, data: Partial<IIssue>) => {
if (!issue.parent) return;
mutate(
SUB_ISSUES(issue.parent!),
(prev_data: any) => {
return {
...prev_data,
sub_issues: prev_data.sub_issues.map((sub_issue: any) => {
if (sub_issue.id === issue.id) {
return {
...sub_issue,
...data,
};
}
return sub_issue;
}),
};
},
false
);
};
return {
subIssues: subIssuesResponse?.sub_issues ?? [],
isLoading,
mutateSubIssues,
};
};

View File

@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
@ -14,7 +15,8 @@ const AUTHORIZED_ROLES = [20, 15, 10];
export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
const { children, className, showProfileIssuesFilter } = props;
// store hooks
const router = useRouter();
const {
membership: { currentWorkspaceRole },
} = useUser();
@ -23,12 +25,14 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
return (
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
<ProfileSidebar />
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
{isAuthorized ? (
{isAuthorized || !isAuthorizedPath ? (
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
) : (
<div className="grid h-full w-full place-items-center text-custom-text-200">

View File

@ -2,7 +2,7 @@ import React, { Fragment, ReactElement } from "react";
import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
// hooks
import { useApplication, useProject } from "hooks/store";
import { useApplication, useProject, useUser } from "hooks/store";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
@ -15,6 +15,7 @@ import { Plus } from "lucide-react";
import emptyAnalytics from "public/empty-state/empty_analytics.webp";
// constants
import { ANALYTICS_TABS } from "constants/analytics";
import { EUserWorkspaceRoles } from "constants/workspace";
// type
import { NextPageWithLayout } from "types/app";
@ -24,8 +25,13 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
commandPalette: { toggleCreateProjectModal },
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { workspaceProjects } = useProject();
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
{workspaceProjects && workspaceProjects.length > 0 ? (
@ -77,6 +83,7 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
toggleCreateProjectModal(true);
},
}}
disabled={!isEditingAllowed}
/>
</>
)}

View File

@ -19,6 +19,7 @@ import { TCycleView, TCycleLayout } from "types";
import { NextPageWithLayout } from "types/app";
// constants
import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import { EUserWorkspaceRoles } from "constants/workspace";
// lib cookie
import { setLocalStorage, getLocalStorage } from "lib/local-storage";
import { NewEmptyState } from "components/common/new-empty-state";
@ -27,7 +28,10 @@ import { NewEmptyState } from "components/common/new-empty-state";
const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const [createModal, setCreateModal] = useState(false);
// store
const { cycle: cycleStore } = useMobxStore();
const {
cycle: cycleStore,
user: { currentProjectRole },
} = useMobxStore();
const { projectCycles } = cycleStore;
// router
const router = useRouter();
@ -75,6 +79,8 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
const cycleLayout = cycleStore?.cycleLayout;
const totalCycles = projectCycles?.length ?? 0;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
if (!workspaceSlug || !projectId) return null;
return (
@ -104,6 +110,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
setCreateModal(true);
},
}}
disabled={!isEditingAllowed}
/>
</div>
) : (

View File

@ -1,6 +1,5 @@
import { ReactElement } from "react";
import useSWR from "swr";
import { useRouter } from "next/router";
import Link from "next/link";
// services
import { UserService } from "services/user.service";
@ -23,9 +22,6 @@ import { NextPageWithLayout } from "types/app";
const userService = new UserService();
const ProfileActivityPage: NextPageWithLayout = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity());
return (

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 254 KiB

Some files were not shown because too many files have changed in this diff Show More