Merge branch 'refactor/mobx-store' of github.com:makeplane/plane into refactor/mobx-store

This commit is contained in:
rahulramesha 2023-12-14 20:22:54 +05:30
commit ccfe2e4b01
313 changed files with 3495 additions and 4677 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

@ -49,7 +49,6 @@ class IssueStateInboxSerializer(BaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
class Meta:

View File

@ -512,7 +512,6 @@ class IssueStateSerializer(DynamicBaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)

View File

@ -44,7 +44,7 @@ urlpatterns = [
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
CycleIssueViewSet.as_view(
{
"get": "retrieve",

View File

@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",

View File

@ -44,7 +44,7 @@ urlpatterns = [
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view(
{
"get": "retrieve",

View File

@ -516,7 +516,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_cycle__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
@ -636,11 +635,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, cycle_id, pk):
def destroy(self, request, slug, project_id, cycle_id, issue_id):
cycle_issue = CycleIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
)
issue_id = cycle_issue.issue_id
issue_activity.delay(
type="cycle.activity.deleted",
requested_data=json.dumps(
@ -650,7 +648,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
}
),
actor_id=str(self.request.user.id),
issue_id=str(cycle_issue.issue_id),
issue_id=str(issue_id),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),

View File

@ -107,7 +107,6 @@ class InboxIssueViewSet(BaseViewSet):
project_id=project_id,
)
.filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
@ -204,9 +203,9 @@ class InboxIssueViewSet(BaseViewSet):
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, pk):
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(
@ -316,19 +315,16 @@ class InboxIssueViewSet(BaseViewSet):
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
pk=issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, pk):
def destroy(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(
@ -350,7 +346,7 @@ class InboxIssueViewSet(BaseViewSet):
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
workspace__slug=slug, project_id=project_id, pk=issue_id
).delete()
inbox_issue.delete()

View File

@ -342,7 +342,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_module__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
@ -451,20 +450,20 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_200_OK,
)
def destroy(self, request, slug, project_id, module_id, pk):
def destroy(self, request, slug, project_id, module_id, issue_id):
module_issue = ModuleIssue.objects.get(
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
)
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
"issues": [str(issue_id)],
}
),
actor_id=str(request.user.id),
issue_id=str(module_issue.issue_id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),

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

@ -4,8 +4,8 @@ import { useTheme } from "next-themes";
import { Dialog, Transition } from "@headlessui/react";
import { Trash2 } from "lucide-react";
import { mutate } from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useUser } from "hooks/store";
// ui
import { Button } from "@plane/ui";
// hooks
@ -22,9 +22,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
// states
const [isDeactivating, setIsDeactivating] = useState(false);
const {
user: { deactivateAccount },
} = useMobxStore();
const { deactivateAccount } = useUser();
const router = useRouter();

View File

@ -1,9 +1,8 @@
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { AuthService } from "services/auth.service";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { GitHubSignInButton, GoogleSignInButton } from "components/account";
@ -21,8 +20,8 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast();
// mobx store
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
try {

View File

@ -1,8 +1,7 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection";
// components
import { LatestFeatureBlock } from "components/common";
@ -38,8 +37,8 @@ export const SignInRoot = observer(() => {
const { handleRedirection } = useSignInRedirection();
// mobx store
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id);

View File

@ -1,9 +1,7 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Control, Controller, UseFormSetValue } from "react-hook-form";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject } from "hooks/store";
// components
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
// types
@ -20,12 +18,7 @@ type Props = {
export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
const { control, setValue, params, fullScreen, isProjectLevel } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { project: projectStore } = useMobxStore();
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
const { workspaceProjects } = useProject();
return (
<div
@ -40,7 +33,11 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
name="project"
control={control}
render={({ field: { value, onChange } }) => (
<SelectProject value={value ?? undefined} onChange={onChange} projects={projectsList ?? undefined} />
<SelectProject
value={value ?? undefined}
onChange={onChange}
projectIds={workspaceProjects ?? undefined}
/>
)}
/>
</div>

View File

@ -1,25 +1,33 @@
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// ui
import { CustomSearchSelect } from "@plane/ui";
// types
import { IProject } from "types";
type Props = {
value: string[] | undefined;
onChange: (val: string[] | null) => void;
projects: IProject[] | undefined;
projectIds: string[] | undefined;
};
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => {
const options = projects?.map((project) => ({
value: project.id,
query: project.name + project.identifier,
content: (
<div className="flex items-center gap-2">
<span className="text-[0.65rem] text-custom-text-200">{project.identifier}</span>
{project.name}
</div>
),
}));
export const SelectProject: React.FC<Props> = observer((props) => {
const { value, onChange, projectIds } = props;
const { getProjectById } = useProject();
const options = projectIds?.map((projectId) => {
const projectDetails = getProjectById(projectId);
return {
value: projectDetails?.id,
query: `${projectDetails?.name} ${projectDetails?.identifier}`,
content: (
<div className="flex items-center gap-2">
<span className="text-[0.65rem] text-custom-text-200">{projectDetails?.identifier}</span>
{projectDetails?.name}
</div>
),
};
});
return (
<CustomSearchSelect
@ -28,9 +36,9 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
options={options}
label={
value && value.length > 0
? projects
?.filter((p) => value.includes(p.id))
.map((p) => p.identifier)
? projectIds
?.filter((p) => value.includes(p))
.map((p) => getProjectById(p)?.name)
.join(", ")
: "All projects"
}
@ -38,4 +46,4 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
multiple
/>
);
};
});

View File

@ -1,65 +1,74 @@
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// icons
import { Contrast, LayoutGrid, Users } from "lucide-react";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper";
// types
import { IProject } from "types";
type Props = {
projects: IProject[];
projectIds: string[];
};
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = (props) => {
const { projects } = props;
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((props) => {
const { projectIds } = props;
const { getProjectById } = useProject();
return (
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
<h4 className="font-medium">Selected Projects</h4>
<div className="mt-4 h-full space-y-6 overflow-y-auto">
{projects.map((project) => (
<div key={project.id} className="w-full">
<div className="flex items-center gap-1 text-sm">
{project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
) : project.icon_prop ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div>
) : (
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p>
<span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span>
</h5>
</div>
<div className="mt-4 w-full space-y-3 pl-2">
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total members</h6>
</div>
<span className="text-custom-text-200">{project.total_members}</span>
{projectIds.map((projectId) => {
const project = getProjectById(projectId);
if (!project) return;
return (
<div key={projectId} className="w-full">
<div className="flex items-center gap-1 text-sm">
{project.emoji ? (
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
) : project.icon_prop ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div>
) : (
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p>
<span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span>
</h5>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total cycles</h6>
<div className="mt-4 w-full space-y-3 pl-2">
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total members</h6>
</div>
<span className="text-custom-text-200">{project.total_members}</span>
</div>
<span className="text-custom-text-200">{project.total_cycles}</span>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total modules</h6>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total cycles</h6>
</div>
<span className="text-custom-text-200">{project.total_cycles}</span>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total modules</h6>
</div>
<span className="text-custom-text-200">{project.total_modules}</span>
</div>
<span className="text-custom-text-200">{project.total_modules}</span>
</div>
</div>
</div>
))}
);
})}
</div>
</div>
);
};
});

View File

@ -1,8 +1,7 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useCycle, useModule, useProject } from "hooks/store";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { renderShortDate } from "helpers/date-time.helper";
@ -11,16 +10,15 @@ import { NETWORK_CHOICES } from "constants/project";
export const CustomAnalyticsSidebarHeader = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { projectId, cycleId, moduleId } = router.query;
const { cycle: cycleStore, module: moduleStore, project: projectStore } = useMobxStore();
const { getProjectById } = useProject();
const { getCycleById } = useCycle();
const { getModuleById } = useModule();
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
const projectDetails =
workspaceSlug && projectId
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
: undefined;
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined;
return (
<>

View File

@ -5,8 +5,8 @@ import { mutate } from "swr";
// services
import { AnalyticsService } from "services/analytics.service";
// hooks
import { useCycle, useModule, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
// ui
@ -29,172 +29,167 @@ type Props = {
const analyticsService = new AnalyticsService();
export const CustomAnalyticsSidebar: React.FC<Props> = observer(
({ analytics, params, fullScreen, isProjectLevel = false }) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { analytics, params, fullScreen, isProjectLevel = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
// toast alert
const { setToastAlert } = useToast();
// store hooks
const { currentUser } = useUser();
const { workspaceProjects, getProjectById } = useProject();
const { fetchCycleDetails, getCycleById } = useCycle();
const { fetchModuleDetails, getModuleById } = useModule();
const { setToastAlert } = useToast();
const projectDetails = projectId ? getProjectById(projectId.toString()) ?? undefined : undefined;
const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore();
const trackExportAnalytics = () => {
if (!currentUser) return;
const user = userStore.currentUser;
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
const projectDetails =
workspaceSlug && projectId
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined
: undefined;
const trackExportAnalytics = () => {
if (!user) return;
const eventPayload: any = {
workspaceSlug: workspaceSlug?.toString(),
params: {
x_axis: params.x_axis,
y_axis: params.y_axis,
group: params.segment,
project: params.project,
},
};
if (projectDetails) {
const workspaceDetails = projectDetails.workspace as IWorkspace;
eventPayload.workspaceId = workspaceDetails.id;
eventPayload.workspaceName = workspaceDetails.name;
eventPayload.projectId = projectDetails.id;
eventPayload.projectIdentifier = projectDetails.identifier;
eventPayload.projectName = projectDetails.name;
}
if (cycleDetails || moduleDetails) {
const details = cycleDetails || moduleDetails;
eventPayload.workspaceId = details?.workspace_detail?.id;
eventPayload.workspaceName = details?.workspace_detail?.name;
eventPayload.projectId = details?.project_detail.id;
eventPayload.projectIdentifier = details?.project_detail.identifier;
eventPayload.projectName = details?.project_detail.name;
}
if (cycleDetails) {
eventPayload.cycleId = cycleDetails.id;
eventPayload.cycleName = cycleDetails.name;
}
if (moduleDetails) {
eventPayload.moduleId = moduleDetails.id;
eventPayload.moduleName = moduleDetails.name;
}
};
const exportAnalytics = () => {
if (!workspaceSlug) return;
const data: IExportAnalyticsFormData = {
const eventPayload: any = {
workspaceSlug: workspaceSlug?.toString(),
params: {
x_axis: params.x_axis,
y_axis: params.y_axis,
};
if (params.segment) data.segment = params.segment;
if (params.project) data.project = params.project;
analyticsService
.exportAnalytics(workspaceSlug.toString(), data)
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: res.message,
});
trackExportAnalytics();
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "There was some error in exporting the analytics. Please try again.",
})
);
group: params.segment,
project: params.project,
},
};
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
if (projectDetails) {
const workspaceDetails = projectDetails.workspace as IWorkspace;
// fetch cycle details
useEffect(() => {
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return;
eventPayload.workspaceId = workspaceDetails.id;
eventPayload.workspaceName = workspaceDetails.name;
eventPayload.projectId = projectDetails.id;
eventPayload.projectIdentifier = projectDetails.identifier;
eventPayload.projectName = projectDetails.name;
}
cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
}, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]);
if (cycleDetails || moduleDetails) {
const details = cycleDetails || moduleDetails;
// fetch module details
useEffect(() => {
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return;
eventPayload.workspaceId = details?.workspace_detail?.id;
eventPayload.workspaceName = details?.workspace_detail?.name;
eventPayload.projectId = details?.project_detail.id;
eventPayload.projectIdentifier = details?.project_detail.identifier;
eventPayload.projectName = details?.project_detail.name;
}
moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
}, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]);
if (cycleDetails) {
eventPayload.cycleId = cycleDetails.id;
eventPayload.cycleName = cycleDetails.name;
}
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
if (moduleDetails) {
eventPayload.moduleId = moduleDetails.id;
eventPayload.moduleName = moduleDetails.name;
}
};
return (
<div
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
fullScreen
? "overflow-hidden border-l border-custom-border-200 md:h-full md:flex-col md:items-start md:space-y-4 md:border-l md:border-custom-border-200 md:py-5"
: ""
}`}
>
<div className="flex flex-wrap items-center gap-2">
const exportAnalytics = () => {
if (!workspaceSlug) return;
const data: IExportAnalyticsFormData = {
x_axis: params.x_axis,
y_axis: params.y_axis,
};
if (params.segment) data.segment = params.segment;
if (params.project) data.project = params.project;
analyticsService
.exportAnalytics(workspaceSlug.toString(), data)
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: res.message,
});
trackExportAnalytics();
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "There was some error in exporting the analytics. Please try again.",
})
);
};
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
// fetch cycle details
useEffect(() => {
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return;
fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
}, [cycleId, cycleDetails, fetchCycleDetails, projectId, workspaceSlug]);
// fetch module details
useEffect(() => {
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return;
fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
}, [moduleId, moduleDetails, fetchModuleDetails, projectId, workspaceSlug]);
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjects;
return (
<div
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
fullScreen
? "overflow-hidden border-l border-custom-border-200 md:h-full md:flex-col md:items-start md:space-y-4 md:border-l md:border-custom-border-200 md:py-5"
: ""
}`}
>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
<LayersIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues
</div>
{isProjectLevel && (
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
<LayersIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues
<CalendarDays className="h-3.5 w-3.5" />
{renderShortDate(
(cycleId
? cycleDetails?.created_at
: moduleId
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
)}
</div>
{isProjectLevel && (
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
<CalendarDays className="h-3.5 w-3.5" />
{renderShortDate(
(cycleId
? cycleDetails?.created_at
: moduleId
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
)}
</div>
)}
</div>
<div className="h-full w-full overflow-hidden">
{fullScreen ? (
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList
projects={projects?.filter((p) => selectedProjects.includes(p.id)) ?? []}
/>
)}
<CustomAnalyticsSidebarHeader />
</>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2 justify-self-end">
<Button
variant="neutral-primary"
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
</Button>
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
Export as CSV
</Button>
</div>
)}
</div>
);
}
);
<div className="h-full w-full overflow-hidden">
{fullScreen ? (
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
)}
<CustomAnalyticsSidebarHeader />
</>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2 justify-self-end">
<Button
variant="neutral-primary"
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
</Button>
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
Export as CSV
</Button>
</div>
</div>
);
});

View File

@ -1,12 +1,12 @@
import React from "react";
// next
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
// layouts
import DefaultLayout from "layouts/default-layout";
// hooks
import useUser from "hooks/use-user";
// images
import ProjectNotAuthorizedImg from "public/auth/project-not-authorized.svg";
import WorkspaceNotAuthorizedImg from "public/auth/workspace-not-authorized.svg";
@ -16,9 +16,12 @@ type Props = {
type: "project" | "workspace";
};
export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
const { user } = useUser();
export const NotAuthorizedView: React.FC<Props> = observer((props) => {
const { actionButton, type } = props;
// router
const { asPath: currentPath } = useRouter();
// store hooks
const { currentUser } = useUser();
return (
<DefaultLayout>
@ -34,9 +37,9 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
<h1 className="text-xl font-medium text-custom-text-100">Oops! You are not authorized to view this page</h1>
<div className="w-full max-w-md text-base text-custom-text-200">
{user ? (
{currentUser ? (
<p>
You have signed in as {user.email}. <br />
You have signed in as {currentUser.email}. <br />
<Link href={`/?next=${currentPath}`}>
<span className="font-medium text-custom-text-100">Sign in</span>
</Link>{" "}
@ -57,4 +60,4 @@ export const NotAuthorizedView: React.FC<Props> = ({ actionButton, type }) => {
</div>
</DefaultLayout>
);
};
});

View File

@ -1,9 +1,8 @@
import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store_legacy/root";
// hooks
import { useProject, useUser } from "hooks/store";
// ui
import { Button } from "@plane/ui";
// icons
@ -12,12 +11,13 @@ import { ClipboardList } from "lucide-react";
import JoinProjectImg from "public/auth/project-not-authorized.svg";
export const JoinProject: React.FC = () => {
// states
const [isJoiningProject, setIsJoiningProject] = useState(false);
// store hooks
const {
project: projectStore,
user: { joinProject },
}: RootStore = useMobxStore();
membership: { joinProject },
} = useUser();
const { fetchProjects } = useProject();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -28,12 +28,8 @@ export const JoinProject: React.FC = () => {
setIsJoiningProject(true);
joinProject(workspaceSlug.toString(), [projectId.toString()])
.then(() => {
projectStore.fetchProjects(workspaceSlug.toString());
})
.finally(() => {
setIsJoiningProject(false);
});
.then(() => fetchProjects(workspaceSlug.toString()))
.finally(() => setIsJoiningProject(false));
};
return (

View File

@ -1,7 +1,7 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject, useUser } from "hooks/store";
// component
import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui";
import { SelectMonthModal } from "components/automation";
@ -23,13 +23,13 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
const { handleChange } = props;
// states
const [monthModal, setmonthModal] = useState(false);
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { user: userStore, project: projectStore } = useMobxStore();
const projectDetails = projectStore.currentProjectDetails;
const userRole = userStore.currentProjectRole;
const isAdmin = userRole === EUserWorkspaceRoles.ADMIN;
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
return (
<>
@ -54,24 +54,28 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
</div>
</div>
<ToggleSwitch
value={projectDetails?.archive_in !== 0}
value={currentProjectDetails?.archive_in !== 0}
onChange={() =>
projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 })
currentProjectDetails?.archive_in === 0
? handleChange({ archive_in: 1 })
: handleChange({ archive_in: 0 })
}
size="sm"
disabled={!isAdmin}
/>
</div>
{projectDetails ? (
projectDetails.archive_in !== 0 && (
{currentProjectDetails ? (
currentProjectDetails.archive_in !== 0 && (
<div className="ml-12">
<div className="flex w-full items-center justify-between gap-2 rounded border border-custom-border-200 bg-custom-background-90 px-5 py-4">
<div className="w-1/2 text-sm font-medium">Auto-archive issues that are closed for</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.archive_in}
label={`${projectDetails?.archive_in} ${projectDetails?.archive_in === 1 ? "Month" : "Months"}`}
value={currentProjectDetails?.archive_in}
label={`${currentProjectDetails?.archive_in} ${
currentProjectDetails?.archive_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ archive_in: val });
}}

View File

@ -1,7 +1,7 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject, useProjectState, useUser } from "hooks/store";
// component
import { SelectMonthModal } from "components/automation";
import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui";
@ -21,15 +21,16 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
const { handleChange } = props;
// states
const [monthModal, setmonthModal] = useState(false);
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { user: userStore, project: projectStore, projectState: projectStateStore } = useMobxStore();
const userRole = userStore.currentProjectRole;
const projectDetails = projectStore.currentProjectDetails;
// const stateGroups = projectStateStore.groupedProjectStates ?? undefined;
const states = projectStateStore.projectStates;
const options = states
const options = projectStates
?.filter((state) => state.group === "cancelled")
.map((state) => ({
value: state.id,
@ -44,17 +45,17 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
const multipleOptions = (options ?? []).length > 1;
const defaultState = states?.find((s) => s.group === "cancelled")?.id || null;
const defaultState = projectStates?.find((s) => s.group === "cancelled")?.id || null;
const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState);
const currentDefaultState = states?.find((s) => s.id === defaultState);
const selectedOption = projectStates?.find((s) => s.id === currentProjectDetails?.default_state ?? defaultState);
const currentDefaultState = projectStates?.find((s) => s.id === defaultState);
const initialValues: Partial<IProject> = {
close_in: 1,
default_state: defaultState,
};
const isAdmin = userRole === EUserWorkspaceRoles.ADMIN;
const isAdmin = currentProjectRole === EUserWorkspaceRoles.ADMIN;
return (
<>
@ -79,9 +80,9 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
</div>
</div>
<ToggleSwitch
value={projectDetails?.close_in !== 0}
value={currentProjectDetails?.close_in !== 0}
onChange={() =>
projectDetails?.close_in === 0
currentProjectDetails?.close_in === 0
? handleChange({ close_in: 1, default_state: defaultState })
: handleChange({ close_in: 0, default_state: null })
}
@ -90,16 +91,18 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
/>
</div>
{projectDetails ? (
projectDetails.close_in !== 0 && (
{currentProjectDetails ? (
currentProjectDetails.close_in !== 0 && (
<div className="ml-12">
<div className="flex flex-col rounded border border-custom-border-200 bg-custom-background-90">
<div className="flex w-full items-center justify-between gap-2 px-5 py-4">
<div className="w-1/2 text-sm font-medium">Auto-close issues that are inactive for</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.close_in}
label={`${projectDetails?.close_in} ${projectDetails?.close_in === 1 ? "Month" : "Months"}`}
value={currentProjectDetails?.close_in}
label={`${currentProjectDetails?.close_in} ${
currentProjectDetails?.close_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ close_in: val });
}}
@ -118,7 +121,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
Customise Time Range
Customize Time Range
</button>
</>
</CustomSelect>
@ -129,7 +132,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
<div className="w-1/2 ">
<CustomSearchSelect
value={projectDetails?.default_state ?? defaultState}
value={currentProjectDetails?.default_state ?? defaultState}
label={
<div className="flex items-center gap-2">
{selectedOption ? (

View File

@ -1,7 +1,7 @@
import { Command } from "cmdk";
import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// ui
import { DiscordIcon } from "@plane/ui";
@ -14,7 +14,7 @@ export const CommandPaletteHelpActions: React.FC<Props> = (props) => {
const {
commandPalette: { toggleShortcutModal },
} = useMobxStore();
} = useApplication();
return (
<Command.Group heading="Help">

View File

@ -5,6 +5,8 @@ import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useUser } from "hooks/store";
// hooks
import useToast from "hooks/use-toast";
// ui
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
@ -29,10 +31,12 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = router.query;
const {
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
projectIssues: { updateIssue },
user: { currentUser },
} = useMobxStore();
const {
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
} = useApplication();
const { currentUser } = useUser();
const { setToastAlert } = useToast();

View File

@ -1,7 +1,7 @@
import { Command } from "cmdk";
import { ContrastIcon, FileText } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// ui
import { DiceIcon, PhotoFilterIcon } from "@plane/ui";
@ -14,8 +14,8 @@ export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
const {
commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal },
trackEvent: { setTrackElement },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
return (
<>

View File

@ -4,8 +4,8 @@ import { useTheme } from "next-themes";
import { Settings } from "lucide-react";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { THEME_OPTIONS } from "constants/themes";
@ -18,9 +18,7 @@ export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
// states
const [mounted, setMounted] = useState(false);
// store
const {
user: { updateCurrentUserTheme },
} = useMobxStore();
const { updateCurrentUserTheme } = useUser();
// hooks
const { setTheme } = useTheme();
const { setToastAlert } = useToast();

View File

@ -5,8 +5,8 @@ import { Command } from "cmdk";
import { Dialog, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
import { FolderPlus, Search, Settings } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// services
import { WorkspaceService } from "services/workspace.service";
import { IssueService } from "services/issue";
@ -62,8 +62,8 @@ export const CommandModal: React.FC = observer(() => {
toggleCreateIssueModal,
toggleCreateProjectModal,
},
trackEvent: { setTrackElement },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
// router
const router = useRouter();

View File

@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CommandModal, ShortcutsModal } from "components/command-palette";
@ -19,8 +20,6 @@ import { copyTextToClipboard } from "helpers/string.helper";
import { IssueService } from "services/issue";
// fetch keys
import { ISSUE_DETAILS } from "constants/fetch-keys";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
const issueService = new IssueService();
@ -28,14 +27,14 @@ const issueService = new IssueService();
export const CommandPalette: FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
// store
const {
commandPalette,
theme: { toggleSidebar },
user: { currentUser },
trackEvent: { setTrackElement },
projectIssues: { removeIssue },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
const { currentUser } = useUser();
const {
toggleCommandPaletteModal,
isCreateIssueModalOpen,

View File

@ -1,5 +1,5 @@
export * from "./actions";
export * from "./shortcuts-modal";
export * from "./command-modal";
export * from "./command-pallette";
export * from "./command-palette";
export * from "./helpers";

View File

@ -6,8 +6,8 @@ import useSWR from "swr";
import { useDropzone } from "react-dropzone";
import { Tab, Transition, Popover } from "@headlessui/react";
import { Control, Controller } from "react-hook-form";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useWorkspace } from "hooks/store";
// services
import { FileService } from "services/file.service";
// hooks
@ -45,25 +45,24 @@ const fileService = new FileService();
export const ImagePickerPopover: React.FC<Props> = observer((props) => {
const { label, value, control, onChange, disabled = false } = props;
// states
const [image, setImage] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [searchParams, setSearchParams] = useState("");
const [formData, setFormData] = useState({
search: "",
});
// refs
const ref = useRef<HTMLDivElement>(null);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
workspace: { currentWorkspace },
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const { data: unsplashImages, error: unsplashError } = useSWR(
`UNSPLASH_IMAGES_${searchParams}`,

View File

@ -2,8 +2,8 @@ import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// services
import { FileService } from "services/file.service";
// hooks
@ -32,12 +32,12 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
// states
const [image, setImage] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
// toast alert
const { setToastAlert } = useToast();
// store hooks
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);

View File

@ -3,8 +3,8 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useDropzone } from "react-dropzone";
import { Transition, Dialog } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useWorkspace } from "hooks/store";
// services
import { FileService } from "services/file.service";
// hooks
@ -40,9 +40,9 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast();
const {
workspace: { currentWorkspace },
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);

View File

@ -1,8 +1,8 @@
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { useTheme } from "next-themes";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useUser } from "hooks/store";
// ui
import { Button, InputColorPicker } from "@plane/ui";
// types
@ -25,8 +25,8 @@ const inputRules = {
};
export const CustomThemeSelector: React.FC = observer(() => {
const { user: userStore } = useMobxStore();
const userTheme = userStore?.currentUser?.theme;
const { currentUser, updateCurrentUser } = useUser();
const userTheme = currentUser?.theme;
// hooks
const { setTheme } = useTheme();
@ -61,7 +61,7 @@ export const CustomThemeSelector: React.FC = observer(() => {
setTheme("custom");
return userStore.updateCurrentUser({ theme: payload });
return updateCurrentUser({ theme: payload });
};
const handleValueChange = (val: string | undefined, onChange: any) => {

View File

@ -3,9 +3,9 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { SingleProgressStats } from "components/core";
@ -67,15 +67,18 @@ interface IActiveCycleDetails {
}
export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = props;
const { cycle: cycleStore, commandPalette: commandPaletteStore } = useMobxStore();
// store hooks
const { cycle: cycleStore } = useMobxStore();
const {
commandPalette: { toggleCreateCycleModal },
} = useApplication();
// 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
);
@ -94,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" />
@ -118,7 +121,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
<button
type="button"
className="text-sm text-custom-primary-100 outline-none"
onClick={() => commandPaletteStore.toggleCreateCycleModal(true)}
onClick={() => toggleCreateCycleModal(true)}
>
Create a new cycle
</button>
@ -187,12 +190,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus === "current"
? "#09A953"
: cycleStatus === "upcoming"
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
</span>
@ -207,12 +210,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
cycleStatus === "current"
? "bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400"
: ""
? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (

View File

@ -1,7 +1,7 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// components
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
// types
@ -17,8 +17,8 @@ export interface ICyclesBoard {
export const CyclesBoard: FC<ICyclesBoard> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props;
const { commandPalette: commandPaletteStore } = useMobxStore();
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
return (
<>

View File

@ -1,7 +1,7 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// components
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
// ui
@ -18,11 +18,11 @@ export interface ICyclesList {
export const CyclesList: FC<ICyclesList> = observer((props) => {
const { cycles, filter, workspaceSlug, projectId } = props;
// store hooks
const {
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
return (
<>

View File

@ -1,19 +1,18 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { KeyedMutator } from "swr";
// hooks
import { useUser } from "hooks/store";
// services
import { CycleService } from "services/cycle.service";
// hooks
import useUser from "hooks/use-user";
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart";
import { CycleGanttBlock } from "components/cycles";
// types
import { ICycle } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
type Props = {
workspaceSlug: string;
@ -24,15 +23,18 @@ type Props = {
// services
const cycleService = new CycleService();
export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) => {
export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { cycles, mutateCycles } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug || !user) return;
if (!workspaceSlug) return;
mutateCycles &&
mutateCycles((prevData: any) => {
if (!prevData) return prevData;
@ -76,7 +78,8 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
}))
: [];
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
const isAllowed =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return (
<div className="h-full w-full overflow-y-auto">
@ -94,4 +97,4 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
/>
</div>
);
};
});

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

@ -1,11 +1,8 @@
import React from "react";
import { useRouter } from "next/router";
// store
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Button, CustomMenu } from "@plane/ui";
@ -27,10 +24,8 @@ export const EstimateListItem: React.FC<Props> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
const {
project: { currentProjectDetails, updateProject },
} = useMobxStore();
// store hooks
const { currentProjectDetails, updateProject } = useProject();
// hooks
const { setToastAlert } = useToast();

View File

@ -2,8 +2,8 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject } from "hooks/store";
// services
import { ProjectExportService } from "services/project";
// hooks
@ -26,28 +26,30 @@ const projectExportService = new ProjectExportService();
export const Exporter: React.FC<Props> = observer((props) => {
const { isOpen, handleClose, user, provider, mutateServices } = props;
// states
const [exportLoading, setExportLoading] = useState(false);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { project: projectStore } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
// store hooks
const { workspaceProjects, getProjectById } = useProject();
// toast alert
const { setToastAlert } = useToast();
const options = projects?.map((project) => ({
value: project.id,
query: project.name + project.identifier,
content: (
<div className="flex items-center gap-2">
<span className="text-[0.65rem] text-custom-text-200">{project.identifier}</span>
{project.name}
</div>
),
}));
const options = workspaceProjects?.map((projectId) => {
const projectDetails = getProjectById(projectId);
return {
value: projectDetails?.id,
query: `${projectDetails?.name} ${projectDetails?.identifier}`,
content: (
<div className="flex items-center gap-2">
<span className="text-[0.65rem] text-custom-text-200">{projectDetails?.identifier}</span>
{projectDetails?.name}
</div>
),
};
});
const [value, setValue] = React.useState<string[]>([]);
const [multiple, setMultiple] = React.useState<boolean>(false);
@ -131,10 +133,12 @@ export const Exporter: React.FC<Props> = observer((props) => {
input
label={
value && value.length > 0
? projects &&
projects
.filter((p) => value.includes(p.id))
.map((p) => p.identifier)
? value
.map((projectId) => {
const projectDetails = getProjectById(projectId);
return projectDetails?.identifier;
})
.join(", ")
: "All projects"
}

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

@ -2,8 +2,8 @@ import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
// ui
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
// helpers
@ -14,14 +14,15 @@ export const CyclesHeader: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
// store hooks
const {
project: projectStore,
user: { currentProjectRole },
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
} = useMobxStore();
const { currentProjectDetails } = projectStore;
commandPalette: { toggleCreateCycleModal },
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const canUserCreateCycle =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
@ -63,7 +64,7 @@ export const CyclesHeader: FC = observer(() => {
prependIcon={<Plus />}
onClick={() => {
setTrackElement("CYCLES_PAGE_HEADER");
commandPaletteStore.toggleCreateCycleModal(true);
toggleCreateCycleModal(true);
}}
>
Add Cycle

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

@ -1,9 +1,8 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
// ui
import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
@ -17,13 +16,12 @@ export const ModulesListHeader: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const {
project: projectStore,
commandPalette: commandPaletteStore,
user: { currentProjectRole },
} = useMobxStore();
const { currentProjectDetails } = projectStore;
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { storedValue: modulesView, setValue: setModulesView } = useLocalStorage("modules_view", "grid");

View File

@ -1,21 +1,18 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { FileText, Plus } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
// services
import { PageService } from "services/page.service";
// constants
import { PAGE_DETAILS } from "constants/fetch-keys";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helper
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import useSWR from "swr";
// fetch-keys
import { PAGE_DETAILS } from "constants/fetch-keys";
export interface IPagesHeaderProps {
showButton?: boolean;
@ -28,8 +25,8 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
const router = useRouter();
const { workspaceSlug, pageId } = router.query;
const { project: projectStore, commandPalette: commandPaletteStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
const { commandPalette: commandPaletteStore } = useApplication();
const { currentProjectDetails } = useProject();
const { data: pageDetails } = useSWR(
workspaceSlug && currentProjectDetails?.id && pageId ? PAGE_DETAILS(pageId as string) : null,

View File

@ -2,10 +2,10 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { FileText, Plus } from "lucide-react";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useApplication, useProject, useUser } from "hooks/store";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helper
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
@ -14,12 +14,14 @@ export const PagesHeader = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// mobx store
// store hooks
const {
user: { currentProjectRole },
project: { currentProjectDetails },
commandPalette: { toggleCreatePageModal },
} = useMobxStore();
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const canUserCreatePage =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);

View File

@ -3,7 +3,7 @@ import useSWR from "swr";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useProject } from "hooks/store";
// ui
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// types
@ -18,12 +18,11 @@ import { renderEmoji } from "helpers/emoji.helper";
const issueArchiveService = new IssueArchiveService();
export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, archivedIssueId } = router.query;
const { project: projectStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
// store hooks
const { currentProjectDetails } = useProject();
const { data: issueDetails } = useSWR<IIssue | undefined>(
workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null,

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useProject } from "hooks/store";
// ui
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
// components
@ -12,13 +12,13 @@ import { CreateInboxIssueModal } from "components/inbox";
import { renderEmoji } from "helpers/emoji.helper";
export const ProjectInboxHeader: FC = observer(() => {
// states
const [createIssueModal, setCreateIssueModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const [createIssueModal, setCreateIssueModal] = useState(false);
const { project: projectStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
// store hooks
const { currentProjectDetails } = useProject();
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">

View File

@ -2,27 +2,26 @@ import { FC } from "react";
import useSWR from "swr";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// ui
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// helper
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// services
import { IssueService } from "services/issue";
// constants
import { ISSUE_DETAILS } from "constants/fetch-keys";
import { useMobxStore } from "lib/mobx/store-provider";
// services
const issueService = new IssueService();
export const ProjectIssueDetailsHeader: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { project: projectStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
// store hooks
const { currentProjectDetails } = useProject();
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,

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

@ -1,13 +1,12 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// ui
import { Breadcrumbs } from "@plane/ui";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
import { useProject, useUser } from "hooks/store";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
@ -17,14 +16,14 @@ export interface IProjectSettingHeader {
export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props) => {
const { title } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
// store hooks
const {
project: projectStore,
user: { currentProjectRole },
} = useMobxStore();
const { currentProjectDetails } = projectStore;
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
if (currentProjectRole && currentProjectRole <= EUserWorkspaceRoles.VIEWER) return null;

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

@ -1,20 +1,30 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
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
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
commandPalette: { toggleCreateViewModal },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { project: projectStore, commandPalette } = useMobxStore();
const { currentProjectDetails } = projectStore;
const canUserCreateIssue =
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
return (
<>
@ -50,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={() => commandPalette.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,23 +1,24 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Search, Plus, Briefcase } from "lucide-react";
// hooks
import { useApplication, useProject, useUser } from "hooks/store";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
export const ProjectsHeader = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
// store
// store hooks
const {
project: projectStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
} = useMobxStore();
eventTracker: { setTrackElement },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { workspaceProjects, searchQuery, setSearchQuery } = useProject();
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
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">
@ -33,28 +34,29 @@ export const ProjectsHeader = observer(() => {
</div>
</div>
<div className="flex items-center gap-3">
{projectsList?.length > 0 && (
{workspaceProjects && workspaceProjects?.length > 0 && (
<div className="flex w-full items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5 text-custom-text-400">
<Search className="h-3.5 w-3.5" />
<input
className="w-full min-w-[234px] border-none bg-transparent text-sm focus:outline-none"
value={projectStore.searchQuery}
onChange={(e) => projectStore.setSearchQuery(e.target.value)}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search"
/>
</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

@ -1,8 +1,8 @@
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useUser, useWorkspace } from "hooks/store";
// components
import { AddComment, IssueActivitySection } from "components/issues";
// services
@ -21,14 +21,15 @@ const issueService = new IssueService();
const issueCommentService = new IssueCommentService();
export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
user: userStore,
trackEvent: { postHogEventTracker },
workspace: { currentWorkspace },
} = useMobxStore();
eventTracker: { postHogEventTracker },
} = useApplication();
const { currentUser } = useUser();
const { currentWorkspace } = useWorkspace();
const { setToastAlert } = useToast();
@ -39,13 +40,11 @@ export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) =
: null
);
const user = userStore.currentUser;
const handleCommentUpdate = async (commentId: string, data: Partial<any>) => {
if (!workspaceSlug || !projectId || !issueDetails.id || !user) return;
if (!workspaceSlug || !projectId || !issueDetails.id || !currentUser) return;
await issueCommentService
.patchIssueComment(workspaceSlug as string, projectId as string, issueDetails.id as string, commentId, data)
.patchIssueComment(workspaceSlug.toString(), projectId.toString(), issueDetails.id, commentId, data)
.then((res) => {
mutateIssueActivity();
postHogEventTracker(
@ -57,19 +56,19 @@ export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) =
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
});
};
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !issueDetails.id || !user) return;
if (!workspaceSlug || !projectId || !issueDetails.id || !currentUser) return;
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
await issueCommentService
.deleteIssueComment(workspaceSlug as string, projectId as string, issueDetails.id as string, commentId)
.deleteIssueComment(workspaceSlug.toString(), projectId.toString(), issueDetails.id, commentId)
.then(() => {
mutateIssueActivity();
postHogEventTracker(
@ -80,14 +79,14 @@ export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) =
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
});
};
const handleAddComment = async (formData: IIssueActivity) => {
if (!workspaceSlug || !issueDetails || !user) return;
if (!workspaceSlug || !issueDetails || !currentUser) return;
await issueCommentService
.createIssueComment(workspaceSlug.toString(), issueDetails.project, issueDetails.id, formData)
@ -102,7 +101,7 @@ export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) =
{
isGrouping: true,
groupType: "Workspace_metrics",
gorupId: currentWorkspace?.id!,
groupId: currentWorkspace?.id!,
}
);
})

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

@ -1,15 +1,13 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "types/instance";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { Eye, EyeOff } from "lucide-react";
export interface IInstanceAIForm {
config: IFormattedInstanceConfiguration;
@ -25,7 +23,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
// states
const [showPassword, setShowPassword] = useState(false);
// store
const { instance: instanceStore } = useMobxStore();
const { instance: instanceStore } = useApplication();
// toast
const { setToastAlert } = useToast();
// form data

View File

@ -6,9 +6,8 @@ import { Eye, EyeOff } from "lucide-react";
// types
import { IFormattedInstanceConfiguration } from "types/instance";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
export interface IInstanceEmailForm {
config: IFormattedInstanceConfiguration;
@ -27,8 +26,8 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store
const { instance: instanceStore } = useMobxStore();
// store hooks
const { instance: instanceStore } = useApplication();
// toast
const { setToastAlert } = useToast();
// form data

View File

@ -5,8 +5,8 @@ import { Button, Input } from "@plane/ui";
// types
import { IInstance, IInstanceAdmin } from "types/instance";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
import { useMobxStore } from "lib/mobx/store-provider";
export interface IInstanceGeneralForm {
instance: IInstance;
@ -20,8 +20,8 @@ export interface GeneralFormValues {
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
const { instance, instanceAdmins } = props;
// store
const { instance: instanceStore } = useMobxStore();
// store hooks
const { instance: instanceStore } = useApplication();
// toast
const { setToastAlert } = useToast();
// form data

View File

@ -1,15 +1,13 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Copy, Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "types/instance";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { Copy, Eye, EyeOff } from "lucide-react";
export interface IInstanceGithubConfigForm {
config: IFormattedInstanceConfiguration;
@ -24,8 +22,8 @@ export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (props) =
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store
const { instance: instanceStore } = useMobxStore();
// store hooks
const { instance: instanceStore } = useApplication();
// toast
const { setToastAlert } = useToast();
// form data

View File

@ -1,15 +1,13 @@
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
import { Copy } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "types/instance";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { Copy } from "lucide-react";
export interface IInstanceGoogleConfigForm {
config: IFormattedInstanceConfiguration;
@ -22,8 +20,8 @@ export interface GoogleConfigFormValues {
export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (props) => {
const { config } = props;
// store
const { instance: instanceStore } = useMobxStore();
// store hooks
const { instance: instanceStore } = useApplication();
// toast
const { setToastAlert } = useToast();
// form data

View File

@ -1,10 +1,10 @@
import { FC, useState, useRef } from "react";
import { Transition } from "@headlessui/react";
import Link from "next/link";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { FileText, HelpCircle, MessagesSquare, MoveLeft } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
// icons
import { DiscordIcon, GithubIcon } from "@plane/ui";
// assets
import packageJson from "package.json";
@ -39,7 +39,7 @@ export const InstanceHelpSection: FC = () => {
// store
const {
theme: { sidebarCollapsed, toggleSidebar },
} = useMobxStore();
} = useApplication();
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);

View File

@ -1,15 +1,13 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "types/instance";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { Eye, EyeOff } from "lucide-react";
export interface IInstanceImageConfigForm {
config: IFormattedInstanceConfiguration;
@ -23,8 +21,8 @@ export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) =>
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store
const { instance: instanceStore } = useMobxStore();
// store hooks
const { instance: instanceStore } = useApplication();
// toast
const { setToastAlert } = useToast();
// form data

View File

@ -1,6 +1,8 @@
import React, { useState } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
// hooks
import { useApplication } from "hooks/store";
// ui
import { Button } from "@plane/ui";
import { UserCog2 } from "lucide-react";
@ -8,17 +10,16 @@ import { UserCog2 } from "lucide-react";
import instanceSetupDone from "public/instance-setup-done.webp";
import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
import { useMobxStore } from "lib/mobx/store-provider";
export const InstanceSetupDone = () => {
// states
const [isRedirecting, setIsRedirecting] = useState(false);
// next-themes
const { resolvedTheme } = useTheme();
// mobx store
// store hooks
const {
instance: { fetchInstanceInfo },
} = useMobxStore();
} = useApplication();
const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;

View File

@ -1,11 +1,10 @@
import { FC } from "react";
import { useForm, Controller } from "react-hook-form";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { XCircle } from "lucide-react";
// hooks
import { useUser } from "hooks/store";
// ui
import { Input, Button } from "@plane/ui";
// icons
import { XCircle } from "lucide-react";
// services
import { AuthService } from "services/auth.service";
const authService = new AuthService();
@ -25,9 +24,8 @@ export interface IInstanceSetupEmailForm {
export const InstanceSetupSignInForm: FC<IInstanceSetupEmailForm> = (props) => {
const { handleNextStep } = props;
const {
user: { fetchCurrentUser },
} = useMobxStore();
// store hooks
const { fetchCurrentUser } = useUser();
// form info
const {
control,

View File

@ -4,15 +4,13 @@ import Image from "next/image";
// components
import { InstanceSetupFormRoot } from "components/instance";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useUser } from "hooks/store";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
export const InstanceSetupView = observer(() => {
// store
const {
user: { fetchCurrentUser },
} = useMobxStore();
// store hooks
const { fetchCurrentUser } = useUser();
const mutateUserInfo = useCallback(() => {
fetchCurrentUser();

View File

@ -8,8 +8,8 @@ import { mutate } from "swr";
import { Menu, Transition } from "@headlessui/react";
// icons
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication, useUser } from "hooks/store";
// hooks
import useToast from "hooks/use-toast";
// ui
@ -26,13 +26,14 @@ const PROFILE_LINKS = [
];
export const InstanceSidebarDropdown = observer(() => {
// router
const router = useRouter();
// store
// store hooks
const {
theme: { sidebarCollapsed },
workspace: { workspaceSlug },
user: { signOut, currentUser, currentUserSettings },
} = useMobxStore();
router: { workspaceSlug },
} = useApplication();
const { signOut, currentUser, currentUserSettings } = useUser();
// hooks
const { setToastAlert } = useToast();
const { setTheme } = useTheme();

View File

@ -1,9 +1,8 @@
import Link from "next/link";
import { useRouter } from "next/router";
// icons
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
// ui
import { Tooltip } from "@plane/ui";
@ -41,9 +40,10 @@ const INSTANCE_ADMIN_LINKS = [
];
export const InstanceAdminSidebarMenu = () => {
// store hooks
const {
theme: { sidebarCollapsed },
} = useMobxStore();
} = useApplication();
// router
const router = useRouter();

View File

@ -1,11 +1,11 @@
import { observer } from "mobx-react-lite";
// hooks
import { useApplication } from "hooks/store";
import useIntegrationPopup from "hooks/use-integration-popup";
// ui
import { Button } from "@plane/ui";
// types
import { IWorkspaceIntegration } from "types";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
workspaceIntegration: false | IWorkspaceIntegration | undefined;
@ -13,9 +13,10 @@ type Props = {
};
export const GithubAuth: React.FC<Props> = observer(({ workspaceIntegration, provider }) => {
// store hooks
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
// hooks
const { startAuth, isConnecting } = useIntegrationPopup({
provider,

View File

@ -1,9 +1,8 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Control, Controller, UseFormWatch } from "react-hook-form";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useProject } from "hooks/store";
// components
import { SelectRepository, TFormValues, TIntegrationSteps } from "components/integration";
// ui
@ -22,21 +21,18 @@ type Props = {
export const GithubImportData: FC<Props> = observer((props) => {
const { handleStepChange, integration, control, watch } = props;
// store hooks
const { workspaceProjects, getProjectById } = useProject();
const router = useRouter();
const { workspaceSlug } = router.query;
const options = workspaceProjects?.map((projectId) => {
const projectDetails = getProjectById(projectId);
const { project: projectStore } = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
const options = projects
? projects.map((project) => ({
value: project.id,
query: project.name,
content: <p>{truncateText(project.name, 25)}</p>,
}))
: undefined;
return {
value: `${projectDetails?.id}`,
query: `${projectDetails?.name}`,
content: <p>{truncateText(projectDetails?.name ?? "", 25)}</p>,
};
});
return (
<div className="mt-6">
@ -74,7 +70,7 @@ export const GithubImportData: FC<Props> = observer((props) => {
<p className="text-xs text-custom-text-200">Select the project to import the issues to.</p>
</div>
<div className="col-span-12 sm:col-span-4">
{projects && (
{workspaceProjects && (
<Controller
control={control}
name="project"
@ -82,11 +78,7 @@ export const GithubImportData: FC<Props> = observer((props) => {
<CustomSearchSelect
value={value}
label={
value ? (
projects.find((p) => p.id === value)?.name
) : (
<span className="text-custom-text-200">Select Project</span>
)
value ? getProjectById(value)?.name : <span className="text-custom-text-200">Select Project</span>
}
onChange={onChange}
options={options}

View File

@ -1,28 +1,23 @@
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import { useFormContext, Controller } from "react-hook-form";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { Plus } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
// components
import { CustomSelect, Input } from "@plane/ui";
// types
import { IJiraImporterForm } from "types";
export const JiraGetImportDetail: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
project: projectStore,
commandPalette: commandPaletteStore,
trackEvent: { setTrackElement },
} = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
eventTracker: { setTrackElement },
} = useApplication();
const { workspaceProjects, getProjectById } = useProject();
// form info
const {
control,
formState: { errors },
@ -170,20 +165,26 @@ export const JiraGetImportDetail: React.FC = observer(() => {
onChange={onChange}
label={
<span>
{value && value !== "" ? (
projects?.find((p) => p.id === value)?.name
{value && value.trim() !== "" ? (
getProjectById(value)?.name
) : (
<span className="text-custom-text-200">Select a project</span>
)}
</span>
}
>
{projects && projects.length > 0 ? (
projects.map((project) => (
<CustomSelect.Option key={project.id} value={project.id}>
{project.name}
</CustomSelect.Option>
))
{workspaceProjects && workspaceProjects.length > 0 ? (
workspaceProjects.map((projectId) => {
const projectDetails = getProjectById(projectId);
if (!projectDetails) return;
return (
<CustomSelect.Option key={projectId} value={projectId}>
{projectDetails.name}
</CustomSelect.Option>
);
})
) : (
<div className="flex cursor-pointer select-none items-center space-x-2 truncate rounded px-1 py-1.5 text-custom-text-200">
<p>You don{"'"}t have any project. Please create a project first.</p>

View File

@ -2,12 +2,12 @@ import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR, { mutate } from "swr";
// services
import { IntegrationService } from "services/integrations";
// hooks
import { useApplication, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
import useIntegrationPopup from "hooks/use-integration-popup";
// ui
@ -20,8 +20,6 @@ import { CheckCircle } from "lucide-react";
import { IAppIntegration, IWorkspaceIntegration } from "types";
// fetch-keys
import { WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
integration: IAppIntegration;
@ -44,20 +42,23 @@ const integrationDetails: { [key: string]: any } = {
const integrationService = new IntegrationService();
export const SingleIntegrationCard: React.FC<Props> = observer(({ integration }) => {
const {
appConfig: { envConfig },
user: { currentWorkspaceRole },
} = useMobxStore();
const isUserAdmin = currentWorkspaceRole === 20;
// states
const [deletingIntegration, setDeletingIntegration] = useState(false);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
config: { envConfig },
} = useApplication();
const {
membership: { currentWorkspaceRole },
} = useUser();
// toast alert
const { setToastAlert } = useToast();
const isUserAdmin = currentWorkspaceRole === 20;
const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({
provider: integration.provider,
github_app_name: envConfig?.github_app_name || "",

View File

@ -2,18 +2,17 @@ import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { observer } from "mobx-react-lite";
// hooks
import { useApplication } from "hooks/store";
import useIntegrationPopup from "hooks/use-integration-popup";
// services
import { AppInstallationService } from "services/app_installation.service";
// ui
import { Loader } from "@plane/ui";
// hooks
import useIntegrationPopup from "hooks/use-integration-popup";
// types
import { IWorkspaceIntegration, ISlackIntegration } from "types";
// fetch-keys
import { SLACK_CHANNEL_INFO } from "constants/fetch-keys";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
integration: IWorkspaceIntegration;
@ -22,10 +21,10 @@ type Props = {
const appInstallationService = new AppInstallationService();
export const SelectChannel: React.FC<Props> = observer(({ integration }) => {
// store
// store hooks
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
// states
const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] = useState<boolean>(false);
const [slackChannel, setSlackChannel] = useState<ISlackIntegration | null>(null);

View File

@ -3,12 +3,11 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
import { useDropzone } from "react-dropzone";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
// services
import { IssueAttachmentService } from "services/issue";
// hooks
import useToast from "hooks/use-toast";
// types
import { IIssueAttachment } from "types";
// fetch-keys
@ -29,12 +28,12 @@ export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
// toast alert
const { setToastAlert } = useToast();
// store hooks
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
const onDrop = useCallback((acceptedFiles: File[]) => {
if (!acceptedFiles[0] || !workspaceSlug) return;

View File

@ -1,12 +1,13 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import useEditorSuggestions from "hooks/use-editor-suggestions";
// services
import { FileService } from "services/file.service";
// icons
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
// hooks
import useUser from "hooks/use-user";
// ui
import { CustomMenu } from "@plane/ui";
import { CommentReaction } from "components/issues";
@ -15,7 +16,6 @@ import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-te
import { timeAgo } from "helpers/date-time.helper";
// types
import type { IIssueActivity } from "types";
import useEditorSuggestions from "hooks/use-editor-suggestions";
// services
const fileService = new FileService();
@ -28,22 +28,18 @@ type Props = {
workspaceSlug: string;
};
export const CommentCard: React.FC<Props> = ({
comment,
handleCommentDeletion,
onSubmit,
showAccessSpecifier = false,
workspaceSlug,
}) => {
const { user } = useUser();
export const CommentCard: React.FC<Props> = observer((props) => {
const { comment, handleCommentDeletion, onSubmit, showAccessSpecifier = false, workspaceSlug } = props;
// states
const [isEditing, setIsEditing] = useState(false);
// refs
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const editorSuggestions = useEditorSuggestions();
const [isEditing, setIsEditing] = useState(false);
// store hooks
const { currentUser } = useUser();
// form info
const {
formState: { isSubmitting },
handleSubmit,
@ -152,7 +148,7 @@ export const CommentCard: React.FC<Props> = ({
</div>
</div>
</div>
{user?.id === comment.actor && (
{currentUser?.id === comment.actor && (
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
<Pencil className="h-3 w-3" />
@ -192,4 +188,4 @@ export const CommentCard: React.FC<Props> = ({
)}
</div>
);
};
});

View File

@ -1,12 +1,14 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import useUser from "hooks/use-user";
import { useUser } from "hooks/store";
import useCommentReaction from "hooks/use-comment-reaction";
// ui
import { ReactionSelector } from "components/core";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IssueCommentReaction } from "types";
type Props = {
@ -15,13 +17,13 @@ type Props = {
readonly?: boolean;
};
export const CommentReaction: FC<Props> = (props) => {
export const CommentReaction: FC<Props> = observer((props) => {
const { projectId, commentId, readonly = false } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser();
// store hooks
const { currentUser } = useUser();
const { commentReactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useCommentReaction(
workspaceSlug,
@ -33,7 +35,7 @@ export const CommentReaction: FC<Props> = (props) => {
if (!workspaceSlug || !projectId || !commentId) return;
const isSelected = commentReactions?.some(
(r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
);
if (isSelected) {
@ -51,7 +53,7 @@ export const CommentReaction: FC<Props> = (props) => {
position="top"
value={
commentReactions
?.filter((reaction: IssueCommentReaction) => reaction.actor === user?.id)
?.filter((reaction: IssueCommentReaction) => reaction.actor === currentUser?.id)
.map((r: IssueCommentReaction) => r.reaction) || []
}
onSelect={handleReactionClick}
@ -70,7 +72,9 @@ export const CommentReaction: FC<Props> = (props) => {
}}
key={reaction}
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
commentReactions?.some((r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction)
commentReactions?.some(
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
)
? "bg-custom-primary-100/10"
: "bg-custom-background-80"
}`}
@ -78,7 +82,9 @@ export const CommentReaction: FC<Props> = (props) => {
<span>{renderEmoji(reaction)}</span>
<span
className={
commentReactions?.some((r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction)
commentReactions?.some(
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
)
? "text-custom-primary-100"
: ""
}
@ -90,4 +96,4 @@ export const CommentReaction: FC<Props> = (props) => {
)}
</div>
);
};
});

View File

@ -1,9 +1,6 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { IssueDraftService } from "services/issue";
// hooks
@ -24,17 +21,14 @@ type Props = {
const issueDraftService = new IssueDraftService();
export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
const { isOpen, handleClose, data, onSubmit } = props;
// states
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { user: userStore } = useMobxStore();
const user = userStore.currentUser;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// toast alert
const { setToastAlert } = useToast();
useEffect(() => {
@ -47,12 +41,12 @@ export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
};
const handleDeletion = async () => {
if (!workspaceSlug || !data || !user) return;
if (!workspaceSlug || !data) return;
setIsDeleteLoading(true);
await issueDraftService
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
.deleteDraftIssue(workspaceSlug.toString(), data.project, data.id)
.then(() => {
setIsDeleteLoading(false);
handleClose();
@ -138,4 +132,4 @@ export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
</Dialog>
</Transition.Root>
);
});
};

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,12 +1,16 @@
import React, { FC, useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { Sparkle, X } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
import useEditorSuggestions from "hooks/use-editor-suggestions";
// services
import { AIService } from "services/ai.service";
import { FileService } from "services/file.service";
// hooks
import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// components
import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues";
@ -21,18 +25,11 @@ import {
} from "components/issues/select";
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
import { RichTextEditorWithRef } from "@plane/rich-text-editor";
// ui
import {} from "components/ui";
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
// icons
import { Sparkle, X } from "lucide-react";
// types
import type { IUser, IIssue, ISearchIssueResponse } from "types";
// components
import { RichTextEditorWithRef } from "@plane/rich-text-editor";
import useEditorSuggestions from "hooks/use-editor-suggestions";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
const aiService = new AIService();
const fileService = new FileService();
@ -123,8 +120,8 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
const { workspaceSlug } = router.query;
// store
const {
appConfig: { envConfig },
} = useMobxStore();
config: { envConfig },
} = useApplication();
// form info
const {
formState: { errors, isSubmitting },

View File

@ -2,8 +2,10 @@ import React, { FC, useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { LayoutPanelTop, Sparkle, X } from "lucide-react";
// hooks
import { useApplication, useUser } from "hooks/store";
import useEditorSuggestions from "hooks/use-editor-suggestions";
// services
import { AIService } from "services/ai.service";
import { FileService } from "services/file.service";
@ -25,15 +27,11 @@ import {
} from "components/issues/select";
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
import { RichTextEditorWithRef } from "@plane/rich-text-editor";
// ui
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
// icons
import { LayoutPanelTop, Sparkle, X } from "lucide-react";
// types
import type { IIssue, ISearchIssueResponse } from "types";
// components
import { RichTextEditorWithRef } from "@plane/rich-text-editor";
import useEditorSuggestions from "hooks/use-editor-suggestions";
const defaultValues: Partial<IIssue> = {
project: "",
@ -106,12 +104,11 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
// store hooks
const {
user: userStore,
appConfig: { envConfig },
} = useMobxStore();
const user = userStore.currentUser;
config: { envConfig },
} = useApplication();
const {} = useUser();
// hooks
const editorSuggestion = useEditorSuggestions();
const { setToastAlert } = useToast();
@ -183,12 +180,12 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
};
const handleAutoGenerateDescription = async () => {
if (!workspaceSlug || !projectId || !user) return;
if (!workspaceSlug || !projectId) return;
setIAmFeelingLucky(true);
aiService
.createGptTask(workspaceSlug as string, projectId as string, {
.createGptTask(workspaceSlug.toString(), projectId.toString(), {
prompt: issueName,
task: "Generate a proper description for this issue.",
})

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];

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