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

This commit is contained in:
rahulramesha 2023-12-15 19:58:03 +05:30
commit db29327eb8
20 changed files with 349 additions and 58 deletions

View File

@ -1,11 +1,13 @@
name: Create PR in Plane EE Repository to sync the changes
name: Create Sync Action
on:
pull_request:
branches:
- master
- develop # Change this to preview
types:
- closed
env:
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
jobs:
create_pr:
@ -16,27 +18,13 @@ jobs:
pull-requests: write
contents: read
steps:
- name: Check SOURCE_REPO
id: check_repo
env:
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
run: |
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
- name: Checkout Code
if: steps.check_repo.outputs.is_correct_repo == 'true'
uses: actions/checkout@v2
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Branch Name
if: steps.check_repo.outputs.is_correct_repo == 'true'
run: |
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
- name: Setup GH CLI
if: steps.check_repo.outputs.is_correct_repo == 'true'
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
@ -46,34 +34,22 @@ jobs:
sudo apt install gh -y
- name: Create Pull Request
if: steps.check_repo.outputs.is_correct_repo == 'true'
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
# Remove double quotes
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
PR_BODY_CLEANED="${PR_BODY//\"/}"
# Construct PR_BODY_CONTENT using a here-document
PR_BODY_CONTENT=$(cat <<EOF
$PR_BODY_CLEANED
EOF
)
PR_TITLE=${{secrets.SYNC_PR_TITLE}}
gh pr create \
--base $TARGET_BRANCH \
--head $SOURCE_BRANCH \
--title "[SYNC] $PR_TITLE_CLEANED" \
--body "$PR_BODY_CONTENT" \
--base $TARGET_BASE_BRANCH \
--head $TARGET_BRANCH \
--title "$PR_TITLE" \
--repo $TARGET_REPO

View File

@ -31,6 +31,7 @@ from .project import (
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer,
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer

View File

@ -159,6 +159,11 @@ class ProjectMemberAdminSerializer(BaseSerializer):
model = ProjectMember
fields = "__all__"
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
class Meta:
model = ProjectMember
fields = ("id", "role", "member", "project")
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True)

View File

@ -62,7 +62,7 @@ class WorkspaceLiteSerializer(BaseSerializer):
class WorkSpaceMemberSerializer(BaseSerializer):
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
member = UserLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
@ -78,7 +78,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer):
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer):
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)

View File

@ -18,6 +18,7 @@ from plane.app.views import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
)
@ -92,6 +93,11 @@ urlpatterns = [
WorkSpaceMemberViewSet.as_view({"get": "list"}),
name="workspace-member",
),
path(
"workspaces/<str:slug>/project-members/",
WorkspaceProjectMemberEndpoint.as_view(),
name="workspace-member-roles",
),
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(

View File

@ -45,6 +45,7 @@ from .workspace import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint
)
from .state import StateViewSet
from .view import (

View File

@ -36,6 +36,7 @@ from plane.app.serializers import (
ProjectFavoriteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectMemberRoleSerializer,
)
from plane.app.permissions import (
@ -710,13 +711,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
project_member = ProjectMember.objects.get(
member=request.user,
workspace__slug=slug,
project_id=project_id,
is_active=True,
)
# Get the list of project members for the project
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
@ -724,10 +719,7 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
).select_related("project", "member", "workspace")
if project_member.role > 10:
serializer = ProjectMemberAdminSerializer(project_members, many=True)
else:
serializer = ProjectMemberSerializer(project_members, many=True)
serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk):

View File

@ -44,6 +44,7 @@ from plane.app.serializers import (
IssueLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
)
from plane.app.views.base import BaseAPIView
from . import BaseViewSet
@ -543,10 +544,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_members = self.get_queryset()
if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
serializer = WorkspaceMemberAdminSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
else:
serializer = WorkSpaceMemberSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
@ -711,6 +717,43 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class WorkspaceProjectMemberEndpoint(BaseAPIView):
serializer_class = ProjectMemberRoleSerializer
model = ProjectMember
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
# Fetch all project IDs where the user is involved
project_ids = ProjectMember.objects.filter(
member=request.user,
member__is_bot=False,
is_active=True,
).values_list('project_id', flat=True).distinct()
# Get all the project members in which the user is involved
project_members = ProjectMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
project_id__in=project_ids,
is_active=True,
).select_related("project", "member", "workspace")
project_members = ProjectMemberRoleSerializer(project_members, many=True).data
project_members_dict = dict()
# Construct a dictionary with project_id as key and project_members as value
for project_member in project_members:
project_id = project_member.pop("project")
if str(project_id) not in project_members_dict:
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
return Response(project_members_dict, status=status.HTTP_200_OK)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
model = Team

View File

@ -118,6 +118,7 @@ export const LinkModal: FC<Props> = (props) => {
ref={ref}
hasError={Boolean(errors.url)}
placeholder="https://..."
pattern="^(https?://).*"
className="w-full"
/>
)}

View File

@ -0,0 +1,5 @@
export const SWR_CONFIG = {
refreshWhenHidden: false,
revalidateIfStale: false,
errorRetryCount: 3,
};

View File

@ -1,6 +1,7 @@
export * from "./use-application";
export * from "./use-cycle";
export * from "./use-label";
export * from "./use-member";
export * from "./use-module";
export * from "./use-page";
export * from "./use-project-publish";

View File

@ -0,0 +1,11 @@
import { useContext } from "react";
// mobx store
import { MobxStoreContext } from "lib/mobx/store-provider";
// types;
import { IMemberRootStore } from "store/member";
export const useMember = (): IMemberRootStore => {
const context = useContext(MobxStoreContext);
if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider");
return context.memberRoot;
};

View File

@ -12,6 +12,9 @@ import { THEMES } from "constants/themes";
import InstanceLayout from "layouts/instance-layout";
// contexts
import { ToastContextProvider } from "contexts/toast.context";
import { SWRConfig } from "swr";
// constants
import { SWR_CONFIG } from "constants/swr-config";
// dynamic imports
const StoreWrapper = dynamic(() => import("lib/wrappers/store-wrapper"), { ssr: false });
const PosthogWrapper = dynamic(() => import("lib/wrappers/posthog-wrapper"), { ssr: false });
@ -51,7 +54,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
posthogAPIKey={envConfig?.posthog_api_key || null}
posthogHost={envConfig?.posthog_host || null}
>
{children}
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PosthogWrapper>
</CrispWrapper>
</StoreWrapper>

37
web/store/member/index.ts Normal file
View File

@ -0,0 +1,37 @@
import { makeObservable, observable } from "mobx";
// types
import { RootStore } from "store/root.store";
import { IUserLite } from "types";
import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store";
import { IProjectMemberStore, ProjectMemberStore } from "./project-member.store";
export interface IMemberRootStore {
// observables
memberMap: Record<string, IUserLite>;
// sub-stores
workspaceMember: IWorkspaceMemberStore;
projectMember: IProjectMemberStore;
}
export class MemberRootStore implements IMemberRootStore {
// observables
memberMap: Record<string, IUserLite> = {};
// root store
rootStore: RootStore;
// sub-stores
workspaceMember: IWorkspaceMemberStore;
projectMember: IProjectMemberStore;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
memberMap: observable,
});
// root store
this.rootStore = _rootStore;
// sub-stores
this.workspaceMember = new WorkspaceMemberStore(_rootStore);
this.projectMember = new ProjectMemberStore(_rootStore);
}
}

View File

@ -0,0 +1,100 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { set } from "lodash";
// services
import { ProjectMemberService } from "services/project";
// types
import { RootStore } from "store/root.store";
import { IProjectMember, IUserLite } from "types";
// constants
import { EUserProjectRoles } from "constants/project";
interface IProjectMemberDetails {
id: string;
member: IUserLite;
role: EUserProjectRoles;
}
export interface IProjectMemberStore {
// observables
projectMemberMap: {
[projectId: string]: Record<string, IProjectMember>;
};
// computed
projectMembers: string[] | null;
// computed actions
getProjectMemberDetails: (projectMemberId: string) => IProjectMemberDetails | null;
// actions
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<IProjectMember[]>;
}
export class ProjectMemberStore implements IProjectMemberStore {
// observables
projectMemberMap: {
[projectId: string]: Record<string, IProjectMember>;
} = {};
// root store
rootStore: RootStore;
// root store memberMap
memberMap: Record<string, IUserLite> = {};
// services
projectMemberService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
projectMemberMap: observable,
// computed
projectMembers: computed,
// computed actions
getProjectMemberDetails: action,
// actions
fetchProjectMembers: action,
});
// root store
this.rootStore = _rootStore;
this.memberMap = this.rootStore.memberRoot.memberMap;
// services
this.projectMemberService = new ProjectMemberService();
}
get projectMembers() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
return Object.keys(this.projectMemberMap?.[projectId] ?? {});
}
getProjectMemberDetails = (projectMemberId: string) => {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
const projectMember = this.projectMemberMap?.[projectId]?.[projectMemberId];
const memberDetails: IProjectMemberDetails = {
id: projectMember.id,
role: projectMember.role,
member: this.memberMap?.[projectMember.member],
};
return memberDetails;
};
fetchProjectMembers = async (workspaceSlug: string, projectId: string) => {
try {
const response = await this.projectMemberService.fetchProjectMembers(workspaceSlug, projectId);
runInAction(() => {
response.forEach((member) => {
set(this.projectMemberMap, [projectId, member.member], member);
});
});
return response;
} catch (error) {
throw error;
}
};
}

View File

@ -0,0 +1,105 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { set } from "lodash";
// services
import { WorkspaceService } from "services/workspace.service";
// types
import { RootStore } from "store/root.store";
import { IUserLite, IWorkspaceMember } from "types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
interface IWorkspaceMembership {
id: string;
member: string;
role: EUserWorkspaceRoles;
}
export interface IWorkspaceMemberStore {
// observables
workspaceMemberMap: {
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
};
// computed
workspaceMembers: string[] | null;
// computed actions
getWorkspaceMemberDetails: (workspaceMemberId: string) => IWorkspaceMember | null;
// actions
fetchWorkspaceMembers: (workspaceSlug: string) => Promise<IWorkspaceMember[]>;
}
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
// observables
workspaceMemberMap: {
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
} = {};
// root store
rootStore: RootStore;
// root store memberMap
memberMap: Record<string, IUserLite> = {};
// services
workspaceService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
workspaceMemberMap: observable,
// computed
workspaceMembers: computed,
// computed actions
getWorkspaceMemberDetails: action,
// actions
fetchWorkspaceMembers: action,
});
// root store
this.rootStore = _rootStore;
this.memberMap = this.rootStore.memberRoot.memberMap;
// services
this.workspaceService = new WorkspaceService();
}
get workspaceMembers() {
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
if (!workspaceSlug) return null;
return Object.keys(this.workspaceMemberMap?.[workspaceSlug] ?? {});
}
getWorkspaceMemberDetails = (workspaceMemberId: string) => {
const workspaceSlug = this.rootStore.app.router.workspaceSlug;
if (!workspaceSlug) return null;
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[workspaceMemberId];
const memberDetails: IWorkspaceMember = {
id: workspaceMember.id,
role: workspaceMember.role,
member: this.memberMap?.[workspaceMember.member],
};
return memberDetails;
};
fetchWorkspaceMembers = async (workspaceSlug: string) => {
try {
const response = await this.workspaceService.fetchWorkspaceMembers(workspaceSlug);
runInAction(() => {
response.forEach((member) => {
set(this.memberMap, member.member.id, member.member);
set(this.workspaceMemberMap, [workspaceSlug, member.member.id], {
id: member.id,
member: member.member.id,
role: member.role,
});
});
});
return response;
} catch (error) {
throw error;
}
};
}

View File

@ -11,6 +11,8 @@ import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
import { IStateStore, StateStore } from "./state.store";
import { IPageStore, PageStore } from "./page.store";
import { ILabelRootStore, LabelRootStore } from "./label";
import { IMemberRootStore, MemberRootStore } from "./member";
enableStaticRendering(typeof window === "undefined");
export class RootStore {
@ -19,6 +21,7 @@ export class RootStore {
workspaceRoot: IWorkspaceRootStore;
projectRoot: IProjectRootStore;
labelRoot: ILabelRootStore;
memberRoot: IMemberRootStore;
cycle: ICycleStore;
module: IModuleStore;
projectView: IProjectViewStore;
@ -32,6 +35,7 @@ export class RootStore {
this.workspaceRoot = new WorkspaceRootStore(this);
this.projectRoot = new ProjectRootStore(this);
this.labelRoot = new LabelRootStore(this);
this.memberRoot = new MemberRootStore(this);
// independent stores
this.state = new StateStore(this);
this.issue = new IssueRootStore(this);

View File

@ -170,7 +170,7 @@ export class CycleIssuesFilterStore extends IssueFilterBaseStore implements ICyc
return filters;
} catch (error) {
this.fetchFilters(workspaceSlug, projectId, cycleId);
console.log("error in fetchCycleFilters", error);
throw error;
}
};
@ -215,7 +215,7 @@ export class CycleIssuesFilterStore extends IssueFilterBaseStore implements ICyc
await this.fetchCycleFilters(workspaceSlug, projectId, cycleId);
return;
} catch (error) {
this.fetchFilters(workspaceSlug, projectId, cycleId);
console.log("error in cycleFetchFilters", error);
throw error;
}
};

View File

@ -170,7 +170,7 @@ export class ModuleIssuesFilterStore extends IssueFilterBaseStore implements IMo
return filters;
} catch (error) {
this.fetchFilters(workspaceSlug, projectId, moduleId);
console.log("error in moduleFetchFilters", error);
throw error;
}
};
@ -216,7 +216,7 @@ export class ModuleIssuesFilterStore extends IssueFilterBaseStore implements IMo
await this.fetchModuleFilters(workspaceSlug, projectId, moduleId);
return;
} catch (error) {
this.fetchFilters(workspaceSlug, projectId, moduleId);
console.log("error in projectFetchFilters", error);
throw error;
}
};

View File

@ -170,7 +170,7 @@ export class ViewIssuesFilterStore extends IssueFilterBaseStore implements IView
return filters;
} catch (error) {
this.fetchFilters(workspaceSlug, projectId, viewId);
console.log("error in viewFetchFilters", error);
throw error;
}
};
@ -216,7 +216,7 @@ export class ViewIssuesFilterStore extends IssueFilterBaseStore implements IView
await this.fetchViewFilters(workspaceSlug, projectId, viewId);
return;
} catch (error) {
this.fetchFilters(workspaceSlug, projectId, viewId);
console.log("error in viewFetchFilters", error);
throw error;
}
};