Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled

Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
chuan
2025-11-07 00:00:52 +08:00
commit 8ebde8aa05
4886 changed files with 462270 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import { set } from "lodash-es";
import { makeObservable, observable } from "mobx";
// types
import type { IUserAccount } from "@plane/types";
// services
import { UserService } from "@/services/user.service";
// store
import type { CoreRootStore } from "../root.store";
export interface IAccountStore {
// observables
isLoading: boolean;
error: any | undefined;
// model observables
provider_account_id: string | undefined;
provider: string | undefined;
}
export class AccountStore implements IAccountStore {
isLoading: boolean = false;
error: any | undefined = undefined;
// model observables
provider_account_id: string | undefined = undefined;
provider: string | undefined = undefined;
// service
userService: UserService;
constructor(
private store: CoreRootStore,
private _account: IUserAccount
) {
makeObservable(this, {
// observables
isLoading: observable.ref,
error: observable,
// model observables
provider_account_id: observable.ref,
provider: observable.ref,
});
// service
this.userService = new UserService();
// set account data
Object.entries(this._account).forEach(([key, value]) => {
set(this, [key], value ?? undefined);
});
}
}

View File

@@ -0,0 +1,338 @@
import { unset, set } from "lodash-es";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// plane imports
import type { TUserPermissions, TUserPermissionsLevel } from "@plane/constants";
import {
EUserPermissions,
EUserPermissionsLevel,
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
} from "@plane/constants";
import type { EUserProjectRoles, IUserProjectsRole, IWorkspaceMemberMe, TProjectMembership } from "@plane/types";
import { EUserWorkspaceRoles } from "@plane/types";
// plane web imports
import { WorkspaceService } from "@/plane-web/services";
import type { RootStore } from "@/plane-web/store/root.store";
// services
import projectMemberService from "@/services/project/project-member.service";
import userService from "@/services/user.service";
// derived services
const workspaceService = new WorkspaceService();
type ETempUserRole = TUserPermissions | EUserWorkspaceRoles | EUserProjectRoles; // TODO: Remove this once we have migrated user permissions to enums to plane constants package
export interface IBaseUserPermissionStore {
loader: boolean;
// observables
workspaceUserInfo: Record<string, IWorkspaceMemberMe>; // workspaceSlug -> IWorkspaceMemberMe
projectUserInfo: Record<string, Record<string, TProjectMembership>>; // workspaceSlug -> projectId -> TProjectMembership
workspaceProjectsPermissions: Record<string, IUserProjectsRole>; // workspaceSlug -> IUserProjectsRole
// computed helpers
workspaceInfoBySlug: (workspaceSlug: string) => IWorkspaceMemberMe | undefined;
getWorkspaceRoleByWorkspaceSlug: (workspaceSlug: string) => TUserPermissions | EUserWorkspaceRoles | undefined;
getProjectRolesByWorkspaceSlug: (workspaceSlug: string) => IUserProjectsRole;
getProjectRoleByWorkspaceSlugAndProjectId: (
workspaceSlug: string,
projectId?: string
) => EUserPermissions | undefined;
allowPermissions: (
allowPermissions: ETempUserRole[],
level: TUserPermissionsLevel,
workspaceSlug?: string,
projectId?: string,
onPermissionAllowed?: () => boolean
) => boolean;
// actions
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<TProjectMembership>;
fetchUserProjectPermissions: (workspaceSlug: string) => Promise<IUserProjectsRole>;
joinProject: (workspaceSlug: string, projectId: string) => Promise<void>;
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
hasPageAccess: (workspaceSlug: string, key: string) => boolean;
}
/**
* @description This store is used to handle permission layer for the currently logged user.
* It manages workspace and project level permissions, roles and access control.
*/
export abstract class BaseUserPermissionStore implements IBaseUserPermissionStore {
loader: boolean = false;
// constants
workspaceUserInfo: Record<string, IWorkspaceMemberMe> = {};
projectUserInfo: Record<string, Record<string, TProjectMembership>> = {};
workspaceProjectsPermissions: Record<string, IUserProjectsRole> = {};
// observables
constructor(protected store: RootStore) {
makeObservable(this, {
// observables
loader: observable.ref,
workspaceUserInfo: observable,
projectUserInfo: observable,
workspaceProjectsPermissions: observable,
// computed
// actions
fetchUserWorkspaceInfo: action,
leaveWorkspace: action,
fetchUserProjectInfo: action,
fetchUserProjectPermissions: action,
joinProject: action,
leaveProject: action,
});
}
// computed helpers
/**
* @description Returns the current workspace information
* @param { string } workspaceSlug
* @returns { IWorkspaceMemberMe | undefined }
*/
workspaceInfoBySlug = computedFn((workspaceSlug: string): IWorkspaceMemberMe | undefined => {
if (!workspaceSlug) return undefined;
return this.workspaceUserInfo[workspaceSlug] || undefined;
});
/**
* @description Returns the workspace role by slug
* @param { string } workspaceSlug
* @returns { TUserPermissions | EUserWorkspaceRoles | undefined }
*/
getWorkspaceRoleByWorkspaceSlug = computedFn(
(workspaceSlug: string): TUserPermissions | EUserWorkspaceRoles | undefined => {
if (!workspaceSlug) return undefined;
return this.workspaceUserInfo[workspaceSlug]?.role as TUserPermissions | EUserWorkspaceRoles | undefined;
}
);
/**
* @description Returns the project membership permission
* @param { string } workspaceSlug
* @param { string } projectId
* @returns { EUserPermissions | undefined }
*/
protected getProjectRole = computedFn((workspaceSlug: string, projectId: string): EUserPermissions | undefined => {
if (!workspaceSlug || !projectId) return undefined;
const projectRole = this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId];
if (!projectRole) return undefined;
const workspaceRole = this.workspaceUserInfo?.[workspaceSlug]?.role;
if (workspaceRole === EUserWorkspaceRoles.ADMIN) return EUserPermissions.ADMIN;
else return projectRole;
});
/**
* @description Returns the project permissions by workspace slug
* @param { string } workspaceSlug
* @returns { IUserProjectsRole }
*/
getProjectRolesByWorkspaceSlug = computedFn((workspaceSlug: string): IUserProjectsRole => {
const projectPermissions = this.workspaceProjectsPermissions[workspaceSlug] || {};
return Object.keys(projectPermissions).reduce((acc, projectId) => {
const projectRole = this.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
if (projectRole) {
acc[projectId] = projectRole;
}
return acc;
}, {} as IUserProjectsRole);
});
/**
* @description Returns the current project permissions
* @param { string } workspaceSlug
* @param { string } projectId
* @returns { EUserPermissions | undefined }
*/
abstract getProjectRoleByWorkspaceSlugAndProjectId: (
workspaceSlug: string,
projectId?: string
) => EUserPermissions | undefined;
/**
* @description Returns whether the user has the permission to access a page
* @param { string } page
* @returns { boolean }
*/
hasPageAccess = computedFn((workspaceSlug: string, key: string): boolean => {
if (!workspaceSlug || !key) return false;
const settings = WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.find((item) => item.key === key);
if (settings) {
return this.allowPermissions(settings.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug);
}
return false;
});
// action helpers
/**
* @description Returns whether the user has the permission to perform an action
* @param { TUserPermissions[] } allowPermissions
* @param { TUserPermissionsLevel } level
* @param { string } workspaceSlug
* @param { string } projectId
* @param { () => boolean } onPermissionAllowed
* @returns { boolean }
*/
allowPermissions = (
allowPermissions: ETempUserRole[],
level: TUserPermissionsLevel,
workspaceSlug?: string,
projectId?: string,
onPermissionAllowed?: () => boolean
): boolean => {
const { workspaceSlug: currentWorkspaceSlug, projectId: currentProjectId } = this.store.router;
if (!workspaceSlug) workspaceSlug = currentWorkspaceSlug;
if (!projectId) projectId = currentProjectId;
let currentUserRole: TUserPermissions | undefined = undefined;
if (level === EUserPermissionsLevel.WORKSPACE) {
currentUserRole = (workspaceSlug && this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug)) as
| EUserPermissions
| undefined;
}
if (level === EUserPermissionsLevel.PROJECT) {
currentUserRole = (workspaceSlug &&
projectId &&
this.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined;
}
if (typeof currentUserRole === "string") {
currentUserRole = parseInt(currentUserRole);
}
if (currentUserRole && typeof currentUserRole === "number" && allowPermissions.includes(currentUserRole)) {
if (onPermissionAllowed) {
return onPermissionAllowed();
} else {
return true;
}
}
return false;
};
// actions
/**
* @description Fetches the user's workspace information
* @param { string } workspaceSlug
* @returns { Promise<IWorkspaceMemberMe | undefined> }
*/
fetchUserWorkspaceInfo = async (workspaceSlug: string): Promise<IWorkspaceMemberMe> => {
try {
this.loader = true;
const response = await workspaceService.workspaceMemberMe(workspaceSlug);
if (response) {
runInAction(() => {
set(this.workspaceUserInfo, [workspaceSlug], response);
this.loader = false;
});
}
return response;
} catch (error) {
console.error("Error fetching user workspace information", error);
this.loader = false;
throw error;
}
};
/**
* @description Leaves a workspace
* @param { string } workspaceSlug
* @returns { Promise<void | undefined> }
*/
leaveWorkspace = async (workspaceSlug: string): Promise<void> => {
try {
await userService.leaveWorkspace(workspaceSlug);
runInAction(() => {
unset(this.workspaceUserInfo, workspaceSlug);
unset(this.projectUserInfo, workspaceSlug);
unset(this.workspaceProjectsPermissions, workspaceSlug);
});
} catch (error) {
console.error("Error user leaving the workspace", error);
throw error;
}
};
/**
* @description Fetches the user's project information
* @param { string } workspaceSlug
* @param { string } projectId
* @returns { Promise<TProjectMembership | undefined> }
*/
fetchUserProjectInfo = async (workspaceSlug: string, projectId: string): Promise<TProjectMembership> => {
try {
const response = await projectMemberService.projectMemberMe(workspaceSlug, projectId);
if (response) {
runInAction(() => {
set(this.projectUserInfo, [workspaceSlug, projectId], response);
set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], response.role);
});
}
return response;
} catch (error) {
console.error("Error fetching user project information", error);
throw error;
}
};
/**
* @description Fetches the user's project permissions
* @param { string } workspaceSlug
* @returns { Promise<IUserProjectsRole | undefined> }
*/
fetchUserProjectPermissions = async (workspaceSlug: string): Promise<IUserProjectsRole> => {
try {
const response = await workspaceService.getWorkspaceUserProjectsRole(workspaceSlug);
runInAction(() => {
set(this.workspaceProjectsPermissions, [workspaceSlug], response);
});
return response;
} catch (error) {
console.error("Error fetching user project permissions", error);
throw error;
}
};
/**
* @description Joins a project
* @param { string } workspaceSlug
* @param { string } projectId
* @returns { Promise<void> }
*/
joinProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
try {
const response = await userService.joinProject(workspaceSlug, [projectId]);
const projectMemberRole = this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug) ?? EUserPermissions.MEMBER;
if (response) {
runInAction(() => {
set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], projectMemberRole);
});
}
} catch (error) {
console.error("Error user joining the project", error);
throw error;
}
};
/**
* @description Leaves a project
* @param { string } workspaceSlug
* @param { string } projectId
* @returns { Promise<void> }
*/
leaveProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
try {
await userService.leaveProject(workspaceSlug, projectId);
runInAction(() => {
unset(this.workspaceProjectsPermissions, [workspaceSlug, projectId]);
unset(this.projectUserInfo, [workspaceSlug, projectId]);
unset(this.store.projectRoot.project.projectMap, [projectId]);
});
} catch (error) {
console.error("Error user leaving the project", error);
throw error;
}
};
}

View File

@@ -0,0 +1,303 @@
import { cloneDeep, set } from "lodash-es";
import { action, makeObservable, observable, runInAction, computed } from "mobx";
// plane imports
import { EUserPermissions, API_BASE_URL } from "@plane/constants";
import type { IUser, TUserPermissions } from "@plane/types";
// local
import { persistence } from "@/local-db/storage.sqlite";
// plane web imports
import type { RootStore } from "@/plane-web/store/root.store";
import type { IUserPermissionStore } from "@/plane-web/store/user/permission.store";
import { UserPermissionStore } from "@/plane-web/store/user/permission.store";
// services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
// stores
import type { IAccountStore } from "@/store/user/account.store";
import type { IUserProfileStore } from "@/store/user/profile.store";
import { ProfileStore } from "@/store/user/profile.store";
// local imports
import type { IUserSettingsStore } from "./settings.store";
import { UserSettingsStore } from "./settings.store";
type TUserErrorStatus = {
status: string;
message: string;
};
export interface IUserStore {
// observables
isAuthenticated: boolean;
isLoading: boolean;
error: TUserErrorStatus | undefined;
data: IUser | undefined;
// store observables
userProfile: IUserProfileStore;
userSettings: IUserSettingsStore;
accounts: Record<string, IAccountStore>;
permission: IUserPermissionStore;
// actions
fetchCurrentUser: () => Promise<IUser | undefined>;
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
handleSetPassword: (csrfToken: string, data: { password: string }) => Promise<IUser | undefined>;
deactivateAccount: () => Promise<void>;
changePassword: (
csrfToken: string,
payload: { old_password?: string; new_password: string }
) => Promise<IUser | undefined>;
reset: () => void;
signOut: () => Promise<void>;
// computed
localDBEnabled: boolean;
canPerformAnyCreateAction: boolean;
projectsWithCreatePermissions: { [projectId: string]: number } | null;
}
export class UserStore implements IUserStore {
// observables
isAuthenticated: boolean = false;
isLoading: boolean = false;
error: TUserErrorStatus | undefined = undefined;
data: IUser | undefined = undefined;
// store observables
userProfile: IUserProfileStore;
userSettings: IUserSettingsStore;
accounts: Record<string, IAccountStore> = {};
permission: IUserPermissionStore;
// service
userService: UserService;
authService: AuthService;
constructor(private store: RootStore) {
// stores
this.userProfile = new ProfileStore(store);
this.userSettings = new UserSettingsStore();
this.permission = new UserPermissionStore(store);
// service
this.userService = new UserService();
this.authService = new AuthService();
// observables
makeObservable(this, {
// observables
isAuthenticated: observable.ref,
isLoading: observable.ref,
error: observable,
// model observables
data: observable,
userProfile: observable,
userSettings: observable,
accounts: observable,
permission: observable,
// actions
fetchCurrentUser: action,
updateCurrentUser: action,
handleSetPassword: action,
deactivateAccount: action,
changePassword: action,
reset: action,
signOut: action,
// computed
canPerformAnyCreateAction: computed,
projectsWithCreatePermissions: computed,
localDBEnabled: computed,
});
}
/**
* @description fetches the current user
* @returns {Promise<IUser>}
*/
fetchCurrentUser = async (): Promise<IUser> => {
try {
runInAction(() => {
this.isLoading = true;
this.error = undefined;
});
const user = await this.userService.currentUser();
if (user && user?.id) {
await Promise.all([
this.userProfile.fetchUserProfile(),
this.userSettings.fetchCurrentUserSettings(),
this.store.workspaceRoot.fetchWorkspaces(),
]);
runInAction(() => {
this.data = user;
this.isLoading = false;
this.isAuthenticated = true;
});
} else
runInAction(() => {
this.data = user;
this.isLoading = false;
this.isAuthenticated = false;
});
return user;
} catch (error) {
runInAction(() => {
this.isLoading = false;
this.isAuthenticated = false;
this.error = {
status: "user-fetch-error",
message: "Failed to fetch current user",
};
});
throw error;
}
};
/**
* @description updates the current user
* @param data
* @returns {Promise<IUser>}
*/
updateCurrentUser = async (data: Partial<IUser>): Promise<IUser> => {
const currentUserData = this.data;
try {
if (currentUserData) {
Object.keys(data).forEach((key: string) => {
const userKey: keyof IUser = key as keyof IUser;
if (this.data) set(this.data, userKey, data[userKey]);
});
}
const user = await this.userService.updateUser(data);
return user;
} catch (error) {
if (currentUserData) {
Object.keys(currentUserData).forEach((key: string) => {
const userKey: keyof IUser = key as keyof IUser;
if (this.data) set(this.data, userKey, currentUserData[userKey]);
});
}
runInAction(() => {
this.error = {
status: "user-update-error",
message: "Failed to update current user",
};
});
throw error;
}
};
/**
* @description update the user password
* @param data
* @returns {Promise<IUser>}
*/
handleSetPassword = async (csrfToken: string, data: { password: string }): Promise<IUser | undefined> => {
const currentUserData = cloneDeep(this.data);
try {
if (currentUserData && currentUserData.is_password_autoset && this.data) {
const user = await this.authService.setPassword(csrfToken, { password: data.password });
set(this.data, ["is_password_autoset"], false);
return user;
}
return undefined;
} catch (error) {
if (this.data) set(this.data, ["is_password_autoset"], true);
runInAction(() => {
this.error = {
status: "user-update-error",
message: "Failed to update current user",
};
});
throw error;
}
};
changePassword = async (
csrfToken: string,
payload: {
old_password?: string;
new_password: string;
}
): Promise<IUser | undefined> => {
try {
const user = await this.userService.changePassword(csrfToken, payload);
if (this.data) set(this.data, ["is_password_autoset"], false);
return user;
} catch (error) {
console.log(error);
throw error;
}
};
/**
* @description deactivates the current user
* @returns {Promise<void>}
*/
deactivateAccount = async (): Promise<void> => {
await this.userService.deactivateAccount();
this.store.resetOnSignOut();
};
/**
* @description resets the user store
* @returns {void}
*/
reset = (): void => {
runInAction(() => {
this.isAuthenticated = false;
this.isLoading = false;
this.error = undefined;
this.data = undefined;
this.userProfile = new ProfileStore(this.store);
this.userSettings = new UserSettingsStore();
this.permission = new UserPermissionStore(this.store);
});
};
/**
* @description signs out the current user
* @returns {Promise<void>}
*/
signOut = async (): Promise<void> => {
await this.authService.signOut(API_BASE_URL);
await persistence.clearStorage(true);
this.store.resetOnSignOut();
};
// helper actions
/**
* @description fetches the projects with write permissions
* @returns {{[projectId: string]: number} || null}
*/
fetchProjectsWithCreatePermissions = (): { [key: string]: TUserPermissions } => {
const { workspaceSlug } = this.store.router;
const allWorkspaceProjectRoles = this.permission.getProjectRolesByWorkspaceSlug(workspaceSlug || "");
const userPermissions =
(allWorkspaceProjectRoles &&
Object.keys(allWorkspaceProjectRoles)
.filter((key) => allWorkspaceProjectRoles[key] >= EUserPermissions.MEMBER)
.reduce(
(res: { [projectId: string]: number }, key: string) => ((res[key] = allWorkspaceProjectRoles[key]), res),
{}
)) ||
null;
return userPermissions;
};
/**
* @description returns projects where user has permissions
* @returns {{[projectId: string]: number} || null}
*/
get projectsWithCreatePermissions() {
return this.fetchProjectsWithCreatePermissions();
}
/**
* @description returns true if user has permissions to write in any project
* @returns {boolean}
*/
get canPerformAnyCreateAction() {
const filteredProjects = this.fetchProjectsWithCreatePermissions();
return filteredProjects ? Object.keys(filteredProjects).length > 0 : false;
}
get localDBEnabled() {
return this.userSettings.canUseLocalDB;
}
}

View File

@@ -0,0 +1,243 @@
import { cloneDeep, set } from "lodash-es";
import { action, makeObservable, observable, runInAction } from "mobx";
// types
import type { IUserTheme, TUserProfile } from "@plane/types";
import { EStartOfTheWeek } from "@plane/types";
// services
import { UserService } from "@/services/user.service";
// store
import type { CoreRootStore } from "../root.store";
type TError = {
status: string;
message: string;
};
export interface IUserProfileStore {
// observables
isLoading: boolean;
error: TError | undefined;
data: TUserProfile;
// actions
fetchUserProfile: () => Promise<TUserProfile | undefined>;
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
finishUserOnboarding: () => Promise<void>;
updateTourCompleted: () => Promise<TUserProfile | undefined>;
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
}
export class ProfileStore implements IUserProfileStore {
isLoading: boolean = false;
error: TError | undefined = undefined;
data: TUserProfile = {
id: undefined,
user: undefined,
role: undefined,
last_workspace_id: undefined,
theme: {
theme: undefined,
text: undefined,
palette: undefined,
primary: undefined,
background: undefined,
darkPalette: undefined,
sidebarText: undefined,
sidebarBackground: undefined,
},
onboarding_step: {
workspace_join: false,
profile_complete: false,
workspace_create: false,
workspace_invite: false,
},
is_onboarded: false,
is_tour_completed: false,
use_case: undefined,
billing_address_country: undefined,
billing_address: undefined,
has_billing_address: false,
has_marketing_email_consent: false,
created_at: "",
updated_at: "",
language: "",
start_of_the_week: EStartOfTheWeek.SUNDAY,
};
// services
userService: UserService;
constructor(public store: CoreRootStore) {
makeObservable(this, {
// observables
isLoading: observable.ref,
error: observable,
data: observable,
// actions
fetchUserProfile: action,
updateUserProfile: action,
updateTourCompleted: action,
updateUserTheme: action,
});
// services
this.userService = new UserService();
}
// helper action
mutateUserProfile = (data: Partial<TUserProfile>) => {
if (!data) return;
Object.entries(data).forEach(([key, value]) => {
if (key in this.data) set(this.data, key, value);
});
};
// actions
/**
* @description fetches user profile information
* @returns {Promise<TUserProfile | undefined>}
*/
fetchUserProfile = async (): Promise<TUserProfile | undefined> => {
try {
runInAction(() => {
this.isLoading = true;
this.error = undefined;
});
const userProfile = await this.userService.getCurrentUserProfile();
runInAction(() => {
this.isLoading = false;
this.data = userProfile;
});
return userProfile;
} catch (error) {
runInAction(() => {
this.isLoading = false;
this.error = {
status: "user-profile-fetch-error",
message: "Failed to fetch user profile",
};
});
throw error;
}
};
/**
* @description updated the user profile information
* @param {Partial<TUserProfile>} data
* @returns {Promise<TUserProfile | undefined>}
*/
updateUserProfile = async (data: Partial<TUserProfile>): Promise<TUserProfile | undefined> => {
const currentUserProfileData = this.data;
try {
if (currentUserProfileData) {
this.mutateUserProfile(data);
}
const userProfile = await this.userService.updateCurrentUserProfile(data);
return userProfile;
} catch {
if (currentUserProfileData) {
this.mutateUserProfile(currentUserProfileData);
}
runInAction(() => {
this.error = {
status: "user-profile-update-error",
message: "Failed to update user profile",
};
});
}
};
/**
* @description finishes the user onboarding
* @returns { void }
*/
finishUserOnboarding = async (): Promise<void> => {
try {
const firstWorkspace = Object.values(this.store.workspaceRoot.workspaces ?? {})?.[0];
const dataToUpdate: Partial<TUserProfile> = {
onboarding_step: {
profile_complete: true,
workspace_join: true,
workspace_create: true,
workspace_invite: true,
},
last_workspace_id: firstWorkspace?.id,
};
// update user onboarding steps
await this.userService.updateCurrentUserProfile(dataToUpdate);
// update user onboarding status
await this.userService.updateUserOnBoard();
// Wait for user settings to be refreshed with cache-busting before updating onboarding status
await Promise.all([
this.fetchUserProfile(),
this.store.user.userSettings.fetchCurrentUserSettings(true), // Cache-busting enabled
]);
// Only after settings are refreshed, update the user profile store to mark as onboarded
runInAction(() => {
this.mutateUserProfile({ ...dataToUpdate, is_onboarded: true });
});
} catch (error) {
runInAction(() => {
this.error = {
status: "user-profile-onboard-finish-error",
message: "Failed to finish user onboarding",
};
});
throw error;
}
};
/**
* @description updates the user tour completed status
* @returns @returns {Promise<TUserProfile | undefined>}
*/
updateTourCompleted = async () => {
const isUserProfileTourCompleted = this.data.is_tour_completed || false;
try {
this.mutateUserProfile({ is_tour_completed: true });
const userProfile = await this.userService.updateUserTourCompleted();
return userProfile;
} catch (error) {
runInAction(() => {
this.mutateUserProfile({ is_tour_completed: isUserProfileTourCompleted });
this.error = {
status: "user-profile-tour-complete-error",
message: "Failed to update user profile is_tour_completed",
};
});
throw error;
}
};
/**
* @description updates the user theme
* @returns @returns {Promise<TUserProfile | undefined>}
*/
updateUserTheme = async (data: Partial<IUserTheme>) => {
const currentProfileTheme = cloneDeep(this.data.theme);
try {
runInAction(() => {
Object.keys(data).forEach((key: string) => {
const userKey: keyof IUserTheme = key as keyof IUserTheme;
if (this.data.theme) set(this.data.theme, userKey, data[userKey]);
});
});
const userProfile = await this.userService.updateCurrentUserProfile({ theme: this.data.theme });
return userProfile;
} catch (error) {
runInAction(() => {
Object.keys(data).forEach((key: string) => {
const userKey: keyof IUserTheme = key as keyof IUserTheme;
if (currentProfileTheme) set(this.data.theme, userKey, currentProfileTheme[userKey]);
});
this.error = {
status: "user-profile-theme-update-error",
message: "Failed to update user profile theme",
};
});
throw error;
}
};
}

View File

@@ -0,0 +1,104 @@
import { action, makeObservable, observable, runInAction } from "mobx";
// plane imports
import type { IUserSettings } from "@plane/types";
// services
import { UserService } from "@/services/user.service";
type TError = {
status: string;
message: string;
};
export interface IUserSettingsStore {
// observables
isLoading: boolean;
error: TError | undefined;
data: IUserSettings;
canUseLocalDB: boolean;
sidebarCollapsed: boolean;
isScrolled: boolean;
// actions
fetchCurrentUserSettings: (bustCache?: boolean) => Promise<IUserSettings | undefined>;
toggleSidebar: (collapsed?: boolean) => void;
toggleIsScrolled: (isScrolled?: boolean) => void;
}
export class UserSettingsStore implements IUserSettingsStore {
// observables
isLoading: boolean = false;
sidebarCollapsed: boolean = true;
error: TError | undefined = undefined;
isScrolled: boolean = false;
data: IUserSettings = {
id: undefined,
email: undefined,
workspace: {
last_workspace_id: undefined,
last_workspace_slug: undefined,
last_workspace_name: undefined,
last_workspace_logo: undefined,
fallback_workspace_id: undefined,
fallback_workspace_slug: undefined,
invites: undefined,
},
};
canUseLocalDB: boolean = false;
// services
userService: UserService;
constructor() {
makeObservable(this, {
// observables
isLoading: observable.ref,
error: observable,
data: observable,
canUseLocalDB: observable.ref,
sidebarCollapsed: observable.ref,
isScrolled: observable.ref,
// actions
fetchCurrentUserSettings: action,
toggleSidebar: action,
toggleIsScrolled: action,
});
// services
this.userService = new UserService();
}
// actions
toggleSidebar = (collapsed?: boolean) => {
this.sidebarCollapsed = collapsed ?? !this.sidebarCollapsed;
};
toggleIsScrolled = (isScrolled?: boolean) => {
this.isScrolled = isScrolled ?? !this.isScrolled;
};
// actions
/**
* @description fetches user profile information
* @returns {Promise<IUserSettings | undefined>}
*/
fetchCurrentUserSettings = async (bustCache: boolean = false) => {
try {
runInAction(() => {
this.isLoading = true;
this.error = undefined;
});
const userSettings = await this.userService.currentUserSettings(bustCache);
runInAction(() => {
this.isLoading = false;
this.data = userSettings;
});
return userSettings;
} catch (error) {
runInAction(() => {
this.isLoading = false;
this.error = {
status: "error",
message: "Failed to fetch user settings",
};
});
throw error;
}
};
}