From f4aa059da6682a44ad32ce15b285b2bd0d72edd4 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Sat, 2 Dec 2023 18:19:29 +0530 Subject: [PATCH 01/17] fix: global issues properties updation issue resolved (#2974) --- .../roots/all-issue-layout-root.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index a49af10ae..9f7fbdb7c 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -63,23 +63,23 @@ export const AllIssueLayoutRoot: React.FC = observer((props) => { const issueActions = { [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const projectId = issue.project; + if (!workspaceSlug || !projectId) return; - await updateIssue(workspaceSlug, issue.project, issue.id, issue); + await updateIssue(workspaceSlug, projectId, issue.id, issue, currentIssueView); }, [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const projectId = issue.project; + if (!workspaceSlug || !projectId) return; - await removeIssue(workspaceSlug, issue.project, issue.id); + await removeIssue(workspaceSlug, projectId, issue.id, currentIssueView); }, }; const handleIssues = useCallback( async (issue: IIssue, action: EIssueActions) => { - if (issueActions && action && issue) { - if (action === EIssueActions.UPDATE) await issueActions[action]!(issue); - if (action === EIssueActions.DELETE) await issueActions[action]!(issue); - } + if (action === EIssueActions.UPDATE) await issueActions[action]!(issue); + if (action === EIssueActions.DELETE) await issueActions[action]!(issue); }, [getIssues] ); From 9756abece42cd43452f724f6719aabe9369a12c6 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Sat, 2 Dec 2023 18:19:59 +0530 Subject: [PATCH 02/17] chore: update instance admin sign in endpoint (#2973) --- web/services/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts index 47325cf30..175fe8a76 100644 --- a/web/services/auth.service.ts +++ b/web/services/auth.service.ts @@ -135,7 +135,7 @@ export class AuthService extends APIService { } async instanceMagicSignIn(data: any): Promise { - const response = await this.post("/api/licenses/instances/admins/magic-sign-in/", data); + const response = await this.post("/api/licenses/instances/admins/magic-sign-in/", data, { headers: {} }); if (response?.status === 200) { this.setAccessToken(response?.data?.access_token); this.setRefreshToken(response?.data?.refresh_token); From 59110c7205eda8084618074495b84ea8ae83b23a Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Sat, 2 Dec 2023 18:23:07 +0530 Subject: [PATCH 03/17] fix: sub display filter params for fetching issues (#2972) * fix add subgroup issue FED-1101 * fix subgroup by None assignee FED-1100 * fix grouping by asignee or labels FED-1096 * fix create view popup FED-1093 * fix subgroup exception in swimlanes * fix show sub issue filter FED-1102 --- web/store/issues/global/filter.store.ts | 7 +++++++ web/store/issues/profile/filter.store.ts | 11 ++++++++--- .../issues/project-issues/archived/filter.store.ts | 11 ++++++++--- web/store/issues/project-issues/cycle/filter.store.ts | 11 ++++++++--- web/store/issues/project-issues/draft/filter.store.ts | 11 ++++++++--- .../issues/project-issues/module/filter.store.ts | 11 ++++++++--- .../project-issues/project-view/filter.store.ts | 11 ++++++++--- .../issues/project-issues/project/filter.store.ts | 11 ++++++++--- web/store/issues/project-issues/utils.ts | 5 +++++ 9 files changed, 68 insertions(+), 21 deletions(-) create mode 100644 web/store/issues/project-issues/utils.ts diff --git a/web/store/issues/global/filter.store.ts b/web/store/issues/global/filter.store.ts index b495bb816..4302acc76 100644 --- a/web/store/issues/global/filter.store.ts +++ b/web/store/issues/global/filter.store.ts @@ -6,6 +6,7 @@ import { EFilterType } from "store/issues/types"; import { IssueFilterBaseStore } from "../project-issues/base-issue-filter.store"; // helpers import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +import { isNil } from "../project-issues/utils"; // services import { WorkspaceService } from "services/workspace.service"; @@ -377,6 +378,12 @@ export class GlobalIssuesFilterStore extends IssueFilterBaseStore implements IGl start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, sub_issue: false, }; diff --git a/web/store/issues/profile/filter.store.ts b/web/store/issues/profile/filter.store.ts index 0b684f7a7..dcb760d1a 100644 --- a/web/store/issues/profile/filter.store.ts +++ b/web/store/issues/profile/filter.store.ts @@ -6,6 +6,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { EFilterType } from "store/issues/types"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { IssueFilterBaseStore } from "../project-issues/base-issue-filter.store"; +import { isNil } from "../project-issues/utils"; interface IProjectIssuesFiltersOptions { filters: IIssueFilterOptions; @@ -285,9 +286,13 @@ export class ProfileIssuesFilterStore extends IssueFilterBaseStore implements IP start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "profile_issues"); diff --git a/web/store/issues/project-issues/archived/filter.store.ts b/web/store/issues/project-issues/archived/filter.store.ts index 77e30c4fc..0b3ce15a9 100644 --- a/web/store/issues/project-issues/archived/filter.store.ts +++ b/web/store/issues/project-issues/archived/filter.store.ts @@ -8,6 +8,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { RootStore } from "store/root"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; import { EFilterType } from "store/issues/types"; +import { isNil } from "../utils"; interface IProjectIssuesFilters { filters: IIssueFilterOptions | undefined; @@ -78,9 +79,13 @@ export class ProjectArchivedIssuesFilterStore start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); diff --git a/web/store/issues/project-issues/cycle/filter.store.ts b/web/store/issues/project-issues/cycle/filter.store.ts index 6e73d7613..b78999d7e 100644 --- a/web/store/issues/project-issues/cycle/filter.store.ts +++ b/web/store/issues/project-issues/cycle/filter.store.ts @@ -11,6 +11,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { RootStore } from "store/root"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; import { EFilterType } from "store/issues/types"; +import { isNil } from "../utils"; interface ICycleIssuesFilterOptions { filters: IIssueFilterOptions; @@ -118,9 +119,13 @@ export class CycleIssuesFilterStore extends IssueFilterBaseStore implements ICyc start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); diff --git a/web/store/issues/project-issues/draft/filter.store.ts b/web/store/issues/project-issues/draft/filter.store.ts index 7cfca229b..0c0a3ba37 100644 --- a/web/store/issues/project-issues/draft/filter.store.ts +++ b/web/store/issues/project-issues/draft/filter.store.ts @@ -8,6 +8,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { RootStore } from "store/root"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; import { EFilterType } from "store/issues/types"; +import { isNil } from "../utils"; interface IProjectIssuesFilters { filters: IIssueFilterOptions | undefined; @@ -75,9 +76,13 @@ export class ProjectDraftIssuesFilterStore extends IssueFilterBaseStore implemen start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); diff --git a/web/store/issues/project-issues/module/filter.store.ts b/web/store/issues/project-issues/module/filter.store.ts index a30103314..103528ba8 100644 --- a/web/store/issues/project-issues/module/filter.store.ts +++ b/web/store/issues/project-issues/module/filter.store.ts @@ -11,6 +11,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { RootStore } from "store/root"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; import { EFilterType } from "store/issues/types"; +import { isNil } from "../utils"; interface IModuleIssuesFilterOptions { filters: IIssueFilterOptions; @@ -118,9 +119,13 @@ export class ModuleIssuesFilterStore extends IssueFilterBaseStore implements IMo start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); diff --git a/web/store/issues/project-issues/project-view/filter.store.ts b/web/store/issues/project-issues/project-view/filter.store.ts index 5aa25f604..215e3749f 100644 --- a/web/store/issues/project-issues/project-view/filter.store.ts +++ b/web/store/issues/project-issues/project-view/filter.store.ts @@ -11,6 +11,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { RootStore } from "store/root"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; import { EFilterType } from "store/issues/types"; +import { isNil } from "../utils"; interface IViewIssuesFilterOptions { filters: IIssueFilterOptions; @@ -118,9 +119,13 @@ export class ViewIssuesFilterStore extends IssueFilterBaseStore implements IView start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); diff --git a/web/store/issues/project-issues/project/filter.store.ts b/web/store/issues/project-issues/project/filter.store.ts index bb64b5784..8caf189ad 100644 --- a/web/store/issues/project-issues/project/filter.store.ts +++ b/web/store/issues/project-issues/project/filter.store.ts @@ -8,6 +8,7 @@ import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { RootStore } from "store/root"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; import { EFilterType } from "store/issues/types"; +import { isNil } from "../utils"; interface IProjectIssuesFilters { filters: IIssueFilterOptions | undefined; @@ -75,9 +76,13 @@ export class ProjectIssuesFilterStore extends IssueFilterBaseStore implements IP start_date: userFilters?.filters?.start_date || undefined, target_date: userFilters?.filters?.target_date || undefined, type: userFilters?.displayFilters?.type || undefined, - sub_issue: userFilters?.displayFilters?.sub_issue || true, - show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, - start_target_date: userFilters?.displayFilters?.start_target_date || true, + sub_issue: isNil(userFilters?.displayFilters?.sub_issue) ? true : userFilters?.displayFilters?.sub_issue, + show_empty_groups: isNil(userFilters?.displayFilters?.show_empty_groups) + ? true + : userFilters?.displayFilters?.show_empty_groups, + start_target_date: isNil(userFilters?.displayFilters?.start_target_date) + ? true + : userFilters?.displayFilters?.start_target_date, }; const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); diff --git a/web/store/issues/project-issues/utils.ts b/web/store/issues/project-issues/utils.ts new file mode 100644 index 000000000..11eb6b90d --- /dev/null +++ b/web/store/issues/project-issues/utils.ts @@ -0,0 +1,5 @@ +export const isNil = (value: any) => { + if (value === undefined || value === null) return true; + + return false; +}; From 79fa860b28416be4ce303236fc3b8d0043528bb6 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:48:40 +0530 Subject: [PATCH 04/17] chore: instance (#2955) --- apiserver/.env.example | 6 +- apiserver/plane/api/serializers/issue.py | 24 ++++-- apiserver/plane/api/serializers/user.py | 6 +- apiserver/plane/app/serializers/user.py | 8 +- apiserver/plane/app/views/auth_extended.py | 32 ++++--- apiserver/plane/app/views/authentication.py | 29 ++++--- apiserver/plane/app/views/oauth.py | 8 -- .../plane/bgtasks/analytic_plot_export.py | 85 +++++++++++-------- .../plane/bgtasks/forgot_password_task.py | 7 +- .../plane/bgtasks/magic_link_code_task.py | 8 +- apiserver/plane/bgtasks/user_count_task.py | 6 +- .../bgtasks/workspace_invitation_task.py | 9 +- apiserver/plane/license/api/views/instance.py | 78 +++++++++-------- .../management/commands/register_instance.py | 16 +--- apiserver/plane/settings/common.py | 8 +- .../templates/emails/auth/magic_signin.html | 42 +++++---- .../templates/emails/exports/issues.html | 9 -- deploy/coolify/coolify-docker-compose.yml | 3 - deploy/selfhost/docker-compose.yml | 18 ++-- deploy/selfhost/variables.env | 12 +-- 20 files changed, 203 insertions(+), 211 deletions(-) delete mode 100644 apiserver/templates/emails/exports/issues.html diff --git a/apiserver/.env.example b/apiserver/.env.example index ace1e07b1..37178b398 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -14,6 +14,11 @@ PGHOST="plane-db" PGDATABASE="plane" DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} +# Oauth variables +GOOGLE_CLIENT_ID="" +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" + # Redis Settings REDIS_HOST="plane-redis" REDIS_PORT="6379" @@ -50,7 +55,6 @@ NGINX_PORT=80 # SignUps ENABLE_SIGNUP="1" - # Enable Email/Password Signup ENABLE_EMAIL_PASSWORD="1" diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 2dbdddfc6..10b3a4f85 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -21,7 +21,8 @@ from plane.db.models import ( from .base import BaseSerializer from .cycle import CycleSerializer, CycleLiteSerializer from .module import ModuleSerializer, ModuleLiteSerializer - +from .user import UserLiteSerializer +from .state import StateLiteSerializer class IssueSerializer(BaseSerializer): assignees = serializers.ListField( @@ -331,12 +332,23 @@ class ModuleIssueSerializer(BaseSerializer): ] -class IssueExpandSerializer(BaseSerializer): - # Serialize the related cycle. It's a OneToOne relation. - cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True) +class LabelLiteSerializer(BaseSerializer): - # Serialize the related module. It's a OneToOne relation. + class Meta: + model = Label + fields = [ + "id", + "name", + "color", + ] + + +class IssueExpandSerializer(BaseSerializer): + cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True) module = ModuleLiteSerializer(source="issue_module.module", read_only=True) + labels = LabelLiteSerializer(read_only=True, many=True) + assignees = UserLiteSerializer(read_only=True, many=True) + state = StateLiteSerializer(read_only=True) class Meta: model = Issue @@ -349,4 +361,4 @@ class IssueExpandSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index e5a77da93..42b6c3967 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -11,10 +11,6 @@ class UserLiteSerializer(BaseSerializer): "first_name", "last_name", "avatar", - "is_bot", "display_name", ] - read_only_fields = [ - "id", - "is_bot", - ] \ No newline at end of file + read_only_fields = fields \ No newline at end of file diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py index 5c9c69e5c..1b94758e8 100644 --- a/apiserver/plane/app/serializers/user.py +++ b/apiserver/plane/app/serializers/user.py @@ -169,8 +169,8 @@ class ChangePasswordSerializer(serializers.Serializer): Serializer for password change endpoint. """ old_password = serializers.CharField(required=True) - new_password = serializers.CharField(required=True) - confirm_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True, min_length=8) + confirm_password = serializers.CharField(required=True, min_length=8) def validate(self, data): if data.get("old_password") == data.get("new_password"): @@ -187,9 +187,7 @@ class ChangePasswordSerializer(serializers.Serializer): class ResetPasswordSerializer(serializers.Serializer): - model = User - """ Serializer for password change endpoint. """ - new_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True, min_length=8) diff --git a/apiserver/plane/app/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py index 1de511f89..2dc0fa983 100644 --- a/apiserver/plane/app/views/auth_extended.py +++ b/apiserver/plane/app/views/auth_extended.py @@ -105,17 +105,21 @@ class ForgotPasswordEndpoint(BaseAPIView): def post(self, request): email = request.data.get("email") - if User.objects.filter(email=email).exists(): - user = User.objects.get(email=email) - uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) - token = PasswordResetTokenGenerator().make_token(user) + try: + validate_email(email) + except ValidationError: + return Response({"error": "Please enter a valid email"}, status=status.HTTP_400_BAD_REQUEST) + # Get the user + user = User.objects.filter(email=email).first() + if user: + # Get the reset token for user + uidb64, token = get_tokens_for_user(user=user) current_site = request.META.get("HTTP_ORIGIN") - + # send the forgot password email forgot_password.delay( user.first_name, user.email, uidb64, token, current_site ) - return Response( {"message": "Check your email to reset your password"}, status=status.HTTP_200_OK, @@ -130,14 +134,18 @@ class ResetPasswordEndpoint(BaseAPIView): def post(self, request, uidb64, token): try: + # Decode the id from the uidb64 id = smart_str(urlsafe_base64_decode(uidb64)) user = User.objects.get(id=id) + + # check if the token is valid for the user if not PasswordResetTokenGenerator().check_token(user, token): return Response( {"error": "Token is invalid"}, status=status.HTTP_401_UNAUTHORIZED, ) + # Reset the password serializer = ResetPasswordSerializer(data=request.data) if serializer.is_valid(): # set_password also hashes the password that the user will get @@ -145,9 +153,9 @@ class ResetPasswordEndpoint(BaseAPIView): user.is_password_autoset = False user.save() + # Log the user in # Generate access token for the user access_token, refresh_token = get_tokens_for_user(user) - data = { "access_token": access_token, "refresh_token": refresh_token, @@ -166,7 +174,6 @@ class ResetPasswordEndpoint(BaseAPIView): class ChangePasswordEndpoint(BaseAPIView): def post(self, request): serializer = ChangePasswordSerializer(data=request.data) - user = User.objects.get(pk=request.user.id) if serializer.is_valid(): if not user.check_password(serializer.data.get("old_password")): @@ -218,16 +225,15 @@ class EmailCheckEndpoint(BaseAPIView): ] def post(self, request): - # get the email - # Check the instance registration instance = Instance.objects.first() - if instance is None: + if instance is None or not instance.is_setup_done: return Response( {"error": "Instance is not configured"}, status=status.HTTP_400_BAD_REQUEST, ) + # Get the configurations instance_configuration = InstanceConfiguration.objects.values("key", "value") email = request.data.get("email", False) @@ -267,7 +273,7 @@ class EmailCheckEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - + # Create the user with default values user = User.objects.create( email=email, username=uuid.uuid4().hex, @@ -325,7 +331,7 @@ class EmailCheckEndpoint(BaseAPIView): first_time=True, ) # Automatically send the email - return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_400_BAD_REQUEST) + return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK) # Existing user else: if type == "magic_code": diff --git a/apiserver/plane/app/views/authentication.py b/apiserver/plane/app/views/authentication.py index 87118b7d5..487cdae48 100644 --- a/apiserver/plane/app/views/authentication.py +++ b/apiserver/plane/app/views/authentication.py @@ -10,14 +10,12 @@ from django.utils import timezone from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.conf import settings -from django.contrib.auth.hashers import make_password # Third party imports from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status from rest_framework_simplejwt.tokens import RefreshToken - from sentry_sdk import capture_message # Module imports @@ -33,7 +31,6 @@ from plane.settings.redis import redis_instance from plane.license.models import InstanceConfiguration, Instance from plane.license.utils.instance_value import get_configuration_value from plane.bgtasks.event_tracking_task import auth_events -from plane.bgtasks.magic_link_code_task import magic_link from plane.bgtasks.user_count_task import update_user_instance_user_count @@ -49,6 +46,14 @@ class SignUpEndpoint(BaseAPIView): permission_classes = (AllowAny,) def post(self, request): + # Check if the instance configuration is done + instance = Instance.objects.first() + if instance is None or not instance.is_setup_done: + return Response( + {"error": "Instance is not configured"}, + status=status.HTTP_400_BAD_REQUEST, + ) + instance_configuration = InstanceConfiguration.objects.values("key", "value") email = request.data.get("email", False) @@ -71,6 +76,7 @@ class SignUpEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + # If the sign up is not enabled and the user does not have invite disallow him from creating the account if ( get_configuration_value( instance_configuration, @@ -124,6 +130,14 @@ class SignInEndpoint(BaseAPIView): permission_classes = (AllowAny,) def post(self, request): + # Check if the instance configuration is done + instance = Instance.objects.first() + if instance is None or not instance.is_setup_done: + return Response( + {"error": "Instance is not configured"}, + status=status.HTTP_400_BAD_REQUEST, + ) + email = request.data.get("email", False) password = request.data.get("password", False) @@ -144,14 +158,6 @@ class SignInEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - # Check if the instance setup is done or not - instance = Instance.objects.first() - if instance is None or not instance.is_setup_done: - return Response( - {"error": "Instance is not configured"}, - status=status.HTTP_400_BAD_REQUEST, - ) - # Get the user user = User.objects.filter(email=email).first() @@ -288,6 +294,7 @@ class MagicSignInEndpoint(BaseAPIView): ] def post(self, request): + # Check if the instance configuration is done instance = Instance.objects.first() if instance is None or not instance.is_setup_done: return Response( diff --git a/apiserver/plane/app/views/oauth.py b/apiserver/plane/app/views/oauth.py index 0dd7fbaf0..826ec4b05 100644 --- a/apiserver/plane/app/views/oauth.py +++ b/apiserver/plane/app/views/oauth.py @@ -303,14 +303,6 @@ class OauthEndpoint(BaseAPIView): instance_configuration = InstanceConfiguration.objects.values( "key", "value" ) - # Check if instance is registered or not - instance = Instance.objects.first() - if instance is None and not instance.is_setup_done: - return Response( - {"error": "Instance is not configured"}, - status=status.HTTP_400_BAD_REQUEST, - ) - if ( get_configuration_value( instance_configuration, diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index e32c2fd68..3d5724605 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -1,7 +1,8 @@ # Python imports import csv import io -import os +import requests +import json # Django imports from django.core.mail import EmailMultiAlternatives, get_connection @@ -17,8 +18,8 @@ from sentry_sdk import capture_exception from plane.db.models import Issue from plane.utils.analytics_plot import build_graph_plot from plane.utils.issue_filters import issue_filters -from plane.license.models import InstanceConfiguration -from plane.license.utils.instance_value import get_configuration_value +from plane.license.models import InstanceConfiguration, Instance +from plane.license.utils.instance_value import get_email_configuration row_mapping = { "state__name": "State", @@ -43,7 +44,7 @@ CYCLE_ID = "issue_cycle__cycle_id" MODULE_ID = "issue_module__module_id" -def send_export_email(email, slug, csv_buffer): +def send_export_email(email, slug, csv_buffer, rows): """Helper function to send export email.""" subject = "Your Export is ready" html_content = render_to_string("emails/exports/analytics.html", {}) @@ -55,47 +56,58 @@ def send_export_email(email, slug, csv_buffer): instance_configuration = InstanceConfiguration.objects.filter( key__startswith="EMAIL_" ).values("key", "value") + + ( + EMAIL_HOST, + EMAIL_HOST_USER, + EMAIL_HOST_PASSWORD, + EMAIL_PORT, + EMAIL_USE_TLS, + EMAIL_FROM, + ) = get_email_configuration(instance_configuration=instance_configuration) + + # Send the email if the users don't have smtp configured + if EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD: + # Check the instance registration + instance = Instance.objects.first() + + headers = { + "Content-Type": "application/json", + "x-instance-id": instance.instance_id, + "x-api-key": instance.api_key, + } + + payload = { + "email": email, + "slug": slug, + "rows": rows, + } + + _ = requests.post( + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/analytics/", + headers=headers, + json=payload, + ) + return + connection = get_connection( - host=get_configuration_value( - instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST") - ), - port=int( - get_configuration_value( - instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT") - ) - ), - username=get_configuration_value( - instance_configuration, - "EMAIL_HOST_USER", - os.environ.get("EMAIL_HOST_USER"), - ), - password=get_configuration_value( - instance_configuration, - "EMAIL_HOST_PASSWORD", - os.environ.get("EMAIL_HOST_PASSWORD"), - ), - use_tls=bool( - get_configuration_value( - instance_configuration, - "EMAIL_USE_TLS", - os.environ.get("EMAIL_USE_TLS", "1"), - ) - ), + host=EMAIL_HOST, + port=int(EMAIL_PORT), + username=EMAIL_HOST_USER, + password=EMAIL_HOST_PASSWORD, + use_tls=bool(EMAIL_USE_TLS), ) msg = EmailMultiAlternatives( subject=subject, body=text_content, - from_email=get_configuration_value( - instance_configuration, - "EMAIL_FROM", - os.environ.get("EMAIL_FROM", "Team Plane "), - ), + from_email=EMAIL_FROM, to=[email], connection=connection, ) msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue()) msg.send(fail_silently=False) + return def get_assignee_details(slug, filters): @@ -463,8 +475,11 @@ def analytic_export_task(email, data, slug): ) csv_buffer = generate_csv_from_rows(rows) - send_export_email(email, slug, csv_buffer) + send_export_email(email, slug, csv_buffer, rows) + return except Exception as e: + print(e) if settings.DEBUG: print(e) capture_exception(e) + return diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 5348a05da..33cd40dc8 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -40,13 +40,10 @@ def forgot_password(first_name, email, uidb64, token, current_site): ) = get_email_configuration(instance_configuration=instance_configuration) # Send the email if the users don't have smtp configured - if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD: + if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD): # Check the instance registration instance = Instance.objects.first() - # send the emails through control center - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) - # headers headers = { "Content-Type": "application/json", @@ -61,7 +58,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): } _ = requests.post( - f"{license_engine_base_url}/api/instances/users/forgot-password/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/forgot-password/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 2caec2b60..a152b4c7f 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -21,7 +21,6 @@ from plane.license.utils.instance_value import get_email_configuration @shared_task def magic_link(email, key, token, current_site): try: - instance_configuration = InstanceConfiguration.objects.filter( key__startswith="EMAIL_" ).values("key", "value") @@ -36,13 +35,10 @@ def magic_link(email, key, token, current_site): ) = get_email_configuration(instance_configuration=instance_configuration) # Send the email if the users don't have smtp configured - if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD: + if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD): # Check the instance registration instance = Instance.objects.first() - # send the emails through control center - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) - headers = { "Content-Type": "application/json", "x-instance-id": instance.instance_id, @@ -55,7 +51,7 @@ def magic_link(email, key, token, current_site): } _ = requests.post( - f"{license_engine_base_url}/api/instances/users/magic-code/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/magic-code/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/bgtasks/user_count_task.py b/apiserver/plane/bgtasks/user_count_task.py index f93c3364e..dd8b19e7d 100644 --- a/apiserver/plane/bgtasks/user_count_task.py +++ b/apiserver/plane/bgtasks/user_count_task.py @@ -32,13 +32,9 @@ def update_user_instance_user_count(): "x-api-key": instance.api_key, } - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL") - if not license_engine_base_url: - raise Exception("License Engine base url is required") - # Update the license engine _ = requests.post( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 2cbfdaff7..fe8708ed7 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -51,15 +51,10 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): ) = get_email_configuration(instance_configuration=instance_configuration) # Send the email if the users don't have smtp configured - if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD: + if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD): # Check the instance registration instance = Instance.objects.first() - # send the emails through control center - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) - if not license_engine_base_url: - raise Exception("License engine base url is required") - headers = { "Content-Type": "application/json", "x-instance-id": instance.instance_id, @@ -73,7 +68,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): "email": email, } _ = requests.post( - f"{license_engine_base_url}/api/instances/users/workspace-invitation/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/workspace-invitation/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 7edc50b27..7dbc5e4b4 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.contrib.auth.hashers import make_password from django.core.validators import validate_email from django.core.exceptions import ValidationError +from django.conf import settings # Third party imports from rest_framework import status @@ -34,7 +35,6 @@ from plane.db.models import User from plane.license.utils.encryption import encrypt_data from plane.settings.redis import redis_instance from plane.bgtasks.magic_link_code_task import magic_link -from plane.license.utils.instance_value import get_configuration_value class InstanceEndpoint(BaseAPIView): @@ -57,25 +57,17 @@ class InstanceEndpoint(BaseAPIView): # Load JSON content from the file data = json.load(file) - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL") - - if not license_engine_base_url: - raise Response( - {"error": "LICENSE_ENGINE_BASE_URL is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - headers = {"Content-Type": "application/json"} payload = { - "instance_key": os.environ.get("INSTANCE_KEY"), + "instance_key":settings.INSTANCE_KEY, "version": data.get("version", 0.1), "machine_signature": os.environ.get("MACHINE_SIGNATURE"), "user_count": User.objects.filter(is_bot=False).count(), } response = requests.post( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps(payload), ) @@ -130,6 +122,24 @@ class InstanceEndpoint(BaseAPIView): serializer = InstanceSerializer(instance, data=request.data, partial=True) if serializer.is_valid(): serializer.save() + # Save the user in control center + headers = { + "Content-Type": "application/json", + "x-instance-id": instance.instance_id, + "x-api-key": instance.api_key, + } + # Update instance settings in the license engine + _ = requests.patch( + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", + headers=headers, + data=json.dumps( + { + "is_support_required": serializer.data["is_support_required"], + "is_telemetry_enabled": serializer.data["is_telemetry_enabled"], + "version": serializer.data["version"], + } + ), + ) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -251,7 +261,6 @@ class AdminMagicSignInGenerateEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - if not email: return Response( {"error": "Please provide a valid email address"}, @@ -409,13 +418,6 @@ class AdminSetUserPasswordEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) - if not license_engine_base_url: - return Response( - {"error": "License engine base url is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - # Save the user in control center headers = { "Content-Type": "application/json", @@ -423,14 +425,14 @@ class AdminSetUserPasswordEndpoint(BaseAPIView): "x-api-key": instance.api_key, } _ = requests.patch( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps({"is_setup_done": True}), ) # Also register the user as admin _ = requests.post( - f"{license_engine_base_url}/api/instances/users/register/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/", headers=headers, data=json.dumps( { @@ -472,24 +474,20 @@ class SignUpScreenVisitedEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) - - if not license_engine_base_url: - return Response( - {"error": "License engine base url is required"}, - status=status.HTTP_400_BAD_REQUEST, + if not instance.is_signup_screen_visited: + instance.is_signup_screen_visited = True + instance.save() + # set the headers + headers = { + "Content-Type": "application/json", + "x-instance-id": instance.instance_id, + "x-api-key": instance.api_key, + } + # create the payload + payload = {"is_signup_screen_visited": True} + _ = requests.patch( + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", + headers=headers, + data=json.dumps(payload), ) - - headers = { - "Content-Type": "application/json", - "x-instance-id": instance.instance_id, - "x-api-key": instance.api_key, - } - - payload = {"is_signup_screen_visited": True} - response = requests.patch( - f"{license_engine_base_url}/api/instances/", - headers=headers, - data=json.dumps(payload), - ) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index b486ecb0b..0970d8093 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -6,6 +6,7 @@ import requests # Django imports from django.core.management.base import BaseCommand, CommandError from django.utils import timezone +from django.conf import settings # Module imports from plane.license.models import Instance @@ -30,31 +31,22 @@ class Command(BaseCommand): data = json.load(file) machine_signature = options.get("machine_signature", False) - instance_key = os.environ.get("INSTANCE_KEY", False) - - # Raise an exception if the admin email is not provided - if not instance_key: - raise CommandError("INSTANCE_KEY is required") + if not machine_signature: raise CommandError("Machine signature is required") - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL") - - if not license_engine_base_url: - raise CommandError("LICENSE_ENGINE_BASE_URL is required") - headers = {"Content-Type": "application/json"} payload = { - "instance_key": instance_key, + "instance_key": settings.INSTANCE_KEY, "version": data.get("version", 0.1), "machine_signature": machine_signature, "user_count": User.objects.filter(is_bot=False).count(), } response = requests.post( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index ba2c941ef..3eca1dee8 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -324,4 +324,10 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 # Posthog settings POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False) -POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False) \ No newline at end of file +POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False) + +# License engine base url +LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "https://control-center.plane.so") + +# instance key +INSTANCE_KEY = os.environ.get("INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3") diff --git a/apiserver/templates/emails/auth/magic_signin.html b/apiserver/templates/emails/auth/magic_signin.html index 4e47487bd..ba469db7e 100644 --- a/apiserver/templates/emails/auth/magic_signin.html +++ b/apiserver/templates/emails/auth/magic_signin.html @@ -149,20 +149,20 @@ padding-top: 10px !important; } .r13-o { - border-bottom-color: #efefef !important; + border-bottom-color: #d9e4ff !important; border-bottom-width: 1px !important; - border-left-color: #efefef !important; + border-left-color: #d9e4ff !important; border-left-width: 1px !important; - border-right-color: #efefef !important; + border-right-color: #d9e4ff !important; border-right-width: 1px !important; border-style: solid !important; - border-top-color: #efefef !important; + border-top-color: #d9e4ff !important; border-top-width: 1px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r14-i { - background-color: #e3e6f1 !important; + background-color: #ecf1ff !important; padding-bottom: 10px !important; padding-left: 10px !important; padding-right: 10px !important; @@ -225,16 +225,11 @@ padding-top: 5px !important; } .r24-o { - border-style: solid !important; - margin-right: 8px !important; - width: 32px !important; - } - .r25-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } - .r26-i { + .r25-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; @@ -664,17 +659,17 @@ width="100%" class="r13-o" style=" - background-color: #e3e6f1; - border-bottom-color: #efefef; + background-color: #ecf1ff; + border-bottom-color: #d9e4ff; border-bottom-width: 1px; border-collapse: separate; - border-left-color: #efefef; + border-left-color: #d9e4ff; border-left-width: 1px; border-radius: 5px; - border-right-color: #efefef; + border-right-color: #d9e4ff; border-right-width: 1px; border-style: solid; - border-top-color: #efefef; + border-top-color: #d9e4ff; border-top-width: 1px; table-layout: fixed; width: 100%; @@ -690,7 +685,7 @@ font-family: georgia, serif; font-size: 16px; word-break: break-word; - background-color: #e3e6f1; + background-color: #ecf1ff; border-radius: 4px; line-height: 3; padding-bottom: 10px; @@ -714,10 +709,10 @@

Please copy and paste this on the screen where you @@ -1251,13 +1246,14 @@ role="presentation" width="100%" class="r22-o" - class="r24-o" style=" + table-layout: fixed; + width: 100%; + " > - - Dear {{username}},
- Your requested Issue's data has been successfully exported from Plane. The export includes all relevant information about issues you requested from your selected projects.
- Please find the attachment and download the CSV file. If you have any questions or need further assistance, please don't hesitate to contact our support team at engineering@plane.so. We're here to help!
- Thank you for using Plane. We hope this export will aid you in effectively managing your projects.
- Regards, - Team Plane - diff --git a/deploy/coolify/coolify-docker-compose.yml b/deploy/coolify/coolify-docker-compose.yml index 6dd361d34..2a21c61a8 100644 --- a/deploy/coolify/coolify-docker-compose.yml +++ b/deploy/coolify/coolify-docker-compose.yml @@ -71,7 +71,6 @@ services: - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - WEB_URL=$SERVICE_FQDN_PLANE_8082 - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"} depends_on: - plane-db - plane-redis @@ -117,7 +116,6 @@ services: - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"} depends_on: - api - plane-db @@ -164,7 +162,6 @@ services: - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"} depends_on: - api - plane-db diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 7eec3fd5a..ba0c28827 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -5,15 +5,16 @@ x-app-env : &app-env - NGINX_PORT=${NGINX_PORT:-80} - WEB_URL=${WEB_URL:-http://localhost} - DEBUG=${DEBUG:-0} - - NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} - - NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} + - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated + - NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} # deprecated + - NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} # deprecated - SENTRY_DSN=${SENTRY_DSN:-""} + - SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"} + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-""} + - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-""} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - DOCKERIZED=${DOCKERIZED:-1} # deprecated - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""} - - SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"} - - ADMIN_EMAIL=${ADMIN_EMAIL:-""} - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-""} # Gunicorn Workers - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} #DB SETTINGS @@ -28,12 +29,12 @@ x-app-env : &app-env - REDIS_HOST=${REDIS_HOST:-plane-redis} - REDIS_PORT=${REDIS_PORT:-6379} - REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/} - # EMAIL SETTINGS + # EMAIL SETTINGS - Deprecated can be configured through admin panel - EMAIL_HOST=${EMAIL_HOST:-""} - EMAIL_HOST_USER=${EMAIL_HOST_USER:-""} - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""} - EMAIL_PORT=${EMAIL_PORT:-587} - - EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"} + - EMAIL_FROM=${EMAIL_FROM:-"Team Plane "} - EMAIL_USE_TLS=${EMAIL_USE_TLS:-1} - EMAIL_USE_SSL=${EMAIL_USE_SSL:-0} - DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so} @@ -42,10 +43,11 @@ x-app-env : &app-env - OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1} - OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"} - GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"} - # LOGIN/SIGNUP SETTINGS + # LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1} - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} + # Application secret - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} # DATA STORE SETTINGS - USE_MINIO=${USE_MINIO:-1} diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index 10ca42879..6be9ca2f4 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -10,10 +10,12 @@ DEBUG=0 NEXT_PUBLIC_ENABLE_OAUTH=0 NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces SENTRY_DSN="" +SENTRY_ENVIRONMENT="production" +GOOGLE_CLIENT_ID="" +GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" DOCKERIZED=1 # deprecated CORS_ALLOWED_ORIGINS="http://localhost" -SENTRY_ENVIRONMENT="production" #DB SETTINGS PGHOST=plane-db @@ -34,7 +36,7 @@ EMAIL_HOST="" EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" EMAIL_PORT=587 -EMAIL_FROM="Team Plane <team@mailer.plane.so>" +EMAIL_FROM="Team Plane " EMAIL_USE_TLS=1 EMAIL_USE_SSL=0 @@ -63,9 +65,3 @@ FILE_SIZE_LIMIT=5242880 # Gunicorn Workers GUNICORN_WORKERS=2 - -# Admin Email -ADMIN_EMAIL="" - -# License Engine url -LICENSE_ENGINE_BASE_URL="" \ No newline at end of file From fa990ed444f2a535258cf96efdf9242fbc240765 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:04:04 +0530 Subject: [PATCH 05/17] refactor: custom hook for sign in redirection (#2969) --- .../account/sign-in-forms/email-form.tsx | 2 +- .../account/sign-in-forms/password.tsx | 16 +- web/components/account/sign-in-forms/root.tsx | 36 ++-- .../sign-in-forms/set-password-link.tsx | 18 +- web/components/page-views/signin.tsx | 163 +++++++----------- web/hooks/use-sign-in-redirection.ts | 74 ++++++++ web/pages/accounts/password.tsx | 39 +---- web/public/onboarding/onboarding-pages.svg | 38 ++-- 8 files changed, 198 insertions(+), 188 deletions(-) create mode 100644 web/hooks/use-sign-in-redirection.ts diff --git a/web/components/account/sign-in-forms/email-form.tsx b/web/components/account/sign-in-forms/email-form.tsx index f704d4134..d9dc1c396 100644 --- a/web/components/account/sign-in-forms/email-form.tsx +++ b/web/components/account/sign-in-forms/email-form.tsx @@ -84,7 +84,7 @@ export const EmailForm: React.FC = (props) => { return ( <>

- Get on your flight deck! + Get on your flight deck

Sign in with the email you used to sign up for Plane diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index 547f59e9d..44c91a51f 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; import { Controller, useForm } from "react-hook-form"; import { XCircle } from "lucide-react"; @@ -36,6 +36,8 @@ const authService = new AuthService(); export const PasswordForm: React.FC = (props) => { const { email, updateEmail, handleStepChange, handleSignInRedirection } = props; + // states + const [isSendingResetPasswordLink, setIsSendingResetPasswordLink] = useState(false); // toast alert const { setToastAlert } = useToast(); // form info @@ -113,6 +115,8 @@ export const PasswordForm: React.FC = (props) => { return; } + setIsSendingResetPasswordLink(true); + authService .sendResetPasswordLink({ email: emailFormValue }) .then(() => handleStepChange(ESignInSteps.SET_PASSWORD_LINK)) @@ -122,7 +126,8 @@ export const PasswordForm: React.FC = (props) => { title: "Error!", message: err?.error ?? "Something went wrong. Please try again.", }) - ); + ) + .finally(() => setIsSendingResetPasswordLink(false)); }; return ( @@ -189,9 +194,12 @@ export const PasswordForm: React.FC = (props) => { diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index 9797c8573..90e60e2b1 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -1,4 +1,6 @@ import React, { useState } from "react"; +// hooks +import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { EmailForm, @@ -19,33 +21,27 @@ export enum ESignInSteps { CREATE_PASSWORD = "CREATE_PASSWORD", } -type Props = { - handleSignInRedirection: () => Promise; -}; - const OAUTH_HIDDEN_STEPS = [ESignInSteps.OPTIONAL_SET_PASSWORD, ESignInSteps.CREATE_PASSWORD]; -export const SignInRoot: React.FC = (props) => { - const { handleSignInRedirection } = props; +export const SignInRoot = () => { // states const [signInStep, setSignInStep] = useState(ESignInSteps.EMAIL); const [email, setEmail] = useState(""); + // sign in redirection hook + const { handleRedirection } = useSignInRedirection(); return ( <>

{signInStep === ESignInSteps.EMAIL && ( - setSignInStep(step)} - updateEmail={(newEmail) => setEmail(newEmail)} - /> + setSignInStep(step)} updateEmail={(newEmail) => setEmail(newEmail)} /> )} {signInStep === ESignInSteps.PASSWORD && ( setEmail(newEmail)} - handleStepChange={(step: ESignInSteps) => setSignInStep(step)} - handleSignInRedirection={handleSignInRedirection} + handleStepChange={(step) => setSignInStep(step)} + handleSignInRedirection={handleRedirection} /> )} {signInStep === ESignInSteps.SET_PASSWORD_LINK && ( @@ -55,30 +51,30 @@ export const SignInRoot: React.FC = (props) => { setEmail(newEmail)} - handleStepChange={(step: ESignInSteps) => setSignInStep(step)} - handleSignInRedirection={handleSignInRedirection} + handleStepChange={(step) => setSignInStep(step)} + handleSignInRedirection={handleRedirection} /> )} {signInStep === ESignInSteps.OPTIONAL_SET_PASSWORD && ( setSignInStep(step)} - handleSignInRedirection={handleSignInRedirection} + handleStepChange={(step) => setSignInStep(step)} + handleSignInRedirection={handleRedirection} /> )} {signInStep === ESignInSteps.CREATE_PASSWORD && ( setSignInStep(step)} - handleSignInRedirection={handleSignInRedirection} + handleStepChange={(step) => setSignInStep(step)} + handleSignInRedirection={handleRedirection} /> )}
{!OAUTH_HIDDEN_STEPS.includes(signInStep) && ( setEmail(newEmail)} - handleStepChange={(step: ESignInSteps) => setSignInStep(step)} - handleSignInRedirection={handleSignInRedirection} + handleStepChange={(step) => setSignInStep(step)} + handleSignInRedirection={handleRedirection} /> )} diff --git a/web/components/account/sign-in-forms/set-password-link.tsx b/web/components/account/sign-in-forms/set-password-link.tsx index f683a26fc..21cc8db17 100644 --- a/web/components/account/sign-in-forms/set-password-link.tsx +++ b/web/components/account/sign-in-forms/set-password-link.tsx @@ -30,7 +30,7 @@ export const SetPasswordLink: React.FC = (props) => { const { control, formState: { errors, isValid }, - watch, + handleSubmit, } = useForm({ defaultValues: { email, @@ -39,11 +39,13 @@ export const SetPasswordLink: React.FC = (props) => { reValidateMode: "onChange", }); - const handleSendNewLink = async () => { + const handleSendNewLink = async (formData: { email: string }) => { setIsSendingNewLink(true); + updateEmail(formData.email); + const payload: IEmailCheckData = { - email: watch("email"), + email: formData.email, type: "password", }; @@ -76,7 +78,7 @@ export const SetPasswordLink: React.FC = (props) => { password

-
+
= (props) => { name="email" type="email" value={value} - onChange={(e) => { - updateEmail(e.target.value); - onChange(e.target.value); - }} + onChange={onChange} ref={ref} hasError={Boolean(errors.email)} placeholder="orville.wright@firstflight.com" @@ -112,11 +111,10 @@ export const SetPasswordLink: React.FC = (props) => { />
)} - {!isNotAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && ( + {isAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && ( {!isCompleted && ( - { - setTrackElement("CYCLE_PAGE_SIDEBAR"); - setCycleDeleteModal(true) - } - }> + { + setTrackElement("CYCLE_PAGE_SIDEBAR"); + setCycleDeleteModal(true); + }} + > Delete cycle diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index a525124ee..f8813b80b 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -75,20 +75,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !moduleId) return; - - mutate( - MODULE_DETAILS(moduleId as string), - (prevData) => ({ - ...(prevData as IModule), - ...data, - }), - false - ); - - moduleService - .patchModule(workspaceSlug as string, projectId as string, moduleId as string, data) - .then(() => mutate(MODULE_DETAILS(moduleId as string))) - .catch((e) => console.log(e)); + moduleStore.updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, data); }; const handleCreateLink = async (formData: ModuleLink) => { From 7b12d54d8335c71c056027cfec5f2eae697652e5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:41:31 +0530 Subject: [PATCH 10/17] chore: draft issue layout and permission validation (#2982) * chore: create draft issue option added in draft issue layout and permission validation added * chore: create draft issue option added in draft issue list layout and permission validation added --- .../issue-layouts/kanban/base-kanban-root.tsx | 2 +- .../kanban/headers/group-by-card.tsx | 24 ++++++++++++++----- .../issue-layouts/list/base-list-root.tsx | 2 +- .../list/headers/group-by-card.tsx | 24 ++++++++++++++----- .../project-issues/draft/issue.store.ts | 2 +- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 66d33ad16..e5d279809 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -224,7 +224,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas isDragStarted={isDragStarted} quickAddCallback={issueStore?.quickAddIssue} viewId={viewId} - disableIssueCreation={!enableIssueCreation} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} isReadOnly={!enableInlineEditing || !isEditingAllowed} currentStore={currentStore} addIssuesToView={addIssuesToView} diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index f58001402..8fc2c6e17 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; // components import { CustomMenu } from "@plane/ui"; import { CreateUpdateIssueModal } from "components/issues/modal"; +import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal"; import { ExistingIssuesListModal } from "components/core"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; @@ -51,6 +52,8 @@ export const HeaderGroupByCard: FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId, cycleId } = router.query; + const isDraftIssue = router.pathname.includes("draft-issue"); + const { setToastAlert } = useToast(); const renderExistingIssueModal = moduleId || cycleId; @@ -73,12 +76,21 @@ export const HeaderGroupByCard: FC = observer((props) => { return ( <> - setIsOpen(false)} - prePopulateData={issuePayload} - currentStore={currentStore} - /> + {isDraftIssue ? ( + setIsOpen(false)} + prePopulateData={issuePayload} + fieldsToShow={["all"]} + /> + ) : ( + setIsOpen(false)} + prePopulateData={issuePayload} + currentStore={currentStore} + /> + )} {renderExistingIssueModal && ( { quickAddCallback={issueStore?.quickAddIssue} enableIssueQuickAdd={!!enableQuickAdd} isReadonly={!enableInlineEditing || !isEditingAllowed} - disableIssueCreation={!enableIssueCreation} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} currentStore={currentStore} addIssuesToView={addIssuesToView} /> diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index c49d33d1e..24dbf435d 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components +import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal"; import { CreateUpdateIssueModal } from "components/issues/modal"; import { ExistingIssuesListModal } from "components/core"; import { CustomMenu } from "@plane/ui"; @@ -32,6 +33,8 @@ export const HeaderGroupByCard = observer( const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); + const isDraftIssue = router.pathname.includes("draft-issue"); + const { setToastAlert } = useToast(); const renderExistingIssueModal = moduleId || cycleId; @@ -90,12 +93,21 @@ export const HeaderGroupByCard = observer( ))} - setIsOpen(false)} - currentStore={currentStore} - prePopulateData={issuePayload} - /> + {isDraftIssue ? ( + setIsOpen(false)} + prePopulateData={issuePayload} + fieldsToShow={["all"]} + /> + ) : ( + setIsOpen(false)} + currentStore={currentStore} + prePopulateData={issuePayload} + /> + )} {renderExistingIssueModal && ( Date: Tue, 5 Dec 2023 13:42:16 +0530 Subject: [PATCH 11/17] fix: sentry dsn error (#2981) --- apiserver/plane/settings/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 3eca1dee8..fff6b9e90 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -290,7 +290,7 @@ CELERY_IMPORTS = ( # Sentry Settings # Enable Sentry Settings -if bool(os.environ.get("SENTRY_DSN", False)): +if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get("SENTRY_DSN").startswith("https://"): sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN", ""), integrations=[ From fd73f18d951ed857db90a23e4a6b9600c709e19c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:44:01 +0530 Subject: [PATCH 12/17] fix: leave project mutation (#2976) --- web/components/project/member-list-item.tsx | 9 +++++++-- web/components/project/sidebar-list-item.tsx | 2 +- web/components/workspace/settings/members-list-item.tsx | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 576accee9..74ff74450 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -33,6 +33,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const { user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject }, projectMember: { removeMemberFromProject, updateMember }, + project: { fetchProjects }, } = useMobxStore(); // hooks const { setToastAlert } = useToast(); @@ -46,7 +47,11 @@ export const ProjectMemberListItem: React.FC = observer((props) => { if (memberDetails.id === currentUser?.id) { await leaveProject(workspaceSlug.toString(), projectId.toString()) - .then(() => router.push(`/${workspaceSlug}/projects`)) + .then(async () => { + await fetchProjects(workspaceSlug.toString()); + + router.push(`/${workspaceSlug}/projects`); + }) .catch((err) => setToastAlert({ type: "error", @@ -174,7 +179,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { onClick={() => setRemoveMemberModal(true)} className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto" > - + )} diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 488ae571a..5ad757e5f 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -284,7 +284,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
- Leave Project + Leave project
)} diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 7536e78c9..751fc14e1 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -243,7 +243,7 @@ export const WorkspaceMembersListItem: FC = observer((props) => { : "opacity-0 pointer-events-none" } > - + From effad33f67621a987a08aa17bbdbadeb275c6111 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:40:29 +0530 Subject: [PATCH 13/17] chore: issue update status in peekview & detail (#2985) --- web/components/inbox/main-content.tsx | 31 ++++++++-- web/components/issues/description-form.tsx | 12 +--- web/components/issues/index.ts | 1 + .../issue-peek-overview/issue-detail.tsx | 22 ++++--- .../issues/issue-peek-overview/view.tsx | 62 ++++++++++--------- web/components/issues/issue-update-status.tsx | 32 ++++++++++ web/components/issues/main-content.tsx | 31 ++++++++-- web/components/issues/sidebar.tsx | 30 +++++++-- 8 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 web/components/issues/issue-update-status.tsx diff --git a/web/components/inbox/main-content.tsx b/web/components/inbox/main-content.tsx index 5d7863196..193f59263 100644 --- a/web/components/inbox/main-content.tsx +++ b/web/components/inbox/main-content.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import Router, { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; @@ -8,10 +8,10 @@ import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues"; +import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction, IssueUpdateStatus } from "components/issues"; import { InboxIssueActivity } from "components/inbox"; // ui -import { Loader } from "@plane/ui"; +import { Loader, StateGroupIcon } from "@plane/ui"; // helpers import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types @@ -31,7 +31,15 @@ export const InboxMainContent: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; - const { inboxIssues: inboxIssuesStore, inboxIssueDetails: inboxIssueDetailsStore, user: userStore } = useMobxStore(); + // states + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); + + const { + inboxIssues: inboxIssuesStore, + inboxIssueDetails: inboxIssueDetailsStore, + user: userStore, + projectState: { states }, + } = useMobxStore(); const user = userStore.currentUser; const userRole = userStore.currentProjectRole; @@ -55,6 +63,9 @@ export const InboxMainContent: React.FC = observer(() => { const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : undefined; const issueDetails = inboxIssueId ? inboxIssueDetailsStore.issueDetails[inboxIssueId.toString()] : undefined; + const currentIssueState = projectId + ? states[projectId.toString()]?.find((s) => s.id === issueDetails?.state) + : undefined; const submitChanges = useCallback( async (formData: Partial) => { @@ -217,8 +228,20 @@ export const InboxMainContent: React.FC = observer(() => { ) : null} +
+ {currentIssueState && ( + + )} + +
setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug as string} issue={{ name: issueDetails.name, diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 43600533b..ef26e22d8 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -26,14 +26,15 @@ export interface IssueDetailsProps { workspaceSlug: string; handleFormSubmit: (value: IssueDescriptionFormValues) => Promise; isAllowed: boolean; + isSubmitting: "submitting" | "submitted" | "saved"; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } const fileService = new FileService(); export const IssueDescriptionForm: FC = (props) => { - const { issue, handleFormSubmit, workspaceSlug, isAllowed } = props; + const { issue, handleFormSubmit, workspaceSlug, isAllowed, isSubmitting, setIsSubmitting } = props; // states - const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [characterLimit, setCharacterLimit] = useState(false); const { setShowAlert } = useReloadConfirmations(); @@ -166,13 +167,6 @@ export const IssueDescriptionForm: FC = (props) => { /> )} /> -
- {isSubmitting === "submitting" ? "Saving..." : "Saved"} -
); diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 0cf3c8bda..f8f0ba003 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -15,6 +15,7 @@ export * from "./sidebar"; export * from "./label"; export * from "./issue-reaction"; export * from "./confirm-issue-discard"; +export * from "./issue-update-status"; // draft issue export * from "./draft-issue-form"; diff --git a/web/components/issues/issue-peek-overview/issue-detail.tsx b/web/components/issues/issue-peek-overview/issue-detail.tsx index fafdccb71..3e90c8b8d 100644 --- a/web/components/issues/issue-peek-overview/issue-detail.tsx +++ b/web/components/issues/issue-peek-overview/issue-detail.tsx @@ -26,16 +26,27 @@ interface IPeekOverviewIssueDetails { issueUpdate: (issue: Partial) => void; issueReactionCreate: (reaction: string) => void; issueReactionRemove: (reaction: string) => void; + isSubmitting: "submitting" | "submitted" | "saved"; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } export const PeekOverviewIssueDetails: FC = (props) => { - const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props; + const { + workspaceSlug, + issue, + issueReactions, + user, + issueUpdate, + issueReactionCreate, + issueReactionRemove, + isSubmitting, + setIsSubmitting, + } = props; // store const { user: userStore } = useMobxStore(); const { currentProjectRole } = userStore; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; // states - const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [characterLimit, setCharacterLimit] = useState(false); // hooks const { setShowAlert } = useReloadConfirmations(); @@ -172,13 +183,6 @@ export const PeekOverviewIssueDetails: FC = (props) = /> )} /> -
- {isSubmitting === "submitting" ? "Saving..." : "Saved"} -
= observer((props) => { const [peekMode, setPeekMode] = useState("side-peek"); const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const updateRoutePeekId = () => { if (issueId != peekIssueId) { @@ -216,33 +216,35 @@ export const IssueView: FC = observer((props) => { )} - -
- {issue?.created_by !== user?.id && - !issue?.assignees.includes(user?.id ?? "") && - !router.pathname.includes("[archivedIssueId]") && ( - - )} - - {!disableUserActions && ( - + )} + - )} + {!disableUserActions && ( + + )} +
@@ -261,6 +263,8 @@ export const IssueView: FC = observer((props) => {
)} setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug} issue={issue} issueUpdate={issueUpdate} @@ -295,6 +299,8 @@ export const IssueView: FC = observer((props) => {
setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug} issue={issue} issueReactions={issueReactions} diff --git a/web/components/issues/issue-update-status.tsx b/web/components/issues/issue-update-status.tsx new file mode 100644 index 000000000..e6852936e --- /dev/null +++ b/web/components/issues/issue-update-status.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { RefreshCw } from "lucide-react"; +// types +import { IIssue } from "types"; + +type Props = { + isSubmitting: "submitting" | "submitted" | "saved"; + issueDetail?: IIssue; +}; + +export const IssueUpdateStatus: React.FC = (props) => { + const { isSubmitting, issueDetail } = props; + return ( + <> + {issueDetail && ( +

+ {issueDetail.project_detail?.identifier}-{issueDetail.sequence_id} +

+ )} +
+ {isSubmitting !== "submitted" && isSubmitting !== "saved" && ( + + )} + {isSubmitting === "submitting" ? "Saving..." : "Saved"} +
+ + ); +}; diff --git a/web/components/issues/main-content.tsx b/web/components/issues/main-content.tsx index 7ea0e7cfe..bd18cb73b 100644 --- a/web/components/issues/main-content.tsx +++ b/web/components/issues/main-content.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR, { mutate } from "swr"; +import { MinusCircle } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services @@ -16,12 +17,12 @@ import { IssueAttachments, IssueDescriptionForm, IssueReaction, + IssueUpdateStatus, } from "components/issues"; +import { useState } from "react"; import { SubIssuesRoot } from "./sub-issues"; // ui -import { CustomMenu, LayersIcon } from "@plane/ui"; -// icons -import { MinusCircle } from "lucide-react"; +import { CustomMenu, LayersIcon, StateGroupIcon } from "@plane/ui"; // types import { IIssue, IIssueComment } from "types"; // fetch-keys @@ -41,15 +42,25 @@ const issueCommentService = new IssueCommentService(); export const IssueMainContent: React.FC = observer((props) => { const { issueDetails, submitChanges, uneditable = false } = props; + // states + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); + const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; const { setToastAlert } = useToast(); - const { user: userStore, project: projectStore } = useMobxStore(); + const { + user: userStore, + project: projectStore, + projectState: { states }, + } = useMobxStore(); const user = userStore.currentUser ?? undefined; const userRole = userStore.currentProjectRole; const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined; + const currentIssueState = projectId + ? states[projectId.toString()]?.find((s) => s.id === issueDetails.state) + : undefined; const { data: siblingIssues } = useSWR( workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, @@ -165,7 +176,19 @@ export const IssueMainContent: React.FC = observer((props) => {
) : null} +
+ {currentIssueState && ( + + )} + +
setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug as string} issue={issueDetails} handleFormSubmit={submitChanges} diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index d6681cb4a..b0315304d 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -33,7 +33,7 @@ import { import { CustomDatePicker } from "components/ui"; // icons import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react"; -import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -80,12 +80,15 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const [linkModal, setLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); - const { user: userStore } = useMobxStore(); + const { + user: userStore, + projectState: { states }, + } = useMobxStore(); const user = userStore.currentUser; const userRole = userStore.currentProjectRole; const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query; const { isEstimateActive } = useEstimateOption(); @@ -248,6 +251,10 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const currentIssueState = projectId + ? states[projectId.toString()]?.find((s) => s.id === issueDetail?.state) + : undefined; + return ( <> = observer((props) => { )}
-

- {issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id} -

+
+ {currentIssueState ? ( + + ) : inboxIssueId ? ( + + ) : null} +

+ {issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id} +

+
{issueDetail?.created_by !== user?.id && !issueDetail?.assignees.includes(user?.id ?? "") && From db1166411a296ffac0c380a333c35995600a84c5 Mon Sep 17 00:00:00 2001 From: sabith-tu <95301637+sabith-tu@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:05:50 +0530 Subject: [PATCH 14/17] style: image picker, spreadsheet view title, icons (#2988) * style: image picker, spreadsheet view title, icons * fix: build error fix --- web/components/core/image-picker-popover.tsx | 2 +- .../issue-layouts/properties/assignee.tsx | 8 ++------ .../spreadsheet/spreadsheet-column.tsx | 7 +++++-- .../spreadsheet/spreadsheet-view.tsx | 11 ++++++++--- .../issues/issue-peek-overview/properties.tsx | 6 +++--- web/components/issues/sidebar.tsx | 4 ++-- web/constants/spreadsheet.ts | 17 +++++++++++++++++ 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index eea18ce43..765bb56b2 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -324,7 +324,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { File formats supported- .jpeg, .jpg, .png, .webp, .svg

-
+
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx index 25b5139f5..0f7a138d4 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-column.tsx @@ -85,14 +85,17 @@ export const SpreadsheetColumn: React.FC = (props) => { customButton={
+ {} + {propertyDetails.title} +
+
{activeSortingProperty === property && (
)} - {propertyDetails.title} +
-
} width="xl" diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index f18336e45..de69c2d4e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -8,7 +8,7 @@ import { SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm, } from "components/issues"; -import { Spinner } from "@plane/ui"; +import { Spinner, LayersIcon } from "@plane/ui"; // types import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types"; import { EIssueActions } from "../types"; @@ -95,9 +95,14 @@ export const SpreadsheetView: React.FC = observer((props) => { >
{displayProperties.key && ( - ID + + #ID + )} - Issue + + + Issue +
{issues.map((issue, index) => diff --git a/web/components/issues/issue-peek-overview/properties.tsx b/web/components/issues/issue-peek-overview/properties.tsx index c0782cf59..fc6a8c860 100644 --- a/web/components/issues/issue-peek-overview/properties.tsx +++ b/web/components/issues/issue-peek-overview/properties.tsx @@ -5,8 +5,8 @@ import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // ui icons -import { DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; -import { CalendarDays, ContrastIcon, Link2, Plus, Signal, Tag, Triangle, User2 } from "lucide-react"; +import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon } from "@plane/ui"; +import { CalendarDays, Link2, Plus, Signal, Tag, Triangle, LayoutPanelTop } from "lucide-react"; import { SidebarAssigneeSelect, SidebarCycleSelect, @@ -289,7 +289,7 @@ export const PeekOverviewProperties: FC = observer((pro {/* parent */}
- +

Parent

diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index b0315304d..378b1e5c5 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -32,7 +32,7 @@ import { // ui import { CustomDatePicker } from "components/ui"; // icons -import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react"; +import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react"; import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; @@ -421,7 +421,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
- +

Parent

diff --git a/web/constants/spreadsheet.ts b/web/constants/spreadsheet.ts index 603e280f5..00a59eddb 100644 --- a/web/constants/spreadsheet.ts +++ b/web/constants/spreadsheet.ts @@ -1,4 +1,8 @@ import { TIssueOrderByOptions } from "types"; +import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip } from "lucide-react"; +import { FC } from "react"; +import { ISvgIcons } from "@plane/ui/src/icons/type"; export const SPREADSHEET_PROPERTY_DETAILS: { [key: string]: { @@ -7,6 +11,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: string; descendingOrderKey: TIssueOrderByOptions; descendingOrderTitle: string; + icon: FC; }; } = { assignee: { @@ -15,6 +20,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-assignees__first_name", descendingOrderTitle: "Z", + icon: UserGroupIcon, }, created_on: { title: "Created on", @@ -22,6 +28,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "created_at", descendingOrderTitle: "Old", + icon: CalendarDays, }, due_date: { title: "Due date", @@ -29,6 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "target_date", descendingOrderTitle: "Old", + icon: CalendarDays, }, estimate: { title: "Estimate", @@ -36,6 +44,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "Low", descendingOrderKey: "-estimate_point", descendingOrderTitle: "High", + icon: Triangle, }, labels: { title: "Labels", @@ -43,6 +52,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-labels__name", descendingOrderTitle: "Z", + icon: Tag, }, priority: { title: "Priority", @@ -50,6 +60,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "None", descendingOrderKey: "-priority", descendingOrderTitle: "Urgent", + icon: Signal, }, start_date: { title: "Start date", @@ -57,6 +68,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "start_date", descendingOrderTitle: "Old", + icon: CalendarDays, }, state: { title: "State", @@ -64,6 +76,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "A", descendingOrderKey: "-state__name", descendingOrderTitle: "Z", + icon: DoubleCircleIcon, }, updated_on: { title: "Updated on", @@ -71,6 +84,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "New", descendingOrderKey: "updated_at", descendingOrderTitle: "Old", + icon: CalendarDays, }, link: { title: "Link", @@ -78,6 +92,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "Most", descendingOrderKey: "link_count", descendingOrderTitle: "Least", + icon: Link2, }, attachment_count: { title: "Attachment", @@ -85,6 +100,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "Most", descendingOrderKey: "attachment_count", descendingOrderTitle: "Least", + icon: Paperclip, }, sub_issue_count: { title: "Sub-issue", @@ -92,5 +108,6 @@ export const SPREADSHEET_PROPERTY_DETAILS: { ascendingOrderTitle: "Most", descendingOrderKey: "sub_issues_count", descendingOrderTitle: "Least", + icon: LayersIcon, }, }; From 9649f42ff31b28c4999121d3f188ea4de649c926 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:06:43 +0530 Subject: [PATCH 15/17] chore: email invite accept validation (#2965) * fix: empty state flickering on accepting only invitation * fix: redirection from workspace-invitaion to onboarding * chore: onboarding step 1 skip on accepting invite from email * fix: dashboard redirection path --- web/components/onboarding/invitations.tsx | 2 +- web/pages/invitations/index.tsx | 2 +- web/pages/onboarding/index.tsx | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/web/components/onboarding/invitations.tsx b/web/components/onboarding/invitations.tsx index 8f650eefd..c724de42c 100644 --- a/web/components/onboarding/invitations.tsx +++ b/web/components/onboarding/invitations.tsx @@ -63,11 +63,11 @@ export const Invitations: React.FC = (props) => { .joinWorkspaces({ invitations: invitationsRespond }) .then(async (res) => { postHogEventTracker("WORKSPACE_USER_INVITE_ACCEPT", { ...res, state: "SUCCESS" }); - await mutateInvitations(); await workspaceStore.fetchWorkspaces(); await mutate(USER_WORKSPACES); await updateLastWorkspace(); await handleNextStep(); + await mutateInvitations(); }) .catch((error) => { console.log(error); diff --git a/web/pages/invitations/index.tsx b/web/pages/invitations/index.tsx index 80738907a..55c0b89aa 100644 --- a/web/pages/invitations/index.tsx +++ b/web/pages/invitations/index.tsx @@ -213,7 +213,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => { image={emptyInvitation} primaryButton={{ text: "Back to dashboard", - onClick: () => router.push(`/${redirectWorkspaceSlug}`), + onClick: () => router.push("/"), }} />
diff --git a/web/pages/onboarding/index.tsx b/web/pages/onboarding/index.tsx index df7df99f4..94c7b751a 100644 --- a/web/pages/onboarding/index.tsx +++ b/web/pages/onboarding/index.tsx @@ -52,6 +52,10 @@ const OnboardingPage: NextPageWithLayout = observer(() => { }, }); + useSWR(`USER_WORKSPACES_LIST`, () => workspaceStore.fetchWorkspaces(), { + shouldRetryOnError: false, + }); + const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS_LIST", () => workspaceService.userWorkspaceInvitations() ); @@ -88,6 +92,19 @@ const OnboardingPage: NextPageWithLayout = observer(() => { const onboardingStep = user.onboarding_step; + if (!onboardingStep.workspace_join && !onboardingStep.workspace_create && workspaces && workspaces?.length > 0) { + await updateCurrentUser({ + onboarding_step: { + ...user.onboarding_step, + workspace_join: true, + workspace_create: true, + }, + last_workspace_id: workspaces[0].id, + }); + setStep(2); + return; + } + if (!onboardingStep.workspace_join && !onboardingStep.workspace_create && step !== 1) setStep(1); if (onboardingStep.workspace_join || onboardingStep.workspace_create) { From 8b2d78ef92351dd7d481a3da754698c5124df570 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:07:25 +0530 Subject: [PATCH 16/17] chore: space ui component revamp and bug fixes (#2980) * chore: replace space ui component with plane ui component * fix: space project icon and user pic bug * chore: code refactor * fix: profile section navbar fix --- space/components/accounts/email-code-form.tsx | 24 ++++--- .../accounts/email-password-form.tsx | 15 ++-- .../accounts/email-reset-password-form.tsx | 9 ++- space/components/accounts/onboarding-form.tsx | 6 +- space/components/issues/navbar/index.tsx | 41 +++++------ .../comment/comment-reactions.tsx | 3 +- .../peek-overview/full-screen-peek-view.tsx | 2 +- .../issues/peek-overview/issue-activity.tsx | 7 +- .../peek-overview/issue-emoji-reactions.tsx | 3 +- .../peek-overview/issue-vote-reactions.tsx | 3 +- .../issues/peek-overview/side-peek-view.tsx | 2 +- space/components/ui/index.ts | 5 -- space/components/ui/input.tsx | 37 ---------- space/components/ui/loader.tsx | 25 ------- space/components/ui/primary-button.tsx | 35 ---------- space/components/ui/secondary-button.tsx | 35 ---------- space/components/ui/tooltip.tsx | 70 ------------------- web/components/profile/navbar.tsx | 2 +- 18 files changed, 62 insertions(+), 262 deletions(-) delete mode 100644 space/components/ui/input.tsx delete mode 100644 space/components/ui/loader.tsx delete mode 100644 space/components/ui/primary-button.tsx delete mode 100644 space/components/ui/secondary-button.tsx delete mode 100644 space/components/ui/tooltip.tsx diff --git a/space/components/accounts/email-code-form.tsx b/space/components/accounts/email-code-form.tsx index b760ccfbb..5b1a15434 100644 --- a/space/components/accounts/email-code-form.tsx +++ b/space/components/accounts/email-code-form.tsx @@ -11,7 +11,7 @@ import useToast from "hooks/use-toast"; import useTimer from "hooks/use-timer"; // ui -import { Input, PrimaryButton } from "components/ui"; +import { Button, Input } from "@plane/ui"; // types type EmailCodeFormValues = { @@ -133,7 +133,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { id="email" type="email" placeholder="Enter your email address..." - className="border-custom-border-300 h-[46px]" + className="border-custom-border-300 h-[46px] w-full" {...register("email", { required: "Email address is required", validate: (value) => @@ -154,7 +154,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { required: "Code is required", })} placeholder="Enter code..." - className="border-custom-border-300 h-[46px]" + className="border-custom-border-300 h-[46px] w-full" /> {errors.token &&
{errors.token.message}
} ) : ( - { handleSubmit(onSubmit)().then(() => { setResendCodeTimer(30); @@ -208,7 +210,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => { loading={isSubmitting} > {isSubmitting ? "Sending code..." : "Send sign in code"} - + )} diff --git a/space/components/accounts/email-password-form.tsx b/space/components/accounts/email-password-form.tsx index 775f1a3c2..b07a26956 100644 --- a/space/components/accounts/email-password-form.tsx +++ b/space/components/accounts/email-password-form.tsx @@ -5,7 +5,8 @@ import { useForm } from "react-hook-form"; // components import { EmailResetPasswordForm } from "./email-reset-password-form"; // ui -import { Input, PrimaryButton } from "components/ui"; +import { Button, Input } from "@plane/ui"; + // types type EmailPasswordFormValues = { email: string; @@ -58,7 +59,7 @@ export const EmailPasswordForm: React.FC = ({ onSubmit }) => { ) || "Email address is not valid", })} placeholder="Enter your email address..." - className="border-custom-border-300 h-[46px]" + className="border-custom-border-300 h-[46px] w-full" /> {errors.email &&
{errors.email.message}
}
@@ -70,7 +71,7 @@ export const EmailPasswordForm: React.FC = ({ onSubmit }) => { required: "Password is required", })} placeholder="Enter your password..." - className="border-custom-border-300 h-[46px]" + className="border-custom-border-300 h-[46px] w-full" /> {errors.password &&
{errors.password.message}
}
@@ -92,14 +93,16 @@ export const EmailPasswordForm: React.FC = ({ onSubmit }) => { )}
- {isSignUpPage ? (isSubmitting ? "Signing up..." : "Sign up") : isSubmitting ? "Signing in..." : "Sign in"} - + {!isSignUpPage && ( diff --git a/space/components/accounts/email-reset-password-form.tsx b/space/components/accounts/email-reset-password-form.tsx index e7752a00f..dc4c32775 100644 --- a/space/components/accounts/email-reset-password-form.tsx +++ b/space/components/accounts/email-reset-password-form.tsx @@ -1,8 +1,7 @@ import React from "react"; import { useForm } from "react-hook-form"; // ui -import { Input } from "components/ui"; -import { Button } from "@plane/ui"; +import { Button, Input } from "@plane/ui"; // types type Props = { setIsResettingPassword: React.Dispatch>; @@ -66,15 +65,15 @@ export const EmailResetPasswordForm: React.FC = ({ setIsResettingPassword ) || "Email address is not valid", })} placeholder="Enter registered email address.." - className="h-[46px] border-custom-border-300" + className="h-[46px] border-custom-border-300 w-full" /> {errors.email &&
{errors.email.message}
}
- -
diff --git a/space/components/accounts/onboarding-form.tsx b/space/components/accounts/onboarding-form.tsx index c3cb972b2..e372ac1e5 100644 --- a/space/components/accounts/onboarding-form.tsx +++ b/space/components/accounts/onboarding-form.tsx @@ -13,7 +13,7 @@ import useToast from "hooks/use-toast"; // services import UserService from "services/user.service"; // ui -import { Input, PrimaryButton } from "components/ui"; +import { Button, Input } from "@plane/ui"; const defaultValues = { first_name: "", @@ -173,9 +173,9 @@ export const OnBoardingForm: React.FC = observer(({ user }) => {
- + ); }); diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index 220991bd9..e4ce36050 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; import Link from "next/link"; -import Image from "next/image"; import { useRouter } from "next/router"; // mobx @@ -11,7 +10,8 @@ import { observer } from "mobx-react-lite"; import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarTheme } from "./theme"; // ui -import { PrimaryButton } from "components/ui"; +import { Avatar, Button } from "@plane/ui"; +import { Briefcase } from "lucide-react"; // lib import { useMobxStore } from "lib/mobx/store-provider"; // store @@ -87,10 +87,24 @@ const IssueNavbar = observer(() => { {/* project detail */}
- {projectStore?.project && projectStore?.project?.emoji ? ( - renderEmoji(projectStore?.project?.emoji) + {projectStore.project ? ( + projectStore.project?.emoji ? ( + + {renderEmoji(projectStore.project.emoji)} + + ) : projectStore.project?.icon_prop ? ( +
+ {renderEmoji(projectStore.project.icon_prop)} +
+ ) : ( + + {projectStore.project?.name.charAt(0)} + + ) ) : ( - plane logo + + + )}
@@ -113,26 +127,13 @@ const IssueNavbar = observer(() => { {user ? (
- {user.avatar && user.avatar !== "" ? ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {user.display_name -
- ) : ( -
- {(user.display_name ?? "A")[0]} -
- )} +
{user.display_name}
) : (
- - - Sign in - - +
)} diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx index 4045d3edf..d53ae4936 100644 --- a/space/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx @@ -6,7 +6,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // ui -import { ReactionSelector, Tooltip } from "components/ui"; +import { ReactionSelector } from "components/ui"; +import { Tooltip } from "@plane/ui"; // helpers import { groupReactions, renderEmoji } from "helpers/emoji.helper"; diff --git a/space/components/issues/peek-overview/full-screen-peek-view.tsx b/space/components/issues/peek-overview/full-screen-peek-view.tsx index 3a66c9abe..e07620c05 100644 --- a/space/components/issues/peek-overview/full-screen-peek-view.tsx +++ b/space/components/issues/peek-overview/full-screen-peek-view.tsx @@ -7,7 +7,7 @@ import { PeekOverviewIssueProperties, } from "components/issues/peek-overview"; // types -import { Loader } from "components/ui/loader"; +import { Loader } from "@plane/ui"; import { IIssue } from "types/issue"; type Props = { diff --git a/space/components/issues/peek-overview/issue-activity.tsx b/space/components/issues/peek-overview/issue-activity.tsx index 2d173487c..cbb27679b 100644 --- a/space/components/issues/peek-overview/issue-activity.tsx +++ b/space/components/issues/peek-overview/issue-activity.tsx @@ -10,7 +10,8 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { CommentCard, AddComment } from "components/issues/peek-overview"; // ui -import { Icon, PrimaryButton } from "components/ui"; +import { Icon } from "components/ui"; +import { Button } from "@plane/ui"; // types import { IIssue } from "types/issue"; @@ -55,9 +56,7 @@ export const PeekOverviewIssueActivity: React.FC = observer(() => { Sign in to add your comment

- - Sign in - +
)} diff --git a/space/components/issues/peek-overview/issue-emoji-reactions.tsx b/space/components/issues/peek-overview/issue-emoji-reactions.tsx index b0c5b0361..dfd45f62b 100644 --- a/space/components/issues/peek-overview/issue-emoji-reactions.tsx +++ b/space/components/issues/peek-overview/issue-emoji-reactions.tsx @@ -6,7 +6,8 @@ import { useMobxStore } from "lib/mobx/store-provider"; // helpers import { groupReactions, renderEmoji } from "helpers/emoji.helper"; // components -import { ReactionSelector, Tooltip } from "components/ui"; +import { ReactionSelector } from "components/ui"; +import { Tooltip } from "@plane/ui"; export const IssueEmojiReactions: React.FC = observer(() => { // router diff --git a/space/components/issues/peek-overview/issue-vote-reactions.tsx b/space/components/issues/peek-overview/issue-vote-reactions.tsx index ac20565ea..8d619681a 100644 --- a/space/components/issues/peek-overview/issue-vote-reactions.tsx +++ b/space/components/issues/peek-overview/issue-vote-reactions.tsx @@ -6,7 +6,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // lib import { useMobxStore } from "lib/mobx/store-provider"; -import { Tooltip } from "components/ui"; +// ui +import { Tooltip } from "@plane/ui"; export const IssueVotes: React.FC = observer(() => { const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/space/components/issues/peek-overview/side-peek-view.tsx b/space/components/issues/peek-overview/side-peek-view.tsx index bacf83420..f0fc3d83e 100644 --- a/space/components/issues/peek-overview/side-peek-view.tsx +++ b/space/components/issues/peek-overview/side-peek-view.tsx @@ -7,7 +7,7 @@ import { PeekOverviewIssueProperties, } from "components/issues/peek-overview"; -import { Loader } from "components/ui/loader"; +import { Loader } from "@plane/ui"; import { IIssue } from "types/issue"; type Props = { diff --git a/space/components/ui/index.ts b/space/components/ui/index.ts index e44096909..1e523d5dd 100644 --- a/space/components/ui/index.ts +++ b/space/components/ui/index.ts @@ -1,8 +1,3 @@ export * from "./dropdown"; -export * from "./input"; -export * from "./loader"; -export * from "./primary-button"; -export * from "./secondary-button"; export * from "./icon"; export * from "./reaction-selector"; -export * from "./tooltip"; diff --git a/space/components/ui/input.tsx b/space/components/ui/input.tsx deleted file mode 100644 index b6be82ae5..000000000 --- a/space/components/ui/input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { forwardRef, Ref } from "react"; - -// types -interface Props extends React.InputHTMLAttributes { - mode?: "primary" | "transparent" | "trueTransparent"; - error?: boolean; - inputSize?: "rg" | "lg"; - fullWidth?: boolean; -} - -export const Input = forwardRef((props: Props, ref: Ref) => { - const { mode = "primary", error, className = "", type, fullWidth = true, id, inputSize = "rg", ...rest } = props; - - return ( - - ); -}); - -Input.displayName = "Input"; - -export default Input; diff --git a/space/components/ui/loader.tsx b/space/components/ui/loader.tsx deleted file mode 100644 index b9d13883a..000000000 --- a/space/components/ui/loader.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -type Props = { - children: React.ReactNode; - className?: string; -}; - -const Loader = ({ children, className = "" }: Props) => ( -
- {children} -
-); - -type ItemProps = { - height?: string; - width?: string; -}; - -const Item: React.FC = ({ height = "auto", width = "auto" }) => ( -
-); - -Loader.Item = Item; - -export { Loader }; diff --git a/space/components/ui/primary-button.tsx b/space/components/ui/primary-button.tsx deleted file mode 100644 index b3e1b82ee..000000000 --- a/space/components/ui/primary-button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -interface ButtonProps extends React.ButtonHTMLAttributes { - size?: "sm" | "md" | "lg"; - outline?: boolean; - loading?: boolean; -} - -export const PrimaryButton: React.FC = ({ - children, - className = "", - onClick, - type = "button", - disabled = false, - loading = false, - size = "sm", - outline = false, -}) => ( - -); diff --git a/space/components/ui/secondary-button.tsx b/space/components/ui/secondary-button.tsx deleted file mode 100644 index 2a9b3d528..000000000 --- a/space/components/ui/secondary-button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -interface ButtonProps extends React.ButtonHTMLAttributes { - size?: "sm" | "md" | "lg"; - outline?: boolean; - loading?: boolean; -} - -export const SecondaryButton: React.FC = ({ - children, - className = "", - onClick, - type = "button", - disabled = false, - loading = false, - size = "sm", - outline = false, -}) => ( - -); diff --git a/space/components/ui/tooltip.tsx b/space/components/ui/tooltip.tsx deleted file mode 100644 index 64876ffc0..000000000 --- a/space/components/ui/tooltip.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; -// next-themes -import { useTheme } from "next-themes"; -// tooltip2 -import { Tooltip2 } from "@blueprintjs/popover2"; - -type Props = { - tooltipHeading?: string; - tooltipContent: string | React.ReactNode; - position?: - | "top" - | "right" - | "bottom" - | "left" - | "auto" - | "auto-end" - | "auto-start" - | "bottom-left" - | "bottom-right" - | "left-bottom" - | "left-top" - | "right-bottom" - | "right-top" - | "top-left" - | "top-right"; - children: JSX.Element; - disabled?: boolean; - className?: string; - openDelay?: number; - closeDelay?: number; -}; - -export const Tooltip: React.FC = ({ - tooltipHeading, - tooltipContent, - position = "top", - children, - disabled = false, - className = "", - openDelay = 200, - closeDelay, -}) => { - const { theme } = useTheme(); - - return ( - - {tooltipHeading && ( -
- {tooltipHeading} -
- )} - {tooltipContent} -
- } - position={position} - renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) => - React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props }) - } - /> - ); -}; diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index 23eea6222..7c9b3b922 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -51,7 +51,7 @@ export const ProfileNavbar: React.FC = (props) => { {tabsList.map((tab) => ( Date: Tue, 5 Dec 2023 17:26:57 +0530 Subject: [PATCH 17/17] chore: workspace profile issues, kanabn DND upgrade, implemented filters in plaen deploy (#2991) --- .../issues/board-views/kanban/block.tsx | 2 +- .../issues/board-views/kanban/header.tsx | 2 +- .../issues/board-views/kanban/index.tsx | 8 +- .../issues/filters-render/index.tsx | 53 --------- .../label/filter-label-block.tsx | 43 ------- .../issues/filters-render/label/index.tsx | 51 -------- .../priority/filter-priority-block.tsx | 42 ------- .../issues/filters-render/priority/index.tsx | 53 --------- .../state/filter-state-block.tsx | 34 ------ .../issues/filters-render/state/index.tsx | 51 -------- .../filters/applied-filters/filters-list.tsx | 80 +++++++++++++ .../issues/filters/applied-filters/label.tsx | 42 +++++++ .../filters/applied-filters/priority.tsx | 31 +++++ .../issues/filters/applied-filters/root.tsx | 90 ++++++++++++++ .../issues/filters/applied-filters/state.tsx | 39 +++++++ .../issues/filters/helpers/dropdown.tsx | 72 ++++++++++++ .../issues/filters/helpers/filter-header.tsx | 22 ++++ .../issues/filters/helpers/filter-option.tsx | 35 ++++++ .../issues/filters/helpers/index.ts | 3 + space/components/issues/filters/index.ts | 11 ++ space/components/issues/filters/labels.tsx | 83 +++++++++++++ space/components/issues/filters/priority.tsx | 51 ++++++++ space/components/issues/filters/root.tsx | 77 ++++++++++++ space/components/issues/filters/selection.tsx | 86 ++++++++++++++ space/components/issues/filters/state.tsx | 78 +++++++++++++ space/components/issues/navbar/index.tsx | 48 ++++++-- .../issues/navbar/issue-board-view.tsx | 3 +- .../components/issues/navbar/issue-filter.tsx | 110 ------------------ space/components/views/project-details.tsx | 8 +- space/layouts/project-layout.tsx | 1 - space/store/issue.ts | 2 +- space/store/issues/base-issue-filter.store.ts | 29 +++++ space/store/issues/helpers.ts | 52 +++++++++ space/store/issues/issue-filters.store.ts | 106 +++++++++++++++++ space/store/issues/types.ts | 36 ++++++ space/store/project.ts | 9 +- space/store/root.ts | 3 + .../issues/issue-layouts/kanban/block.tsx | 39 +++++-- web/components/issues/modal.tsx | 2 +- web/components/profile/profile-issues.tsx | 6 +- .../profile/[userId]/assigned.tsx | 3 +- .../issues/base-issue-kanban-helper.store.ts | 2 - web/store/issues/profile/issue.store.ts | 2 - 43 files changed, 1117 insertions(+), 483 deletions(-) delete mode 100644 space/components/issues/filters-render/index.tsx delete mode 100644 space/components/issues/filters-render/label/filter-label-block.tsx delete mode 100644 space/components/issues/filters-render/label/index.tsx delete mode 100644 space/components/issues/filters-render/priority/filter-priority-block.tsx delete mode 100644 space/components/issues/filters-render/priority/index.tsx delete mode 100644 space/components/issues/filters-render/state/filter-state-block.tsx delete mode 100644 space/components/issues/filters-render/state/index.tsx create mode 100644 space/components/issues/filters/applied-filters/filters-list.tsx create mode 100644 space/components/issues/filters/applied-filters/label.tsx create mode 100644 space/components/issues/filters/applied-filters/priority.tsx create mode 100644 space/components/issues/filters/applied-filters/root.tsx create mode 100644 space/components/issues/filters/applied-filters/state.tsx create mode 100644 space/components/issues/filters/helpers/dropdown.tsx create mode 100644 space/components/issues/filters/helpers/filter-header.tsx create mode 100644 space/components/issues/filters/helpers/filter-option.tsx create mode 100644 space/components/issues/filters/helpers/index.ts create mode 100644 space/components/issues/filters/index.ts create mode 100644 space/components/issues/filters/labels.tsx create mode 100644 space/components/issues/filters/priority.tsx create mode 100644 space/components/issues/filters/root.tsx create mode 100644 space/components/issues/filters/selection.tsx create mode 100644 space/components/issues/filters/state.tsx delete mode 100644 space/components/issues/navbar/issue-filter.tsx create mode 100644 space/store/issues/base-issue-filter.store.ts create mode 100644 space/store/issues/helpers.ts create mode 100644 space/store/issues/issue-filters.store.ts create mode 100644 space/store/issues/types.ts diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx index e44f1dba0..34e4cb3f1 100644 --- a/space/components/issues/board-views/kanban/block.tsx +++ b/space/components/issues/board-views/kanban/block.tsx @@ -13,7 +13,7 @@ import { IIssue } from "types/issue"; import { RootStore } from "store/root"; import { useRouter } from "next/router"; -export const IssueListBlock = observer(({ issue }: { issue: IIssue }) => { +export const IssueKanBanBlock = observer(({ issue }: { issue: IIssue }) => { const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore(); // router diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx index 8f2f28496..488d94b59 100644 --- a/space/components/issues/board-views/kanban/header.tsx +++ b/space/components/issues/board-views/kanban/header.tsx @@ -10,7 +10,7 @@ import { StateGroupIcon } from "@plane/ui"; import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { +export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => { const store: RootStore = useMobxStore(); const stateGroup = issueGroupFilter(state.group); diff --git a/space/components/issues/board-views/kanban/index.tsx b/space/components/issues/board-views/kanban/index.tsx index b45b037d2..cc00f931e 100644 --- a/space/components/issues/board-views/kanban/index.tsx +++ b/space/components/issues/board-views/kanban/index.tsx @@ -3,8 +3,8 @@ // mobx react lite import { observer } from "mobx-react-lite"; // components -import { IssueListHeader } from "components/issues/board-views/kanban/header"; -import { IssueListBlock } from "components/issues/board-views/kanban/block"; +import { IssueKanBanHeader } from "components/issues/board-views/kanban/header"; +import { IssueKanBanBlock } from "components/issues/board-views/kanban/block"; // ui import { Icon } from "components/ui"; // interfaces @@ -23,14 +23,14 @@ export const IssueKanbanView = observer(() => { store?.issue?.states.map((_state: IIssueState) => (
- +
{store.issue.getFilteredIssuesByState(_state.id) && store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - + ))}
) : ( diff --git a/space/components/issues/filters-render/index.tsx b/space/components/issues/filters-render/index.tsx deleted file mode 100644 index d797d1506..000000000 --- a/space/components/issues/filters-render/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import IssueStateFilter from "./state"; -import IssueLabelFilter from "./label"; -import IssuePriorityFilter from "./priority"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearAllFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "all", - // removeAll: true, - // }) - // ); - }; - - // if (store.issue.getIfFiltersIsEmpty()) return null; - - return ( -
-
- {/* state */} - {/* {store.issue.checkIfFilterExistsForKey("state") && } */} - {/* labels */} - {/* {store.issue.checkIfFilterExistsForKey("label") && } */} - {/* priority */} - {/* {store.issue.checkIfFilterExistsForKey("priority") && } */} - {/* clear all filters */} -
-
Clear all filters
-
- close -
-
-
-
- ); -}); - -export default IssueFilter; diff --git a/space/components/issues/filters-render/label/filter-label-block.tsx b/space/components/issues/filters-render/label/filter-label-block.tsx deleted file mode 100644 index a54fb65e4..000000000 --- a/space/components/issues/filters-render/label/filter-label-block.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssueLabel } from "types/issue"; - -export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => { - const store = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const removeLabelFromFilter = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "label", - // value: label?.id, - // }) - // ); - }; - - return ( -
-
- -
{label?.name}
-
- close -
-
- ); -}); diff --git a/space/components/issues/filters-render/label/index.tsx b/space/components/issues/filters-render/label/index.tsx deleted file mode 100644 index 1d9a4f990..000000000 --- a/space/components/issues/filters-render/label/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { RenderIssueLabel } from "./filter-label-block"; -// interfaces -import { IIssueLabel } from "types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueLabelFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearLabelFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "label", - // removeAll: true, - // }) - // ); - }; - - return ( - <> -
-
Labels
-
- {/* {store?.issue?.labels && - store?.issue?.labels.map( - (_label: IIssueLabel, _index: number) => - store.issue.getUserSelectedFilter("label", _label.id) && ( - - ) - )} */} -
-
- close -
-
- - ); -}); - -export default IssueLabelFilter; diff --git a/space/components/issues/filters-render/priority/filter-priority-block.tsx b/space/components/issues/filters-render/priority/filter-priority-block.tsx deleted file mode 100644 index 5fd1ef1a7..000000000 --- a/space/components/issues/filters-render/priority/filter-priority-block.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssuePriorityFilters } from "types/issue"; - -export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => { - const store = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const removePriorityFromFilter = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "priority", - // value: priority?.key, - // }) - // ); - }; - - return ( -
-
- {priority?.icon} -
-
{priority?.title}
-
- close -
-
- ); -}); diff --git a/space/components/issues/filters-render/priority/index.tsx b/space/components/issues/filters-render/priority/index.tsx deleted file mode 100644 index 100ba1761..000000000 --- a/space/components/issues/filters-render/priority/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// components -import { RenderIssuePriority } from "./filter-priority-block"; -// interfaces -import { IIssuePriorityFilters } from "types/issue"; -// constants -import { issuePriorityFilters } from "constants/data"; - -const IssuePriorityFilter = observer(() => { - const store = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearPriorityFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "priority", - // removeAll: true, - // }) - // ); - }; - - return ( - <> -
-
Priority
-
- {/* {issuePriorityFilters.map( - (_priority: IIssuePriorityFilters, _index: number) => - store.issue.getUserSelectedFilter("priority", _priority.key) && ( - - ) - )} */} -
-
{ - clearPriorityFilters(); - }} - > - close -
-
- - ); -}); - -export default IssuePriorityFilter; diff --git a/space/components/issues/filters-render/state/filter-state-block.tsx b/space/components/issues/filters-render/state/filter-state-block.tsx deleted file mode 100644 index b9c8ed4ec..000000000 --- a/space/components/issues/filters-render/state/filter-state-block.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { observer } from "mobx-react-lite"; -// interfaces -import { IIssueState } from "types/issue"; -// constants -import { issueGroupFilter } from "constants/data"; - -export const RenderIssueState = observer(({ state }: { state: IIssueState }) => { - const stateGroup = issueGroupFilter(state.group); - - const removeStateFromFilter = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "state", - // value: state?.id, - // }) - // ); - }; - - if (stateGroup === null) return <>; - return ( -
-
- {/* */} -
-
{state?.name}
-
- close -
-
- ); -}); diff --git a/space/components/issues/filters-render/state/index.tsx b/space/components/issues/filters-render/state/index.tsx deleted file mode 100644 index 0198c5215..000000000 --- a/space/components/issues/filters-render/state/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { RenderIssueState } from "./filter-state-block"; -// interfaces -import { IIssueState } from "types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueStateFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearStateFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "state", - // removeAll: true, - // }) - // ); - }; - - return ( - <> -
-
State
-
- {/* {store?.issue?.states && - store?.issue?.states.map( - (_state: IIssueState, _index: number) => - store.issue.getUserSelectedFilter("state", _state.id) && ( - - ) - )} */} -
-
- close -
-
- - ); -}); - -export default IssueStateFilter; diff --git a/space/components/issues/filters/applied-filters/filters-list.tsx b/space/components/issues/filters/applied-filters/filters-list.tsx new file mode 100644 index 000000000..898898232 --- /dev/null +++ b/space/components/issues/filters/applied-filters/filters-list.tsx @@ -0,0 +1,80 @@ +// components +import { AppliedLabelsFilters } from "./label"; +import { AppliedPriorityFilters } from "./priority"; +import { AppliedStateFilters } from "./state"; +// icons +import { X } from "lucide-react"; +// helpers +import { IIssueFilterOptions } from "store/issues/types"; +import { IIssueLabel, IIssueState } from "types/issue"; +// types + +type Props = { + appliedFilters: IIssueFilterOptions; + handleRemoveAllFilters: () => void; + handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; + labels?: IIssueLabel[] | undefined; + states?: IIssueState[] | undefined; +}; + +export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); + +export const AppliedFiltersList: React.FC = (props) => { + const { appliedFilters, handleRemoveAllFilters, handleRemoveFilter, labels, states } = props; + + return ( +
+ {Object.entries(appliedFilters).map(([key, value]) => { + const filterKey = key as keyof IIssueFilterOptions; + + if (!value) return; + + return ( +
+ {replaceUnderscoreIfSnakeCase(filterKey)} +
+ {filterKey === "priority" && ( + handleRemoveFilter("priority", val)} values={value} /> + )} + + {filterKey === "labels" && labels && ( + handleRemoveFilter("labels", val)} + labels={labels} + values={value} + /> + )} + + {filterKey === "state" && states && ( + handleRemoveFilter("state", val)} + states={states} + values={value} + /> + )} + + +
+
+ ); + })} + +
+ ); +}; diff --git a/space/components/issues/filters/applied-filters/label.tsx b/space/components/issues/filters/applied-filters/label.tsx new file mode 100644 index 000000000..ecf824210 --- /dev/null +++ b/space/components/issues/filters/applied-filters/label.tsx @@ -0,0 +1,42 @@ +import { X } from "lucide-react"; +// types +import { IIssueLabel } from "types/issue"; + +type Props = { + handleRemove: (val: string) => void; + labels: IIssueLabel[] | undefined; + values: string[]; +}; + +export const AppliedLabelsFilters: React.FC = (props) => { + const { handleRemove, labels, values } = props; + + return ( + <> + {values.map((labelId) => { + const labelDetails = labels?.find((l) => l.id === labelId); + + if (!labelDetails) return null; + + return ( +
+ + {labelDetails.name} + +
+ ); + })} + + ); +}; diff --git a/space/components/issues/filters/applied-filters/priority.tsx b/space/components/issues/filters/applied-filters/priority.tsx new file mode 100644 index 000000000..f051abf2d --- /dev/null +++ b/space/components/issues/filters/applied-filters/priority.tsx @@ -0,0 +1,31 @@ +import { PriorityIcon } from "@plane/ui"; +import { X } from "lucide-react"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedPriorityFilters: React.FC = (props) => { + const { handleRemove, values } = props; + + return ( + <> + {values && + values.length > 0 && + values.map((priority) => ( +
+ + {priority} + +
+ ))} + + ); +}; diff --git a/space/components/issues/filters/applied-filters/root.tsx b/space/components/issues/filters/applied-filters/root.tsx new file mode 100644 index 000000000..3f77dcc06 --- /dev/null +++ b/space/components/issues/filters/applied-filters/root.tsx @@ -0,0 +1,90 @@ +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// components +import { AppliedFiltersList } from "./filters-list"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +import { IIssueFilterOptions } from "store/issues/types"; + +export const IssueAppliedFilters: FC = observer(() => { + const router = useRouter(); + const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as { + workspace_slug: string; + project_slug: string; + }; + + const { + issuesFilter: { issueFilters, updateFilters }, + issue: { states, labels }, + project: { activeBoard }, + }: RootStore = useMobxStore(); + + const userFilters = issueFilters?.filters || {}; + + const appliedFilters: IIssueFilterOptions = {}; + Object.entries(userFilters).forEach(([key, value]) => { + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + appliedFilters[key as keyof IIssueFilterOptions] = value; + }); + + const updateRouteParams = useCallback( + (key: keyof IIssueFilterOptions | null, value: string[] | null, clearFields: boolean = false) => { + const state = key === "state" ? value || [] : issueFilters?.filters?.state ?? []; + const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? []; + const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? []; + + let params: any = { board: activeBoard || "list" }; + if (!clearFields) { + if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; + if (state.length > 0) params = { ...params, states: state.join(",") }; + if (labels.length > 0) params = { ...params, labels: labels.join(",") }; + } + + router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); + }, + [workspaceSlug, projectId, activeBoard, issueFilters, router] + ); + + const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { + if (!projectId) return; + if (!value) { + updateFilters(projectId, { [key]: null }); + return; + } + + let newValues = issueFilters?.filters?.[key] ?? []; + newValues = newValues.filter((val) => val !== value); + + updateFilters(projectId, { [key]: newValues }); + updateRouteParams(key, newValues); + }; + + const handleRemoveAllFilters = () => { + if (!projectId) return; + + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + + updateFilters(projectId, { ...newFilters }); + updateRouteParams(null, null, true); + }; + + if (Object.keys(appliedFilters).length === 0) return null; + + return ( +
+ +
+ ); +}); diff --git a/space/components/issues/filters/applied-filters/state.tsx b/space/components/issues/filters/applied-filters/state.tsx new file mode 100644 index 000000000..f238197b8 --- /dev/null +++ b/space/components/issues/filters/applied-filters/state.tsx @@ -0,0 +1,39 @@ +import { X } from "lucide-react"; +import { StateGroupIcon } from "@plane/ui"; +// icons +import { IIssueState } from "types/issue"; +// types + +type Props = { + handleRemove: (val: string) => void; + states: IIssueState[]; + values: string[]; +}; + +export const AppliedStateFilters: React.FC = (props) => { + const { handleRemove, states, values } = props; + + return ( + <> + {values.map((stateId) => { + const stateDetails = states?.find((s) => s.id === stateId); + + if (!stateDetails) return null; + + return ( +
+ + {stateDetails.name} + +
+ ); + })} + + ); +}; diff --git a/space/components/issues/filters/helpers/dropdown.tsx b/space/components/issues/filters/helpers/dropdown.tsx new file mode 100644 index 000000000..0f93b75c9 --- /dev/null +++ b/space/components/issues/filters/helpers/dropdown.tsx @@ -0,0 +1,72 @@ +import React, { Fragment, useState } from "react"; +import { usePopper } from "react-popper"; +import { Popover, Transition } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; +// ui +import { Button } from "@plane/ui"; +// icons +import { ChevronUp } from "lucide-react"; + +type Props = { + children: React.ReactNode; + title?: string; + placement?: Placement; +}; + +export const FiltersDropdown: React.FC = (props) => { + const { children, title = "Dropdown", placement } = props; + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "auto", + }); + + return ( + + {({ open }) => { + if (open) { + } + return ( + <> + + + + + +
+
{children}
+
+
+
+ + ); + }} +
+ ); +}; diff --git a/space/components/issues/filters/helpers/filter-header.tsx b/space/components/issues/filters/helpers/filter-header.tsx new file mode 100644 index 000000000..4513b0795 --- /dev/null +++ b/space/components/issues/filters/helpers/filter-header.tsx @@ -0,0 +1,22 @@ +import React from "react"; +// lucide icons +import { ChevronDown, ChevronUp } from "lucide-react"; + +interface IFilterHeader { + title: string; + isPreviewEnabled: boolean; + handleIsPreviewEnabled: () => void; +} + +export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => ( +
+
{title}
+ +
+); diff --git a/space/components/issues/filters/helpers/filter-option.tsx b/space/components/issues/filters/helpers/filter-option.tsx new file mode 100644 index 000000000..4b6f1b041 --- /dev/null +++ b/space/components/issues/filters/helpers/filter-option.tsx @@ -0,0 +1,35 @@ +import React from "react"; +// lucide icons +import { Check } from "lucide-react"; + +type Props = { + icon?: React.ReactNode; + isChecked: boolean; + title: React.ReactNode; + onClick?: () => void; + multiple?: boolean; +}; + +export const FilterOption: React.FC = (props) => { + const { icon, isChecked, multiple = true, onClick, title } = props; + + return ( + + ); +}; diff --git a/space/components/issues/filters/helpers/index.ts b/space/components/issues/filters/helpers/index.ts new file mode 100644 index 000000000..ef38d9884 --- /dev/null +++ b/space/components/issues/filters/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./dropdown"; +export * from "./filter-header"; +export * from "./filter-option"; diff --git a/space/components/issues/filters/index.ts b/space/components/issues/filters/index.ts new file mode 100644 index 000000000..56a01386d --- /dev/null +++ b/space/components/issues/filters/index.ts @@ -0,0 +1,11 @@ +// filters +export * from "./root"; +export * from "./selection"; + +// properties +export * from "./state"; +export * from "./priority"; +export * from "./labels"; + +// helpers +export * from "./helpers"; diff --git a/space/components/issues/filters/labels.tsx b/space/components/issues/filters/labels.tsx new file mode 100644 index 000000000..4b8aa3b4f --- /dev/null +++ b/space/components/issues/filters/labels.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; + +// components +import { FilterHeader, FilterOption } from "./helpers"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IIssueLabel } from "types/issue"; + +const LabelIcons = ({ color }: { color: string }) => ( + +); + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + labels: IIssueLabel[] | undefined; + searchQuery: string; +}; + +export const FilterLabels: React.FC = (props) => { + const { appliedFilters, handleUpdate, labels, searchQuery } = props; + + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((label) => ( + handleUpdate(label?.id)} + icon={} + title={label.name} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}; diff --git a/space/components/issues/filters/priority.tsx b/space/components/issues/filters/priority.tsx new file mode 100644 index 000000000..94a7f6a8c --- /dev/null +++ b/space/components/issues/filters/priority.tsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +// ui +import { PriorityIcon } from "@plane/ui"; +// components +import { FilterHeader, FilterOption } from "./helpers"; +// constants +import { issuePriorityFilters } from "constants/data"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterPriority: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = issuePriorityFilters.filter((p) => p.key.includes(searchQuery.toLowerCase())); + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((priority) => ( + handleUpdate(priority.key)} + icon={} + title={priority.title} + /> + )) + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/space/components/issues/filters/root.tsx b/space/components/issues/filters/root.tsx new file mode 100644 index 000000000..eb9946a24 --- /dev/null +++ b/space/components/issues/filters/root.tsx @@ -0,0 +1,77 @@ +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// components +import { FiltersDropdown } from "./helpers/dropdown"; +import { FilterSelection } from "./selection"; +// types +import { IIssueFilterOptions } from "store/issues/types"; +// helpers +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "store/issues/helpers"; +// store +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; + +export const IssueFiltersDropdown: FC = observer(() => { + const router = useRouter(); + const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as { + workspace_slug: string; + project_slug: string; + }; + + const { + project: { activeBoard }, + issue: { states, labels }, + issuesFilter: { issueFilters, updateFilters }, + }: RootStore = useMobxStore(); + + const updateRouteParams = useCallback( + (key: keyof IIssueFilterOptions, value: string[]) => { + const state = key === "state" ? value : issueFilters?.filters?.state ?? []; + const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? []; + const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? []; + + let params: any = { board: activeBoard || "list" }; + if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; + if (state.length > 0) params = { ...params, states: state.join(",") }; + if (labels.length > 0) params = { ...params, labels: labels.join(",") }; + + router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); + }, + [workspaceSlug, projectId, activeBoard, issueFilters, router] + ); + + const handleFilters = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; + + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(projectId, { [key]: newValues }); + updateRouteParams(key, newValues); + }, + [projectId, issueFilters, updateFilters, updateRouteParams] + ); + + return ( +
+ + + +
+ ); +}); diff --git a/space/components/issues/filters/selection.tsx b/space/components/issues/filters/selection.tsx new file mode 100644 index 000000000..e479a7d59 --- /dev/null +++ b/space/components/issues/filters/selection.tsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Search, X } from "lucide-react"; +// components +import { FilterLabels, FilterPriority, FilterState } from "./"; +// types + +// filter helpers +import { ILayoutDisplayFiltersOptions } from "store/issues/helpers"; +import { IIssueFilterOptions } from "store/issues/types"; +import { IIssueState, IIssueLabel } from "types/issue"; + +type Props = { + filters: IIssueFilterOptions; + handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; + layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; + labels?: IIssueLabel[] | undefined; + states?: IIssueState[] | undefined; +}; + +export const FilterSelection: React.FC = observer((props) => { + const { filters, handleFilters, layoutDisplayFiltersOptions, labels, states } = props; + + const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); + + const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter); + + return ( +
+
+
+ + setFiltersSearchQuery(e.target.value)} + autoFocus + /> + {filtersSearchQuery !== "" && ( + + )} +
+
+
+ {/* priority */} + {isFilterEnabled("priority") && ( +
+ handleFilters("priority", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* state */} + {isFilterEnabled("state") && ( +
+ handleFilters("state", val)} + searchQuery={filtersSearchQuery} + states={states} + /> +
+ )} + + {/* labels */} + {isFilterEnabled("labels") && ( +
+ handleFilters("labels", val)} + labels={labels} + searchQuery={filtersSearchQuery} + /> +
+ )} +
+
+ ); +}); diff --git a/space/components/issues/filters/state.tsx b/space/components/issues/filters/state.tsx new file mode 100644 index 000000000..1175a5ed6 --- /dev/null +++ b/space/components/issues/filters/state.tsx @@ -0,0 +1,78 @@ +import React, { useState } from "react"; +// components +import { FilterHeader, FilterOption } from "./helpers"; +// ui +import { Loader, StateGroupIcon } from "@plane/ui"; +// types +import { IIssueState } from "types/issue"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; + states: IIssueState[] | undefined; +}; + +export const FilterState: React.FC = (props) => { + const { appliedFilters, handleUpdate, searchQuery, states } = props; + + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((state) => ( + handleUpdate(state.id)} + icon={} + title={state.name} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}; diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index e4ce36050..d6491bc26 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -9,6 +9,7 @@ import { observer } from "mobx-react-lite"; // import { NavbarSearch } from "./search"; import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarTheme } from "./theme"; +import { IssueFiltersDropdown } from "components/issues/filters"; // ui import { Avatar, Button } from "@plane/ui"; import { Briefcase } from "lucide-react"; @@ -16,6 +17,7 @@ import { Briefcase } from "lucide-react"; import { useMobxStore } from "lib/mobx/store-provider"; // store import { RootStore } from "store/root"; +import { TIssueBoardKeys } from "types/issue"; const renderEmoji = (emoji: string | { name: string; color: string }) => { if (!emoji) return; @@ -30,10 +32,21 @@ const renderEmoji = (emoji: string | { name: string; color: string }) => { }; const IssueNavbar = observer(() => { - const { project: projectStore, user: userStore }: RootStore = useMobxStore(); + const { + project: projectStore, + user: userStore, + issuesFilter: { updateFilters }, + }: RootStore = useMobxStore(); // router const router = useRouter(); - const { workspace_slug, project_slug, board } = router.query; + const { workspace_slug, project_slug, board, states, priorities, labels } = router.query as { + workspace_slug: string; + project_slug: string; + board: string; + states: string; + priorities: string; + labels: string; + }; const user = userStore?.currentUser; @@ -46,7 +59,7 @@ const IssueNavbar = observer(() => { useEffect(() => { if (workspace_slug && project_slug && projectStore?.deploySettings) { const viewsAcceptable: string[] = []; - let currentBoard: string | null = null; + let currentBoard: TIssueBoardKeys | null = null; if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list"); if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban"); @@ -56,31 +69,41 @@ const IssueNavbar = observer(() => { if (board) { if (viewsAcceptable.includes(board.toString())) { - currentBoard = board.toString(); + currentBoard = board.toString() as TIssueBoardKeys; } else { if (viewsAcceptable && viewsAcceptable.length > 0) { - currentBoard = viewsAcceptable[0]; + currentBoard = viewsAcceptable[0] as TIssueBoardKeys; } } } else { if (viewsAcceptable && viewsAcceptable.length > 0) { - currentBoard = viewsAcceptable[0]; + currentBoard = viewsAcceptable[0] as TIssueBoardKeys; } } if (currentBoard) { if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) { + let params: any = { board: currentBoard }; + if (priorities && priorities.length > 0) params = { ...params, priorities: priorities }; + if (states && states.length > 0) params = { ...params, states: states }; + if (labels && labels.length > 0) params = { ...params, labels: labels }; + + let storeParams: any = {}; + if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") }; + if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") }; + if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") }; + + if (storeParams) updateFilters(project_slug, storeParams); + projectStore.setActiveBoard(currentBoard); router.push({ pathname: `/${workspace_slug}/${project_slug}`, - query: { - board: currentBoard, - }, + query: { ...params }, }); } } } - }, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings]); + }, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings, updateFilters]); return (
@@ -120,6 +143,11 @@ const IssueNavbar = observer(() => {
+ {/* issue filters */} +
+ +
+ {/* theming */}
diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx index 16b09229a..906d3543d 100644 --- a/space/components/issues/navbar/issue-board-view.tsx +++ b/space/components/issues/navbar/issue-board-view.tsx @@ -5,6 +5,7 @@ import { issueViews } from "constants/data"; // mobx import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +import { TIssueBoardKeys } from "types/issue"; export const NavbarIssueBoardView = observer(() => { const { @@ -15,7 +16,7 @@ export const NavbarIssueBoardView = observer(() => { const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; const handleCurrentBoardView = (boardView: string) => { - setActiveBoard(boardView); + setActiveBoard(boardView as TIssueBoardKeys); router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`); }; diff --git a/space/components/issues/navbar/issue-filter.tsx b/space/components/issues/navbar/issue-filter.tsx deleted file mode 100644 index 83d5159d6..000000000 --- a/space/components/issues/navbar/issue-filter.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { ChevronDown } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; -// components -import { Dropdown } from "components/ui/dropdown"; -// constants -import { issueGroupFilter } from "constants/data"; - -const PRIORITIES = ["urgent", "high", "medium", "low"]; - -export const NavbarIssueFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const pathName = router.asPath; - - const handleOnSelect = (key: "states" | "labels" | "priorities", value: string) => { - // if (key === "states") { - // store.issue.userSelectedStates = store.issue.userSelectedStates.includes(value) - // ? store.issue.userSelectedStates.filter((s) => s !== value) - // : [...store.issue.userSelectedStates, value]; - // } else if (key === "labels") { - // store.issue.userSelectedLabels = store.issue.userSelectedLabels.includes(value) - // ? store.issue.userSelectedLabels.filter((l) => l !== value) - // : [...store.issue.userSelectedLabels, value]; - // } else if (key === "priorities") { - // store.issue.userSelectedPriorities = store.issue.userSelectedPriorities.includes(value) - // ? store.issue.userSelectedPriorities.filter((p) => p !== value) - // : [...store.issue.userSelectedPriorities, value]; - // } - // const paramsCommaSeparated = `${`board=${store.issue.currentIssueBoardView || "list"}`}${ - // store.issue.userSelectedPriorities.length > 0 ? `&priorities=${store.issue.userSelectedPriorities.join(",")}` : "" - // }${store.issue.userSelectedStates.length > 0 ? `&states=${store.issue.userSelectedStates.join(",")}` : ""}${ - // store.issue.userSelectedLabels.length > 0 ? `&labels=${store.issue.userSelectedLabels.join(",")}` : "" - // }`; - // router.replace(`${pathName}?${paramsCommaSeparated}`); - }; - - return ( - - Filters -
) : ( projectStore?.activeBoard && ( - <> +
+ {/* applied filters */} + + {projectStore?.activeBoard === "list" && (
@@ -85,7 +89,7 @@ export const ProjectDetailsView = observer(() => { {projectStore?.activeBoard === "calendar" && } {projectStore?.activeBoard === "spreadsheet" && } {projectStore?.activeBoard === "gantt" && } - +
) )} diff --git a/space/layouts/project-layout.tsx b/space/layouts/project-layout.tsx index 1a0b7899e..c8bdfd9a1 100644 --- a/space/layouts/project-layout.tsx +++ b/space/layouts/project-layout.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import Image from "next/image"; // mobx diff --git a/space/store/issue.ts b/space/store/issue.ts index d47336984..02dd3cdd0 100644 --- a/space/store/issue.ts +++ b/space/store/issue.ts @@ -1,4 +1,4 @@ -import { observable, action, computed, makeObservable, runInAction, reaction } from "mobx"; +import { observable, action, computed, makeObservable, runInAction } from "mobx"; // services import IssueService from "services/issue.service"; // store diff --git a/space/store/issues/base-issue-filter.store.ts b/space/store/issues/base-issue-filter.store.ts new file mode 100644 index 000000000..2cd2e3bc9 --- /dev/null +++ b/space/store/issues/base-issue-filter.store.ts @@ -0,0 +1,29 @@ +// types +import { RootStore } from "store/root"; + +export interface IIssueFilterBaseStore { + // helper methods + computedFilter(filters: any, filteredParams: any): any; +} + +export class IssueFilterBaseStore implements IIssueFilterBaseStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + // root store + this.rootStore = _rootStore; + } + + // helper methods + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; +} diff --git a/space/store/issues/helpers.ts b/space/store/issues/helpers.ts new file mode 100644 index 000000000..a862ca6e0 --- /dev/null +++ b/space/store/issues/helpers.ts @@ -0,0 +1,52 @@ +import { TIssueBoardKeys } from "types/issue"; +import { IIssueFilterOptions, TIssueParams } from "./types"; + +export const isNil = (value: any) => { + if (value === undefined || value === null) return true; + + return false; +}; + +export interface ILayoutDisplayFiltersOptions { + filters: (keyof IIssueFilterOptions)[]; + display_properties: boolean | null; + display_filters: null; + extra_options: null; +} + +export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { + [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; +} = { + issues: { + list: { + filters: ["priority", "state", "labels"], + display_properties: null, + display_filters: null, + extra_options: null, + }, + kanban: { + filters: ["priority", "state", "labels"], + display_properties: null, + display_filters: null, + extra_options: null, + }, + }, +}; + +export const handleIssueQueryParamsByLayout = ( + layout: TIssueBoardKeys | undefined, + viewType: "issues" +): TIssueParams[] | null => { + const queryParams: TIssueParams[] = []; + + if (!layout) return null; + + const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout]; + + // add filters query params + layoutOptions.filters.forEach((option) => { + queryParams.push(option); + }); + + return queryParams; +}; diff --git a/space/store/issues/issue-filters.store.ts b/space/store/issues/issue-filters.store.ts new file mode 100644 index 000000000..f2408e290 --- /dev/null +++ b/space/store/issues/issue-filters.store.ts @@ -0,0 +1,106 @@ +import { action, makeObservable, observable, runInAction, computed } from "mobx"; +// types +import { RootStore } from "store/root"; +import { IIssueFilterOptions, TIssueParams } from "./types"; +import { handleIssueQueryParamsByLayout } from "./helpers"; +import { IssueFilterBaseStore } from "./base-issue-filter.store"; + +interface IFiltersOptions { + filters: IIssueFilterOptions; +} + +export interface IIssuesFilterStore { + // observables + projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined; + // computed + issueFilters: IFiltersOptions | undefined; + appliedFilters: TIssueParams[] | undefined; + // helpers + issueDisplayFilters: (projectId: string) => IFiltersOptions | undefined; + // actions + updateFilters: (projectId: string, filters: IIssueFilterOptions) => Promise; +} + +export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFilterStore { + // observables + projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined = undefined; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observables + projectIssueFilters: observable.ref, + // computed + issueFilters: computed, + appliedFilters: computed, + // actions + updateFilters: action, + }); + // root store + this.rootStore = _rootStore; + } + + // helpers + issueDisplayFilters = (projectId: string) => { + if (!projectId) return undefined; + return this.projectIssueFilters?.[projectId] || undefined; + }; + + // actions + + updateFilters = async (projectId: string, filters: IIssueFilterOptions) => { + try { + let _projectIssueFilters = { ...this.projectIssueFilters }; + if (!_projectIssueFilters) _projectIssueFilters = {}; + if (!_projectIssueFilters[projectId]) _projectIssueFilters[projectId] = { filters: {} }; + + const _filters = { + filters: { ..._projectIssueFilters[projectId].filters }, + }; + + _filters.filters = { ..._filters.filters, ...filters }; + + _projectIssueFilters[projectId] = { + filters: _filters.filters, + }; + + runInAction(() => { + this.projectIssueFilters = _projectIssueFilters; + }); + + return _filters; + } catch (error) { + throw error; + } + }; + + get issueFilters() { + const projectId = this.rootStore.project.project?.id; + if (!projectId) return undefined; + + const issueFilters = this.issueDisplayFilters(projectId); + if (!issueFilters) return undefined; + + return issueFilters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + const layout = this.rootStore.project?.activeBoard; + if (!userFilters || !layout) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state: userFilters?.filters?.state || undefined, + labels: userFilters?.filters?.labels || undefined, + }; + + const filteredParams = handleIssueQueryParamsByLayout(layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } +} diff --git a/space/store/issues/types.ts b/space/store/issues/types.ts new file mode 100644 index 000000000..d1de0a5ea --- /dev/null +++ b/space/store/issues/types.ts @@ -0,0 +1,36 @@ +import { IIssue } from "types/issue"; + +export type TIssueGroupByOptions = "state" | "priority" | "labels" | null; + +export type TIssueParams = "priority" | "state" | "labels"; + +export interface IIssueFilterOptions { + state?: string[] | null; + labels?: string[] | null; + priority?: string[] | null; +} + +// issues +export interface IGroupedIssues { + [group_id: string]: string[]; +} + +export interface ISubGroupedIssues { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +} + +export type TUnGroupedIssues = string[]; + +export interface IIssueResponse { + [issue_id: string]: IIssue; +} + +export type TLoader = "init-loader" | "mutation" | undefined; + +export interface ViewFlags { + enableQuickAdd: boolean; + enableIssueCreation: boolean; + enableInlineEditing: boolean; +} diff --git a/space/store/project.ts b/space/store/project.ts index ddd589f9a..76b4d06cb 100644 --- a/space/store/project.ts +++ b/space/store/project.ts @@ -2,6 +2,7 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // service import ProjectService from "services/project.service"; +import { TIssueBoardKeys } from "types/issue"; // types import { IWorkspace, IProject, IProjectSettings } from "types/project"; @@ -12,9 +13,9 @@ export interface IProjectStore { project: IProject | null; deploySettings: IProjectSettings | null; viewOptions: any; - activeBoard: string | null; + activeBoard: TIssueBoardKeys | null; fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise; - setActiveBoard: (value: string) => void; + setActiveBoard: (value: TIssueBoardKeys) => void; } class ProjectStore implements IProjectStore { @@ -25,7 +26,7 @@ class ProjectStore implements IProjectStore { project: IProject | null = null; deploySettings: IProjectSettings | null = null; viewOptions: any = null; - activeBoard: string | null = null; + activeBoard: TIssueBoardKeys | null = null; // root store rootStore; // service @@ -80,7 +81,7 @@ class ProjectStore implements IProjectStore { } }; - setActiveBoard = (boardValue: string) => { + setActiveBoard = (boardValue: TIssueBoardKeys) => { this.activeBoard = boardValue; }; } diff --git a/space/store/root.ts b/space/store/root.ts index 22b951d20..5a9e0bca1 100644 --- a/space/store/root.ts +++ b/space/store/root.ts @@ -6,6 +6,7 @@ import IssueStore, { IIssueStore } from "./issue"; import ProjectStore, { IProjectStore } from "./project"; import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; import { IMentionsStore, MentionsStore } from "./mentions.store"; +import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store"; enableStaticRendering(typeof window === "undefined"); @@ -15,6 +16,7 @@ export class RootStore { issueDetails: IIssueDetailStore; project: IProjectStore; mentionsStore: IMentionsStore; + issuesFilter: IIssuesFilterStore; constructor() { this.user = new UserStore(this); @@ -22,5 +24,6 @@ export class RootStore { this.project = new ProjectStore(this); this.issueDetails = new IssueDetailStore(this); this.mentionsStore = new MentionsStore(this); + this.issuesFilter = new IssuesFilterStore(this); } } diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 316d88144..4b00361b0 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,4 +1,6 @@ +import { memo } from "react"; import { Draggable } from "@hello-pangea/dnd"; +import isEqual from "lodash/isEqual"; // components import { KanBanProperties } from "./properties"; // ui @@ -21,7 +23,7 @@ interface IssueBlockProps { isReadOnly: boolean; } -export const KanbanIssueBlock: React.FC = (props) => { +export const KanBanIssueMemoBlock: React.FC = (props) => { const { sub_group_id, columnId, @@ -63,30 +65,36 @@ export const KanbanIssueBlock: React.FC = (props) => { {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} - onClick={handleIssuePeekOverview} > {issue.tempId !== undefined && (
)} -
- {quickActions( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !columnId && columnId === "null" ? null : columnId, - issue - )} -
{displayProperties && displayProperties?.key && ( -
- {issue.project_detail.identifier}-{issue.sequence_id} +
+
+ {issue.project_detail.identifier}-{issue.sequence_id} +
+
+ {quickActions( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !columnId && columnId === "null" ? null : columnId, + issue + )} +
)} -
{issue.name}
+
+ {issue.name} +
= (props) => { ); }; + +const validateMemo = (prevProps: IssueBlockProps, nextProps: IssueBlockProps) => { + if (prevProps.issue != nextProps.issue) return true; + return false; +}; + +export const KanbanIssueBlock = memo(KanBanIssueMemoBlock, validateMemo); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index 3d5780c06..65ae1b12f 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -240,7 +240,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (handleSubmit) { await handleSubmit(res); } else { - currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); + if (viewId) currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res, payload.cycle); if (payload.module && payload.module !== "") await addIssueToModule(res, payload.module); diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 11974caaa..cfb8cdbe6 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -16,6 +16,8 @@ interface IProfileIssuesPage { } export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { + const { type } = props; + const router = useRouter(); const { workspaceSlug, userId } = router.query as { workspaceSlug: string; @@ -28,11 +30,11 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { }: RootStore = useMobxStore(); useSWR( - workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${props.type}` : null, + workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null, async () => { if (workspaceSlug && userId) { await fetchFilters(workspaceSlug); - await fetchIssues(workspaceSlug, userId, getIssues ? "mutation" : "init-loader", props.type); + await fetchIssues(workspaceSlug, userId, getIssues ? "mutation" : "init-loader", type); } } ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index a2c09dd98..cd2eb09bc 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -1,5 +1,4 @@ import React, { ReactElement } from "react"; -import { observer } from "mobx-react-lite"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; @@ -9,7 +8,7 @@ import { UserProfileHeader } from "components/headers"; import { NextPageWithLayout } from "types/app"; import { ProfileIssuesPage } from "components/profile/profile-issues"; -const ProfileAssignedIssuesPage: NextPageWithLayout = observer(() => ); +const ProfileAssignedIssuesPage: NextPageWithLayout = () => ; ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/store/issues/base-issue-kanban-helper.store.ts b/web/store/issues/base-issue-kanban-helper.store.ts index 62b25fe22..e21c85e84 100644 --- a/web/store/issues/base-issue-kanban-helper.store.ts +++ b/web/store/issues/base-issue-kanban-helper.store.ts @@ -118,8 +118,6 @@ export class KanBanHelpers implements IKanBanHelpers { const [removed] = sourceIssues.splice(source.index, 1); - console.log("removed", removed); - if (removed) { if (viewId) store?.removeIssue(workspaceSlug, projectId, removed, viewId); else store?.removeIssue(workspaceSlug, projectId, removed); diff --git a/web/store/issues/profile/issue.store.ts b/web/store/issues/profile/issue.store.ts index 37b9f3085..eb02796c6 100644 --- a/web/store/issues/profile/issue.store.ts +++ b/web/store/issues/profile/issue.store.ts @@ -28,7 +28,6 @@ export interface IProfileIssuesStore { workspaceSlug: string, userId: string, loadType: TLoader, - _?: string, type?: "assigned" | "created" | "subscribed" ) => Promise; createIssue: (workspaceSlug: string, userId: string, data: Partial) => Promise; @@ -151,7 +150,6 @@ export class ProfileIssuesStore extends IssueBaseStore implements IProfileIssues workspaceSlug: string, userId: string, loadType: TLoader = "init-loader", - _?: string, type?: "assigned" | "created" | "subscribed" ) => { try {