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,538 @@
import { set } from "lodash-es";
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
// plane imports
import { EPageAccess } from "@plane/constants";
import type { TChangeHandlerProps } from "@plane/propel/emoji-icon-picker";
import type { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
// plane web store
import { ExtendedBasePage } from "@/plane-web/store/pages/extended-base-page";
import type { RootStore } from "@/plane-web/store/root.store";
// local imports
import { PageEditorInstance } from "./page-editor-info";
export type TBasePage = TPage & {
// observables
isSubmitting: TNameDescriptionLoader;
// computed
asJSON: TPage | undefined;
isCurrentUserOwner: boolean;
// helpers
oldName: string;
setIsSubmitting: (value: TNameDescriptionLoader) => void;
cleanup: () => void;
// actions
update: (pageData: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
updateTitle: (title: string) => void;
updateDescription: (document: TDocumentPayload) => Promise<void>;
makePublic: (params: { shouldSync?: boolean }) => Promise<void>;
makePrivate: (params: { shouldSync?: boolean }) => Promise<void>;
lock: (params: { shouldSync?: boolean; recursive?: boolean }) => Promise<void>;
unlock: (params: { shouldSync?: boolean; recursive?: boolean }) => Promise<void>;
archive: (params: { shouldSync?: boolean; archived_at?: string | null }) => Promise<void>;
restore: (params: { shouldSync?: boolean }) => Promise<void>;
updatePageLogo: (value: TChangeHandlerProps) => Promise<void>;
addToFavorites: () => Promise<void>;
removePageFromFavorites: () => Promise<void>;
duplicate: () => Promise<TPage | undefined>;
mutateProperties: (data: Partial<TPage>, shouldUpdateName?: boolean) => void;
// sub-store
editor: PageEditorInstance;
};
export type TBasePagePermissions = {
canCurrentUserAccessPage: boolean;
canCurrentUserEditPage: boolean;
canCurrentUserDuplicatePage: boolean;
canCurrentUserLockPage: boolean;
canCurrentUserChangeAccess: boolean;
canCurrentUserArchivePage: boolean;
canCurrentUserDeletePage: boolean;
canCurrentUserFavoritePage: boolean;
canCurrentUserMovePage: boolean;
isContentEditable: boolean;
};
export type TBasePageServices = {
update: (payload: Partial<TPage>) => Promise<Partial<TPage>>;
updateDescription: (document: TDocumentPayload) => Promise<void>;
updateAccess: (payload: Pick<TPage, "access">) => Promise<void>;
lock: () => Promise<void>;
unlock: () => Promise<void>;
archive: () => Promise<{
archived_at: string;
}>;
restore: () => Promise<void>;
duplicate: () => Promise<TPage>;
};
export type TPageInstance = TBasePage &
TBasePagePermissions & {
getRedirectionLink: () => string;
};
export class BasePage extends ExtendedBasePage implements TBasePage {
// loaders
isSubmitting: TNameDescriptionLoader = "saved";
// page properties
id: string | undefined;
name: string | undefined;
logo_props: TLogoProps | undefined;
description: object | undefined;
description_html: string | undefined;
color: string | undefined;
label_ids: string[] | undefined;
owned_by: string | undefined;
access: EPageAccess | undefined;
is_favorite: boolean;
is_locked: boolean;
archived_at: string | null | undefined;
workspace: string | undefined;
project_ids?: string[] | undefined;
created_by: string | undefined;
updated_by: string | undefined;
created_at: Date | undefined;
updated_at: Date | undefined;
deleted_at: Date | undefined;
// helpers
oldName: string = "";
// services
services: TBasePageServices;
// reactions
disposers: Array<() => void> = [];
// root store
rootStore: RootStore;
// sub-store
editor: PageEditorInstance;
constructor(
private store: RootStore,
page: TPage,
services: TBasePageServices
) {
super(store, page, services);
this.id = page?.id || undefined;
this.name = page?.name;
this.logo_props = page?.logo_props || undefined;
this.description = page?.description || undefined;
this.description_html = page?.description_html || undefined;
this.color = page?.color || undefined;
this.label_ids = page?.label_ids || undefined;
this.owned_by = page?.owned_by || undefined;
this.access = page?.access || EPageAccess.PUBLIC;
this.is_favorite = page?.is_favorite || false;
this.is_locked = page?.is_locked || false;
this.archived_at = page?.archived_at || undefined;
this.workspace = page?.workspace || undefined;
this.project_ids = page?.project_ids || undefined;
this.created_by = page?.created_by || undefined;
this.updated_by = page?.updated_by || undefined;
this.created_at = page?.created_at || undefined;
this.updated_at = page?.updated_at || undefined;
this.oldName = page?.name || "";
this.deleted_at = page?.deleted_at || undefined;
makeObservable(this, {
// loaders
isSubmitting: observable.ref,
// page properties
id: observable.ref,
name: observable.ref,
logo_props: observable.ref,
description: observable,
description_html: observable.ref,
color: observable.ref,
label_ids: observable,
owned_by: observable.ref,
access: observable.ref,
is_favorite: observable.ref,
is_locked: observable.ref,
archived_at: observable.ref,
workspace: observable.ref,
project_ids: observable,
created_by: observable.ref,
updated_by: observable.ref,
created_at: observable.ref,
updated_at: observable.ref,
deleted_at: observable.ref,
// helpers
oldName: observable.ref,
setIsSubmitting: action,
cleanup: action,
// computed
asJSON: computed,
isCurrentUserOwner: computed,
// actions
update: action,
updateTitle: action,
updateDescription: action,
makePublic: action,
makePrivate: action,
lock: action,
unlock: action,
archive: action,
restore: action,
updatePageLogo: action,
addToFavorites: action,
removePageFromFavorites: action,
duplicate: action,
mutateProperties: action,
});
// init
this.services = services;
this.rootStore = store;
this.editor = new PageEditorInstance();
const titleDisposer = reaction(
() => this.name,
(name) => {
this.isSubmitting = "submitting";
this.services
.update({
name,
})
.catch(() =>
runInAction(() => {
this.name = this.oldName;
})
)
.finally(() =>
runInAction(() => {
this.isSubmitting = "submitted";
})
);
},
{ delay: 2000 }
);
this.disposers.push(titleDisposer);
}
// computed
get asJSON() {
return {
id: this.id,
name: this.name,
description: this.description,
description_html: this.description_html,
color: this.color,
label_ids: this.label_ids,
owned_by: this.owned_by,
access: this.access,
logo_props: this.logo_props,
is_favorite: this.is_favorite,
is_locked: this.is_locked,
archived_at: this.archived_at,
workspace: this.workspace,
project_ids: this.project_ids,
created_by: this.created_by,
updated_by: this.updated_by,
created_at: this.created_at,
updated_at: this.updated_at,
deleted_at: this.deleted_at,
...this.asJSONExtended,
};
}
get isCurrentUserOwner() {
const currentUserId = this.store.user.data?.id;
if (!currentUserId) return false;
return this.owned_by === currentUserId;
}
/**
* @description update the submitting state
* @param value
*/
setIsSubmitting = (value: TNameDescriptionLoader) => {
runInAction(() => {
this.isSubmitting = value;
});
};
cleanup = () => {
this.disposers.forEach((disposer) => {
disposer();
});
};
/**
* @description update the page
* @param {Partial<TPage>} pageData
*/
update = async (pageData: Partial<TPage>) => {
const currentPage = this.asJSON;
try {
runInAction(() => {
Object.keys(pageData).forEach((key) => {
const currentPageKey = key as keyof TPage;
set(this, key, pageData[currentPageKey] || undefined);
});
});
return await this.services.update(currentPage);
} catch (error) {
runInAction(() => {
Object.keys(pageData).forEach((key) => {
const currentPageKey = key as keyof TPage;
set(this, key, currentPage?.[currentPageKey] || undefined);
});
});
throw error;
}
};
/**
* @description update the page title
* @param title
*/
updateTitle = (title: string) => {
this.oldName = this.name ?? "";
this.name = title;
};
/**
* @description update the page description
* @param {TDocumentPayload} document
*/
updateDescription = async (document: TDocumentPayload) => {
const currentDescription = this.description_html;
runInAction(() => {
this.description_html = document.description_html;
});
try {
await this.services.updateDescription(document);
} catch (error) {
runInAction(() => {
this.description_html = currentDescription;
});
throw error;
}
};
/**
* @description make the page public
*/
makePublic = async ({ shouldSync = true }) => {
const pageAccess = this.access;
runInAction(() => {
this.access = EPageAccess.PUBLIC;
});
if (shouldSync) {
try {
await this.services.updateAccess({
access: EPageAccess.PUBLIC,
});
} catch (error) {
runInAction(() => {
this.access = pageAccess;
});
throw error;
}
}
};
/**
* @description make the page private
*/
makePrivate = async ({ shouldSync = true }) => {
const pageAccess = this.access;
runInAction(() => {
this.access = EPageAccess.PRIVATE;
});
if (shouldSync) {
try {
await this.services.updateAccess({
access: EPageAccess.PRIVATE,
});
} catch (error) {
runInAction(() => {
this.access = pageAccess;
});
throw error;
}
}
};
/**
* @description lock the page
*/
lock = async ({ shouldSync = true }) => {
const pageIsLocked = this.is_locked;
runInAction(() => (this.is_locked = true));
if (shouldSync) {
await this.services.lock().catch((error) => {
runInAction(() => {
this.is_locked = pageIsLocked;
});
throw error;
});
}
};
/**
* @description unlock the page
*/
unlock = async ({ shouldSync = true }) => {
const pageIsLocked = this.is_locked;
runInAction(() => (this.is_locked = false));
if (shouldSync) {
await this.services.unlock().catch((error) => {
runInAction(() => {
this.is_locked = pageIsLocked;
});
throw error;
});
}
};
/**
* @description archive the page
*/
archive = async ({ shouldSync = true, archived_at }: { shouldSync?: boolean; archived_at?: string | null }) => {
if (!this.id) return undefined;
try {
runInAction(() => {
this.archived_at = archived_at ?? new Date().toISOString();
});
if (this.rootStore.favorite.entityMap[this.id]) this.rootStore.favorite.removeFavoriteFromStore(this.id);
if (shouldSync) {
const response = await this.services.archive();
runInAction(() => {
this.archived_at = response.archived_at;
});
}
} catch (error) {
console.error(error);
runInAction(() => {
this.archived_at = null;
});
}
};
/**
* @description restore the page
*/
restore = async ({ shouldSync = true }: { shouldSync?: boolean }) => {
const archivedAtBeforeRestore = this.archived_at;
try {
runInAction(() => {
this.archived_at = null;
});
if (shouldSync) {
await this.services.restore();
}
} catch (error) {
console.error(error);
runInAction(() => {
this.archived_at = archivedAtBeforeRestore;
});
throw error;
}
};
updatePageLogo = async (value: TChangeHandlerProps) => {
const originalLogoProps = { ...this.logo_props };
try {
let logoValue = {};
if (value?.type === "emoji")
logoValue = {
value: value.value,
url: undefined,
};
else if (value?.type === "icon") logoValue = value.value;
const logoProps: TLogoProps = {
in_use: value?.type,
[value?.type]: logoValue,
};
runInAction(() => {
this.logo_props = logoProps;
});
await this.services.update({
logo_props: logoProps,
});
} catch (error) {
console.error("Error in updating page logo", error);
runInAction(() => {
this.logo_props = originalLogoProps as TLogoProps;
});
throw error;
}
};
/**
* @description add the page to favorites
*/
addToFavorites = async () => {
const { workspaceSlug } = this.store.router;
const projectId = this.project_ids?.[0] ?? null;
if (!workspaceSlug || !this.id) return undefined;
const pageIsFavorite = this.is_favorite;
runInAction(() => {
this.is_favorite = true;
});
await this.rootStore.favorite
.addFavorite(workspaceSlug.toString(), {
entity_type: "page",
entity_identifier: this.id,
project_id: projectId,
entity_data: { name: this.name || "" },
})
.catch((error) => {
runInAction(() => {
this.is_favorite = pageIsFavorite;
});
throw error;
});
};
/**
* @description remove the page from favorites
*/
removePageFromFavorites = async () => {
const { workspaceSlug } = this.store.router;
if (!workspaceSlug || !this.id) return undefined;
const pageIsFavorite = this.is_favorite;
runInAction(() => {
this.is_favorite = false;
});
await this.rootStore.favorite.removeFavoriteEntity(workspaceSlug, this.id).catch((error) => {
runInAction(() => {
this.is_favorite = pageIsFavorite;
});
throw error;
});
};
/**
* @description duplicate the page
*/
duplicate = async () => await this.services.duplicate();
/**
* @description mutate multiple properties at once
* @param data Partial<TPage>
*/
mutateProperties = (data: Partial<TPage>, shouldUpdateName: boolean = true) => {
Object.keys(data).forEach((key) => {
const value = data[key as keyof TPage];
if (key === "name" && !shouldUpdateName) return;
set(this, key, value);
});
};
}

View File

@@ -0,0 +1,41 @@
import { action, makeObservable, observable, runInAction } from "mobx";
// plane imports
import type { EditorRefApi, TEditorAsset } from "@plane/editor";
export type TPageEditorInstance = {
// observables
assetsList: TEditorAsset[];
editorRef: EditorRefApi | null;
// actions
setEditorRef: (editorRef: EditorRefApi | null) => void;
updateAssetsList: (assets: TEditorAsset[]) => void;
};
export class PageEditorInstance implements TPageEditorInstance {
// observables
editorRef: EditorRefApi | null = null;
assetsList: TEditorAsset[] = [];
constructor() {
makeObservable(this, {
// observables
editorRef: observable.ref,
assetsList: observable,
// actions
setEditorRef: action,
updateAssetsList: action,
});
}
setEditorRef: TPageEditorInstance["setEditorRef"] = (editorRef) => {
runInAction(() => {
this.editorRef = editorRef;
});
};
updateAssetsList: TPageEditorInstance["updateAssetsList"] = (assets) => {
runInAction(() => {
this.assetsList = assets;
});
};
}

View File

@@ -0,0 +1,364 @@
import { unset, set } from "lodash-es";
import { makeObservable, observable, runInAction, action, reaction, computed } from "mobx";
import { computedFn } from "mobx-utils";
// types
import { EUserPermissions } from "@plane/constants";
import type { TPage, TPageFilters, TPageNavigationTabs } from "@plane/types";
import { EUserProjectRoles } from "@plane/types";
// helpers
import { filterPagesByPageType, getPageName, orderPages, shouldFilterPage } from "@plane/utils";
// plane web constants
// plane web store
import type { RootStore } from "@/plane-web/store/root.store";
// services
import { ProjectPageService } from "@/services/page";
// store
import type { CoreRootStore } from "../root.store";
import type { TProjectPage } from "./project-page";
import { ProjectPage } from "./project-page";
type TLoader = "init-loader" | "mutation-loader" | undefined;
type TError = { title: string; description: string };
export const ROLE_PERMISSIONS_TO_CREATE_PAGE = [
EUserPermissions.ADMIN,
EUserPermissions.MEMBER,
EUserProjectRoles.ADMIN,
EUserProjectRoles.MEMBER,
];
export interface IProjectPageStore {
// observables
loader: TLoader;
data: Record<string, TProjectPage>; // pageId => Page
error: TError | undefined;
filters: TPageFilters;
// computed
isAnyPageAvailable: boolean;
canCurrentUserCreatePage: boolean;
// helper actions
getCurrentProjectPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
getCurrentProjectPageIds: (projectId: string) => string[];
getCurrentProjectFilteredPageIdsByTab: (pageType: TPageNavigationTabs) => string[] | undefined;
getPageById: (pageId: string) => TProjectPage | undefined;
updateFilters: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
clearAllFilters: () => void;
// actions
fetchPagesList: (
workspaceSlug: string,
projectId: string,
pageType?: TPageNavigationTabs
) => Promise<TPage[] | undefined>;
fetchPageDetails: (
workspaceSlug: string,
projectId: string,
pageId: string,
options?: { trackVisit?: boolean }
) => Promise<TPage | undefined>;
createPage: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
removePage: (params: { pageId: string; shouldSync?: boolean }) => Promise<void>;
movePage: (workspaceSlug: string, projectId: string, pageId: string, newProjectId: string) => Promise<void>;
}
export class ProjectPageStore implements IProjectPageStore {
// observables
loader: TLoader = "init-loader";
data: Record<string, TProjectPage> = {}; // pageId => Page
error: TError | undefined = undefined;
filters: TPageFilters = {
searchQuery: "",
sortKey: "updated_at",
sortBy: "desc",
};
// service
service: ProjectPageService;
rootStore: CoreRootStore;
constructor(private store: RootStore) {
makeObservable(this, {
// observables
loader: observable.ref,
data: observable,
error: observable,
filters: observable,
// computed
isAnyPageAvailable: computed,
canCurrentUserCreatePage: computed,
// helper actions
updateFilters: action,
clearAllFilters: action,
// actions
fetchPagesList: action,
fetchPageDetails: action,
createPage: action,
removePage: action,
movePage: action,
});
this.rootStore = store;
// service
this.service = new ProjectPageService();
// initialize display filters of the current project
reaction(
() => this.store.router.projectId,
(projectId) => {
if (!projectId) return;
this.filters.searchQuery = "";
}
);
}
/**
* @description check if any page is available
*/
get isAnyPageAvailable() {
if (this.loader) return true;
return Object.keys(this.data).length > 0;
}
/**
* @description returns true if the current logged in user can create a page
*/
get canCurrentUserCreatePage() {
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
);
return !!currentUserProjectRole && ROLE_PERMISSIONS_TO_CREATE_PAGE.includes(currentUserProjectRole);
}
/**
* @description get the current project page ids based on the pageType
* @param {TPageNavigationTabs} pageType
*/
getCurrentProjectPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => {
const { projectId } = this.store.router;
if (!projectId) return undefined;
// helps to filter pages based on the pageType
let pagesByType = filterPagesByPageType(pageType, Object.values(this?.data || {}));
pagesByType = pagesByType.filter((p) => p.project_ids?.includes(projectId));
const pages = (pagesByType.map((page) => page.id) as string[]) || undefined;
return pages ?? undefined;
});
/**
* @description get the current project page ids
* @param {string} projectId
*/
getCurrentProjectPageIds = computedFn((projectId: string) => {
if (!projectId) return [];
const pages = Object.values(this?.data || {}).filter((page) => page.project_ids?.includes(projectId));
return pages.map((page) => page.id) as string[];
});
/**
* @description get the current project filtered page ids based on the pageType
* @param {TPageNavigationTabs} pageType
*/
getCurrentProjectFilteredPageIdsByTab = computedFn((pageType: TPageNavigationTabs) => {
const { projectId } = this.store.router;
if (!projectId) return undefined;
// helps to filter pages based on the pageType
const pagesByType = filterPagesByPageType(pageType, Object.values(this?.data || {}));
let filteredPages = pagesByType.filter(
(p) =>
p.project_ids?.includes(projectId) &&
getPageName(p.name).toLowerCase().includes(this.filters.searchQuery.toLowerCase()) &&
shouldFilterPage(p, this.filters.filters)
);
filteredPages = orderPages(filteredPages, this.filters.sortKey, this.filters.sortBy);
const pages = (filteredPages.map((page) => page.id) as string[]) || undefined;
return pages ?? undefined;
});
/**
* @description get the page store by id
* @param {string} pageId
*/
getPageById = computedFn((pageId: string) => this.data?.[pageId] || undefined);
updateFilters = <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => {
runInAction(() => {
set(this.filters, [filterKey], filterValue);
});
};
/**
* @description clear all the filters
*/
clearAllFilters = () =>
runInAction(() => {
set(this.filters, ["filters"], {});
});
/**
* @description fetch all the pages
*/
fetchPagesList = async (workspaceSlug: string, projectId: string, pageType?: TPageNavigationTabs) => {
try {
if (!workspaceSlug || !projectId) return undefined;
const currentPageIds = pageType ? this.getCurrentProjectPageIdsByTab(pageType) : undefined;
runInAction(() => {
this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
this.error = undefined;
});
const pages = await this.service.fetchAll(workspaceSlug, projectId);
runInAction(() => {
for (const page of pages) {
if (page?.id) {
const existingPage = this.getPageById(page.id);
if (existingPage) {
// If page already exists, update all fields except name
const { name, ...otherFields } = page;
existingPage.mutateProperties(otherFields, false);
} else {
// If new page, create a new instance with all data
set(this.data, [page.id], new ProjectPage(this.store, page));
}
}
}
this.loader = undefined;
});
return pages;
} catch (error) {
runInAction(() => {
this.loader = undefined;
this.error = {
title: "Failed",
description: "Failed to fetch the pages, Please try again later.",
};
});
throw error;
}
};
/**
* @description fetch the details of a page
* @param {string} pageId
*/
fetchPageDetails = async (...args: Parameters<IProjectPageStore["fetchPageDetails"]>) => {
const [workspaceSlug, projectId, pageId, options] = args;
const { trackVisit } = options || {};
try {
if (!workspaceSlug || !projectId || !pageId) return undefined;
const currentPageId = this.getPageById(pageId);
runInAction(() => {
this.loader = currentPageId ? `mutation-loader` : `init-loader`;
this.error = undefined;
});
const page = await this.service.fetchById(workspaceSlug, projectId, pageId, trackVisit ?? true);
runInAction(() => {
if (page?.id) {
const pageInstance = this.getPageById(page.id);
if (pageInstance) {
pageInstance.mutateProperties(page, false);
} else {
set(this.data, [page.id], new ProjectPage(this.store, page));
}
}
this.loader = undefined;
});
return page;
} catch (error) {
runInAction(() => {
this.loader = undefined;
this.error = {
title: "Failed",
description: "Failed to fetch the page, Please try again later.",
};
});
throw error;
}
};
/**
* @description create a page
* @param {Partial<TPage>} pageData
*/
createPage = async (pageData: Partial<TPage>) => {
try {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId) return undefined;
runInAction(() => {
this.loader = "mutation-loader";
this.error = undefined;
});
const page = await this.service.create(workspaceSlug, projectId, pageData);
runInAction(() => {
if (page?.id) set(this.data, [page.id], new ProjectPage(this.store, page));
this.loader = undefined;
});
return page;
} catch (error) {
runInAction(() => {
this.loader = undefined;
this.error = {
title: "Failed",
description: "Failed to create a page, Please try again later.",
};
});
throw error;
}
};
/**
* @description delete a page
* @param {string} pageId
*/
removePage = async ({ pageId, shouldSync = true }: { pageId: string; shouldSync?: boolean }) => {
try {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !pageId) return undefined;
await this.service.remove(workspaceSlug, projectId, pageId);
runInAction(() => {
unset(this.data, [pageId]);
if (this.rootStore.favorite.entityMap[pageId]) this.rootStore.favorite.removeFavoriteFromStore(pageId);
});
} catch (error) {
runInAction(() => {
this.loader = undefined;
this.error = {
title: "Failed",
description: "Failed to delete a page, Please try again later.",
};
});
throw error;
}
};
/**
* @description move a page to a new project
* @param {string} workspaceSlug
* @param {string} projectId
* @param {string} pageId
* @param {string} newProjectId
*/
movePage = async (workspaceSlug: string, projectId: string, pageId: string, newProjectId: string) => {
try {
await this.service.move(workspaceSlug, projectId, pageId, newProjectId);
runInAction(() => {
unset(this.data, [pageId]);
});
} catch (error) {
console.error("Unable to move page", error);
throw error;
}
};
}

View File

@@ -0,0 +1,184 @@
import { computed, makeObservable } from "mobx";
import { computedFn } from "mobx-utils";
// constants
import { EPageAccess, EUserPermissions } from "@plane/constants";
import type { TPage } from "@plane/types";
// plane web store
import type { RootStore } from "@/plane-web/store/root.store";
// services
import { ProjectPageService } from "@/services/page";
const projectPageService = new ProjectPageService();
// store
import { BasePage } from "./base-page";
import type { TPageInstance } from "./base-page";
export type TProjectPage = TPageInstance;
export class ProjectPage extends BasePage implements TProjectPage {
constructor(store: RootStore, page: TPage) {
// required fields for API calls
const { workspaceSlug } = store.router;
const projectId = page.project_ids?.[0];
// initialize base instance
super(store, page, {
update: async (payload) => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
return await projectPageService.update(workspaceSlug, projectId, page.id, payload);
},
updateDescription: async (document) => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
await projectPageService.updateDescription(workspaceSlug, projectId, page.id, document);
},
updateAccess: async (payload) => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
await projectPageService.updateAccess(workspaceSlug, projectId, page.id, payload);
},
lock: async () => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
await projectPageService.lock(workspaceSlug, projectId, page.id);
},
unlock: async () => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
await projectPageService.unlock(workspaceSlug, projectId, page.id);
},
archive: async () => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
return await projectPageService.archive(workspaceSlug, projectId, page.id);
},
restore: async () => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
await projectPageService.restore(workspaceSlug, projectId, page.id);
},
duplicate: async () => {
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
return await projectPageService.duplicate(workspaceSlug, projectId, page.id);
},
});
makeObservable(this, {
// computed
canCurrentUserAccessPage: computed,
canCurrentUserEditPage: computed,
canCurrentUserDuplicatePage: computed,
canCurrentUserLockPage: computed,
canCurrentUserChangeAccess: computed,
canCurrentUserArchivePage: computed,
canCurrentUserDeletePage: computed,
canCurrentUserFavoritePage: computed,
canCurrentUserMovePage: computed,
isContentEditable: computed,
});
}
private getHighestRoleAcrossProjects = computedFn((): EUserPermissions | undefined => {
const { workspaceSlug } = this.rootStore.router;
if (!workspaceSlug || !this.project_ids?.length) return;
let highestRole: EUserPermissions | undefined = undefined;
this.project_ids.map((projectId) => {
const currentUserProjectRole = this.rootStore.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
);
if (currentUserProjectRole) {
if (!highestRole) highestRole = currentUserProjectRole;
else if (currentUserProjectRole > highestRole) highestRole = currentUserProjectRole;
}
});
return highestRole;
});
/**
* @description returns true if the current logged in user can access the page
*/
get canCurrentUserAccessPage() {
const isPagePublic = this.access === EPageAccess.PUBLIC;
return isPagePublic || this.isCurrentUserOwner;
}
/**
* @description returns true if the current logged in user can edit the page
*/
get canCurrentUserEditPage() {
const highestRole = this.getHighestRoleAcrossProjects();
const isPagePublic = this.access === EPageAccess.PUBLIC;
return (
(isPagePublic && !!highestRole && highestRole >= EUserPermissions.MEMBER) ||
(!isPagePublic && this.isCurrentUserOwner)
);
}
/**
* @description returns true if the current logged in user can create a duplicate the page
*/
get canCurrentUserDuplicatePage() {
const highestRole = this.getHighestRoleAcrossProjects();
return !!highestRole && highestRole >= EUserPermissions.MEMBER;
}
/**
* @description returns true if the current logged in user can lock the page
*/
get canCurrentUserLockPage() {
const highestRole = this.getHighestRoleAcrossProjects();
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the current logged in user can change the access of the page
*/
get canCurrentUserChangeAccess() {
const highestRole = this.getHighestRoleAcrossProjects();
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the current logged in user can archive the page
*/
get canCurrentUserArchivePage() {
const highestRole = this.getHighestRoleAcrossProjects();
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the current logged in user can delete the page
*/
get canCurrentUserDeletePage() {
const highestRole = this.getHighestRoleAcrossProjects();
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the current logged in user can favorite the page
*/
get canCurrentUserFavoritePage() {
const highestRole = this.getHighestRoleAcrossProjects();
return !!highestRole && highestRole >= EUserPermissions.MEMBER;
}
/**
* @description returns true if the current logged in user can move the page
*/
get canCurrentUserMovePage() {
const highestRole = this.getHighestRoleAcrossProjects();
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the page can be edited
*/
get isContentEditable() {
const highestRole = this.getHighestRoleAcrossProjects();
const isOwner = this.isCurrentUserOwner;
const isPublic = this.access === EPageAccess.PUBLIC;
const isArchived = this.archived_at;
const isLocked = this.is_locked;
return (
!isArchived && !isLocked && (isOwner || (isPublic && !!highestRole && highestRole >= EUserPermissions.MEMBER))
);
}
getRedirectionLink = computedFn(() => {
const { workspaceSlug } = this.rootStore.router;
return `/${workspaceSlug}/projects/${this.project_ids?.[0]}/pages/${this.id}`;
});
}