Files
plane/web/ee/store/projects/projects.ts
Nikhil 9b05964f8a [WEB-2963] feat: project templates (#2693)
* chore: add template

* chore: add page template and project template migrations

* chore: add migration file for project and page templates

* chore: page templates

* chore: update template

* feat: page templates

* chore: update model

* chore: asset copy

* chore: update page templates

* feat: add project template create/update endpoint

* improvement: update all templates endpoint

* chore: project template basic types and services

* feat: project template instance and store

* feat: project template store hooks

* chore: feature flags and minor improvements

* feat: project templates list settings

* feat: project templates

* chore: common constants across templates

* improvement: work item template util types

* improvement: moved all project related types to packages and re-exported them to avoid changes in files.

* feat: create project template form with all basic fields and attributes

* chore: add project creation bgtask from template

* chore: update project template configuration

* chore: update project template migrations

* chore: added migration for workspace connection disconnect_meta

* chore: update migration files

* chore: model changes

* chore: add project template validation

* chore: update template task

* chore: project templates endpoints

* fix: base serializers

* chore: add comment edit migrations

* chore: minor improvements and base template store fixes

* chore: minor improvements

* improvement: common template related components

* feat: project template features

* chore: minor formatting

* chore: minor translation updates

* feat: update project template form data

* chore: work item template util improvements

* feat: use project templates in create project modal

* chore: use template endpoint

* chore: update imports

* feat: project creation using templates

* fix: build error

* feat: work item type creation form

* chore: convert project work item types form data to required schema

* feat: update work item type form data extraction and sanitization

* chore: add project template default settings

* chore: update formatting

* improvement: sync all project level features

* revert: utils package build related changes

* feat: project epics in templates

* chore: update background task to handle member and intake creation

* improvement: epic name while creating template

* fix: api server

* revert: utils build

* feat: added work item labels while creating project templates

* chore: update project template task

* refactor: project state list components

* improvement: additional classNames for the group list item and state item

* fix: options

* fix: epic creations

* feat: add project states while creating project templates

* improvements: add empty state for labels

* chore: remove unused exports

* fix: build error

* chore: add intake setting schema

* feat: intake feature settings for project templates

* feat: intake settings and deploy board when creating project from template

* fix: feature children layout

* fix: form keydown events

* fix: template description

* fix: delete state button type

* improvement: UI enhancements related to background, border and shadow

* improvement: project templates dropdown

* improvement: remove empty templates list sections

* improvement: remove empty templates list sections

* improvement: enhance project level template list modularity

* improvement: minor component improvements

* chore: minor type changes

* improvement: template list loader

* improvement: project template form background color

* improvement: work item template form background color

* chore: add asset url validation and resolve pr comments

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
2025-04-09 15:22:04 +05:30

217 lines
7.3 KiB
TypeScript

/* eslint-disable no-useless-catch */
import set from "lodash/set";
import update from "lodash/update";
import { action, makeObservable, observable, reaction, runInAction } from "mobx";
// store
import { ProjectService } from "@/plane-web/services";
import { RootStore } from "@/plane-web/store/root.store";
import {
TProject,
TProjectAttributesParams,
TProjectAttributesResponse,
TProjectFeatures,
TProjectFeaturesList,
} from "@/plane-web/types";
import { IProjectAttachmentStore, ProjectAttachmentStore } from "./project-details/attachment.store";
import { IProjectLinkStore, ProjectLinkStore } from "./project-details/link.store";
import { IProjectReactionStore, ProjectReactionStore } from "./project-details/project_reaction.store";
import { IProjectUpdateStore, ProjectUpdateStore } from "./project-details/updates.store";
export interface IProjectStore {
reactionStore: IProjectReactionStore;
attachmentStore: IProjectAttachmentStore;
featuresLoader: boolean;
features: Record<string, TProjectFeatures>; // projectId -> project features
// computed methods
isProjectFeatureEnabled: (projectId: string, featureKey: keyof TProjectFeaturesList) => boolean;
// helpers
getProjectFeatures: (projectId: string) => TProjectFeatures | undefined;
// actions
toggleProjectFeatures: (
workspaceSlug: string,
projectId: string,
data: Partial<TProjectFeaturesList>,
shouldSync?: boolean
) => Promise<void>;
createProjectUsingTemplate: (workspaceSlug: string, templateId: string, data: Partial<TProject>) => Promise<TProject>;
fetchProjectFeatures: (workspaceSlug: string) => Promise<void>;
fetchProjectAttributes: (
workspaceSlug: string,
params?: TProjectAttributesParams
) => Promise<TProjectAttributesResponse[]>;
// store
linkStore: IProjectLinkStore;
updatesStore: IProjectUpdateStore;
}
export class ProjectStore implements IProjectStore {
features: Record<string, TProjectFeatures> = {};
featuresLoader: boolean = false;
//store
rootStore: RootStore;
linkStore: IProjectLinkStore;
attachmentStore: IProjectAttachmentStore;
updatesStore: IProjectUpdateStore;
reactionStore: IProjectReactionStore;
// services
projectService;
constructor(public store: RootStore) {
makeObservable(this, {
// observables
featuresLoader: observable.ref,
features: observable,
// actions
createProjectUsingTemplate: action,
fetchProjectFeatures: action,
fetchProjectAttributes: action,
});
this.rootStore = store;
this.linkStore = new ProjectLinkStore(this);
this.attachmentStore = new ProjectAttachmentStore(this);
this.updatesStore = new ProjectUpdateStore();
this.reactionStore = new ProjectReactionStore();
// services
this.projectService = new ProjectService();
// reaction to add project features when a new project is added to project map
reaction(
() => ({
projectIds: Object.keys(this.rootStore.projectRoot.project.projectMap),
}),
({ projectIds }) => {
for (const projectId of projectIds) {
if (!this.features[projectId]) {
this.features[projectId] = {
is_project_updates_enabled: false,
is_epic_enabled: false,
is_issue_type_enabled: false,
is_time_tracking_enabled: false,
is_workflow_enabled: false,
project_id: projectId,
};
}
}
}
);
}
// computed methods
/**
* @description check if a project feature is enabled
* @param { string } projectId
* @param { keyof TProjectFeatures } featureKey
* @returns { boolean }
*/
isProjectFeatureEnabled = (projectId: string, featureKey: keyof TProjectFeaturesList): boolean => {
const projectFeatures = this.features[projectId];
return projectFeatures?.[featureKey] ?? false;
};
// helpers
/**
* Get project feature by project id
* @param projectId
* @returns project feature
*/
getProjectFeatures = (projectId: string): TProjectFeatures | undefined => this.features[projectId];
// actions
/**
* Creates a project in the workspace using a template
* @param workspaceSlug
* @param templateId
* @param data
* @returns Promise<TProject>
*/
createProjectUsingTemplate = async (workspaceSlug: string, templateId: string, data: Partial<TProject>) => {
try {
const response = await this.projectService.createProjectUsingTemplate(workspaceSlug, templateId, data);
this.rootStore.projectRoot.project.processProjectAfterCreation(workspaceSlug, response);
// Get template detail to set project features
const template = this.rootStore.templatesRoot.projectTemplates.getTemplateById(templateId);
if (template && response.id) {
set(this.features, response.id, {
is_project_updates_enabled: template.template_data.is_project_updates_enabled,
is_epic_enabled: template.template_data.is_epic_enabled,
is_issue_type_enabled: template.template_data.is_issue_type_enabled,
is_time_tracking_enabled: template.template_data.is_time_tracking_enabled,
is_workflow_enabled: template.template_data.is_workflow_enabled,
project_id: response.id,
});
}
// fetch project work item types and epics
await this.rootStore.issueTypes.fetchAll(workspaceSlug, response.id);
return response;
} catch (error) {
console.error("Error creating project using template", error);
throw error;
}
};
fetchProjectFeatures = async (workspaceSlug: string): Promise<void> => {
try {
this.featuresLoader = true;
const projectFeatures = await this.projectService.getProjectFeatures(workspaceSlug);
runInAction(() => {
for (const feature of projectFeatures) {
this.features[feature.project_id] = feature;
}
});
} catch (error) {
console.error("Error fetching project features", error);
throw error;
} finally {
runInAction(() => {
this.featuresLoader = false;
});
}
};
toggleProjectFeatures = async (
workspaceSlug: string,
projectId: string,
data: Partial<TProjectFeaturesList>,
shouldSync: boolean = true
): Promise<void> => {
const initialState = this.features[projectId];
try {
this.features[projectId] = {
...this.features[projectId],
...data,
};
if (shouldSync) {
await this.projectService.toggleProjectFeatures(workspaceSlug, projectId, data);
}
} catch (error) {
console.error(error);
this.features[projectId] = initialState;
throw error;
}
};
/**
* Fetches project attributes.
* @param workspaceSlug
* @param params optional params to filter attributes to filter projects
* @returns array of project attributes
*/
fetchProjectAttributes = async (workspaceSlug: string, params?: TProjectAttributesParams) => {
try {
const response = await this.projectService.getProjectAttributes(workspaceSlug, params);
runInAction(() => {
for (const attribute of response) {
const { project_id, ...rest } = attribute;
update(this.store.projectRoot.project.projectMap, [project_id], (p) => ({ ...p, ...rest }));
}
});
return response;
} catch (error) {
console.log("Error while fetching project attributes", error);
throw error;
}
};
}