From 9bff10de6d880ab0d9a7e3e076939bfd6f9dd710 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:06:49 +0530 Subject: [PATCH 01/72] chore: changed issue priority from NULL to none (#2142) * chore: changed issue priority from NULL to none * fix: deleted the migration file --- apiserver/plane/api/views/issue.py | 8 ++++---- apiserver/plane/api/views/workspace.py | 2 +- apiserver/plane/db/models/issue.py | 4 ++-- apiserver/plane/utils/issue_filters.py | 24 ++++-------------------- 4 files changed, 11 insertions(+), 27 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 334ad2514..cf4fa46d4 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -178,7 +178,7 @@ class IssueViewSet(BaseViewSet): filters = issue_filters(request.query_params, "GET") # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] + priority_order = ["urgent", "high", "medium", "low", "none"] state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] order_by_param = request.GET.get("order_by", "-created_at") @@ -331,7 +331,7 @@ class UserWorkSpaceIssues(BaseAPIView): try: filters = issue_filters(request.query_params, "GET") # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] + priority_order = ["urgent", "high", "medium", "low", "none"] state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] order_by_param = request.GET.get("order_by", "-created_at") @@ -1068,7 +1068,7 @@ class IssueArchiveViewSet(BaseViewSet): show_sub_issues = request.GET.get("show_sub_issues", "true") # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] + priority_order = ["urgent", "high", "medium", "low", "none"] state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] order_by_param = request.GET.get("order_by", "-created_at") @@ -2078,7 +2078,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView): filters = issue_filters(request.query_params, "GET") # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", None] + priority_order = ["urgent", "high", "medium", "low", "none"] state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] order_by_param = request.GET.get("order_by", "-created_at") diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 2ec3f324a..2d1ee8132 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1072,7 +1072,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): .order_by("state_group") ) - priority_order = ["urgent", "high", "medium", "low", None] + priority_order = ["urgent", "high", "medium", "low", "none"] priority_distribution = ( Issue.issue_objects.filter( diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 78e958380..dd16cd963 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -38,6 +38,7 @@ class Issue(ProjectBaseModel): ("high", "High"), ("medium", "Medium"), ("low", "Low"), + ("none", "None") ) parent = models.ForeignKey( "self", @@ -64,8 +65,7 @@ class Issue(ProjectBaseModel): max_length=30, choices=PRIORITY_CHOICES, verbose_name="Issue Priority", - null=True, - blank=True, + default="none", ) start_date = models.DateField(null=True, blank=True) target_date = models.DateField(null=True, blank=True) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 34e1e8203..226d909cd 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -1,6 +1,7 @@ from django.utils.timezone import make_aware from django.utils.dateparse import parse_datetime + def filter_state(params, filter, method): if method == "GET": states = params.get("state").split(",") @@ -23,7 +24,6 @@ def filter_state_group(params, filter, method): return filter - def filter_estimate_point(params, filter, method): if method == "GET": estimate_points = params.get("estimate_point").split(",") @@ -39,25 +39,10 @@ def filter_priority(params, filter, method): if method == "GET": priorities = params.get("priority").split(",") if len(priorities) and "" not in priorities: - if len(priorities) == 1 and "null" in priorities: - filter["priority__isnull"] = True - elif len(priorities) > 1 and "null" in priorities: - filter["priority__isnull"] = True - filter["priority__in"] = [p for p in priorities if p != "null"] - else: - filter["priority__in"] = [p for p in priorities if p != "null"] - + filter["priority__in"] = priorities else: if params.get("priority", None) and len(params.get("priority")): - priorities = params.get("priority") - if len(priorities) == 1 and "null" in priorities: - filter["priority__isnull"] = True - elif len(priorities) > 1 and "null" in priorities: - filter["priority__isnull"] = True - filter["priority__in"] = [p for p in priorities if p != "null"] - else: - filter["priority__in"] = [p for p in priorities if p != "null"] - + filter["priority__in"] = params.get("priority") return filter @@ -229,7 +214,6 @@ def filter_issue_state_type(params, filter, method): return filter - def filter_project(params, filter, method): if method == "GET": projects = params.get("project").split(",") @@ -329,7 +313,7 @@ def issue_filters(query_params, method): "module": filter_module, "inbox_status": filter_inbox_status, "sub_issue": filter_sub_issue_toggle, - "subscriber": filter_subscribed_issues, + "subscriber": filter_subscribed_issues, "start_target_date": filter_start_target_date_issues, } From 2186db8bbaa07d8a5160d6a5897481cc11181477 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:35:15 +0530 Subject: [PATCH 02/72] feat: users can select timezone during onboarding (#2148) feat: using Intl timezone will be automatically selected but they have the option to change it --- web/components/onboarding/user-details.tsx | 40 +++++++++++++++++++++- web/helpers/date-time.helper.ts | 2 ++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index 222fc1143..b2ba931c0 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -9,13 +9,16 @@ import useToast from "hooks/use-toast"; // services import userService from "services/user.service"; // ui -import { CustomSelect, Input, PrimaryButton } from "components/ui"; +import { CustomSearchSelect, CustomSelect, Input, PrimaryButton } from "components/ui"; // types import { ICurrentUserResponse, IUser } from "types"; // fetch-keys import { CURRENT_USER } from "constants/fetch-keys"; +// helpers +import { getUserTimeZoneFromWindow } from "helpers/date-time.helper"; // constants import { USER_ROLES } from "constants/workspace"; +import { TIME_ZONES } from "constants/timezones"; const defaultValues: Partial = { first_name: "", @@ -27,6 +30,12 @@ type Props = { user?: IUser; }; +const timeZoneOptions = TIME_ZONES.map((timeZone) => ({ + value: timeZone.value, + query: timeZone.label + " " + timeZone.value, + content: timeZone.label, +})); + export const UserDetails: React.FC = ({ user }) => { const { setToastAlert } = useToast(); @@ -84,6 +93,7 @@ export const UserDetails: React.FC = ({ user }) => { first_name: user.first_name, last_name: user.last_name, role: user.role, + user_timezone: getUserTimeZoneFromWindow(), }); } }, [user, reset]); @@ -162,6 +172,34 @@ export const UserDetails: React.FC = ({ user }) => { {errors.role && {errors.role.message}} +
+ What time zone are you in? +
+ ( + t.value === value)?.label ?? value + : "Select a timezone" + } + options={timeZoneOptions} + onChange={onChange} + verticalPosition="top" + optionsClassName="w-full" + input + /> + )} + /> + {errors?.user_timezone && ( + {errors.user_timezone.message} + )} +
+
diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index 39a68bf3b..6784a9aa6 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -403,3 +403,5 @@ export const findTotalDaysInRange = ( return diffInDays; }; + +export const getUserTimeZoneFromWindow = () => Intl.DateTimeFormat().resolvedOptions().timeZone; From cdb888c23e962128151308cb33243642e560c438 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 12 Sep 2023 20:32:26 +0530 Subject: [PATCH 03/72] fix: selfhosted fixes (#2154) * fix: selfhosted fixes * fix: updated env example --- .env.example | 2 ++ replace-env-vars.sh | 2 +- space/additional.d.ts | 2 ++ space/components/accounts/sign-in.tsx | 4 ++-- space/next.config.js | 5 ++++- space/package.json | 1 + space/pages/onboarding/index.tsx | 7 +++---- space/tsconfig.json | 2 +- 8 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 space/additional.d.ts diff --git a/.env.example b/.env.example index 1d95c56a0..9fe0f47d9 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,8 @@ NEXT_PUBLIC_SLACK_CLIENT_ID="" 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 diff --git a/replace-env-vars.sh b/replace-env-vars.sh index afdc1492e..949ffd7d7 100644 --- a/replace-env-vars.sh +++ b/replace-env-vars.sh @@ -12,4 +12,4 @@ 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}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" +grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}" diff --git a/space/additional.d.ts b/space/additional.d.ts new file mode 100644 index 000000000..f400344c6 --- /dev/null +++ b/space/additional.d.ts @@ -0,0 +1,2 @@ +// additional.d.ts +/// diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx index b21ef167d..ed55f7697 100644 --- a/space/components/accounts/sign-in.tsx +++ b/space/components/accounts/sign-in.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // components import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts"; // images -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; +const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : ""; export const SignInView = observer(() => { const { user: userStore } = useMobxStore(); @@ -112,7 +112,7 @@ export const SignInView = observer(() => {
- Plane Logo + Plane Logo
diff --git a/space/next.config.js b/space/next.config.js index 392a4cab9..bd3749f10 100644 --- a/space/next.config.js +++ b/space/next.config.js @@ -12,7 +12,10 @@ const nextConfig = { }; if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) { - const nextConfigWithNginx = withImages({ basePath: "/spaces", ...nextConfig }); + const nextConfigWithNginx = withImages({ + basePath: "/spaces", + ...nextConfig, + }); module.exports = nextConfigWithNginx; } else { module.exports = nextConfig; diff --git a/space/package.json b/space/package.json index 768abb8ff..f2bb39df6 100644 --- a/space/package.json +++ b/space/package.json @@ -69,6 +69,7 @@ "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.48.2", "autoprefixer": "^10.4.13", "eslint": "8.34.0", "eslint-config-custom": "*", diff --git a/space/pages/onboarding/index.tsx b/space/pages/onboarding/index.tsx index 2f08ee648..5cb168d38 100644 --- a/space/pages/onboarding/index.tsx +++ b/space/pages/onboarding/index.tsx @@ -1,7 +1,4 @@ import React, { useEffect } from "react"; -import Image from "next/image"; -// assets -import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // mobx import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; @@ -12,6 +9,8 @@ import useToast from "hooks/use-toast"; // components import { OnBoardingForm } from "components/accounts/onboarding-form"; +const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : ""; + const OnBoardingPage = () => { const { user: userStore } = useMobxStore(); @@ -34,7 +33,7 @@ const OnBoardingPage = () => {
- Plane logo + Plane logo
diff --git a/space/tsconfig.json b/space/tsconfig.json index 63c95575d..3047edb7c 100644 --- a/space/tsconfig.json +++ b/space/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "tsconfig/nextjs.json", - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"], "exclude": ["node_modules"], "compilerOptions": { "baseUrl": ".", From 8e9a4dca7862c450e3b5b3c6aa5fcc26b2d36435 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 12 Sep 2023 22:27:15 +0530 Subject: [PATCH 04/72] refactor: view props structure (#2159) * chore: update view_props types * refactor: view props structure --- .../core/filters/issues-view-filter.tsx | 149 +++---- .../core/modals/bulk-delete-issues-modal.tsx | 6 +- web/components/core/views/all-views.tsx | 18 +- .../core/views/board-view/all-boards.tsx | 17 +- .../core/views/board-view/board-header.tsx | 24 +- .../core/views/board-view/single-board.tsx | 16 +- .../core/views/board-view/single-issue.tsx | 20 +- .../core/views/calendar-view/calendar.tsx | 20 +- web/components/core/views/issues-view.tsx | 69 ++-- .../core/views/list-view/all-lists.tsx | 9 +- .../core/views/list-view/single-issue.tsx | 9 +- .../core/views/list-view/single-list.tsx | 12 +- .../spreadsheet-view/spreadsheet-columns.tsx | 6 +- .../gantt-chart/cycle-issues-layout.tsx | 4 +- web/components/issues/delete-issue-modal.tsx | 6 +- web/components/issues/gantt-chart/layout.tsx | 4 +- web/components/issues/modal.tsx | 12 +- .../my-issues/my-issues-view-options.tsx | 192 ++++----- .../issues/my-issues/my-issues-view.tsx | 43 +- .../issues/view-select/due-date.tsx | 6 +- .../issues/view-select/start-date.tsx | 6 +- .../gantt-chart/module-issues-layout.tsx | 4 +- .../profile/profile-issues-view-options.tsx | 193 ++++----- .../profile/profile-issues-view.tsx | 52 ++- web/contexts/issue-view.context.tsx | 375 ++++-------------- web/contexts/profile-issues-context.tsx | 199 +++------- web/hooks/gantt-chart/cycle-issues-view.tsx | 8 +- web/hooks/gantt-chart/issue-view.tsx | 8 +- web/hooks/gantt-chart/module-issues-view.tsx | 8 +- web/hooks/my-issues/use-my-issues-filter.tsx | 127 +++--- web/hooks/my-issues/use-my-issues.tsx | 12 +- web/hooks/use-calendar-issues-view.tsx | 16 +- web/hooks/use-issues-view.tsx | 47 +-- web/hooks/use-profile-issues.tsx | 38 +- web/hooks/use-spreadsheet-issues-view.tsx | 16 +- web/pages/[workspaceSlug]/me/my-issues.tsx | 1 - .../[projectId]/archived-issues/index.tsx | 4 - web/services/issues.service.ts | 1 - web/services/modules.service.ts | 2 +- web/services/project.service.ts | 6 +- web/types/index.d.ts | 1 + web/types/issues.d.ts | 59 +-- web/types/projects.d.ts | 13 +- web/types/view-props.d.ts | 63 +++ web/types/workspace.d.ts | 10 +- 45 files changed, 765 insertions(+), 1146 deletions(-) create mode 100644 web/types/view-props.d.ts diff --git a/web/components/core/filters/issues-view-filter.tsx b/web/components/core/filters/issues-view-filter.tsx index 1266bd5a3..6354625dc 100644 --- a/web/components/core/filters/issues-view-filter.tsx +++ b/web/components/core/filters/issues-view-filter.tsx @@ -58,16 +58,8 @@ export const IssuesFilterView: React.FC = () => { const isArchivedIssues = router.pathname.includes("archived-issues"); const { - issueView, - setIssueView, - groupByProperty, - setGroupByProperty, - orderBy, - setOrderBy, - showEmptyGroups, - showSubIssues, - setShowSubIssues, - setShowEmptyGroups, + displayFilters, + setDisplayFilters, filters, setFilters, resetFilterToDefault, @@ -96,11 +88,11 @@ export const IssuesFilterView: React.FC = () => {
- {issueView !== "gantt_chart" && ( + {displayFilters.layout !== "gantt_chart" && (

Display Properties

@@ -310,7 +315,7 @@ export const IssuesFilterView: React.FC = () => { if (key === "estimate" && !isEstimateActive) return null; if ( - issueView === "spreadsheet" && + displayFilters.layout === "spreadsheet" && (key === "attachment_count" || key === "link" || key === "sub_issue_count") @@ -318,7 +323,7 @@ export const IssuesFilterView: React.FC = () => { return null; if ( - issueView !== "spreadsheet" && + displayFilters.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on") ) return null; diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 59e7c8a84..375e155a4 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user ); const { setToastAlert } = useToast(); - const { issueView, params } = useIssuesView(); + const { displayFilters, params } = useIssuesView(); const { params: calendarParams } = useCalendarIssuesView(); const { order_by, group_by, ...viewGanttParams } = params; @@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user message: "Issues deleted successfully!", }); - if (issueView === "calendar") mutate(calendarFetchKey); - else if (issueView === "gantt_chart") mutate(ganttFetchKey); + if (displayFilters.layout === "calendar") mutate(calendarFetchKey); + else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey); else { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)); diff --git a/web/components/core/views/all-views.tsx b/web/components/core/views/all-views.tsx index eb54ccb2a..3b95ed863 100644 --- a/web/components/core/views/all-views.tsx +++ b/web/components/core/views/all-views.tsx @@ -80,7 +80,7 @@ export const AllViews: React.FC = ({ const { user } = useUser(); const { memberRole } = useProjectMyMembership(); - const { groupedIssues, isEmpty, issueView } = viewProps; + const { groupedIssues, isEmpty, displayFilters } = viewProps; const { data: stateGroups } = useSWR( workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, @@ -117,11 +117,11 @@ export const AllViews: React.FC = ({ {groupedIssues ? ( !isEmpty || - issueView === "kanban" || - issueView === "calendar" || - issueView === "gantt_chart" ? ( + displayFilters?.layout === "kanban" || + displayFilters?.layout === "calendar" || + displayFilters?.layout === "gantt_chart" ? ( <> - {issueView === "list" ? ( + {displayFilters?.layout === "list" ? ( = ({ userAuth={memberRole} viewProps={viewProps} /> - ) : issueView === "kanban" ? ( + ) : displayFilters?.layout === "kanban" ? ( = ({ userAuth={memberRole} viewProps={viewProps} /> - ) : issueView === "calendar" ? ( + ) : displayFilters?.layout === "calendar" ? ( = ({ user={user} userAuth={memberRole} /> - ) : issueView === "spreadsheet" ? ( + ) : displayFilters?.layout === "spreadsheet" ? ( = ({ userAuth={memberRole} /> ) : ( - issueView === "gantt_chart" && + displayFilters?.layout === "gantt_chart" && )} ) : router.pathname.includes("archived-issues") ? ( diff --git a/web/components/core/views/board-view/all-boards.tsx b/web/components/core/views/board-view/all-boards.tsx index ea0f64ace..0d5a4534e 100644 --- a/web/components/core/views/board-view/all-boards.tsx +++ b/web/components/core/views/board-view/all-boards.tsx @@ -36,7 +36,7 @@ export const AllBoards: React.FC = ({ userAuth, viewProps, }) => { - const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps; + const { displayFilters, groupedIssues } = viewProps; return ( <> @@ -44,9 +44,12 @@ export const AllBoards: React.FC = ({
{Object.keys(groupedIssues).map((singleGroup, index) => { const currentState = - selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; + displayFilters?.group_by === "state" + ? states?.find((s) => s.id === singleGroup) + : null; - if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null; + if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) + return null; return ( = ({ /> ); })} - {!showEmptyGroups && ( + {!displayFilters?.show_empty_groups && (

Hidden groups

{Object.keys(groupedIssues).map((singleGroup, index) => { const currentState = - selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; + displayFilters?.group_by === "state" + ? states?.find((s) => s.id === singleGroup) + : null; if (groupedIssues[singleGroup].length === 0) return ( @@ -91,7 +96,7 @@ export const AllBoards: React.FC = ({ /> )}

- {selectedGroup === "state" + {displayFilters?.group_by === "state" ? addSpaceIfCamelCase(currentState?.name ?? "") : addSpaceIfCamelCase(singleGroup)}

diff --git a/web/components/core/views/board-view/board-header.tsx b/web/components/core/views/board-view/board-header.tsx index 144bdb826..1cbfdc81a 100644 --- a/web/components/core/views/board-view/board-header.tsx +++ b/web/components/core/views/board-view/board-header.tsx @@ -48,22 +48,28 @@ export const BoardHeader: React.FC = ({ const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { groupedIssues, groupByProperty: selectedGroup } = viewProps; + const { displayFilters, groupedIssues } = viewProps; + + console.log("dF", displayFilters); const { data: issueLabels } = useSWR( - workspaceSlug && projectId && selectedGroup === "labels" + workspaceSlug && projectId && displayFilters?.group_by === "labels" ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, - workspaceSlug && projectId && selectedGroup === "labels" + workspaceSlug && projectId && displayFilters?.group_by === "labels" ? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString()) : null ); const { data: members } = useSWR( - workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees") + workspaceSlug && + projectId && + (displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees") ? PROJECT_MEMBERS(projectId.toString()) : null, - workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees") + workspaceSlug && + projectId && + (displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees") ? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString()) : null ); @@ -73,7 +79,7 @@ export const BoardHeader: React.FC = ({ const getGroupTitle = () => { let title = addSpaceIfCamelCase(groupTitle); - switch (selectedGroup) { + switch (displayFilters?.group_by) { case "state": title = addSpaceIfCamelCase(currentState?.name ?? ""); break; @@ -97,7 +103,7 @@ export const BoardHeader: React.FC = ({ const getGroupIcon = () => { let icon; - switch (selectedGroup) { + switch (displayFilters?.group_by) { case "state": icon = currentState && ( = ({ {getGroupIcon()}

= ({ )} - {!disableAddIssue && !disableUserActions && selectedGroup !== "created_by" && ( + {!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && ( +

+
= ({ const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; - const { calendarIssues, params, displayFilters, setDisplayFilters } = useCalendarIssuesView(); + const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } = + useCalendarIssuesView(); const totalDate = eachDayOfInterval({ start: calendarDates.startDate, @@ -170,75 +172,85 @@ export const CalendarView: React.FC = ({ const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; - return calendarIssues ? ( -
- -
- + return ( + <> + mutateIssues()} + projectId={projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} + /> + {calendarIssues ? ( +
+ +
+ -
- {weeks.map((date, index) => (
- - {isMonthlyView - ? formatDate(date, "eee").substring(0, 3) - : formatDate(date, "eee")} - - {!isMonthlyView && {formatDate(date, "d")}} + {weeks.map((date, index) => ( +
+ + {isMonthlyView + ? formatDate(date, "eee").substring(0, 3) + : formatDate(date, "eee")} + + {!isMonthlyView && {formatDate(date, "d")}} +
+ ))}
- ))} -
-
- {currentViewDaysData.map((date, index) => ( - - ))} -
+
+ {currentViewDaysData.map((date, index) => ( + + ))} +
+
+
- -
- ) : ( -
- -
+ ) : ( +
+ +
+ )} + ); }; diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index f6c1cc2f7..3db571c99 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from "react"; -import Link from "next/link"; import { useRouter } from "next/router"; import { mutate } from "swr"; @@ -158,6 +157,15 @@ export const SingleCalendarIssue: React.FC = ({ ? Object.values(properties).some((value) => value === true) : false; + const openPeekOverview = () => { + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekIssue: issue.id }, + }); + }; + return (
= ({
)} - - - {properties.key && ( - - - {issue.project_detail?.identifier}-{issue.sequence_id} - - - )} - - {truncateText(issue.name, 25)} + + + {displayProperties && (
{properties.priority && ( diff --git a/web/components/core/views/gantt-chart-view/index.tsx b/web/components/core/views/gantt-chart-view/index.tsx index a881cb7aa..2cd10f95f 100644 --- a/web/components/core/views/gantt-chart-view/index.tsx +++ b/web/components/core/views/gantt-chart-view/index.tsx @@ -6,20 +6,24 @@ import { IssueGanttChartView } from "components/issues"; import { ModuleIssuesGanttChartView } from "components/modules"; import { ViewIssuesGanttChartView } from "components/views"; -export const GanttChartView = () => { +type Props = { + disableUserActions: boolean; +}; + +export const GanttChartView: React.FC = ({ disableUserActions }) => { const router = useRouter(); const { cycleId, moduleId, viewId } = router.query; return ( <> {cycleId ? ( - + ) : moduleId ? ( - + ) : viewId ? ( - + ) : ( - + )} ); diff --git a/web/components/core/views/issues-view.tsx b/web/components/core/views/issues-view.tsx index b4dd665dd..e0e7e8c94 100644 --- a/web/components/core/views/issues-view.tsx +++ b/web/components/core/views/issues-view.tsx @@ -19,7 +19,7 @@ import useIssuesProperties from "hooks/use-issue-properties"; import useProjectMembers from "hooks/use-project-members"; // components import { FiltersList, AllViews } from "components/core"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues"; import { CreateUpdateViewModal } from "components/views"; // ui import { PrimaryButton, SecondaryButton } from "components/ui"; @@ -462,6 +462,7 @@ export const IssuesView: React.FC = ({ data={issueToDelete} user={user} /> + {areFiltersApplied && ( <>
diff --git a/web/components/core/views/list-view/all-lists.tsx b/web/components/core/views/list-view/all-lists.tsx index 282e27755..bb0a7c0fb 100644 --- a/web/components/core/views/list-view/all-lists.tsx +++ b/web/components/core/views/list-view/all-lists.tsx @@ -1,5 +1,12 @@ +import { useRouter } from "next/router"; + +// hooks +import useMyIssues from "hooks/my-issues/use-my-issues"; +import useIssuesView from "hooks/use-issues-view"; +import useProfileIssues from "hooks/use-profile-issues"; // components import { SingleList } from "components/core/views/list-view/single-list"; +import { IssuePeekOverview } from "components/issues"; // types import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types"; @@ -9,6 +16,8 @@ type Props = { addIssueToGroup: (groupTitle: string) => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; openIssuesListModal?: (() => void) | null; + myIssueProjectId?: string | null; + handleMyIssueOpen?: (issue: IIssue) => void; removeIssue: ((bridgeId: string, issueId: string) => void) | null; disableUserActions: boolean; disableAddIssueOption?: boolean; @@ -23,16 +32,39 @@ export const AllLists: React.FC = ({ disableUserActions, disableAddIssueOption = false, openIssuesListModal, + handleMyIssueOpen, + myIssueProjectId, removeIssue, states, user, userAuth, viewProps, }) => { + const router = useRouter(); + const { workspaceSlug, projectId, userId } = router.query; + + const isProfileIssue = + router.pathname.includes("assigned") || + router.pathname.includes("created") || + router.pathname.includes("subscribed"); + + const isMyIssue = router.pathname.includes("my-issues"); + const { mutateIssues } = useIssuesView(); + const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); + const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString()); + const { displayFilters, groupedIssues } = viewProps; return ( <> + + isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues() + } + projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} + /> {groupedIssues && (
{Object.keys(groupedIssues).map((singleGroup) => { @@ -51,6 +83,7 @@ export const AllLists: React.FC = ({ currentState={currentState} addIssueToGroup={() => addIssueToGroup(singleGroup)} handleIssueAction={handleIssueAction} + handleMyIssueOpen={handleMyIssueOpen} openIssuesListModal={openIssuesListModal} removeIssue={removeIssue} disableUserActions={disableUserActions} diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index 1e5d551c3..ab5c080ca 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -61,6 +61,7 @@ type Props = { makeIssueCopy: () => void; removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; + handleMyIssueOpen?: (issue: IIssue) => void; disableUserActions: boolean; user: ICurrentUserResponse | undefined; userAuth: UserAuth; @@ -76,6 +77,7 @@ export const SingleListIssue: React.FC = ({ removeIssue, groupTitle, handleDeleteIssue, + handleMyIssueOpen, disableUserActions, user, userAuth, @@ -178,6 +180,16 @@ export const SingleListIssue: React.FC = ({ ? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}` : `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`; + const openPeekOverview = (issue: IIssue) => { + const { query } = router; + + if (handleMyIssueOpen) handleMyIssueOpen(issue); + router.push({ + pathname: router.pathname, + query: { ...query, peekIssue: issue.id }, + }); + }; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues; @@ -220,23 +232,27 @@ export const SingleListIssue: React.FC = ({ }} >
void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; openIssuesListModal?: (() => void) | null; + handleMyIssueOpen?: (issue: IIssue) => void; removeIssue: ((bridgeId: string, issueId: string) => void) | null; disableUserActions: boolean; disableAddIssueOption?: boolean; @@ -55,6 +56,7 @@ export const SingleList: React.FC = ({ addIssueToGroup, handleIssueAction, openIssuesListModal, + handleMyIssueOpen, removeIssue, disableUserActions, disableAddIssueOption = false, @@ -251,6 +253,7 @@ export const SingleList: React.FC = ({ editIssue={() => handleIssueAction(issue, "edit")} makeIssueCopy={() => handleIssueAction(issue, "copy")} handleDeleteIssue={() => handleIssueAction(issue, "delete")} + handleMyIssueOpen={handleMyIssueOpen} removeIssue={() => { if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id); diff --git a/web/components/cycles/gantt-chart/cycle-issues-layout.tsx b/web/components/cycles/gantt-chart/cycle-issues-layout.tsx index 1c78da096..b70b16f03 100644 --- a/web/components/cycles/gantt-chart/cycle-issues-layout.tsx +++ b/web/components/cycles/gantt-chart/cycle-issues-layout.tsx @@ -8,11 +8,15 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues"; // types import { IIssue } from "types"; -export const CycleIssuesGanttChartView = () => { +type Props = { + disableUserActions: boolean; +}; + +export const CycleIssuesGanttChartView: React.FC = ({ disableUserActions }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -30,23 +34,31 @@ export const CycleIssuesGanttChartView = () => { const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; return ( -
- - updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) - } - SidebarBlockRender={IssueGanttSidebarBlock} - BlockRender={IssueGanttBlock} - enableBlockLeftResize={isAllowed} - enableBlockRightResize={isAllowed} - enableBlockMove={isAllowed} - enableReorder={displayFilters.order_by === "sort_order" && isAllowed} - bottomSpacing + <> + mutateGanttIssues()} + projectId={projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} /> -
+
+ + updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } + SidebarBlockRender={IssueGanttSidebarBlock} + BlockRender={IssueGanttBlock} + enableBlockLeftResize={isAllowed} + enableBlockRightResize={isAllowed} + enableBlockMove={isAllowed} + enableReorder={displayFilters.order_by === "sort_order" && isAllowed} + bottomSpacing + /> +
+ ); }; diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 177155f10..ae8a01896 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -23,14 +23,7 @@ import { import { CreateStateModal } from "components/states"; import { CreateLabelModal } from "components/labels"; // ui -import { - CustomMenu, - Input, - Loader, - PrimaryButton, - SecondaryButton, - ToggleSwitch, -} from "components/ui"; +import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui"; import { TipTapEditor } from "components/tiptap"; // icons import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline"; diff --git a/web/components/issues/gantt-chart/blocks.tsx b/web/components/issues/gantt-chart/blocks.tsx index 0834e3e79..ef4919780 100644 --- a/web/components/issues/gantt-chart/blocks.tsx +++ b/web/components/issues/gantt-chart/blocks.tsx @@ -11,13 +11,21 @@ import { IIssue } from "types"; export const IssueGanttBlock = ({ data }: { data: IIssue }) => { const router = useRouter(); - const { workspaceSlug } = router.query; + + const openPeekOverview = () => { + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekIssue: data.id }, + }); + }; return (
router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} + onClick={openPeekOverview} >
{ // rendering issues on gantt sidebar export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => { const router = useRouter(); - const { workspaceSlug } = router.query; const duration = findTotalDaysInRange(data?.start_date ?? "", data?.target_date ?? "", true); + const openPeekOverview = () => { + const { query } = router; + + router.push({ + pathname: router.pathname, + query: { ...query, peekIssue: data.id }, + }); + }; + return (
router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} + onClick={openPeekOverview} >
diff --git a/web/components/issues/gantt-chart/layout.tsx b/web/components/issues/gantt-chart/layout.tsx index a78319a4b..ed4cd3d70 100644 --- a/web/components/issues/gantt-chart/layout.tsx +++ b/web/components/issues/gantt-chart/layout.tsx @@ -8,11 +8,15 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues"; // types import { IIssue } from "types"; -export const IssueGanttChartView = () => { +type Props = { + disableUserActions: boolean; +}; + +export const IssueGanttChartView: React.FC = ({ disableUserActions }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -29,23 +33,31 @@ export const IssueGanttChartView = () => { const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; return ( -
- - updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) - } - BlockRender={IssueGanttBlock} - SidebarBlockRender={IssueGanttSidebarBlock} - enableBlockLeftResize={isAllowed} - enableBlockRightResize={isAllowed} - enableBlockMove={isAllowed} - enableReorder={displayFilters.order_by === "sort_order" && isAllowed} - bottomSpacing + <> + mutateGanttIssues()} + projectId={projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} /> -
+
+ + updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } + BlockRender={IssueGanttBlock} + SidebarBlockRender={IssueGanttSidebarBlock} + enableBlockLeftResize={isAllowed} + enableBlockRightResize={isAllowed} + enableBlockMove={isAllowed} + enableReorder={displayFilters.order_by === "sort_order" && isAllowed} + bottomSpacing + /> +
+ ); }; diff --git a/web/components/issues/my-issues/my-issues-view.tsx b/web/components/issues/my-issues/my-issues-view.tsx index 81a456079..7dc5c8d20 100644 --- a/web/components/issues/my-issues/my-issues-view.tsx +++ b/web/components/issues/my-issues/my-issues-view.tsx @@ -57,7 +57,7 @@ export const MyIssuesView: React.FC = ({ const { user } = useUserAuth(); const { groupedIssues, mutateMyIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString()); - const { filters, setFilters, displayFilters, setDisplayFilters, properties } = useMyIssuesFilters( + const { filters, setFilters, displayFilters, properties } = useMyIssuesFilters( workspaceSlug?.toString() ); diff --git a/web/components/modules/gantt-chart/module-issues-layout.tsx b/web/components/modules/gantt-chart/module-issues-layout.tsx index ca8aae527..c7bef4b26 100644 --- a/web/components/modules/gantt-chart/module-issues-layout.tsx +++ b/web/components/modules/gantt-chart/module-issues-layout.tsx @@ -1,5 +1,3 @@ -import { FC } from "react"; - import { useRouter } from "next/router"; // hooks @@ -10,13 +8,13 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues"; // types import { IIssue } from "types"; -type Props = {}; +type Props = { disableUserActions: boolean }; -export const ModuleIssuesGanttChartView: FC = ({}) => { +export const ModuleIssuesGanttChartView: React.FC = ({ disableUserActions }) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; @@ -34,23 +32,31 @@ export const ModuleIssuesGanttChartView: FC = ({}) => { const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; return ( -
- - updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) - } - SidebarBlockRender={IssueGanttSidebarBlock} - BlockRender={IssueGanttBlock} - enableBlockLeftResize={isAllowed} - enableBlockRightResize={isAllowed} - enableBlockMove={isAllowed} - enableReorder={displayFilters.order_by === "sort_order" && isAllowed} - bottomSpacing + <> + mutateGanttIssues()} + projectId={projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} /> -
+
+ + updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } + SidebarBlockRender={IssueGanttSidebarBlock} + BlockRender={IssueGanttBlock} + enableBlockLeftResize={isAllowed} + enableBlockRightResize={isAllowed} + enableBlockMove={isAllowed} + enableReorder={displayFilters.order_by === "sort_order" && isAllowed} + bottomSpacing + /> +
+ ); }; diff --git a/web/components/views/gantt-chart.tsx b/web/components/views/gantt-chart.tsx index b25f034cd..6f43a32e6 100644 --- a/web/components/views/gantt-chart.tsx +++ b/web/components/views/gantt-chart.tsx @@ -1,5 +1,3 @@ -import { FC } from "react"; - import { useRouter } from "next/router"; // hooks @@ -9,13 +7,13 @@ import { updateGanttIssue } from "components/gantt-chart/hooks/block-update"; import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; -import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues"; +import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues"; // types import { IIssue } from "types"; -type Props = {}; +type Props = { disableUserActions: boolean }; -export const ViewIssuesGanttChartView: FC = ({}) => { +export const ViewIssuesGanttChartView: React.FC = ({ disableUserActions }) => { const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query; @@ -31,22 +29,30 @@ export const ViewIssuesGanttChartView: FC = ({}) => { const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; return ( -
- - updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) - } - SidebarBlockRender={IssueGanttSidebarBlock} - BlockRender={IssueGanttBlock} - enableBlockLeftResize={isAllowed} - enableBlockRightResize={isAllowed} - enableBlockMove={isAllowed} - enableReorder={isAllowed} + <> + mutateGanttIssues()} + projectId={projectId?.toString() ?? ""} + workspaceSlug={workspaceSlug?.toString() ?? ""} + readOnly={disableUserActions} /> -
+
+ + updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString()) + } + SidebarBlockRender={IssueGanttSidebarBlock} + BlockRender={IssueGanttBlock} + enableBlockLeftResize={isAllowed} + enableBlockRightResize={isAllowed} + enableBlockMove={isAllowed} + enableReorder={isAllowed} + /> +
+ ); }; diff --git a/web/hooks/use-calendar-issues-view.tsx b/web/hooks/use-calendar-issues-view.tsx index 362cd741f..289aed2ac 100644 --- a/web/hooks/use-calendar-issues-view.tsx +++ b/web/hooks/use-calendar-issues-view.tsx @@ -44,7 +44,7 @@ const useCalendarIssuesView = () => { target_date: displayFilters?.calendar_date_range, }; - const { data: projectCalendarIssues } = useSWR( + const { data: projectCalendarIssues, mutate: mutateProjectCalendarIssues } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params) : null, @@ -54,7 +54,7 @@ const useCalendarIssuesView = () => { : null ); - const { data: cycleCalendarIssues } = useSWR( + const { data: cycleCalendarIssues, mutate: mutateCycleCalendarIssues } = useSWR( workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) : null, @@ -69,7 +69,7 @@ const useCalendarIssuesView = () => { : null ); - const { data: moduleCalendarIssues } = useSWR( + const { data: moduleCalendarIssues, mutate: mutateModuleCalendarIssues } = useSWR( workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : null, @@ -84,7 +84,7 @@ const useCalendarIssuesView = () => { : null ); - const { data: viewCalendarIssues } = useSWR( + const { data: viewCalendarIssues, mutate: mutateViewCalendarIssues } = useSWR( workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null, workspaceSlug && projectId && viewId && params ? () => @@ -104,6 +104,13 @@ const useCalendarIssuesView = () => { displayFilters, setDisplayFilters, calendarIssues: calendarIssues ?? [], + mutateIssues: cycleId + ? mutateCycleCalendarIssues + : moduleId + ? mutateModuleCalendarIssues + : viewId + ? mutateViewCalendarIssues + : mutateProjectCalendarIssues, filters, setFilters, params, From 9bac7cb03627a04d47ea2fbbc15bbfce9ae2b69f Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 13 Sep 2023 19:41:11 +0530 Subject: [PATCH 18/72] feat: issue link to create relation between issues (#2171) * feat: issue linking * fix: search params to filter out selected issue * style: changed icons * fix: build error on web-view * fix: build error * fix: build error on web-view component --- web/components/icons/index.ts | 1 + web/components/icons/related-icon.tsx | 41 ++++ .../issues/sidebar-select/blocked.tsx | 64 ++++-- .../issues/sidebar-select/blocker.tsx | 71 +++++-- .../issues/sidebar-select/duplicate.tsx | 172 ++++++++++++++++ web/components/issues/sidebar-select/index.ts | 2 + .../issues/sidebar-select/relates-to.tsx | 172 ++++++++++++++++ web/components/issues/sidebar.tsx | 72 ++++++- .../web-view/issue-properties-detail.tsx | 192 +++++++++++++----- web/components/web-view/select-blocked.tsx | 2 +- web/components/web-view/select-blocker.tsx | 2 +- web/pages/[workspaceSlug]/editor.tsx | 4 +- .../projects/[projectId]/issues/[issueId].tsx | 4 +- .../projects/[projectId]/issues/[issueId].tsx | 4 +- web/services/issues.service.ts | 46 +++++ web/services/track-event.service.ts | 16 ++ web/types/issues.d.ts | 27 ++- web/types/projects.d.ts | 2 +- 18 files changed, 793 insertions(+), 101 deletions(-) create mode 100644 web/components/icons/related-icon.tsx create mode 100644 web/components/issues/sidebar-select/duplicate.tsx create mode 100644 web/components/issues/sidebar-select/relates-to.tsx diff --git a/web/components/icons/index.ts b/web/components/icons/index.ts index d3be7f2a8..ab661a092 100644 --- a/web/components/icons/index.ts +++ b/web/components/icons/index.ts @@ -83,3 +83,4 @@ export * from "./archive-icon"; export * from "./clock-icon"; export * from "./bell-icon"; export * from "./single-comment-icon"; +export * from "./related-icon"; diff --git a/web/components/icons/related-icon.tsx b/web/components/icons/related-icon.tsx new file mode 100644 index 000000000..3abb4b1c3 --- /dev/null +++ b/web/components/icons/related-icon.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const RelatedIcon: React.FC = ({ + width = "24", + height = "24", + color = "rgb(var(--color-text-200))", + className, +}) => ( + + + + + +); diff --git a/web/components/issues/sidebar-select/blocked.tsx b/web/components/issues/sidebar-select/blocked.tsx index 02cfd3b16..9554a83ba 100644 --- a/web/components/issues/sidebar-select/blocked.tsx +++ b/web/components/issues/sidebar-select/blocked.tsx @@ -1,11 +1,13 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; - // react-hook-form import { UseFormWatch } from "react-hook-form"; // hooks import useToast from "hooks/use-toast"; +import useUser from "hooks/use-user"; +// services +import issuesService from "services/issues.service"; // components import { ExistingIssuesListModal } from "components/core"; // icons @@ -29,10 +31,11 @@ export const SidebarBlockedSelect: React.FC = ({ }) => { const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); + const { user } = useUser(); const { setToastAlert } = useToast(); const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; const handleClose = () => { setIsBlockedModalOpen(false); @@ -62,21 +65,39 @@ export const SidebarBlockedSelect: React.FC = ({ }, })); - const newBlocked = [...watch("blocked_issues"), ...selectedIssues]; + if (!user) return; + + issuesService + .createIssueRelation(workspaceSlug as string, projectId as string, issueId as string, user, { + related_list: [ + ...selectedIssues.map((issue) => ({ + issue: issueId as string, + relation_type: "blocked_by" as const, + related_issue_detail: issue.blocked_issue_detail, + related_issue: issue.blocked_issue_detail.id, + })), + ], + }) + .then((response) => { + submitChanges({ + related_issues: [ + ...watch("related_issues")?.filter((i) => i.relation_type !== "blocked_by"), + ...response, + ], + }); + }); - submitChanges({ - blocked_issues: newBlocked, - blocks_list: newBlocked.map((i) => i.blocked_issue_detail?.id ?? ""), - }); handleClose(); }; + const blockedByIssue = watch("related_issues")?.filter((i) => i.relation_type === "blocked_by"); + return ( <> setIsBlockedModalOpen(false)} - searchParams={{ blocker_blocked_by: true, issue_id: issueId }} + searchParams={{ issue_relation: true, issue_id: issueId }} handleOnSubmit={onSubmit} workspaceLevelToggle /> @@ -87,33 +108,42 @@ export const SidebarBlockedSelect: React.FC = ({
- {watch("blocked_issues") && watch("blocked_issues").length > 0 - ? watch("blocked_issues").map((issue) => ( + {blockedByIssue && blockedByIssue.length > 0 + ? blockedByIssue.map((relation) => (
- {`${issue.blocked_issue_detail?.project_detail.identifier}-${issue.blocked_issue_detail?.sequence_id}`} + {`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`}
- {watch("blocker_issues") && watch("blocker_issues").length > 0 - ? watch("blocker_issues").map((issue) => ( + {blockerIssue && blockerIssue.length > 0 + ? blockerIssue.map((relation) => (
- {`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`} + {`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`} +
+ )) + : null} +
+ +
+
+ + ); +}; diff --git a/web/components/issues/sidebar-select/index.ts b/web/components/issues/sidebar-select/index.ts index 5035325fd..8b083841e 100644 --- a/web/components/issues/sidebar-select/index.ts +++ b/web/components/issues/sidebar-select/index.ts @@ -8,3 +8,5 @@ export * from "./module"; export * from "./parent"; export * from "./priority"; export * from "./state"; +export * from "./duplicate"; +export * from "./relates-to"; diff --git a/web/components/issues/sidebar-select/relates-to.tsx b/web/components/issues/sidebar-select/relates-to.tsx new file mode 100644 index 000000000..fb878daee --- /dev/null +++ b/web/components/issues/sidebar-select/relates-to.tsx @@ -0,0 +1,172 @@ +import React, { useState } from "react"; + +import { useRouter } from "next/router"; +// react-hook-form +import { UseFormWatch } from "react-hook-form"; +// hooks +import useToast from "hooks/use-toast"; +import useUser from "hooks/use-user"; +// icons +import { X } from "lucide-react"; +import { BlockerIcon, RelatedIcon } from "components/icons"; +// components +import { ExistingIssuesListModal } from "components/core"; +// services +import issuesService from "services/issues.service"; +// types +import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types"; + +type Props = { + issueId?: string; + submitChanges: (formData: Partial) => void; + watch: UseFormWatch; + disabled?: boolean; +}; + +export const SidebarRelatesSelect: React.FC = (props) => { + const { issueId, submitChanges, watch, disabled = false } = props; + + const [isRelatesToModalOpen, setIsRelatesToModalOpen] = useState(false); + + const { user } = useUser(); + const { setToastAlert } = useToast(); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const handleClose = () => { + setIsRelatesToModalOpen(false); + }; + + const onSubmit = async (data: ISearchIssueResponse[]) => { + if (data.length === 0) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Please select at least one issue.", + }); + + return; + } + + const selectedIssues: { blocker_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({ + blocker_issue_detail: { + id: i.id, + name: i.name, + sequence_id: i.sequence_id, + project_detail: { + id: i.project_id, + identifier: i.project__identifier, + name: i.project__name, + }, + }, + })); + + if (!user) return; + + issuesService + .createIssueRelation(workspaceSlug as string, projectId as string, issueId as string, user, { + related_list: [ + ...selectedIssues.map((issue) => ({ + issue: issueId as string, + related_issue_detail: issue.blocker_issue_detail, + related_issue: issue.blocker_issue_detail.id, + relation_type: "relates_to" as const, + })), + ], + }) + .then((response) => { + submitChanges({ + related_issues: [...watch("related_issues"), ...(response ?? [])], + }); + }); + + handleClose(); + }; + + const relatedToIssueRelation = [ + ...(watch("related_issues")?.filter((i) => i.relation_type === "relates_to") ?? []), + ...(watch("issue_relations") ?? []) + ?.filter((i) => i.relation_type === "relates_to") + .map((i) => ({ + ...i, + related_issue_detail: i.issue_detail, + related_issue: i.issue_detail?.id, + })), + ]; + + return ( + <> + setIsRelatesToModalOpen(false)} + searchParams={{ issue_relation: true, issue_id: issueId }} + handleOnSubmit={onSubmit} + workspaceLevelToggle + /> +
+
+ +

Relates to

+
+
+
+ {relatedToIssueRelation && relatedToIssueRelation.length > 0 + ? relatedToIssueRelation.map((relation) => ( +
+ + + {`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`} + + +
+ )) + : null} +
+ +
+
+ + ); +}; diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index a33d17705..1f48f7307 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -30,6 +30,8 @@ import { SidebarStateSelect, SidebarEstimateSelect, SidebarLabelSelect, + SidebarDuplicateSelect, + SidebarRelatesSelect, } from "components/issues"; // ui import { CustomDatePicker, Icon } from "components/ui"; @@ -76,6 +78,8 @@ type Props = { | "delete" | "all" | "subscribe" + | "duplicate" + | "relates_to" )[]; uneditable?: boolean; }; @@ -464,7 +468,19 @@ export const IssueDetailsSidebar: React.FC = ({ {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( { + mutate( + ISSUE_DETAILS(issueId as string), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...data, + }; + }, + false + ); + }} watch={watchIssue} disabled={memberRole.isGuest || memberRole.isViewer || uneditable} /> @@ -472,7 +488,59 @@ export const IssueDetailsSidebar: React.FC = ({ {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( { + mutate( + ISSUE_DETAILS(issueId as string), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...data, + }; + }, + false + ); + }} + watch={watchIssue} + disabled={memberRole.isGuest || memberRole.isViewer || uneditable} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && ( + { + mutate( + ISSUE_DETAILS(issueId as string), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...data, + }; + }, + false + ); + }} + watch={watchIssue} + disabled={memberRole.isGuest || memberRole.isViewer || uneditable} + /> + )} + {(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && ( + { + mutate( + ISSUE_DETAILS(issueId as string), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...data, + }; + }, + false + ); + }} watch={watchIssue} disabled={memberRole.isGuest || memberRole.isViewer || uneditable} /> diff --git a/web/components/web-view/issue-properties-detail.tsx b/web/components/web-view/issue-properties-detail.tsx index 2fe0356f5..089f8950f 100644 --- a/web/components/web-view/issue-properties-detail.tsx +++ b/web/components/web-view/issue-properties-detail.tsx @@ -4,9 +4,21 @@ import React, { useState } from "react"; // next import { useRouter } from "next/router"; +// swr +import { mutate } from "swr"; + // react hook forms import { Control, Controller, useWatch } from "react-hook-form"; +// services +import issuesService from "services/issues.service"; + +// hooks +import useUser from "hooks/use-user"; + +// fetch keys +import { ISSUE_DETAILS } from "constants/fetch-keys"; + // icons import { BlockedIcon, BlockerIcon } from "components/icons"; import { ChevronDown, PlayIcon, User, X, CalendarDays, LayoutGrid, Users } from "lucide-react"; @@ -26,6 +38,7 @@ import { EstimateSelect, ParentSelect, BlockerSelect, + BlockedSelect, } from "components/web-view"; // types @@ -39,15 +52,16 @@ type Props = { export const IssuePropertiesDetail: React.FC = (props) => { const { control, submitChanges } = props; - const blockerIssue = useWatch({ - control, - name: "blocker_issues", - }); + const blockerIssue = + useWatch({ + control, + name: "issue_relations", + })?.filter((i) => i.relation_type === "blocked_by") || []; const blockedIssue = useWatch({ control, - name: "blocked_issues", - }); + name: "related_issues", + })?.filter((i) => i.relation_type === "blocked_by"); const startDate = useWatch({ control, @@ -55,12 +69,28 @@ export const IssuePropertiesDetail: React.FC = (props) => { }); const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId, issueId } = router.query; + + const { user } = useUser(); const [isViewAllOpen, setIsViewAllOpen] = useState(false); const { isEstimateActive } = useEstimateOption(); + const handleMutation = (data: any) => { + mutate( + ISSUE_DETAILS(issueId as string), + (prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + ...data, + }; + }, + false + ); + }; + return (
@@ -188,51 +218,80 @@ export const IssuePropertiesDetail: React.FC = (props) => { Blocking
- ( - - submitChanges({ - blocker_issues: val, - blockers_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""), - }) - } - /> - )} + { + if (!user || !workspaceSlug || !projectId || !issueId) return; + + issuesService + .createIssueRelation( + workspaceSlug as string, + projectId as string, + issueId as string, + user, + { + related_list: [ + ...val.map((issue: any) => ({ + issue: issue.blocker_issue_detail.id, + relation_type: "blocked_by" as const, + related_issue: issueId as string, + related_issue_detail: issue.blocker_issue_detail, + })), + ], + } + ) + .then((response) => { + handleMutation({ + issue_relations: [ + ...blockerIssue, + ...(response ?? []).map((i: any) => ({ + id: i.id, + relation_type: i.relation_type, + issue_detail: i.related_issue_detail, + issue: i.related_issue, + })), + ], + }); + }); + }} />
{blockerIssue && blockerIssue.map((issue) => (
- {`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`} + {`${issue.issue_detail?.project_detail.identifier}-${issue.issue_detail?.sequence_id}`}
- ( - - submitChanges({ - blocked_issues: val, - blocks_list: val?.map((i: any) => i.blocker_issue_detail?.id ?? ""), - }) - } - /> - )} + { + if (!user || !workspaceSlug || !projectId || !issueId) return; + + issuesService + .createIssueRelation( + workspaceSlug as string, + projectId as string, + issueId as string, + user, + { + related_list: [ + ...val.map((issue: any) => ({ + issue: issue.blocked_issue_detail.id, + relation_type: "blocked_by" as const, + related_issue: issueId as string, + related_issue_detail: issue.blocked_issue_detail, + })), + ], + } + ) + .then((response) => { + handleMutation({ + related_issues: [ + ...blockedIssue, + ...(response ?? []).map((i: any) => ({ + id: i.id, + relation_type: i.relation_type, + issue_detail: i.related_issue_detail, + issue: i.related_issue, + })), + ], + }); + }); + }} />
{blockedIssue && blockedIssue.map((issue) => (
- {`${issue?.blocked_issue_detail?.project_detail?.identifier}-${issue?.blocked_issue_detail?.sequence_id}`} + {`${issue?.related_issue_detail?.project_detail?.identifier}-${issue?.related_issue_detail?.sequence_id}`} diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index ad65714aa..f6cf95f2d 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -124,7 +124,7 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80" onClick={() => setmonthModal(true)} > - Customize Time Range + Customise Time Range diff --git a/web/components/automation/select-month-modal.tsx b/web/components/automation/select-month-modal.tsx index b91c03391..18239d62b 100644 --- a/web/components/automation/select-month-modal.tsx +++ b/web/components/automation/select-month-modal.tsx @@ -104,7 +104,7 @@ export const SelectMonthModal: React.FC = ({ as="h3" className="text-lg font-medium leading-6 text-custom-text-100" > - Customize Time Range + Customise Time Range
From d0f6ca3bace3c38a0d826ec1c91ce403d3132c24 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Wed, 13 Sep 2023 20:21:02 +0530 Subject: [PATCH 21/72] [chore] Update `setup.sh`, with removed replacement script & added project-level ENVs (#2115) * chore: Updated Setup Script for Splitting Env File * chore: updated dockerfile for using inproject env varaibles * chore: removed husky replacement script * chore: updated shell script using sed * chore: updated dockerfiles with removed cp statement * chore: added example env for apiserver * chore: refactored secret generation for backend * chore: removed replacement script * chore: updated docker-compose with removed env variables * chore: resolved comments in setup.sh and docker-compose * chore: removed secret key placeholder in apiserver example env * chore: updated root env for project less env variables * chore: removed project level env update from root env logic * chore: updated API_BASE_URL in .env.example * chore: restored docker argument as env NEXT_PUBLIC_API_BASE_URL * chore: added pg missing env variables * [chore] Updated web and deploy backend configuration for reverse proxy & decoupled Plane Deploy URL generation for web (#2135) * chore: removed api url build arg from compose * chore: set public api default argument to black string for self hosted * chore: updated web services to accept blank string as API URL * chore: added env variables for pg compose service * chore: modified space app services to use accept empty string as api base * chore: conditionally trigger web url value based on argument * fix: made web to use identical host with spaces suffix on absense of Deploy URL for deploy * chore: added example env for PUBLIC_DEPLOY Env * chore: updated web dockerfile with addition as PLANE_DEPLOY Argument * API BASE URL global update * API BASE URL replace with api server * api base url fixes * typo fixes --------- Co-authored-by: sriram veeraghanta * dev: remove API_BASE_URL from environment variable --------- Co-authored-by: sriram veeraghanta Co-authored-by: pablohashescobar --- .env.example | 54 --------------- apiserver/.env.example | 60 ++++++++++++++++ docker-compose.yml | 69 +------------------ replace-env-vars.sh | 15 ---- setup.sh | 17 ++--- space/.env.example | 2 - space/Dockerfile.space | 18 ++--- space/helpers/common.helper.ts | 2 + space/services/authentication.service.ts | 3 +- space/services/file.service.ts | 6 +- space/services/issue.service.ts | 3 +- space/services/project.service.ts | 3 +- space/services/user.service.ts | 3 +- start.sh | 4 -- web/.env.example | 4 +- web/Dockerfile.web | 28 +++----- .../project/publish-project/modal.tsx | 6 +- web/helpers/common.helper.ts | 5 ++ web/layouts/app-layout/app-header.tsx | 6 +- web/lib/auth.ts | 33 ++++----- web/services/ai.service.ts | 8 +-- web/services/analytics.service.ts | 5 +- web/services/app-installations.service.ts | 5 +- web/services/authentication.service.ts | 5 +- web/services/cycles.service.ts | 6 +- web/services/estimates.service.ts | 5 +- web/services/file.service.ts | 5 +- web/services/inbox.service.ts | 5 +- web/services/integration/csv.services.ts | 6 +- web/services/integration/github.service.ts | 3 +- web/services/integration/index.ts | 5 +- web/services/integration/jira.service.ts | 4 +- web/services/issues.service.ts | 5 +- web/services/modules.service.ts | 6 +- web/services/notifications.service.ts | 7 +- web/services/pages.service.ts | 6 +- web/services/project-publish.service.ts | 5 +- web/services/project.service.ts | 6 +- web/services/reaction.service.ts | 6 +- web/services/state.service.ts | 6 +- web/services/user.service.ts | 4 +- web/services/views.service.ts | 4 +- web/services/workspace.service.ts | 7 +- 43 files changed, 179 insertions(+), 286 deletions(-) create mode 100644 apiserver/.env.example delete mode 100644 replace-env-vars.sh create mode 100644 space/helpers/common.helper.ts diff --git a/.env.example b/.env.example index 9fe0f47d9..082aa753b 100644 --- a/.env.example +++ b/.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 PGUSER="plane" PGPASSWORD="plane" @@ -45,15 +10,6 @@ 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 " -EMAIL_USE_TLS="1" -EMAIL_USE_SSL="0" - # AWS Settings AWS_REGION="" 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 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 @@ -80,10 +33,3 @@ USE_MINIO=1 # Nginx Configuration 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 diff --git a/apiserver/.env.example b/apiserver/.env.example new file mode 100644 index 000000000..a2a214fe6 --- /dev/null +++ b/apiserver/.env.example @@ -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 " +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" diff --git a/docker-compose.yml b/docker-compose.yml index cf631face..e3c1b37be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,5 @@ 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: plane-web: container_name: planefrontend @@ -40,23 +8,8 @@ services: dockerfile: ./web/Dockerfile.web args: DOCKER_BUILDKIT: 1 - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 - NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces restart: always 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: - plane-api - plane-worker @@ -68,14 +21,8 @@ services: dockerfile: ./space/Dockerfile.space args: DOCKER_BUILDKIT: 1 - NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 - NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 restart: always 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: - plane-api - plane-worker @@ -91,9 +38,7 @@ services: restart: always command: ./bin/takeoff env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-db - plane-redis @@ -108,9 +53,7 @@ services: restart: always command: ./bin/worker env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-api - plane-db @@ -126,9 +69,7 @@ services: restart: always command: ./bin/beat env_file: - - .env - environment: - <<: *api-and-worker-env + - ./apiserver/.env depends_on: - plane-api - plane-db @@ -163,8 +104,6 @@ services: command: server /export --console-address ":9090" volumes: - uploads:/export - env_file: - - .env environment: MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} @@ -187,8 +126,6 @@ services: restart: always ports: - ${NGINX_PORT}:80 - env_file: - - .env environment: FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} diff --git a/replace-env-vars.sh b/replace-env-vars.sh deleted file mode 100644 index 949ffd7d7..000000000 --- a/replace-env-vars.sh +++ /dev/null @@ -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" "{}" diff --git a/setup.sh b/setup.sh index 235e1a977..87c0f445b 100755 --- a/setup.sh +++ b/setup.sh @@ -5,15 +5,12 @@ cp ./.env.example ./.env export LC_ALL=C export LC_CTYPE=C - -# Generate the NEXT_PUBLIC_API_BASE_URL with given IP -echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env +cp ./web/.env.example ./web/.env +cp ./space/.env.example ./space/.env +cp ./apiserver/.env.example ./apiserver/.env # Generate the SECRET_KEY that will be used by django -echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env - -# WEB_URL for email redirection and image saving -echo -e "WEB_URL=$1" >> ./.env +echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env # Generate Prompt for taking tiptap auth key echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n" @@ -21,9 +18,7 @@ echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m" echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n" -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/ -//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc - +//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc \ No newline at end of file diff --git a/space/.env.example b/space/.env.example index 238f70854..c7063c155 100644 --- a/space/.env.example +++ b/space/.env.example @@ -1,5 +1,3 @@ -# 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 diff --git a/space/Dockerfile.space b/space/Dockerfile.space index 963dad136..12c309134 100644 --- a/space/Dockerfile.space +++ b/space/Dockerfile.space @@ -1,7 +1,6 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat WORKDIR /app -ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . @@ -20,19 +19,16 @@ RUN yarn install --network-timeout 500000 COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json -COPY replace-env-vars.sh /usr/local/bin/ 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 -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 /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 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/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 -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 -COPY replace-env-vars.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 USER captain diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts new file mode 100644 index 000000000..d96c342b5 --- /dev/null +++ b/space/helpers/common.helper.ts @@ -0,0 +1,2 @@ +export const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL !== undefined ? process.env.NEXT_PUBLIC_API_BASE_URL : "http://localhost:8000"; diff --git a/space/services/authentication.service.ts b/space/services/authentication.service.ts index a6f1ec90f..4d861994f 100644 --- a/space/services/authentication.service.ts +++ b/space/services/authentication.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class AuthService extends APIService { constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async emailLogin(data: any) { diff --git a/space/services/file.service.ts b/space/services/file.service.ts index 5ef34fc76..d9783d29c 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -1,7 +1,5 @@ -// services import APIService from "services/api.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; interface UnSplashImage { id: string; @@ -29,7 +27,7 @@ interface UnSplashImageUrls { class FileServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async uploadFile(workspaceSlug: string, file: FormData): Promise { diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts index 835778fb2..5feb1b00b 100644 --- a/space/services/issue.service.ts +++ b/space/services/issue.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class IssueService extends APIService { 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 { diff --git a/space/services/project.service.ts b/space/services/project.service.ts index 291a5f323..0d6eca951 100644 --- a/space/services/project.service.ts +++ b/space/services/project.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class ProjectService extends APIService { 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 { diff --git a/space/services/user.service.ts b/space/services/user.service.ts index 9a324bb95..21e9f941e 100644 --- a/space/services/user.service.ts +++ b/space/services/user.service.ts @@ -1,9 +1,10 @@ // services import APIService from "services/api.service"; +import { API_BASE_URL } from "helpers/common.helper"; class UserService extends APIService { constructor() { - super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async currentUser(): Promise { diff --git a/start.sh b/start.sh index dcb97db6d..2685c3826 100644 --- a/start.sh +++ b/start.sh @@ -1,9 +1,5 @@ #!/bin/sh 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.." node $1 diff --git a/web/.env.example b/web/.env.example index 50a6209b2..88a2064c5 100644 --- a/web/.env.example +++ b/web/.env.example @@ -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 NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS= # Google Client ID for Google OAuth @@ -23,4 +21,4 @@ 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="" \ No newline at end of file +NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces" \ No newline at end of file diff --git a/web/Dockerfile.web b/web/Dockerfile.web index 40946fa2d..d9260e61d 100644 --- a/web/Dockerfile.web +++ b/web/Dockerfile.web @@ -2,7 +2,6 @@ FROM node:18-alpine AS builder RUN apk add --no-cache libc6-compat # Set working directory WORKDIR /app -ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER RUN yarn global add turbo COPY . . @@ -14,8 +13,8 @@ FROM node:18-alpine AS installer RUN apk add --no-cache libc6-compat WORKDIR /app -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 -ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces +ARG NEXT_PUBLIC_API_BASE_URL="" +ARG NEXT_PUBLIC_DEPLOY_URL="" # First install the dependencies (as they change less often) COPY .gitignore .gitignore @@ -26,18 +25,12 @@ RUN yarn install --network-timeout 500000 # Build the project COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json -COPY replace-env-vars.sh /usr/local/bin/ USER root -RUN chmod +x /usr/local/bin/replace-env-vars.sh - -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 +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL 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 WORKDIR /app @@ -52,20 +45,15 @@ COPY --from=installer /app/web/package.json . # Automatically leverage output traces to reduce image size # 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 ./web/.next -ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 -ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces - -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 +ARG NEXT_PUBLIC_API_BASE_URL="" +ARG NEXT_PUBLIC_DEPLOY_URL="" +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL USER root -COPY replace-env-vars.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 USER captain diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index 173a5242c..56ed10ee0 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -63,7 +63,11 @@ export const PublishProjectModal: React.FC = observer(() => { const [isUnpublishing, setIsUnpublishing] = 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 { workspaceSlug } = router.query; diff --git a/web/helpers/common.helper.ts b/web/helpers/common.helper.ts index 4220a7174..0829863c9 100644 --- a/web/helpers/common.helper.ts +++ b/web/helpers/common.helper.ts @@ -16,3 +16,8 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) => if (callNow) func(...args); }; }; + +export const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL !== undefined + ? process.env.NEXT_PUBLIC_API_BASE_URL + : "http://localhost:8000"; diff --git a/web/layouts/app-layout/app-header.tsx b/web/layouts/app-layout/app-header.tsx index f2fbdc78c..27c52f654 100644 --- a/web/layouts/app-layout/app-header.tsx +++ b/web/layouts/app-layout/app-header.tsx @@ -17,7 +17,11 @@ type Props = { }; 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 = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => { const { projectDetails } = useProjectDetails(); diff --git a/web/lib/auth.ts b/web/lib/auth.ts index 47a52663d..56cfab9ae 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -2,6 +2,8 @@ import { convertCookieStringToObject } from "./cookie"; // types import type { IProjectMember, IUser, IWorkspace, IWorkspaceMember } from "types"; +// helper +import { API_BASE_URL } from "helpers/common.helper"; export const requiredAuth = async (cookie?: string) => { const cookies = convertCookieStringToObject(cookie); @@ -9,12 +11,10 @@ export const requiredAuth = async (cookie?: string) => { if (!token) return null; - const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so"; - let user: IUser | null = null; try { - const data = await fetch(`${baseUrl}/api/users/me/`, { + const data = await fetch(`${API_BASE_URL}/api/users/me/`, { method: "GET", headers: { "Content-Type": "application/json", @@ -41,13 +41,11 @@ export const requiredAdmin = async (workspaceSlug: string, projectId: string, co const cookies = convertCookieStringToObject(cookie); const token = cookies?.accessToken; - const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so"; - let memberDetail: IProjectMember | null = null; try { 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", headers: { @@ -75,17 +73,18 @@ export const requiredWorkspaceAdmin = async (workspaceSlug: string, cookie?: str const cookies = convertCookieStringToObject(cookie); const token = cookies?.accessToken; - const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so"; - let memberDetail: IWorkspaceMember | null = null; try { - const data = await fetch(`${baseUrl}/api/workspaces/${workspaceSlug}/workspace-members/me/`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const data = await fetch( + `${API_BASE_URL}/api/workspaces/${workspaceSlug}/workspace-members/me/`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) .then((res) => res.json()) .then((data) => data); @@ -119,13 +118,11 @@ export const homePageRedirect = async (cookie?: string) => { let workspaces: IWorkspace[] = []; - const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.plane.so"; - const cookies = convertCookieStringToObject(cookie); const token = cookies?.accessToken; try { - const data = await fetch(`${baseUrl}/api/users/me/workspaces/`, { + const data = await fetch(`${API_BASE_URL}/api/users/me/workspaces/`, { method: "GET", headers: { "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", headers: { "Content-Type": "application/json", diff --git a/web/services/ai.service.ts b/web/services/ai.service.ts index ecb1ada52..33b59f3df 100644 --- a/web/services/ai.service.ts +++ b/web/services/ai.service.ts @@ -1,18 +1,16 @@ -// services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - // types import { ICurrentUserResponse, IGptResponse } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class AiServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createGptTask( diff --git a/web/services/analytics.service.ts b/web/services/analytics.service.ts index 0b38f8c57..66b6569e8 100644 --- a/web/services/analytics.service.ts +++ b/web/services/analytics.service.ts @@ -8,12 +8,11 @@ import { IExportAnalyticsFormData, ISaveAnalyticsFormData, } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; class AnalyticsServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise { diff --git a/web/services/app-installations.service.ts b/web/services/app-installations.service.ts index dea6fa421..8b18cddfc 100644 --- a/web/services/app-installations.service.ts +++ b/web/services/app-installations.service.ts @@ -1,12 +1,11 @@ // services import axios from "axios"; import APIService from "services/api.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; class AppInstallationsService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async addInstallationApp(workspaceSlug: string, provider: string, data: any): Promise { diff --git a/web/services/authentication.service.ts b/web/services/authentication.service.ts index e4a33bff8..58eaa2aff 100644 --- a/web/services/authentication.service.ts +++ b/web/services/authentication.service.ts @@ -1,12 +1,11 @@ // services import APIService from "services/api.service"; import { ICurrentUserResponse } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; class AuthService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async emailLogin(data: any) { diff --git a/web/services/cycles.service.ts b/web/services/cycles.service.ts index 89cd50a2f..e0e42c687 100644 --- a/web/services/cycles.service.ts +++ b/web/services/cycles.service.ts @@ -1,18 +1,16 @@ // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - // types import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class ProjectCycleServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createCycle( diff --git a/web/services/estimates.service.ts b/web/services/estimates.service.ts index 8b0fe25f4..eaa49e57c 100644 --- a/web/services/estimates.service.ts +++ b/web/services/estimates.service.ts @@ -3,15 +3,14 @@ import APIService from "services/api.service"; // types import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types"; import trackEventServices from "services/track-event.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class ProjectEstimateServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createEstimate( diff --git a/web/services/file.service.ts b/web/services/file.service.ts index d2f01428d..cbed73fc8 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -1,7 +1,6 @@ // services import APIService from "services/api.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; interface UnSplashImage { id: string; @@ -29,7 +28,7 @@ interface UnSplashImageUrls { class FileServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async uploadFile(workspaceSlug: string, file: FormData): Promise { diff --git a/web/services/inbox.service.ts b/web/services/inbox.service.ts index 61949c877..16eed288e 100644 --- a/web/services/inbox.service.ts +++ b/web/services/inbox.service.ts @@ -1,7 +1,6 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; @@ -19,7 +18,7 @@ import type { class InboxServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getInboxes(workspaceSlug: string, projectId: string): Promise { diff --git a/web/services/integration/csv.services.ts b/web/services/integration/csv.services.ts index f19cc4a74..0b53f7778 100644 --- a/web/services/integration/csv.services.ts +++ b/web/services/integration/csv.services.ts @@ -1,16 +1,14 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - import { ICurrentUserResponse } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class CSVIntegrationService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async exportCSVService( diff --git a/web/services/integration/github.service.ts b/web/services/integration/github.service.ts index 494785f04..58aa12318 100644 --- a/web/services/integration/github.service.ts +++ b/web/services/integration/github.service.ts @@ -1,5 +1,6 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; +import { API_BASE_URL } from "helpers/common.helper"; import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types"; @@ -11,7 +12,7 @@ const trackEvent = const integrationServiceType: string = "github"; class GithubIntegrationService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async listAllRepositories(workspaceSlug: string, integrationSlug: string): Promise { diff --git a/web/services/integration/index.ts b/web/services/integration/index.ts index 2b32a5bd0..b1bbefda8 100644 --- a/web/services/integration/index.ts +++ b/web/services/integration/index.ts @@ -9,15 +9,14 @@ import { IWorkspaceIntegration, IExportServiceResponse, } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class IntegrationService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getAppIntegrationsList(): Promise { diff --git a/web/services/integration/jira.service.ts b/web/services/integration/jira.service.ts index 8f6a9fec9..d4620f3ff 100644 --- a/web/services/integration/jira.service.ts +++ b/web/services/integration/jira.service.ts @@ -1,6 +1,6 @@ import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - +import { API_BASE_URL } from "helpers/common.helper"; // types import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types"; @@ -11,7 +11,7 @@ const trackEvent = class JiraImportedService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getJiraProjectInfo(workspaceSlug: string, params: IJiraMetadata): Promise { diff --git a/web/services/issues.service.ts b/web/services/issues.service.ts index 7b1481812..ede46ad5f 100644 --- a/web/services/issues.service.ts +++ b/web/services/issues.service.ts @@ -10,15 +10,14 @@ import type { IIssueLabels, ISubIssueResponse, } from "types"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class ProjectIssuesServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createIssues( diff --git a/web/services/modules.service.ts b/web/services/modules.service.ts index 898164366..dcac98d50 100644 --- a/web/services/modules.service.ts +++ b/web/services/modules.service.ts @@ -1,9 +1,9 @@ // services import APIService from "services/api.service"; import trackEventServices from "./track-event.service"; - // types -import type { IModule, IIssue, ICurrentUserResponse } from "types"; +import type { IIssueViewOptions, IModule, IIssue, ICurrentUserResponse } from "types"; +import { API_BASE_URL } from "helpers/common.helper"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -12,7 +12,7 @@ const trackEvent = class ProjectIssuesServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getModules(workspaceSlug: string, projectId: string): Promise { diff --git a/web/services/notifications.service.ts b/web/services/notifications.service.ts index 01c139b51..8ff64e915 100644 --- a/web/services/notifications.service.ts +++ b/web/services/notifications.service.ts @@ -1,8 +1,5 @@ // services import APIService from "services/api.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - // types import type { IUserNotification, @@ -11,10 +8,12 @@ import type { PaginatedUserNotification, IMarkAllAsReadPayload, } from "types"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; class UserNotificationsServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getUserNotifications( diff --git a/web/services/pages.service.ts b/web/services/pages.service.ts index 72b12012e..b9dc580f3 100644 --- a/web/services/pages.service.ts +++ b/web/services/pages.service.ts @@ -1,18 +1,16 @@ +import { API_BASE_URL } from "helpers/common.helper"; // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - // types import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types"; -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class PageServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createPage( diff --git a/web/services/project-publish.service.ts b/web/services/project-publish.service.ts index 4ee01a94b..05555bd29 100644 --- a/web/services/project-publish.service.ts +++ b/web/services/project-publish.service.ts @@ -1,3 +1,4 @@ +import { API_BASE_URL } from "helpers/common.helper"; // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; @@ -5,14 +6,12 @@ import trackEventServices from "services/track-event.service"; import { ICurrentUserResponse } from "types"; import { IProjectPublishSettings } from "store/project-publish"; -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class ProjectServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async getProjectSettingsAsync( diff --git a/web/services/project.service.ts b/web/services/project.service.ts index 0d97b9efb..829f9903d 100644 --- a/web/services/project.service.ts +++ b/web/services/project.service.ts @@ -1,7 +1,7 @@ +import { API_BASE_URL } from "helpers/common.helper"; // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - // types import type { GithubRepositoriesResponse, @@ -16,14 +16,12 @@ import type { TProjectIssuesSearchParams, } from "types"; -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; export class ProjectServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createProject( diff --git a/web/services/reaction.service.ts b/web/services/reaction.service.ts index 3ba8a83e4..35dc915a1 100644 --- a/web/services/reaction.service.ts +++ b/web/services/reaction.service.ts @@ -1,7 +1,7 @@ +import { API_BASE_URL } from "helpers/common.helper"; // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - // types import type { ICurrentUserResponse, @@ -11,14 +11,12 @@ import type { IssueCommentReactionForm, } from "types"; -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class ReactionService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createIssueReaction( diff --git a/web/services/state.service.ts b/web/services/state.service.ts index 52481f8bb..a97753d6d 100644 --- a/web/services/state.service.ts +++ b/web/services/state.service.ts @@ -1,8 +1,8 @@ // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = 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 { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createState( diff --git a/web/services/user.service.ts b/web/services/user.service.ts index 0e5def647..6a8523348 100644 --- a/web/services/user.service.ts +++ b/web/services/user.service.ts @@ -12,14 +12,14 @@ import type { IUserWorkspaceDashboard, } from "types"; -const { NEXT_PUBLIC_API_BASE_URL } = process.env; +import { API_BASE_URL } from "helpers/common.helper"; const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1"; class UserService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } currentUserConfig() { diff --git a/web/services/views.service.ts b/web/services/views.service.ts index e1d25925e..a684e41ff 100644 --- a/web/services/views.service.ts +++ b/web/services/views.service.ts @@ -6,6 +6,8 @@ import { ICurrentUserResponse } from "types"; // types import { IView } from "types/views"; +import { API_BASE_URL } from "helpers/common.helper"; + const { NEXT_PUBLIC_API_BASE_URL } = process.env; const trackEvent = @@ -13,7 +15,7 @@ const trackEvent = class ViewServices extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async createView( diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 8097253e6..0539cdf8c 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -1,9 +1,8 @@ // services import APIService from "services/api.service"; import trackEventServices from "services/track-event.service"; - -const { NEXT_PUBLIC_API_BASE_URL } = process.env; - +// helpers +import { API_BASE_URL } from "helpers/common.helper"; // types import { IWorkspace, @@ -22,7 +21,7 @@ const trackEvent = class WorkspaceService extends APIService { constructor() { - super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + super(API_BASE_URL); } async userWorkspaces(): Promise { From 87abf3ccb1b2c23b260f934c3c90ff2e47e89eee Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 13 Sep 2023 23:09:55 +0530 Subject: [PATCH 22/72] style: project setting ui revamp (#2177) * style: project settings navigation sidebar added * chore: emoji and image picker close on outside click added * style: project setting general page revamp * style: project setting member page revamp * style: project setting features page revamp * style: project setting state page revamp * style: project setting integrations page revamp * style: project setting estimates page revamp * style: project setting automation page revamp * style: project setting label page revamp * chore: member select improvement for member setting page * chore: toggle switch component improvement * style: project automation setting ui improvement * style: module icon added * style: toggle switch improvement * style: ui and spacing consistency * style: project label setting revamp * style: project state setting ui improvement * chore: integration setting repo select validation * chore: code refactor * fix: build fix --- .../automation/auto-archive-automation.tsx | 93 ++-- .../automation/auto-close-automation.tsx | 177 +++---- web/components/core/image-picker-popover.tsx | 12 +- web/components/emoji-icon-picker/index.tsx | 13 +- web/components/estimates/single-estimate.tsx | 2 +- web/components/icons/index.ts | 1 + web/components/icons/module-icon.tsx | 59 +++ .../integration/github/select-repository.tsx | 2 + .../integration/slack/select-channel.tsx | 8 +- .../labels/create-update-label-inline.tsx | 16 +- web/components/labels/single-label-group.tsx | 73 +-- web/components/labels/single-label.tsx | 57 ++- web/components/project/index.ts | 3 +- web/components/project/member-select.tsx | 74 +++ web/components/project/settings-header.tsx | 13 - web/components/project/settings-sidebar.tsx | 72 +++ .../project/single-integration-card.tsx | 11 +- web/components/states/single-state.tsx | 73 +-- .../integration-and-import-export-banner.tsx | 4 +- web/components/ui/toggle-switch.tsx | 16 +- web/package.json | 2 +- .../[projectId]/settings/automations.tsx | 15 +- .../projects/[projectId]/settings/control.tsx | 241 --------- .../[projectId]/settings/estimates.tsx | 120 ++--- .../[projectId]/settings/features.tsx | 64 ++- .../projects/[projectId]/settings/index.tsx | 458 ++++++++++-------- .../[projectId]/settings/integrations.tsx | 12 +- .../projects/[projectId]/settings/labels.tsx | 29 +- .../projects/[projectId]/settings/members.tsx | 214 +++++++- .../projects/[projectId]/settings/states.tsx | 30 +- web/services/modules.service.ts | 2 +- 31 files changed, 1090 insertions(+), 876 deletions(-) create mode 100644 web/components/icons/module-icon.tsx create mode 100644 web/components/project/member-select.tsx delete mode 100644 web/components/project/settings-header.tsx create mode 100644 web/components/project/settings-sidebar.tsx delete mode 100644 web/pages/[workspaceSlug]/projects/[projectId]/settings/control.tsx diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 50ab4f904..bb4e72e0c 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -3,8 +3,8 @@ import React, { useState } from "react"; // component import { CustomSelect, ToggleSwitch } from "components/ui"; import { SelectMonthModal } from "components/automation"; -// icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; +// icon +import { ArchiveRestore } from "lucide-react"; // constants import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; // types @@ -28,14 +28,18 @@ export const AutoArchiveAutomation: React.FC = ({ projectDetails, handleC handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
-
-
-

Auto-archive closed issues

-

- Plane will automatically archive issues that have been completed or cancelled for the - configured time period. -

+
+
+
+
+ +
+
+

Auto-archive closed issues

+

+ Plane will auto archive issues that have been completed or canceled. +

+
= ({ projectDetails, handleC size="sm" />
- {projectDetails?.archive_in !== 0 && ( -
-
- Auto-archive issues that are closed for -
-
- { - handleChange({ archive_in: val }); - }} - input - verticalPosition="top" - width="w-full" - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - + {projectDetails?.archive_in !== 0 && ( +
+
+
+ Auto-archive issues that are closed for +
+
+ { + handleChange({ archive_in: val }); + }} + input + verticalPosition="bottom" + width="w-full" + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + + +
)} diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index f6cf95f2d..8235c8063 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -5,11 +5,12 @@ import useSWR from "swr"; import { useRouter } from "next/router"; // component -import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui"; +import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui"; import { SelectMonthModal } from "components/automation"; // icons -import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import { Squares2X2Icon } from "@heroicons/react/24/outline"; import { StateGroupIcon } from "components/icons"; +import { ArchiveX } from "lucide-react"; // services import stateService from "services/state.service"; // constants @@ -76,14 +77,18 @@ export const AutoCloseAutomation: React.FC = ({ projectDetails, handleCha handleChange={handleChange} /> -
-
-
-

Auto-close inactive issues

-

- Plane will automatically close the issues that have not been updated for the - configured time period. -

+
+
+
+
+ +
+
+

Auto-close issues

+

+ Plane will automatically close issue that haven’t been completed or canceled. +

+
= ({ projectDetails, handleCha size="sm" />
+ {projectDetails?.close_in !== 0 && ( -
-
-
- Auto-close issues that are inactive for +
+
+
+
+ Auto-close issues that are inactive for +
+
+ { + handleChange({ close_in: val }); + }} + input + width="w-full" + > + <> + {PROJECT_AUTOMATION_MONTHS.map((month) => ( + + {month.label} + + ))} + + + +
-
- { - handleChange({ close_in: val }); - }} - input - width="w-full" - > - <> - {PROJECT_AUTOMATION_MONTHS.map((month) => ( - - {month.label} - - ))} - - - -
-
-
-
Auto-close Status
-
- - {selectedOption ? ( - - ) : currentDefaultState ? ( - - ) : ( - - )} - {selectedOption?.name - ? selectedOption.name - : currentDefaultState?.name ?? ( - State - )} -
- } - onChange={(val: string) => { - handleChange({ default_state: val }); - }} - options={options} - disabled={!multipleOptions} - width="w-full" - input - /> + +
+
Auto-close Status
+
+ + {selectedOption ? ( + + ) : currentDefaultState ? ( + + ) : ( + + )} + {selectedOption?.name + ? selectedOption.name + : currentDefaultState?.name ?? ( + State + )} +
+ } + onChange={(val: string) => { + handleChange({ default_state: val }); + }} + options={options} + disabled={!multipleOptions} + width="w-full" + input + /> +
diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 5f13d960e..957f1131c 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -20,6 +20,7 @@ import fileService from "services/file.service"; import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui"; // hooks import useWorkspaceDetails from "hooks/use-workspace-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; const unsplashEnabled = process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" || @@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC = ({ fileService.getUnsplashImages(1, searchParams) ); + const imagePickerRef = useRef(null); + const { workspaceDetails } = useWorkspaceDetails(); const onDrop = useCallback((acceptedFiles: File[]) => { @@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC = ({ onChange(images[0].urls.regular); }, [value, onChange, images]); + useOutsideClickDetector(imagePickerRef, () => setIsOpen(false)); + if (!unsplashEnabled) return null; return ( setIsOpen((prev) => !prev)} disabled={disabled} > @@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC = ({ leaveTo="transform opacity-0 scale-95" > -
+
diff --git a/web/components/emoji-icon-picker/index.tsx b/web/components/emoji-icon-picker/index.tsx index 7af3bb74f..ab4eb022e 100644 --- a/web/components/emoji-icon-picker/index.tsx +++ b/web/components/emoji-icon-picker/index.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; // headless ui import { Tab, Transition, Popover } from "@headlessui/react"; // react colors import { TwitterPicker } from "react-color"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types import { Props } from "./types"; // emojis @@ -36,6 +38,8 @@ const EmojiIconPicker: React.FC = ({ const [recentEmojis, setRecentEmojis] = useState([]); + const emojiPickerRef = useRef(null); + useEffect(() => { setRecentEmojis(getRecentEmojis()); }, []); @@ -44,6 +48,8 @@ const EmojiIconPicker: React.FC = ({ if (!value || value?.length === 0) onChange(getRandomEmoji()); }, [value, onChange]); + useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false)); + return ( = ({ leaveTo="transform opacity-0 scale-95" > -
+
{tabOptions.map((tab) => ( diff --git a/web/components/estimates/single-estimate.tsx b/web/components/estimates/single-estimate.tsx index 3adf986ae..43edfcb2c 100644 --- a/web/components/estimates/single-estimate.tsx +++ b/web/components/estimates/single-estimate.tsx @@ -66,7 +66,7 @@ export const SingleEstimate: React.FC = ({ return ( <> -
+
diff --git a/web/components/icons/index.ts b/web/components/icons/index.ts index ab661a092..bf3e94332 100644 --- a/web/components/icons/index.ts +++ b/web/components/icons/index.ts @@ -84,3 +84,4 @@ export * from "./clock-icon"; export * from "./bell-icon"; export * from "./single-comment-icon"; export * from "./related-icon"; +export * from "./module-icon"; \ No newline at end of file diff --git a/web/components/icons/module-icon.tsx b/web/components/icons/module-icon.tsx new file mode 100644 index 000000000..dbe58eb53 --- /dev/null +++ b/web/components/icons/module-icon.tsx @@ -0,0 +1,59 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const ModuleIcon: React.FC = ({ + width = "24", + height = "24", + className, + color = "#F15B5B", +}) => ( + + + + + + + +); diff --git a/web/components/integration/github/select-repository.tsx b/web/components/integration/github/select-repository.tsx index 9857c0088..b46942e6d 100644 --- a/web/components/integration/github/select-repository.tsx +++ b/web/components/integration/github/select-repository.tsx @@ -66,6 +66,8 @@ export const SelectRepository: React.FC = ({ content:

{truncateText(repo.full_name, characterLimit)}

, })) ?? []; + if (userRepositories.length < 1) return null; + return ( = ({ integration }) => { {projectIntegration ? ( diff --git a/web/components/labels/create-update-label-inline.tsx b/web/components/labels/create-update-label-inline.tsx index 6306d14ca..61064e777 100644 --- a/web/components/labels/create-update-label-inline.tsx +++ b/web/components/labels/create-update-label-inline.tsx @@ -17,7 +17,7 @@ import issuesService from "services/issues.service"; // ui import { Input, PrimaryButton, SecondaryButton } from "components/ui"; // icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { Component } from "lucide-react"; // types import { IIssueLabels } from "types"; // fetch-keys @@ -132,7 +132,7 @@ export const CreateUpdateLabelInline = forwardRef( return (
( open ? "text-custom-text-100" : "text-custom-text-200" }`} > - -