mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into dev/migration_priority_drafts_relation_props
This commit is contained in:
commit
67a5e7debc
54
.env.example
54
.env.example
@ -1,38 +1,3 @@
|
|||||||
# Frontend
|
|
||||||
# Extra image domains that need to be added for Next Image
|
|
||||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
|
||||||
# Google Client ID for Google OAuth
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
|
||||||
# Github ID for Github OAuth
|
|
||||||
NEXT_PUBLIC_GITHUB_ID=""
|
|
||||||
# Github App Name for GitHub Integration
|
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
|
||||||
# Sentry DSN for error monitoring
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=""
|
|
||||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
|
||||||
# Enable/Disable sentry
|
|
||||||
NEXT_PUBLIC_ENABLE_SENTRY=0
|
|
||||||
# Enable/Disable session recording
|
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
|
||||||
# Enable/Disable event tracking
|
|
||||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
|
||||||
# Slack for Slack Integration
|
|
||||||
NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
|
||||||
# For Telemetry, set it to "app.plane.so"
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
|
||||||
# public boards deploy url
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
|
||||||
# plane deploy using nginx
|
|
||||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
# Debug value for api server use it as 0 for production use
|
|
||||||
DEBUG=0
|
|
||||||
|
|
||||||
# Error logs
|
|
||||||
SENTRY_DSN=""
|
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
PGUSER="plane"
|
||||||
PGPASSWORD="plane"
|
PGPASSWORD="plane"
|
||||||
@ -45,15 +10,6 @@ REDIS_HOST="plane-redis"
|
|||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
# Email Settings
|
|
||||||
EMAIL_HOST=""
|
|
||||||
EMAIL_HOST_USER=""
|
|
||||||
EMAIL_HOST_PASSWORD=""
|
|
||||||
EMAIL_PORT=587
|
|
||||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
|
||||||
EMAIL_USE_TLS="1"
|
|
||||||
EMAIL_USE_SSL="0"
|
|
||||||
|
|
||||||
# AWS Settings
|
# AWS Settings
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
AWS_ACCESS_KEY_ID="access-key"
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
@ -69,9 +25,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
|||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
# Github
|
|
||||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
|
||||||
|
|
||||||
# Settings related to Docker
|
# Settings related to Docker
|
||||||
DOCKERIZED=1
|
DOCKERIZED=1
|
||||||
# set to 1 If using the pre-configured minio setup
|
# set to 1 If using the pre-configured minio setup
|
||||||
@ -80,10 +33,3 @@ USE_MINIO=1
|
|||||||
# Nginx Configuration
|
# Nginx Configuration
|
||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|
||||||
# Default Creds
|
|
||||||
DEFAULT_EMAIL="captain@plane.so"
|
|
||||||
DEFAULT_PASSWORD="password123"
|
|
||||||
|
|
||||||
# SignUps
|
|
||||||
ENABLE_SIGNUP="1"
|
|
||||||
# Auto generated and Required that will be generated from setup.sh
|
|
||||||
|
60
apiserver/.env.example
Normal file
60
apiserver/.env.example
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Backend
|
||||||
|
# Debug value for api server use it as 0 for production use
|
||||||
|
DEBUG=0
|
||||||
|
|
||||||
|
# Error logs
|
||||||
|
SENTRY_DSN=""
|
||||||
|
|
||||||
|
# Database Settings
|
||||||
|
PGUSER="plane"
|
||||||
|
PGPASSWORD="plane"
|
||||||
|
PGHOST="plane-db"
|
||||||
|
PGDATABASE="plane"
|
||||||
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
|
# Redis Settings
|
||||||
|
REDIS_HOST="plane-redis"
|
||||||
|
REDIS_PORT="6379"
|
||||||
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
|
# Email Settings
|
||||||
|
EMAIL_HOST=""
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||||
|
EMAIL_USE_TLS="1"
|
||||||
|
EMAIL_USE_SSL="0"
|
||||||
|
|
||||||
|
# AWS Settings
|
||||||
|
AWS_REGION=""
|
||||||
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
|
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||||
|
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||||
|
# Changing this requires change in the nginx.conf for uploads if using minio setup
|
||||||
|
AWS_S3_BUCKET_NAME="uploads"
|
||||||
|
# Maximum file upload limit
|
||||||
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
|
# GPT settings
|
||||||
|
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
||||||
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
|
# Github
|
||||||
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
|
|
||||||
|
# Settings related to Docker
|
||||||
|
DOCKERIZED=1
|
||||||
|
# set to 1 If using the pre-configured minio setup
|
||||||
|
USE_MINIO=1
|
||||||
|
|
||||||
|
# Nginx Configuration
|
||||||
|
NGINX_PORT=80
|
||||||
|
|
||||||
|
# Default Creds
|
||||||
|
DEFAULT_EMAIL="captain@plane.so"
|
||||||
|
DEFAULT_PASSWORD="password123"
|
||||||
|
|
||||||
|
# SignUps
|
||||||
|
ENABLE_SIGNUP="1"
|
@ -49,6 +49,7 @@ class IssueFlatSerializer(BaseSerializer):
|
|||||||
"target_date",
|
"target_date",
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"sort_order",
|
"sort_order",
|
||||||
|
"is_draft",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1038,6 +1038,7 @@ urlpatterns = [
|
|||||||
IssueDraftViewSet.as_view(
|
IssueDraftViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "list",
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-issue-draft",
|
name="project-issue-draft",
|
||||||
@ -1047,6 +1048,7 @@ urlpatterns = [
|
|||||||
IssueDraftViewSet.as_view(
|
IssueDraftViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
"delete": "destroy",
|
"delete": "destroy",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -508,7 +508,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
issue_activities = (
|
issue_activities = (
|
||||||
IssueActivity.objects.filter(issue_id=issue_id)
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
.filter(
|
.filter(
|
||||||
~Q(field__in=["comment", "vote", "reaction"]),
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
project__project_projectmember__member=self.request.user,
|
project__project_projectmember__member=self.request.user,
|
||||||
)
|
)
|
||||||
.select_related("actor", "workspace", "issue", "project")
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
@ -2358,6 +2358,47 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
serializer_class = IssueFlatSerializer
|
serializer_class = IssueFlatSerializer
|
||||||
model = Issue
|
model = Issue
|
||||||
|
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.updated",
|
||||||
|
requested_data=requested_data,
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.deleted",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
{"issue_id": str(self.kwargs.get("pk", None))}
|
||||||
|
),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.objects.annotate(
|
Issue.objects.annotate(
|
||||||
@ -2383,6 +2424,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
@ -2492,6 +2534,40 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
"default_assignee_id": project.default_assignee_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(is_draft=True)
|
||||||
|
|
||||||
|
# Track the issue
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.created",
|
||||||
|
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
except Project.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
try:
|
try:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
|
@ -518,11 +518,6 @@ def update_issue_activity(
|
|||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
}
|
}
|
||||||
|
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
|
||||||
current_instance = (
|
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
|
||||||
)
|
|
||||||
|
|
||||||
for key in requested_data:
|
for key in requested_data:
|
||||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
@ -1095,6 +1090,69 @@ def delete_issue_relation_activity(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"drafted the issue",
|
||||||
|
field="draft",
|
||||||
|
verb="created",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"created the issue",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"updated the draft issue",
|
||||||
|
field="draft",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def delete_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"deleted the draft issue",
|
||||||
|
field="draft",
|
||||||
|
verb="deleted",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1166,6 +1224,9 @@ def issue_activity(
|
|||||||
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
||||||
"issue_vote.activity.created": create_issue_vote_activity,
|
"issue_vote.activity.created": create_issue_vote_activity,
|
||||||
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
||||||
|
"issue_draft.activity.created": create_draft_issue_activity,
|
||||||
|
"issue_draft.activity.updated": update_draft_issue_activity,
|
||||||
|
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
||||||
}
|
}
|
||||||
|
|
||||||
func = ACTIVITY_MAPPER.get(type)
|
func = ACTIVITY_MAPPER.get(type)
|
||||||
|
@ -39,10 +39,86 @@ def group_results(results_data, group_by, sub_group_by=False):
|
|||||||
|
|
||||||
for value in results_data:
|
for value in results_data:
|
||||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
|
group_attribute = resolve_keys(group_by, value)
|
||||||
|
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
|
||||||
|
if len(main_group_attribute):
|
||||||
|
for attrib in main_group_attribute:
|
||||||
|
if str(attrib) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(attrib)] = {}
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(attrib)]:
|
||||||
|
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(attrib)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(None)] = {}
|
||||||
|
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||||
|
|
||||||
|
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
|
||||||
if str(main_group_attribute) not in main_responsive_dict:
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
main_responsive_dict[str(main_group_attribute)] = {}
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||||
|
|
||||||
|
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
|
||||||
|
if len(main_group_attribute):
|
||||||
|
for main_attrib in main_group_attribute:
|
||||||
|
if str(main_attrib) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_attrib)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(main_attrib)]:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(main_attrib)]:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)] = []
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(None)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(None)] = []
|
||||||
|
main_responsive_dict[str(None)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
group_attribute = resolve_keys(group_by, value)
|
group_attribute = resolve_keys(group_by, value)
|
||||||
if str(group_attribute) in main_responsive_dict:
|
|
||||||
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
|
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||||
else:
|
else:
|
||||||
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
||||||
|
@ -1,37 +1,5 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
x-api-and-worker-env: &api-and-worker-env
|
|
||||||
DEBUG: ${DEBUG}
|
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
|
||||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
|
||||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
|
||||||
REDIS_URL: redis://plane-redis:6379/
|
|
||||||
EMAIL_HOST: ${EMAIL_HOST}
|
|
||||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
|
||||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
|
||||||
EMAIL_PORT: ${EMAIL_PORT}
|
|
||||||
EMAIL_FROM: ${EMAIL_FROM}
|
|
||||||
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
|
|
||||||
EMAIL_USE_SSL: ${EMAIL_USE_SSL}
|
|
||||||
AWS_REGION: ${AWS_REGION}
|
|
||||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
|
||||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
|
||||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
|
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
|
|
||||||
WEB_URL: ${WEB_URL}
|
|
||||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
|
||||||
DISABLE_COLLECTSTATIC: 1
|
|
||||||
DOCKERIZED: 1
|
|
||||||
OPENAI_API_BASE: ${OPENAI_API_BASE}
|
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
|
||||||
GPT_ENGINE: ${GPT_ENGINE}
|
|
||||||
SECRET_KEY: ${SECRET_KEY}
|
|
||||||
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
|
|
||||||
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
|
|
||||||
USE_MINIO: ${USE_MINIO}
|
|
||||||
ENABLE_SIGNUP: ${ENABLE_SIGNUP}
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
plane-web:
|
plane-web:
|
||||||
container_name: planefrontend
|
container_name: planefrontend
|
||||||
@ -40,23 +8,8 @@ services:
|
|||||||
dockerfile: ./web/Dockerfile.web
|
dockerfile: ./web/Dockerfile.web
|
||||||
args:
|
args:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
|
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
|
||||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_SENTRY: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
|
|
||||||
NEXT_PUBLIC_TRACK_EVENTS: "0"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-worker
|
- plane-worker
|
||||||
@ -68,14 +21,8 @@ services:
|
|||||||
dockerfile: ./space/Dockerfile.space
|
dockerfile: ./space/Dockerfile.space
|
||||||
args:
|
args:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-worker
|
- plane-worker
|
||||||
@ -91,9 +38,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/takeoff
|
command: ./bin/takeoff
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
@ -108,9 +53,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/worker
|
command: ./bin/worker
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-db
|
- plane-db
|
||||||
@ -126,9 +69,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/beat
|
command: ./bin/beat
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- plane-api
|
||||||
- plane-db
|
- plane-db
|
||||||
@ -163,8 +104,6 @@ services:
|
|||||||
command: server /export --console-address ":9090"
|
command: server /export --console-address ":9090"
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/export
|
- uploads:/export
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||||
@ -187,8 +126,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT}:80
|
- ${NGINX_PORT}:80
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
events { }
|
events { }
|
||||||
|
|
||||||
|
|
||||||
http {
|
http {
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
FROM=$1
|
|
||||||
TO=$2
|
|
||||||
DIRECTORY=$3
|
|
||||||
|
|
||||||
if [ "${FROM}" = "${TO}" ]; then
|
|
||||||
echo "Nothing to replace, the value is already set to ${TO}."
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Only perform action if $FROM and $TO are different.
|
|
||||||
echo "Replacing all statically built instances of $FROM with this string $TO ."
|
|
||||||
|
|
||||||
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
|
13
setup.sh
13
setup.sh
@ -5,15 +5,12 @@ cp ./.env.example ./.env
|
|||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
export LC_CTYPE=C
|
export LC_CTYPE=C
|
||||||
|
|
||||||
|
cp ./web/.env.example ./web/.env
|
||||||
# Generate the NEXT_PUBLIC_API_BASE_URL with given IP
|
cp ./space/.env.example ./space/.env
|
||||||
echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env
|
cp ./apiserver/.env.example ./apiserver/.env
|
||||||
|
|
||||||
# Generate the SECRET_KEY that will be used by django
|
# Generate the SECRET_KEY that will be used by django
|
||||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env
|
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||||
|
|
||||||
# WEB_URL for email redirection and image saving
|
|
||||||
echo -e "WEB_URL=$1" >> ./.env
|
|
||||||
|
|
||||||
# Generate Prompt for taking tiptap auth key
|
# Generate Prompt for taking tiptap auth key
|
||||||
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
||||||
@ -23,7 +20,5 @@ echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, afte
|
|||||||
|
|
||||||
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
|
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
|
||||||
|
|
||||||
|
|
||||||
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
|
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
|
||||||
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
||||||
|
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
# Base url for the API requests
|
|
||||||
NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
# Public boards deploy URL
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
|
||||||
# Google Client ID for Google OAuth
|
# Google Client ID for Google OAuth
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||||
# Flag to toggle OAuth
|
# Flag to toggle OAuth
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=1
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -1,7 +1,6 @@
|
|||||||
FROM node:18-alpine AS builder
|
FROM node:18-alpine AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
|
||||||
|
|
||||||
RUN yarn global add turbo
|
RUN yarn global add turbo
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -20,19 +19,16 @@ RUN yarn install --network-timeout 500000
|
|||||||
|
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
USER root
|
USER root
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||||
|
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||||
|
|
||||||
RUN yarn turbo run build --filter=space
|
RUN yarn turbo run build --filter=space
|
||||||
|
|
||||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space
|
|
||||||
|
|
||||||
FROM node:18-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -48,14 +44,14 @@ COPY --from=installer --chown=captain:plane /app/space/.next/standalone ./
|
|||||||
COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next
|
COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next
|
||||||
COPY --from=installer --chown=captain:plane /app/space/public ./space/public
|
COPY --from=installer --chown=captain:plane /app/space/public ./space/public
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
COPY start.sh /usr/local/bin/
|
COPY start.sh /usr/local/bin/
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// components
|
// components
|
||||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||||
// images
|
// images
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
export const SignInView = observer(() => {
|
export const SignInView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
1
space/helpers/common.helper.ts
Normal file
1
space/helpers/common.helper.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
|
@ -5,7 +5,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// components
|
// components
|
||||||
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
||||||
|
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
const OnBoardingPage = () => {
|
const OnBoardingPage = () => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class AuthService extends APIService {
|
class AuthService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async emailLogin(data: any) {
|
async emailLogin(data: any) {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
// services
|
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
interface UnSplashImage {
|
interface UnSplashImage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -29,7 +27,7 @@ interface UnSplashImageUrls {
|
|||||||
|
|
||||||
class FileServices extends APIService {
|
class FileServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class IssueService extends APIService {
|
class IssueService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
|
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class ProjectService extends APIService {
|
class ProjectService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
|
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class UserService extends APIService {
|
class UserService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async currentUser(): Promise<any> {
|
async currentUser(): Promise<any> {
|
||||||
|
4
start.sh
4
start.sh
@ -1,9 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL
|
|
||||||
# NOTE: if these values are the same, this will be skipped.
|
|
||||||
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2
|
|
||||||
|
|
||||||
echo "Starting Plane Frontend.."
|
echo "Starting Plane Frontend.."
|
||||||
node $1
|
node $1
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Base url for the API requests
|
|
||||||
NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
# Extra image domains that need to be added for Next Image
|
# Extra image domains that need to be added for Next Image
|
||||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
||||||
# Google Client ID for Google OAuth
|
# Google Client ID for Google OAuth
|
||||||
@ -23,4 +21,4 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
|||||||
# For Telemetry, set it to "app.plane.so"
|
# For Telemetry, set it to "app.plane.so"
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
||||||
# Public boards deploy URL
|
# Public boards deploy URL
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces"
|
@ -2,7 +2,6 @@ FROM node:18-alpine AS builder
|
|||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
|
||||||
|
|
||||||
RUN yarn global add turbo
|
RUN yarn global add turbo
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -14,8 +13,8 @@ FROM node:18-alpine AS installer
|
|||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
ARG NEXT_PUBLIC_DEPLOY_URL=""
|
||||||
|
|
||||||
# First install the dependencies (as they change less often)
|
# First install the dependencies (as they change less often)
|
||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
@ -26,18 +25,12 @@ RUN yarn install --network-timeout 500000
|
|||||||
# Build the project
|
# Build the project
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
USER root
|
USER root
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
|
||||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
|
||||||
|
|
||||||
RUN yarn turbo run build --filter=web
|
RUN yarn turbo run build --filter=web
|
||||||
|
|
||||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} web
|
|
||||||
|
|
||||||
FROM node:18-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -52,20 +45,15 @@ COPY --from=installer /app/web/package.json .
|
|||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
COPY --from=installer --chown=captain:plane /app/web/.next/standalone ./
|
COPY --from=installer --chown=captain:plane /app/web/.next/standalone ./
|
||||||
|
|
||||||
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
|
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
ARG NEXT_PUBLIC_DEPLOY_URL=""
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
||||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
COPY start.sh /usr/local/bin/
|
COPY start.sh /usr/local/bin/
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -3,8 +3,8 @@ import React, { useState } from "react";
|
|||||||
// component
|
// component
|
||||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icons
|
// icon
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ArchiveRestore } from "lucide-react";
|
||||||
// constants
|
// constants
|
||||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
// types
|
// types
|
||||||
@ -28,15 +28,19 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
handleClose={() => setmonthModal(false)}
|
handleClose={() => setmonthModal(false)}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90">
|
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
<p className="text-sm text-custom-text-200">
|
<ArchiveRestore className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
|
||||||
Plane will automatically archive issues that have been completed or cancelled for the
|
</div>
|
||||||
configured time period.
|
<div className="">
|
||||||
|
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
|
||||||
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
|
Plane will auto archive issues that have been completed or canceled.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.archive_in !== 0}
|
value={projectDetails?.archive_in !== 0}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
@ -47,9 +51,11 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{projectDetails?.archive_in !== 0 && (
|
{projectDetails?.archive_in !== 0 && (
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
<div className="ml-12">
|
||||||
<div className="w-1/2 text-base font-medium">
|
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
||||||
|
<div className="w-1/2 text-sm font-medium">
|
||||||
Auto-archive issues that are closed for
|
Auto-archive issues that are closed for
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
@ -62,19 +68,19 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
handleChange({ archive_in: val });
|
handleChange({ archive_in: val });
|
||||||
}}
|
}}
|
||||||
input
|
input
|
||||||
verticalPosition="top"
|
verticalPosition="bottom"
|
||||||
width="w-full"
|
width="w-full"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
<CustomSelect.Option key={month.label} value={month.value}>
|
<CustomSelect.Option key={month.label} value={month.value}>
|
||||||
{month.label}
|
<span className="text-sm">{month.label}</span>
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
className="flex w-full text-sm select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={() => setmonthModal(true)}
|
onClick={() => setmonthModal(true)}
|
||||||
>
|
>
|
||||||
Customise Time Range
|
Customise Time Range
|
||||||
@ -83,6 +89,7 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -5,11 +5,12 @@ import useSWR from "swr";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// component
|
// component
|
||||||
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
import { ArchiveX } from "lucide-react";
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
// constants
|
// constants
|
||||||
@ -76,15 +77,19 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90">
|
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
<p className="text-sm text-custom-text-200">
|
<ArchiveX className="h-4 w-4 text-red-500 flex-shrink-0" />
|
||||||
Plane will automatically close the issues that have not been updated for the
|
</div>
|
||||||
configured time period.
|
<div className="">
|
||||||
|
<h4 className="text-sm font-medium">Auto-close issues</h4>
|
||||||
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
|
Plane will automatically close issue that haven’t been completed or canceled.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.close_in !== 0}
|
value={projectDetails?.close_in !== 0}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
@ -95,10 +100,12 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{projectDetails?.close_in !== 0 && (
|
{projectDetails?.close_in !== 0 && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="ml-12">
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-1/2 text-base font-medium">
|
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
||||||
|
<div className="w-1/2 text-sm font-medium">
|
||||||
Auto-close issues that are inactive for
|
Auto-close issues that are inactive for
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
@ -130,8 +137,9 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
|
||||||
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
|
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
||||||
|
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
||||||
<div className="w-1/2 ">
|
<div className="w-1/2 ">
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={
|
value={
|
||||||
@ -174,6 +182,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -20,6 +20,7 @@ import fileService from "services/file.service";
|
|||||||
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
const unsplashEnabled =
|
const unsplashEnabled =
|
||||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
||||||
@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
fileService.getUnsplashImages(1, searchParams)
|
fileService.getUnsplashImages(1, searchParams)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const imagePickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { workspaceDetails } = useWorkspaceDetails();
|
const { workspaceDetails } = useWorkspaceDetails();
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
onChange(images[0].urls.regular);
|
onChange(images[0].urls.regular);
|
||||||
}, [value, onChange, images]);
|
}, [value, onChange, images]);
|
||||||
|
|
||||||
|
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
||||||
|
|
||||||
if (!unsplashEnabled) return null;
|
if (!unsplashEnabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref}>
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className="rounded-md border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
className="rounded-sm border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||||
<div className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl">
|
<div
|
||||||
|
ref={imagePickerRef}
|
||||||
|
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
|
||||||
|
>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<div>
|
<div>
|
||||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
// react colors
|
// react colors
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// types
|
// types
|
||||||
import { Props } from "./types";
|
import { Props } from "./types";
|
||||||
// emojis
|
// emojis
|
||||||
@ -36,6 +38,8 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
|
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecentEmojis(getRecentEmojis());
|
setRecentEmojis(getRecentEmojis());
|
||||||
}, []);
|
}, []);
|
||||||
@ -44,6 +48,8 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
if (!value || value?.length === 0) onChange(getRandomEmoji());
|
if (!value || value?.length === 0) onChange(getRandomEmoji());
|
||||||
}, [value, onChange]);
|
}, [value, onChange]);
|
||||||
|
|
||||||
|
useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative z-[1]">
|
<Popover className="relative z-[1]">
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
@ -63,7 +69,10 @@ const EmojiIconPicker: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||||
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl">
|
<div
|
||||||
|
ref={emojiPickerRef}
|
||||||
|
className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl"
|
||||||
|
>
|
||||||
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
<Tab.Group as="div" className="flex h-full w-full flex-col">
|
||||||
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
|
@ -66,7 +66,7 @@ export const SingleEstimate: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gap-2 py-3">
|
<div className="gap-2 p-4 border-b border-custom-border-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
|
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
|
||||||
|
@ -84,3 +84,4 @@ export * from "./clock-icon";
|
|||||||
export * from "./bell-icon";
|
export * from "./bell-icon";
|
||||||
export * from "./single-comment-icon";
|
export * from "./single-comment-icon";
|
||||||
export * from "./related-icon";
|
export * from "./related-icon";
|
||||||
|
export * from "./module-icon";
|
59
web/components/icons/module-icon.tsx
Normal file
59
web/components/icons/module-icon.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Props } from "./types";
|
||||||
|
|
||||||
|
export const ModuleIcon: React.FC<Props> = ({
|
||||||
|
width = "24",
|
||||||
|
height = "24",
|
||||||
|
className,
|
||||||
|
color = "#F15B5B",
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z"
|
||||||
|
stroke="#F15B5B"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.84925 4.66667H4.81221C4.73039 4.66667 4.66406 4.733 4.66406 4.81482V5.85185C4.66406 5.93367 4.73039 6 4.81221 6H5.84925C5.93107 6 5.9974 5.93367 5.9974 5.85185V4.81482C5.9974 4.733 5.93107 4.66667 5.84925 4.66667Z"
|
||||||
|
fill={color}
|
||||||
|
stroke="#F15B5B"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.84925 10H4.81221C4.73039 10 4.66406 10.0663 4.66406 10.1481V11.1852C4.66406 11.267 4.73039 11.3333 4.81221 11.3333H5.84925C5.93107 11.3333 5.9974 11.267 5.9974 11.1852V10.1481C5.9974 10.0663 5.93107 10 5.84925 10Z"
|
||||||
|
fill={color}
|
||||||
|
stroke="#F15B5B"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.1852 4.66667H10.1481C10.0663 4.66667 10 4.733 10 4.81482V5.85185C10 5.93367 10.0663 6 10.1481 6H11.1852C11.267 6 11.3333 5.93367 11.3333 5.85185V4.81482C11.3333 4.733 11.267 4.66667 11.1852 4.66667Z"
|
||||||
|
fill={color}
|
||||||
|
stroke="#F15B5B"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11.1852 10H10.1481C10.0663 10 10 10.0663 10 10.1481V11.1852C10 11.267 10.0663 11.3333 10.1481 11.3333H11.1852C11.267 11.3333 11.3333 11.267 11.3333 11.1852V10.1481C11.3333 10.0663 11.267 10 11.1852 10Z"
|
||||||
|
fill={color}
|
||||||
|
stroke="#F15B5B"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
@ -66,6 +66,8 @@ export const SelectRepository: React.FC<Props> = ({
|
|||||||
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
|
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
|
if (userRepositories.length < 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -83,9 +83,7 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
|
|||||||
{projectIntegration ? (
|
{projectIntegration ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
className={`relative inline-flex h-4 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none bg-gray-700`}
|
||||||
slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
|
|
||||||
}`}
|
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked
|
aria-checked
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -94,8 +92,8 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
className={`self-center inline-block h-2 w-2 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
slackChannelAvailabilityToggle ? "translate-x-5" : "translate-x-0"
|
slackChannelAvailabilityToggle ? "translate-x-3" : "translate-x-0"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -17,7 +17,7 @@ import issuesService from "services/issues.service";
|
|||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { Component } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -132,7 +132,7 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${
|
className={`flex scroll-m-8 items-center gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-2 ${
|
||||||
labelForm ? "" : "hidden"
|
labelForm ? "" : "hidden"
|
||||||
}`}
|
}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -146,18 +146,12 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
|
|||||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<Component
|
||||||
className="h-5 w-5 rounded"
|
className="h-4 w-4 text-custom-text-100 flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: watch("color"),
|
color: watch("color"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ChevronDownIcon
|
|
||||||
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
|
|
||||||
open ? "text-gray-600" : "text-gray-400"
|
|
||||||
}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -13,12 +13,12 @@ import { CustomMenu } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
RectangleGroupIcon,
|
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Component, X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssueLabels } from "types";
|
import { ICurrentUserResponse, IIssueLabels } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -76,20 +76,18 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
as="div"
|
as="div"
|
||||||
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100"
|
className="rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-3 text-custom-text-100"
|
||||||
defaultOpen
|
defaultOpen
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex cursor-pointer items-center justify-between gap-2">
|
<div className="flex cursor-pointer items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>
|
<Component className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
|
||||||
<RectangleGroupIcon className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
<h6>{label.name}</h6>
|
<h6>{label.name}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CustomMenu ellipsis>
|
<CustomMenu ellipsis buttonClassName="!text-custom-sidebar-text-400">
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@ -112,7 +110,9 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<span>
|
<span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`h-4 w-4 text-custom-text-100 ${!open ? "rotate-90 transform" : ""}`}
|
className={`h-4 w-4 text-custom-sidebar-text-400 ${
|
||||||
|
!open ? "rotate-90 transform" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
@ -128,15 +128,15 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0"
|
leaveTo="transform opacity-0"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="mt-3 ml-6 space-y-3">
|
<div className="mt-2.5 ml-6">
|
||||||
{labelChildren.map((child) => (
|
{labelChildren.map((child) => (
|
||||||
<div
|
<div
|
||||||
key={child.id}
|
key={child.id}
|
||||||
className="group flex items-center justify-between rounded-md border border-custom-border-200 p-2 text-sm"
|
className="group flex items-center justify-between border-b border-custom-border-200 px-4 py-2.5 text-sm last:border-0"
|
||||||
>
|
>
|
||||||
<h5 className="flex items-center gap-3">
|
<h5 className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
child.color && child.color !== "" ? child.color : "#000000",
|
child.color && child.color !== "" ? child.color : "#000000",
|
||||||
@ -144,8 +144,15 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
{child.name}
|
{child.name}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||||
<CustomMenu ellipsis>
|
<div className="h-4 w-4">
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div className="h-4 w-4">
|
||||||
|
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
@ -158,14 +165,18 @@ export const SingleLabelGroup: React.FC<Props> = ({
|
|||||||
<span>Edit label</span>
|
<span>Edit label</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<TrashIcon className="h-4 w-4" />
|
|
||||||
<span>Delete label</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-start gap-2"
|
||||||
|
onClick={handleLabelDelete}
|
||||||
|
>
|
||||||
|
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,8 @@ import { CustomMenu } from "components/ui";
|
|||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabels } from "types";
|
||||||
//icons
|
//icons
|
||||||
import { RectangleGroupIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { RectangleGroupIcon, PencilIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { Component, X } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: IIssueLabels;
|
label: IIssueLabels;
|
||||||
@ -20,8 +21,8 @@ export const SingleLabel: React.FC<Props> = ({
|
|||||||
editLabel,
|
editLabel,
|
||||||
handleLabelDelete,
|
handleLabelDelete,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5">
|
<div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-2.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="group flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
@ -31,7 +32,15 @@ export const SingleLabel: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<h6 className="text-sm">{label.name}</h6>
|
<h6 className="text-sm">{label.name}</h6>
|
||||||
</div>
|
</div>
|
||||||
<CustomMenu ellipsis>
|
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||||
|
<div className="h-4 w-4">
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div className="h-4 w-4">
|
||||||
|
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<RectangleGroupIcon className="h-4 w-4" />
|
<RectangleGroupIcon className="h-4 w-4" />
|
||||||
@ -44,13 +53,15 @@ export const SingleLabel: React.FC<Props> = ({
|
|||||||
<span>Edit label</span>
|
<span>Edit label</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<TrashIcon className="h-4 w-4" />
|
|
||||||
<span>Delete label</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button className="flex items-center justify-start gap-2" onClick={handleLabelDelete}>
|
||||||
|
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
export * from "./create-project-modal";
|
export * from "./create-project-modal";
|
||||||
export * from "./delete-project-modal";
|
export * from "./delete-project-modal";
|
||||||
export * from "./sidebar-list";
|
export * from "./sidebar-list";
|
||||||
export * from "./settings-header";
|
export * from "./settings-sidebar";
|
||||||
export * from "./single-integration-card";
|
export * from "./single-integration-card";
|
||||||
export * from "./single-project-card";
|
export * from "./single-project-card";
|
||||||
export * from "./single-sidebar-project";
|
export * from "./single-sidebar-project";
|
||||||
export * from "./confirm-project-leave-modal";
|
export * from "./confirm-project-leave-modal";
|
||||||
|
export * from "./member-select";
|
||||||
|
74
web/components/project/member-select.tsx
Normal file
74
web/components/project/member-select.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// ui
|
||||||
|
import { Avatar, CustomSearchSelect } from "components/ui";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: any;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: members } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = members?.map((member) => ({
|
||||||
|
value: member.member.id,
|
||||||
|
query: member.member.display_name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.display_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedOption = members?.find((m) => m.member.id === value)?.member;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomSearchSelect
|
||||||
|
value={value}
|
||||||
|
label={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedOption && <Avatar user={selectedOption} />}
|
||||||
|
{selectedOption ? (
|
||||||
|
selectedOption?.display_name
|
||||||
|
) : (
|
||||||
|
<span className="text-sm py-0.5 text-custom-text-200">Select</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonClassName="!px-3 !py-2"
|
||||||
|
options={
|
||||||
|
options &&
|
||||||
|
options && [
|
||||||
|
...options,
|
||||||
|
{
|
||||||
|
value: "none",
|
||||||
|
query: "none",
|
||||||
|
content: <div className="flex items-center gap-2">None</div>,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
maxHeight="md"
|
||||||
|
position="right"
|
||||||
|
width="w-full"
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -63,7 +63,11 @@ export const PublishProjectModal: React.FC<Props> = observer(() => {
|
|||||||
const [isUnpublishing, setIsUnpublishing] = useState(false);
|
const [isUnpublishing, setIsUnpublishing] = useState(false);
|
||||||
const [isUpdateRequired, setIsUpdateRequired] = useState(false);
|
const [isUpdateRequired, setIsUpdateRequired] = useState(false);
|
||||||
|
|
||||||
const plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL ?? "http://localhost:4000";
|
let plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && !plane_deploy_url) {
|
||||||
|
plane_deploy_url= window.location.protocol + "//" + window.location.host + "/spaces";
|
||||||
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import SettingsNavbar from "layouts/settings-navbar";
|
|
||||||
|
|
||||||
export const SettingsHeader = () => (
|
|
||||||
<div className="mb-8 space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-semibold">Project Settings</h3>
|
|
||||||
<p className="mt-1 text-sm text-custom-text-200">
|
|
||||||
This information will be displayed to every member of the project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<SettingsNavbar />
|
|
||||||
</div>
|
|
||||||
);
|
|
72
web/components/project/settings-sidebar.tsx
Normal file
72
web/components/project/settings-sidebar.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const SettingsSidebar = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const projectLinks: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: "General",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Members",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Features",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "States",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Labels",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Integrations",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Estimates",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Automations",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-80 px-9">
|
||||||
|
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
|
||||||
|
<div className="flex flex-col gap-1 w-full">
|
||||||
|
{projectLinks.map((link) => (
|
||||||
|
<Link key={link.href} href={link.href}>
|
||||||
|
<a>
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||||
|
(
|
||||||
|
link.label === "Import"
|
||||||
|
? router.asPath.includes(link.href)
|
||||||
|
: router.asPath === link.href
|
||||||
|
)
|
||||||
|
? "bg-custom-primary-100/10 text-custom-primary-100"
|
||||||
|
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -30,8 +30,7 @@ const integrationDetails: { [key: string]: any } = {
|
|||||||
},
|
},
|
||||||
slack: {
|
slack: {
|
||||||
logo: SlackLogo,
|
logo: SlackLogo,
|
||||||
description:
|
description: "Get regular updates and control which notification you want to receive.",
|
||||||
"Connect your slack channel to this project to get regular updates. Control which notification you want to receive.",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,19 +92,19 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{integration && (
|
{integration && (
|
||||||
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5">
|
<div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="h-12 w-12 flex-shrink-0">
|
<div className="h-10 w-10 flex-shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={integrationDetails[integration.integration_detail.provider].logo}
|
src={integrationDetails[integration.integration_detail.provider].logo}
|
||||||
alt={`${integration.integration_detail.title} Logo`}
|
alt={`${integration.integration_detail.title} Logo`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="flex items-center gap-4 text-xl font-semibold">
|
<h3 className="flex items-center gap-4 text-sm font-medium">
|
||||||
{integration.integration_detail.title}
|
{integration.integration_detail.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
{integrationDetails[integration.integration_detail.provider].description}
|
{integrationDetails[integration.integration_detail.provider].description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,13 +9,10 @@ import stateService from "services/state.service";
|
|||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "components/ui";
|
import { Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { ArrowDownIcon, ArrowUpIcon } from "@heroicons/react/24/outline";
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
PencilSquareIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
import { Pencil, X } from "lucide-react";
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
import { groupBy, orderArrayBy } from "helpers/array.helper";
|
||||||
@ -160,15 +157,15 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center justify-between gap-2 border-custom-border-200 bg-custom-background-100 p-5 first:rounded-t-[10px] last:rounded-b-[10px]">
|
<div className="group flex items-center justify-between gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="20px" width="20px" />
|
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6>
|
<h6 className="text-sm font-medium">{addSpaceIfCamelCase(state.name)}</h6>
|
||||||
<p className="text-xs text-custom-text-200">{state.description}</p>
|
<p className="text-xs text-custom-text-200">{state.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="group flex items-center gap-2.5">
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -192,20 +189,25 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="hidden text-xs text-custom-text-200 group-hover:inline-block"
|
className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
|
||||||
onClick={handleMakeDefault}
|
onClick={handleMakeDefault}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Set as default
|
Mark as default
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<div className=" items-center gap-2.5 hidden group-hover:flex">
|
||||||
<button type="button" className="grid place-items-center" onClick={handleEditState}>
|
|
||||||
<PencilSquareIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${
|
className="grid place-items-center group-hover:opacity-100 opacity-0"
|
||||||
|
onClick={handleEditState}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`group-hover:opacity-100 opacity-0 ${
|
||||||
state.default || groupLength === 1 ? "cursor-not-allowed" : ""
|
state.default || groupLength === 1 ? "cursor-not-allowed" : ""
|
||||||
} grid place-items-center`}
|
} grid place-items-center`}
|
||||||
onClick={handleDeleteState}
|
onClick={handleDeleteState}
|
||||||
@ -213,17 +215,18 @@ export const SingleState: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{state.default ? (
|
{state.default ? (
|
||||||
<Tooltip tooltipContent="Cannot delete the default state.">
|
<Tooltip tooltipContent="Cannot delete the default state.">
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : groupLength === 1 ? (
|
) : groupLength === 1 ? (
|
||||||
<Tooltip tooltipContent="Cannot have an empty group.">
|
<Tooltip tooltipContent="Cannot have an empty group.">
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,8 +6,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
|
||||||
<div className="flex flex-col items-start gap-3">
|
<div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200">
|
||||||
<h3 className="text-2xl font-semibold">{bannerName}</h3>
|
<h3 className="text-xl font-medium">{bannerName}</h3>
|
||||||
{description && (
|
{description && (
|
||||||
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
|
||||||
<ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" />
|
<ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" />
|
||||||
|
@ -18,24 +18,24 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={`relative flex-shrink-0 inline-flex ${
|
className={`relative flex-shrink-0 inline-flex ${
|
||||||
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
|
size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
|
||||||
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
} flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
value ? "bg-green-500" : "bg-custom-background-80"
|
value ? "bg-custom-primary-100" : "bg-gray-700"
|
||||||
} ${className || ""}`}
|
} ${className || ""}`}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{label}</span>
|
<span className="sr-only">{label}</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`inline-block ${
|
className={`self-center inline-block ${
|
||||||
size === "sm" ? "h-2.5 w-2.5" : size === "md" ? "h-3 w-3" : "h-5 w-5"
|
size === "sm" ? "h-2 w-2" : size === "md" ? "h-3 w-3" : "h-4 w-4"
|
||||||
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
|
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
value
|
value
|
||||||
? (size === "sm"
|
? (size === "sm"
|
||||||
? "translate-x-2.5"
|
|
||||||
: size === "md"
|
|
||||||
? "translate-x-3"
|
? "translate-x-3"
|
||||||
|
: size === "md"
|
||||||
|
? "translate-x-4"
|
||||||
: "translate-x-5") + " bg-white"
|
: "translate-x-5") + " bg-white"
|
||||||
: "translate-x-0 bg-custom-background-90"
|
: "translate-x-1 bg-custom-background-90"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -16,3 +16,7 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) =>
|
|||||||
if (callNow) func(...args);
|
if (callNow) func(...args);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||||
|
? process.env.NEXT_PUBLIC_API_BASE_URL
|
||||||
|
: "";
|
||||||
|
@ -17,7 +17,11 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { NEXT_PUBLIC_DEPLOY_URL } = process.env;
|
const { NEXT_PUBLIC_DEPLOY_URL } = process.env;
|
||||||
const plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL ? NEXT_PUBLIC_DEPLOY_URL : "http://localhost:3001";
|
let plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && !plane_deploy_url) {
|
||||||
|
plane_deploy_url= window.location.protocol + "//" + window.location.host + "/spaces";
|
||||||
|
}
|
||||||
|
|
||||||
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => {
|
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => {
|
||||||
const { projectDetails } = useProjectDetails();
|
const { projectDetails } = useProjectDetails();
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import { convertCookieStringToObject } from "./cookie";
|
import { convertCookieStringToObject } from "./cookie";
|
||||||
// types
|
// types
|
||||||
import type { IProjectMember, IUser, IWorkspace, IWorkspaceMember } from "types";
|
import type { IProjectMember, IUser, IWorkspace, IWorkspaceMember } from "types";
|
||||||
|
// helper
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
export const requiredAuth = async (cookie?: string) => {
|
export const requiredAuth = async (cookie?: string) => {
|
||||||
const cookies = convertCookieStringToObject(cookie);
|
const cookies = convertCookieStringToObject(cookie);
|
||||||
@ -9,12 +11,10 @@ export const requiredAuth = async (cookie?: string) => {
|
|||||||
|
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so";
|
|
||||||
|
|
||||||
let user: IUser | null = null;
|
let user: IUser | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetch(`${baseUrl}/api/users/me/`, {
|
const data = await fetch(`${API_BASE_URL}/api/users/me/`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -41,13 +41,11 @@ export const requiredAdmin = async (workspaceSlug: string, projectId: string, co
|
|||||||
const cookies = convertCookieStringToObject(cookie);
|
const cookies = convertCookieStringToObject(cookie);
|
||||||
const token = cookies?.accessToken;
|
const token = cookies?.accessToken;
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so";
|
|
||||||
|
|
||||||
let memberDetail: IProjectMember | null = null;
|
let memberDetail: IProjectMember | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetch(
|
const data = await fetch(
|
||||||
`${baseUrl}/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`,
|
`${API_BASE_URL}/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@ -75,17 +73,18 @@ export const requiredWorkspaceAdmin = async (workspaceSlug: string, cookie?: str
|
|||||||
const cookies = convertCookieStringToObject(cookie);
|
const cookies = convertCookieStringToObject(cookie);
|
||||||
const token = cookies?.accessToken;
|
const token = cookies?.accessToken;
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so";
|
|
||||||
|
|
||||||
let memberDetail: IWorkspaceMember | null = null;
|
let memberDetail: IWorkspaceMember | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetch(`${baseUrl}/api/workspaces/${workspaceSlug}/workspace-members/me/`, {
|
const data = await fetch(
|
||||||
|
`${API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-members/me/`,
|
||||||
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => data);
|
.then((data) => data);
|
||||||
|
|
||||||
@ -119,13 +118,11 @@ export const homePageRedirect = async (cookie?: string) => {
|
|||||||
|
|
||||||
let workspaces: IWorkspace[] = [];
|
let workspaces: IWorkspace[] = [];
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so";
|
|
||||||
|
|
||||||
const cookies = convertCookieStringToObject(cookie);
|
const cookies = convertCookieStringToObject(cookie);
|
||||||
const token = cookies?.accessToken;
|
const token = cookies?.accessToken;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetch(`${baseUrl}/api/users/me/workspaces/`, {
|
const data = await fetch(`${API_BASE_URL}/api/users/me/workspaces/`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -166,7 +163,7 @@ export const homePageRedirect = async (cookie?: string) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitations = await fetch(`${baseUrl}/api/users/me/invitations/workspaces/`, {
|
const invitations = await fetch(`${API_BASE_URL}/api/users/me/invitations/workspaces/`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lowlight": "^2.9.0",
|
"lowlight": "^2.9.0",
|
||||||
"lucide-react": "^0.263.1",
|
"lucide-react": "^0.269.0",
|
||||||
"mobx": "^6.10.0",
|
"mobx": "^6.10.0",
|
||||||
"mobx-react-lite": "^4.0.3",
|
"mobx-react-lite": "^4.0.3",
|
||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
|
@ -13,8 +13,8 @@ import useUserAuth from "hooks/use-user-auth";
|
|||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { SettingsHeader } from "components/project";
|
|
||||||
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
|
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
|
||||||
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// types
|
// types
|
||||||
@ -75,11 +75,16 @@ const AutomationsSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="space-y-5">
|
<SettingsSidebar />
|
||||||
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} />
|
</div>
|
||||||
|
<section className="pr-9 py-8 w-full">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">Automations</h3>
|
||||||
|
</div>
|
||||||
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} />
|
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} />
|
||||||
|
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
|
@ -1,241 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
// layouts
|
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
|
||||||
// services
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
|
||||||
// components
|
|
||||||
import { SettingsHeader } from "components/project";
|
|
||||||
// ui
|
|
||||||
import { CustomSelect, Loader, SecondaryButton } from "components/ui";
|
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|
||||||
// types
|
|
||||||
import { IProject, IUserLite, IWorkspace } from "types";
|
|
||||||
import type { NextPage } from "next";
|
|
||||||
// fetch-keys
|
|
||||||
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
|
||||||
// helper
|
|
||||||
import { truncateText } from "helpers/string.helper";
|
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
|
||||||
project_lead: null,
|
|
||||||
default_assignee: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ControlSettings: NextPage = () => {
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR<IProject>(
|
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: people } = useSWR(
|
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
control,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm<IProject>({ defaultValues });
|
|
||||||
|
|
||||||
const onSubmit = async (formData: IProject) => {
|
|
||||||
if (!workspaceSlug || !projectId || !projectDetails) return;
|
|
||||||
|
|
||||||
const payload: Partial<IProject> = {
|
|
||||||
default_assignee: formData.default_assignee,
|
|
||||||
project_lead: formData.project_lead,
|
|
||||||
};
|
|
||||||
|
|
||||||
await projectService
|
|
||||||
.updateProject(workspaceSlug as string, projectId as string, payload, user)
|
|
||||||
.then((res) => {
|
|
||||||
mutate(PROJECT_DETAILS(projectId as string));
|
|
||||||
|
|
||||||
mutate(
|
|
||||||
PROJECTS_LIST(workspaceSlug as string, {
|
|
||||||
is_favorite: "all",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
title: "Success",
|
|
||||||
type: "success",
|
|
||||||
message: "Project updated successfully",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectDetails)
|
|
||||||
reset({
|
|
||||||
...projectDetails,
|
|
||||||
default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee,
|
|
||||||
project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead,
|
|
||||||
workspace: (projectDetails.workspace as IWorkspace).id,
|
|
||||||
});
|
|
||||||
}, [projectDetails, reset]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProjectAuthorizationWrapper
|
|
||||||
breadcrumbs={
|
|
||||||
<Breadcrumbs>
|
|
||||||
<BreadcrumbItem
|
|
||||||
title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
|
|
||||||
link={`/${workspaceSlug}/projects/${projectId}/issues`}
|
|
||||||
linkTruncate
|
|
||||||
/>
|
|
||||||
<BreadcrumbItem title="Control Settings" unshrinkTitle />
|
|
||||||
</Breadcrumbs>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
|
|
||||||
<SettingsHeader />
|
|
||||||
<div className="space-y-8 sm:space-y-12">
|
|
||||||
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Project Lead</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">Select the project leader.</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
{projectDetails ? (
|
|
||||||
<Controller
|
|
||||||
name="project_lead"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomSelect
|
|
||||||
{...field}
|
|
||||||
label={
|
|
||||||
people?.find((person) => person.member.id === field.value)?.member
|
|
||||||
.display_name ?? <span className="text-custom-text-200">Select lead</span>
|
|
||||||
}
|
|
||||||
width="w-full"
|
|
||||||
input
|
|
||||||
>
|
|
||||||
{people?.map((person) => (
|
|
||||||
<CustomSelect.Option
|
|
||||||
key={person.member.id}
|
|
||||||
value={person.member.id}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{person.member.avatar && person.member.avatar !== "" ? (
|
|
||||||
<div className="relative h-4 w-4">
|
|
||||||
<img
|
|
||||||
src={person.member.avatar}
|
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
|
|
||||||
alt="User Avatar"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
|
|
||||||
{person.member.display_name?.charAt(0)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{person.member.display_name}
|
|
||||||
</div>
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader className="h-9 w-full">
|
|
||||||
<Loader.Item width="100%" height="100%" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Default Assignee</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
Select the default assignee for the project.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
{projectDetails ? (
|
|
||||||
<Controller
|
|
||||||
name="default_assignee"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CustomSelect
|
|
||||||
{...field}
|
|
||||||
label={
|
|
||||||
people?.find((p) => p.member.id === field.value)?.member.display_name ?? (
|
|
||||||
<span className="text-custom-text-200">Select default assignee</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
width="w-full"
|
|
||||||
input
|
|
||||||
>
|
|
||||||
{people?.map((person) => (
|
|
||||||
<CustomSelect.Option
|
|
||||||
key={person.member.id}
|
|
||||||
value={person.member.id}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{person.member.avatar && person.member.avatar !== "" ? (
|
|
||||||
<div className="relative h-4 w-4">
|
|
||||||
<img
|
|
||||||
src={person.member.avatar}
|
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
|
|
||||||
alt="User Avatar"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
|
|
||||||
{person.member.display_name?.charAt(0)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{person.member.display_name}
|
|
||||||
</div>
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader className="h-9 w-full">
|
|
||||||
<Loader.Item width="100%" height="100%" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="sm:text-right">
|
|
||||||
<SecondaryButton type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
|
||||||
</SecondaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ProjectAuthorizationWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ControlSettings;
|
|
@ -13,12 +13,12 @@ import useProjectDetails from "hooks/use-project-details";
|
|||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates";
|
import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates";
|
||||||
import { SettingsHeader } from "components/project";
|
import { SettingsSidebar } from "components/project";
|
||||||
//hooks
|
//hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Loader, SecondaryButton } from "components/ui";
|
import { EmptyState, Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
@ -125,22 +125,23 @@ const EstimatesSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="h-full flex flex-col p-8 overflow-hidden">
|
<div className="flex flex-row gap-2 h-full overflow-hidden">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="flex items-center justify-between">
|
<SettingsSidebar />
|
||||||
<h3 className="text-2xl font-semibold">Estimates</h3>
|
</div>
|
||||||
|
<div className="pr-9 py-8 flex flex-col w-full">
|
||||||
|
<section className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">Estimates</h3>
|
||||||
<div className="col-span-12 space-y-5 sm:col-span-7">
|
<div className="col-span-12 space-y-5 sm:col-span-7">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<PrimaryButton
|
||||||
className="flex cursor-pointer items-center gap-2 text-custom-primary-100 hover:text-custom-primary-200"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEstimateToUpdate(undefined);
|
setEstimateToUpdate(undefined);
|
||||||
setEstimateFormOpen(true);
|
setEstimateFormOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
Add Estimate
|
||||||
Create New Estimate
|
</PrimaryButton>
|
||||||
</div>
|
|
||||||
{projectDetails?.estimate && (
|
{projectDetails?.estimate && (
|
||||||
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
|
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
|
||||||
)}
|
)}
|
||||||
@ -149,7 +150,7 @@ const EstimatesSettings: NextPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
{estimatesList ? (
|
{estimatesList ? (
|
||||||
estimatesList.length > 0 ? (
|
estimatesList.length > 0 ? (
|
||||||
<section className="h-full mt-5 divide-y divide-custom-border-200 rounded-xl border border-custom-border-200 bg-custom-background-100 px-6 overflow-y-auto">
|
<section className="h-full bg-custom-background-100 overflow-y-auto">
|
||||||
{estimatesList.map((estimate) => (
|
{estimatesList.map((estimate) => (
|
||||||
<SingleEstimate
|
<SingleEstimate
|
||||||
key={estimate.id}
|
key={estimate.id}
|
||||||
@ -186,6 +187,7 @@ const EstimatesSettings: NextPage = () => {
|
|||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -13,13 +13,13 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// components
|
// components
|
||||||
import { SettingsHeader } from "components/project";
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { SecondaryButton, ToggleSwitch } from "components/ui";
|
import { ToggleSwitch } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { ContrastIcon, PeopleGroupIcon, ViewListIcon, InboxIcon } from "components/icons";
|
import { ModuleIcon } from "components/icons";
|
||||||
import { DocumentTextIcon } from "@heroicons/react/24/outline";
|
import { Contrast, FileText, Inbox, Layers } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
@ -33,35 +33,35 @@ const featuresList = [
|
|||||||
title: "Cycles",
|
title: "Cycles",
|
||||||
description:
|
description:
|
||||||
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
|
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
|
||||||
icon: <ContrastIcon color="#3f76ff" width={28} height={28} className="flex-shrink-0" />,
|
icon: <Contrast className="h-4 w-4 text-custom-primary-100 flex-shrink-0" />,
|
||||||
property: "cycle_view",
|
property: "cycle_view",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Modules",
|
title: "Modules",
|
||||||
description:
|
description:
|
||||||
"Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
|
"Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
|
||||||
icon: <PeopleGroupIcon color="#ff6b00" width={28} height={28} className="flex-shrink-0" />,
|
icon: <ModuleIcon width={16} height={16} className="flex-shrink-0" />,
|
||||||
property: "module_view",
|
property: "module_view",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Views",
|
title: "Views",
|
||||||
description:
|
description:
|
||||||
"Views are enabled for all the projects in this workspace. Access it from the sidebar.",
|
"Views are enabled for all the projects in this workspace. Access it from the sidebar.",
|
||||||
icon: <ViewListIcon color="#05c3ff" width={28} height={28} className="flex-shrink-0" />,
|
icon: <Layers className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
|
||||||
property: "issue_views_view",
|
property: "issue_views_view",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pages",
|
title: "Pages",
|
||||||
description:
|
description:
|
||||||
"Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
|
"Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
|
||||||
icon: <DocumentTextIcon color="#fcbe1d" width={28} height={28} className="flex-shrink-0" />,
|
icon: <FileText className="h-4 w-4 text-red-400 flex-shrink-0" />,
|
||||||
property: "page_view",
|
property: "page_view",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Inbox",
|
title: "Inbox",
|
||||||
description:
|
description:
|
||||||
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
|
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
|
||||||
icon: <InboxIcon color="#fcbe1d" width={24} height={24} className="flex-shrink-0" />,
|
icon: <Inbox className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
|
||||||
property: "inbox_view",
|
property: "inbox_view",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -149,21 +149,29 @@ const FeaturesSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="space-y-5">
|
<SettingsSidebar />
|
||||||
<h3 className="text-2xl font-semibold">Features</h3>
|
</div>
|
||||||
<div className="space-y-5">
|
<section className="pr-9 py-8 w-full">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">Features</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
{featuresList.map((feature) => (
|
{featuresList.map((feature) => (
|
||||||
<div
|
<div
|
||||||
key={feature.property}
|
key={feature.property}
|
||||||
className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5"
|
className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-200 bg-custom-background-100 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
{feature.icon}
|
{feature.icon}
|
||||||
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<h4 className="text-lg font-semibold">{feature.title}</h4>
|
<h4 className="text-sm font-medium">{feature.title}</h4>
|
||||||
<p className="text-sm text-custom-text-200">{feature.description}</p>
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@ -187,29 +195,11 @@ const FeaturesSettings: NextPage = () => {
|
|||||||
[feature.property]: !projectDetails?.[feature.property as keyof IProject],
|
[feature.property]: !projectDetails?.[feature.property as keyof IProject],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<a
|
|
||||||
href="https://plane.so/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="hover:text-custom-text-100"
|
|
||||||
>
|
|
||||||
<SecondaryButton outline>Plane is open-source, view Roadmap</SecondaryButton>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/makeplane/plane"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="hover:text-custom-text-100"
|
|
||||||
>
|
|
||||||
<SecondaryButton outline>Star us on GitHub</SecondaryButton>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
|
@ -4,6 +4,8 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// layouts
|
// layouts
|
||||||
@ -11,7 +13,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
|||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// components
|
// components
|
||||||
import { DeleteProjectModal, SettingsHeader } from "components/project";
|
import { DeleteProjectModal, SettingsSidebar } from "components/project";
|
||||||
import { ImagePickerPopover } from "components/core";
|
import { ImagePickerPopover } from "components/core";
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
// hooks
|
// hooks
|
||||||
@ -25,11 +27,14 @@ import {
|
|||||||
CustomSelect,
|
CustomSelect,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
DangerButton,
|
DangerButton,
|
||||||
|
Icon,
|
||||||
|
PrimaryButton,
|
||||||
} from "components/ui";
|
} from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IProject, IWorkspace } from "types";
|
import { IProject, IWorkspace } from "types";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
@ -185,17 +190,21 @@ const GeneralSettings: NextPage = () => {
|
|||||||
onClose={() => setSelectedProject(null)}
|
onClose={() => setSelectedProject(null)}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<SettingsHeader />
|
<div className="flex flex-row gap-2">
|
||||||
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}>
|
<div className="w-80 py-8">
|
||||||
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
<SettingsSidebar />
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Icon & Name</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
Select an icon and a name for your project.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 flex gap-2 sm:col-span-6">
|
<div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
|
<div className="relative h-44 w-full mt-6">
|
||||||
|
<img
|
||||||
|
src={watch("cover_image")!}
|
||||||
|
alt={watch("cover_image")!}
|
||||||
|
className="h-44 w-full rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex items-end justify-between absolute bottom-4 w-full px-4">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex items-center justify-center bg-custom-background-90 h-[52px] w-[52px] rounded-lg">
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<div className="h-7 w-7 grid place-items-center">
|
<div className="h-7 w-7 grid place-items-center">
|
||||||
<Controller
|
<Controller
|
||||||
@ -216,66 +225,24 @@ const GeneralSettings: NextPage = () => {
|
|||||||
<Loader.Item height="46px" width="46px" />
|
<Loader.Item height="46px" width="46px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 text-white">
|
||||||
|
<span className="text-lg font-semibold">{watch("name")}</span>
|
||||||
|
<span className="flex items-center gap-2 text-sm">
|
||||||
|
<span>
|
||||||
|
{watch("identifier")} . {currentNetwork?.label}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<Input
|
<div>
|
||||||
id="name"
|
<Controller
|
||||||
name="name"
|
control={control}
|
||||||
error={errors.name}
|
name="cover_image"
|
||||||
register={register}
|
render={({ field: { value, onChange } }) => (
|
||||||
placeholder="Project Name"
|
|
||||||
validations={{
|
|
||||||
required: "Name is required",
|
|
||||||
}}
|
|
||||||
disabled={!isAdmin}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader>
|
|
||||||
<Loader.Item height="46px" width="225px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Description</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">Give a description to your project.</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
{projectDetails ? (
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
error={errors.description}
|
|
||||||
register={register}
|
|
||||||
placeholder="Enter project description"
|
|
||||||
validations={{}}
|
|
||||||
className="min-h-[46px] text-sm"
|
|
||||||
disabled={!isAdmin}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader className="w-full">
|
|
||||||
<Loader.Item height="46px" width="full" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Cover Photo</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
Select your cover photo from the given library.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
{watch("cover_image") ? (
|
|
||||||
<div className="h-32 w-full rounded border border-custom-border-200 p-1">
|
|
||||||
<div className="relative h-full w-full rounded">
|
|
||||||
<img
|
|
||||||
src={watch("cover_image")!}
|
|
||||||
className="absolute top-0 left-0 h-full w-full object-cover rounded"
|
|
||||||
alt={projectDetails?.name ?? "Cover image"}
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-0 flex w-full justify-end">
|
|
||||||
<ImagePickerPopover
|
<ImagePickerPopover
|
||||||
label={"Change cover"}
|
label={"Change cover"}
|
||||||
onChange={(imageUrl) => {
|
onChange={(imageUrl) => {
|
||||||
@ -284,24 +251,64 @@ const GeneralSettings: NextPage = () => {
|
|||||||
value={watch("cover_image")}
|
value={watch("cover_image")}
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="w-full">
|
<Loader>
|
||||||
<Loader.Item height="46px" width="full" />
|
<Loader.Item height="32px" width="108px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Identifier</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
Create a 1-6 characters{"'"} identifier for the project.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
|
<div className="flex flex-col gap-8 my-8">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Project Name</h4>
|
||||||
|
{projectDetails ? (
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
className="!p-3 rounded-md font-medium"
|
||||||
|
placeholder="Project Name"
|
||||||
|
validations={{
|
||||||
|
required: "Name is required",
|
||||||
|
}}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader>
|
||||||
|
<Loader.Item height="46px" width="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm">Description</h4>
|
||||||
|
{projectDetails ? (
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
error={errors.description}
|
||||||
|
register={register}
|
||||||
|
placeholder="Enter project description"
|
||||||
|
validations={{}}
|
||||||
|
className="min-h-[102px] text-sm"
|
||||||
|
disabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader className="w-full">
|
||||||
|
<Loader.Item height="102px" width="full" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-10 w-full">
|
||||||
|
<div className="flex flex-col gap-1 w-1/2">
|
||||||
|
<h4 className="text-sm">Identifier</h4>
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<Input
|
<Input
|
||||||
id="identifier"
|
id="identifier"
|
||||||
@ -313,7 +320,8 @@ const GeneralSettings: NextPage = () => {
|
|||||||
validations={{
|
validations={{
|
||||||
required: "Identifier is required",
|
required: "Identifier is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
|
/^[A-Z0-9]+$/.test(value.toUpperCase()) ||
|
||||||
|
"Identifier must be in uppercase.",
|
||||||
minLength: {
|
minLength: {
|
||||||
value: 1,
|
value: 1,
|
||||||
message: "Identifier must at least be of 1 character",
|
message: "Identifier must at least be of 1 character",
|
||||||
@ -327,17 +335,13 @@ const GeneralSettings: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loader>
|
<Loader>
|
||||||
<Loader.Item height="46px" width="160px" />
|
<Loader.Item height="36px" width="100%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
<div className="flex flex-col gap-1 w-1/2">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<h4 className="text-sm">Network</h4>
|
||||||
<h4 className="text-lg font-semibold">Network</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">Select privacy type for the project.</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="network"
|
name="network"
|
||||||
@ -347,6 +351,7 @@ const GeneralSettings: NextPage = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label={currentNetwork?.label ?? "Select network"}
|
label={currentNetwork?.label ?? "Select network"}
|
||||||
|
className="!border-custom-border-200 !shadow-none"
|
||||||
input
|
input
|
||||||
disabled={!isAdmin}
|
disabled={!isAdmin}
|
||||||
>
|
>
|
||||||
@ -360,54 +365,83 @@ const GeneralSettings: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="w-full">
|
<Loader className="w-full">
|
||||||
<Loader.Item height="46px" width="160px" />
|
<Loader.Item height="46px" width="100%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && (
|
<div className="flex items-center justify-between py-2">
|
||||||
<>
|
|
||||||
<div className="sm:text-right">
|
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<SecondaryButton type="submit" loading={isSubmitting} disabled={!isAdmin}>
|
<>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting} disabled={!isAdmin}>
|
||||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||||
</SecondaryButton>
|
</PrimaryButton>
|
||||||
|
<span className="text-sm text-custom-sidebar-text-400 italic">
|
||||||
|
Created on {renderShortDateWithYearFormat(projectDetails?.created_at)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="mt-2 w-full">
|
<Loader className="mt-2 w-full">
|
||||||
<Loader.Item height="34px" width="100px" />
|
<Loader.Item height="34px" width="100px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<h4 className="text-lg font-semibold">Danger Zone</h4>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
The danger zone of the project delete page is a critical area that requires
|
|
||||||
careful consideration and attention. When deleting a project, all of the data
|
|
||||||
and resources within that project will be permanently removed and cannot be
|
|
||||||
recovered.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
|
<Disclosure as="div" className="border-t border-custom-border-400">
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="w-full">
|
||||||
|
<Disclosure.Button
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-between w-full py-4"
|
||||||
|
>
|
||||||
|
<span className="text-xl tracking-tight">Danger Zone</span>
|
||||||
|
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" />
|
||||||
|
</Disclosure.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform opacity-0"
|
||||||
|
enterTo="transform opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform opacity-100"
|
||||||
|
leaveTo="transform opacity-0"
|
||||||
|
>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<span className="text-sm tracking-tight">
|
||||||
|
The danger zone of the project delete page is a critical area that
|
||||||
|
requires careful consideration and attention. When deleting a project, all
|
||||||
|
of the data and resources within that project will be permanently removed
|
||||||
|
and cannot be recovered.
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<div>
|
<div>
|
||||||
<DangerButton
|
<DangerButton
|
||||||
onClick={() => setSelectedProject(projectDetails.id ?? null)}
|
onClick={() => setSelectedProject(projectDetails.id ?? null)}
|
||||||
|
className="!text-sm"
|
||||||
outline
|
outline
|
||||||
>
|
>
|
||||||
Delete Project
|
Delete my project
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="mt-2 w-full">
|
<Loader className="mt-2 w-full">
|
||||||
<Loader.Item height="46px" width="100px" />
|
<Loader.Item height="38px" width="144px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</Disclosure.Panel>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
|
@ -10,7 +10,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
|||||||
import IntegrationService from "services/integration";
|
import IntegrationService from "services/integration";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
// components
|
// components
|
||||||
import { SettingsHeader, SingleIntegration } from "components/project";
|
import { SettingsSidebar, SingleIntegration } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, IntegrationAndImportExportBanner, Loader } from "components/ui";
|
import { EmptyState, IntegrationAndImportExportBanner, Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
@ -58,13 +58,15 @@ const ProjectIntegrations: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="h-full flex flex-col p-8 overflow-hidden">
|
<div className="flex flex-row gap-2 h-full overflow-hidden">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
|
<SettingsSidebar />
|
||||||
|
</div>
|
||||||
{workspaceIntegrations ? (
|
{workspaceIntegrations ? (
|
||||||
workspaceIntegrations.length > 0 ? (
|
workspaceIntegrations.length > 0 ? (
|
||||||
<section className="space-y-8 overflow-y-auto">
|
<section className="pr-9 py-8 overflow-y-auto w-full">
|
||||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||||
<div className="space-y-5">
|
<div>
|
||||||
{workspaceIntegrations.map((integration) => (
|
{workspaceIntegrations.map((integration) => (
|
||||||
<SingleIntegration
|
<SingleIntegration
|
||||||
key={integration.integration_detail.id}
|
key={integration.integration_detail.id}
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
SingleLabel,
|
SingleLabel,
|
||||||
SingleLabelGroup,
|
SingleLabelGroup,
|
||||||
} from "components/labels";
|
} from "components/labels";
|
||||||
import { SettingsHeader } from "components/project";
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Loader, PrimaryButton } from "components/ui";
|
import { EmptyState, Loader, PrimaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
@ -113,20 +113,23 @@ const LabelsSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="grid grid-cols-12 gap-10">
|
<SettingsSidebar />
|
||||||
<div className="col-span-12 sm:col-span-5">
|
</div>
|
||||||
<h3 className="text-2xl font-semibold">Labels</h3>
|
<section className="pr-9 py-8 gap-10 w-full">
|
||||||
<p className="text-custom-text-200">Manage the labels of this project.</p>
|
<div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
|
||||||
<PrimaryButton onClick={newLabel} size="sm" className="mt-4">
|
<h3 className="text-xl font-medium">Labels</h3>
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PrimaryButton
|
||||||
New label
|
onClick={newLabel}
|
||||||
</span>
|
size="sm"
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
Add label
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 space-y-5 sm:col-span-7">
|
<div className="space-y-3 py-6">
|
||||||
{labelForm && (
|
{labelForm && (
|
||||||
<CreateUpdateLabelInline
|
<CreateUpdateLabelInline
|
||||||
labelForm={labelForm}
|
labelForm={labelForm}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
@ -13,22 +13,35 @@ import useToast from "hooks/use-toast";
|
|||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
import useProjectMembers from "hooks/use-project-members";
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||||
// components
|
// components
|
||||||
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
|
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
|
||||||
import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
|
import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
|
||||||
import { SettingsHeader } from "components/project";
|
import { MemberSelect, SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, CustomSelect, Loader } from "components/ui";
|
import {
|
||||||
|
CustomMenu,
|
||||||
|
CustomSearchSelect,
|
||||||
|
CustomSelect,
|
||||||
|
Icon,
|
||||||
|
Loader,
|
||||||
|
PrimaryButton,
|
||||||
|
SecondaryButton,
|
||||||
|
} from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import { IProject, IUserLite, IWorkspace } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import {
|
||||||
|
PROJECTS_LIST,
|
||||||
|
PROJECT_DETAILS,
|
||||||
PROJECT_INVITATIONS_WITH_EMAIL,
|
PROJECT_INVITATIONS_WITH_EMAIL,
|
||||||
|
PROJECT_MEMBERS,
|
||||||
PROJECT_MEMBERS_WITH_EMAIL,
|
PROJECT_MEMBERS_WITH_EMAIL,
|
||||||
WORKSPACE_DETAILS,
|
WORKSPACE_DETAILS,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
@ -37,6 +50,11 @@ import { ROLE } from "constants/workspace";
|
|||||||
// helper
|
// helper
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IProject> = {
|
||||||
|
project_lead: null,
|
||||||
|
default_assignee: null,
|
||||||
|
};
|
||||||
|
|
||||||
const MembersSettings: NextPage = () => {
|
const MembersSettings: NextPage = () => {
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
const [inviteModal, setInviteModal] = useState(false);
|
||||||
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
|
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
|
||||||
@ -55,11 +73,25 @@ const MembersSettings: NextPage = () => {
|
|||||||
Boolean(workspaceSlug && projectId)
|
Boolean(workspaceSlug && projectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useForm<IProject>({ defaultValues });
|
||||||
|
|
||||||
const { data: activeWorkspace } = useSWR(
|
const { data: activeWorkspace } = useSWR(
|
||||||
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
||||||
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: people } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const { data: projectMembers, mutate: mutateMembers } = useSWR(
|
const { data: projectMembers, mutate: mutateMembers } = useSWR(
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
|
? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
|
||||||
@ -110,6 +142,76 @@ const MembersSettings: NextPage = () => {
|
|||||||
|
|
||||||
const handleProjectInvitationSuccess = () => {};
|
const handleProjectInvitationSuccess = () => {};
|
||||||
|
|
||||||
|
const onSubmit = async (formData: IProject) => {
|
||||||
|
if (!workspaceSlug || !projectId || !projectDetails) return;
|
||||||
|
|
||||||
|
const payload: Partial<IProject> = {
|
||||||
|
default_assignee: formData.default_assignee,
|
||||||
|
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectService
|
||||||
|
.updateProject(workspaceSlug as string, projectId as string, payload, user)
|
||||||
|
.then((res) => {
|
||||||
|
mutate(PROJECT_DETAILS(projectId as string));
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, {
|
||||||
|
is_favorite: "all",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
type: "success",
|
||||||
|
message: "Project updated successfully",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectDetails)
|
||||||
|
reset({
|
||||||
|
...projectDetails,
|
||||||
|
default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee,
|
||||||
|
project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead,
|
||||||
|
workspace: (projectDetails.workspace as IWorkspace).id,
|
||||||
|
});
|
||||||
|
}, [projectDetails, reset]);
|
||||||
|
|
||||||
|
const submitChanges = async (formData: Partial<IProject>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const payload: Partial<IProject> = {
|
||||||
|
default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee,
|
||||||
|
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectService
|
||||||
|
.updateProject(workspaceSlug as string, projectId as string, payload, user)
|
||||||
|
.then((res) => {
|
||||||
|
mutate(PROJECT_DETAILS(projectId as string));
|
||||||
|
|
||||||
|
mutate(
|
||||||
|
PROJECTS_LIST(workspaceSlug as string, {
|
||||||
|
is_favorite: "all",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
type: "success",
|
||||||
|
message: "Project updated successfully",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectAuthorizationWrapper
|
<ProjectAuthorizationWrapper
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
@ -171,19 +273,69 @@ const MembersSettings: NextPage = () => {
|
|||||||
user={user}
|
user={user}
|
||||||
onSuccess={() => mutateMembers()}
|
onSuccess={() => mutateMembers()}
|
||||||
/>
|
/>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<section className="space-y-5">
|
<SettingsSidebar />
|
||||||
<div className="flex items-end justify-between gap-4">
|
</div>
|
||||||
<h3 className="text-2xl font-semibold">Members</h3>
|
<section className="pr-9 py-8 w-full">
|
||||||
<button
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
type="button"
|
<h3 className="text-xl font-medium">Defaults</h3>
|
||||||
className="flex items-center gap-2 text-custom-primary outline-none"
|
</div>
|
||||||
onClick={() => setInviteModal(true)}
|
<div className="flex flex-col gap-2 pb-4 w-full">
|
||||||
>
|
<div className="flex items-center py-8 gap-4 w-full">
|
||||||
<PlusIcon className="h-4 w-4" />
|
<div className="flex flex-col gap-2 w-1/2">
|
||||||
Add Member
|
<h4 className="text-sm">Project Lead</h4>
|
||||||
</button>
|
<div className="">
|
||||||
|
{projectDetails ? (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="project_lead"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<MemberSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
submitChanges({ project_lead: val });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader className="h-9 w-full">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 w-1/2">
|
||||||
|
<h4 className="text-sm">Default Assignee</h4>
|
||||||
|
<div className="">
|
||||||
|
{projectDetails ? (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_assignee"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<MemberSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
submitChanges({ default_assignee: val });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader className="h-9 w-full">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
|
||||||
|
<h4 className="text-xl font-medium border-b border-custom-border-100">Members</h4>
|
||||||
|
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
{!projectMembers || !projectInvitations ? (
|
{!projectMembers || !projectInvitations ? (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-5">
|
||||||
@ -193,10 +345,13 @@ const MembersSettings: NextPage = () => {
|
|||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 px-6">
|
<div className="divide-y divide-custom-border-200">
|
||||||
{members.length > 0
|
{members.length > 0
|
||||||
? members.map((member) => (
|
? members.map((member) => (
|
||||||
<div key={member.id} className="flex items-center justify-between py-6">
|
<div
|
||||||
|
key={member.id}
|
||||||
|
className="flex items-center justify-between px-3.5 py-[18px]"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-x-6 gap-y-2">
|
<div className="flex items-center gap-x-6 gap-y-2">
|
||||||
{member.avatar && member.avatar !== "" ? (
|
{member.avatar && member.avatar !== "" ? (
|
||||||
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
|
||||||
@ -242,7 +397,20 @@ const MembersSettings: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
label={ROLE[member.role as keyof typeof ROLE]}
|
customButton={
|
||||||
|
<button className="flex item-center gap-1">
|
||||||
|
<span
|
||||||
|
className={`flex items-center text-sm font-medium ${
|
||||||
|
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{ROLE[member.role as keyof typeof ROLE]}
|
||||||
|
</span>
|
||||||
|
{member.memberId !== user?.id && (
|
||||||
|
<Icon iconName="expand_more" className="text-lg font-medium" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
value={member.role}
|
value={member.role}
|
||||||
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
|
||||||
if (!activeWorkspace || !projectDetails) return;
|
if (!activeWorkspace || !projectDetails) return;
|
||||||
@ -306,7 +474,11 @@ const MembersSettings: NextPage = () => {
|
|||||||
>
|
>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
<span>Remove member</span>
|
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
{member.memberId !== user?.id ? "Remove member" : "Leave project"}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
SingleState,
|
SingleState,
|
||||||
StateGroup,
|
StateGroup,
|
||||||
} from "components/states";
|
} from "components/states";
|
||||||
import { SettingsHeader } from "components/project";
|
import { SettingsSidebar } from "components/project";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
@ -73,31 +73,33 @@ const StatesSettings: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
<div className="flex flex-row gap-2">
|
||||||
<SettingsHeader />
|
<div className="w-80 py-8">
|
||||||
<div className="grid grid-cols-12 gap-10">
|
<SettingsSidebar />
|
||||||
<div className="col-span-12 sm:col-span-5">
|
|
||||||
<h3 className="text-2xl font-semibold text-custom-text-100">States</h3>
|
|
||||||
<p className="text-custom-text-200">Manage the states of this project.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 space-y-8 sm:col-span-7">
|
<div className="pr-9 py-8 gap-10 w-full">
|
||||||
|
<div className="flex items-center py-3.5 border-b border-custom-border-200">
|
||||||
|
<h3 className="text-xl font-medium">States</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8 py-6">
|
||||||
{states && projectDetails && orderedStateGroups ? (
|
{states && projectDetails && orderedStateGroups ? (
|
||||||
Object.keys(orderedStateGroups).map((key) => {
|
Object.keys(orderedStateGroups).map((key) => {
|
||||||
if (orderedStateGroups[key].length !== 0)
|
if (orderedStateGroups[key].length !== 0)
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<div className="mb-2 flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<h4 className="text-custom-text-200 capitalize">{key}</h4>
|
<h4 className="text-base font-medium text-custom-text-200 capitalize">
|
||||||
|
{key}
|
||||||
|
</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 text-custom-primary-100 hover:text-custom-primary-200 outline-none"
|
className="flex items-center gap-2 text-custom-primary-100 px-2 hover:text-custom-primary-200 outline-none"
|
||||||
onClick={() => setActiveGroup(key as keyof StateGroup)}
|
onClick={() => setActiveGroup(key as keyof StateGroup)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200">
|
<div className="flex flex-col gap-2 rounded">
|
||||||
{key === activeGroup && (
|
{key === activeGroup && (
|
||||||
<CreateUpdateStateInline
|
<CreateUpdateStateInline
|
||||||
groupLength={orderedStateGroups[key].length}
|
groupLength={orderedStateGroups[key].length}
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
// services
|
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IGptResponse } from "types";
|
import { ICurrentUserResponse, IGptResponse } from "types";
|
||||||
|
// helpers
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class AiServices extends APIService {
|
class AiServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createGptTask(
|
async createGptTask(
|
||||||
|
@ -8,12 +8,11 @@ import {
|
|||||||
IExportAnalyticsFormData,
|
IExportAnalyticsFormData,
|
||||||
ISaveAnalyticsFormData,
|
ISaveAnalyticsFormData,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
class AnalyticsServices extends APIService {
|
class AnalyticsServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise<IAnalyticsResponse> {
|
async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise<IAnalyticsResponse> {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
// services
|
// services
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
class AppInstallationsService extends APIService {
|
class AppInstallationsService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInstallationApp(workspaceSlug: string, provider: string, data: any): Promise<any> {
|
async addInstallationApp(workspaceSlug: string, provider: string, data: any): Promise<any> {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
class AuthService extends APIService {
|
class AuthService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async emailLogin(data: any) {
|
async emailLogin(data: any) {
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types";
|
import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class ProjectCycleServices extends APIService {
|
class ProjectCycleServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCycle(
|
async createCycle(
|
||||||
|
@ -3,15 +3,14 @@ import APIService from "services/api.service";
|
|||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types";
|
import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class ProjectEstimateServices extends APIService {
|
class ProjectEstimateServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEstimate(
|
async createEstimate(
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
interface UnSplashImage {
|
interface UnSplashImage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -29,7 +28,7 @@ interface UnSplashImageUrls {
|
|||||||
|
|
||||||
class FileServices extends APIService {
|
class FileServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
@ -19,7 +18,7 @@ import type {
|
|||||||
|
|
||||||
class InboxServices extends APIService {
|
class InboxServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInboxes(workspaceSlug: string, projectId: string): Promise<IInbox[]> {
|
async getInboxes(workspaceSlug: string, projectId: string): Promise<IInbox[]> {
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class CSVIntegrationService extends APIService {
|
class CSVIntegrationService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportCSVService(
|
async exportCSVService(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types";
|
import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types";
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ const trackEvent =
|
|||||||
const integrationServiceType: string = "github";
|
const integrationServiceType: string = "github";
|
||||||
class GithubIntegrationService extends APIService {
|
class GithubIntegrationService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listAllRepositories(workspaceSlug: string, integrationSlug: string): Promise<any> {
|
async listAllRepositories(workspaceSlug: string, integrationSlug: string): Promise<any> {
|
||||||
|
@ -9,15 +9,14 @@ import {
|
|||||||
IWorkspaceIntegration,
|
IWorkspaceIntegration,
|
||||||
IExportServiceResponse,
|
IExportServiceResponse,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class IntegrationService extends APIService {
|
class IntegrationService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAppIntegrationsList(): Promise<IAppIntegration[]> {
|
async getAppIntegrationsList(): Promise<IAppIntegration[]> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types";
|
import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types";
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ const trackEvent =
|
|||||||
|
|
||||||
class JiraImportedService extends APIService {
|
class JiraImportedService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise<IJiraResponse> {
|
async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise<IJiraResponse> {
|
||||||
|
@ -10,15 +10,14 @@ import type {
|
|||||||
IIssueLabels,
|
IIssueLabels,
|
||||||
ISubIssueResponse,
|
ISubIssueResponse,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class ProjectIssuesServices extends APIService {
|
class ProjectIssuesServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIssues(
|
async createIssues(
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "./track-event.service";
|
import trackEventServices from "./track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IModule, IIssue, ICurrentUserResponse } from "types";
|
import type { IModule, IIssue, ICurrentUserResponse } from "types";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const trackEvent =
|
|||||||
|
|
||||||
class ProjectIssuesServices extends APIService {
|
class ProjectIssuesServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getModules(workspaceSlug: string, projectId: string): Promise<IModule[]> {
|
async getModules(workspaceSlug: string, projectId: string): Promise<IModule[]> {
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type {
|
import type {
|
||||||
IUserNotification,
|
IUserNotification,
|
||||||
@ -11,10 +8,12 @@ import type {
|
|||||||
PaginatedUserNotification,
|
PaginatedUserNotification,
|
||||||
IMarkAllAsReadPayload,
|
IMarkAllAsReadPayload,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
// helpers
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class UserNotificationsServices extends APIService {
|
class UserNotificationsServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserNotifications(
|
async getUserNotifications(
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types";
|
import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class PageServices extends APIService {
|
class PageServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPage(
|
async createPage(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
@ -5,14 +6,12 @@ import trackEventServices from "services/track-event.service";
|
|||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
import { IProjectPublishSettings } from "store/project-publish";
|
import { IProjectPublishSettings } from "store/project-publish";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class ProjectServices extends APIService {
|
class ProjectServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectSettingsAsync(
|
async getProjectSettingsAsync(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type {
|
import type {
|
||||||
GithubRepositoriesResponse,
|
GithubRepositoriesResponse,
|
||||||
@ -16,14 +16,12 @@ import type {
|
|||||||
TProjectIssuesSearchParams,
|
TProjectIssuesSearchParams,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
export class ProjectServices extends APIService {
|
export class ProjectServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProject(
|
async createProject(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type {
|
import type {
|
||||||
ICurrentUserResponse,
|
ICurrentUserResponse,
|
||||||
@ -11,14 +11,12 @@ import type {
|
|||||||
IssueCommentReactionForm,
|
IssueCommentReactionForm,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class ReactionService extends APIService {
|
class ReactionService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createIssueReaction(
|
async createIssueReaction(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// helpers
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
@ -12,7 +12,7 @@ import type { ICurrentUserResponse, IState, IStateResponse } from "types";
|
|||||||
|
|
||||||
class ProjectStateServices extends APIService {
|
class ProjectStateServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createState(
|
async createState(
|
||||||
|
@ -12,14 +12,14 @@ import type {
|
|||||||
IUserWorkspaceDashboard,
|
IUserWorkspaceDashboard,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||||
|
|
||||||
class UserService extends APIService {
|
class UserService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentUserConfig() {
|
currentUserConfig() {
|
||||||
|
@ -6,6 +6,8 @@ import { ICurrentUserResponse } from "types";
|
|||||||
// types
|
// types
|
||||||
import { IView } from "types/views";
|
import { IView } from "types/views";
|
||||||
|
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
const trackEvent =
|
const trackEvent =
|
||||||
@ -13,7 +15,7 @@ const trackEvent =
|
|||||||
|
|
||||||
class ViewServices extends APIService {
|
class ViewServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createView(
|
async createView(
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import trackEventServices from "services/track-event.service";
|
||||||
|
// helpers
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import {
|
import {
|
||||||
IWorkspace,
|
IWorkspace,
|
||||||
@ -22,7 +21,7 @@ const trackEvent =
|
|||||||
|
|
||||||
class WorkspaceService extends APIService {
|
class WorkspaceService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async userWorkspaces(): Promise<IWorkspace[]> {
|
async userWorkspaces(): Promise<IWorkspace[]> {
|
||||||
|
Loading…
Reference in New Issue
Block a user