-
-
-
{state?.name}
+
+
+
+
+
{state?.name ?? "State"}
+
-
+
);
});
diff --git a/space/core/components/issues/issue-layouts/utils.tsx b/space/core/components/issues/issue-layouts/utils.tsx
new file mode 100644
index 00000000000..edb67b4f711
--- /dev/null
+++ b/space/core/components/issues/issue-layouts/utils.tsx
@@ -0,0 +1,240 @@
+"use client";
+
+import isNil from "lodash/isNil";
+import { ContrastIcon } from "lucide-react";
+// types
+import {
+ GroupByColumnTypes,
+ IGroupByColumn,
+ TCycleGroups,
+ IIssueDisplayProperties,
+ TGroupedIssues,
+} from "@plane/types";
+// ui
+import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
+// components
+// constants
+import { ISSUE_PRIORITIES } from "@/constants/issue";
+// stores
+import { ICycleStore } from "@/store/cycle.store";
+import { IIssueLabelStore } from "@/store/label.store";
+import { IIssueMemberStore } from "@/store/members.store";
+import { IIssueModuleStore } from "@/store/module.store";
+import { IStateStore } from "@/store/state.store";
+
+export const HIGHLIGHT_CLASS = "highlight";
+export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
+
+export const getGroupByColumns = (
+ groupBy: GroupByColumnTypes | null,
+ cycle: ICycleStore,
+ module: IIssueModuleStore,
+ label: IIssueLabelStore,
+ projectState: IStateStore,
+ member: IIssueMemberStore,
+ includeNone?: boolean
+): IGroupByColumn[] | undefined => {
+ switch (groupBy) {
+ case "cycle":
+ return getCycleColumns(cycle);
+ case "module":
+ return getModuleColumns(module);
+ case "state":
+ return getStateColumns(projectState);
+ case "priority":
+ return getPriorityColumns();
+ case "labels":
+ return getLabelsColumns(label) as any;
+ case "assignees":
+ return getAssigneeColumns(member) as any;
+ case "created_by":
+ return getCreatedByColumns(member) as any;
+ default:
+ if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }];
+ }
+};
+
+const getCycleColumns = (cycleStore: ICycleStore): IGroupByColumn[] | undefined => {
+ const { cycles } = cycleStore;
+
+ if (!cycles) return;
+
+ const cycleGroups: IGroupByColumn[] = [];
+
+ cycles.map((cycle) => {
+ if (cycle) {
+ const cycleStatus = cycle?.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft";
+ cycleGroups.push({
+ id: cycle.id,
+ name: cycle.name,
+ icon:
,
+ payload: { cycle_id: cycle.id },
+ });
+ }
+ });
+ cycleGroups.push({
+ id: "None",
+ name: "None",
+ icon:
,
+ payload: { cycle_id: null },
+ });
+
+ return cycleGroups;
+};
+
+const getModuleColumns = (moduleStore: IIssueModuleStore): IGroupByColumn[] | undefined => {
+ const { modules } = moduleStore;
+
+ if (!modules) return;
+
+ const moduleGroups: IGroupByColumn[] = [];
+
+ modules.map((moduleInfo) => {
+ if (moduleInfo)
+ moduleGroups.push({
+ id: moduleInfo.id,
+ name: moduleInfo.name,
+ icon:
,
+ payload: { module_ids: [moduleInfo.id] },
+ });
+ }) as any;
+ moduleGroups.push({
+ id: "None",
+ name: "None",
+ icon:
,
+ payload: { module_ids: [] },
+ });
+
+ return moduleGroups as any;
+};
+
+const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => {
+ const { states } = projectState;
+ if (!states) return;
+
+ return states.map((state) => ({
+ id: state.id,
+ name: state.name,
+ icon: (
+
+
+
+ ),
+ payload: { state_id: state.id },
+ })) as any;
+};
+
+const getPriorityColumns = () => {
+ const priorities = ISSUE_PRIORITIES;
+
+ return priorities.map((priority) => ({
+ id: priority.key,
+ name: priority.title,
+ icon:
,
+ payload: { priority: priority.key },
+ }));
+};
+
+const getLabelsColumns = (label: IIssueLabelStore) => {
+ const { labels: storeLabels } = label;
+
+ if (!storeLabels) return;
+
+ const labels = [...storeLabels, { id: "None", name: "None", color: "#666" }];
+
+ return labels.map((label) => ({
+ id: label.id,
+ name: label.name,
+ icon: (
+
+ ),
+ payload: label?.id === "None" ? {} : { label_ids: [label.id] },
+ }));
+};
+
+const getAssigneeColumns = (member: IIssueMemberStore) => {
+ const { members } = member;
+
+ if (!members) return;
+
+ const assigneeColumns: any = members.map((member) => ({
+ id: member.id,
+ name: member?.member__display_name || "",
+ icon:
,
+ payload: { assignee_ids: [member.id] },
+ }));
+
+ assigneeColumns.push({ id: "None", name: "None", icon:
, payload: {} });
+
+ return assigneeColumns;
+};
+
+const getCreatedByColumns = (member: IIssueMemberStore) => {
+ const { members } = member;
+
+ if (!members) return;
+
+ return members.map((member) => ({
+ id: member.id,
+ name: member?.member__display_name || "",
+ icon:
,
+ payload: {},
+ }));
+};
+
+export const getDisplayPropertiesCount = (
+ displayProperties: IIssueDisplayProperties,
+ ignoreFields?: (keyof IIssueDisplayProperties)[]
+) => {
+ const propertyKeys = Object.keys(displayProperties) as (keyof IIssueDisplayProperties)[];
+
+ let count = 0;
+
+ for (const propertyKey of propertyKeys) {
+ if (ignoreFields && ignoreFields.includes(propertyKey)) continue;
+ if (displayProperties[propertyKey]) count++;
+ }
+
+ return count;
+};
+
+export const getIssueBlockId = (
+ issueId: string | undefined,
+ groupId: string | undefined,
+ subGroupId?: string | undefined
+) => `issue_${issueId}_${groupId}_${subGroupId}`;
+
+/**
+ * returns empty Array if groupId is None
+ * @param groupId
+ * @returns
+ */
+export const getGroupId = (groupId: string) => {
+ if (groupId === "None") return [];
+ return [groupId];
+};
+
+/**
+ * method that removes Null or undefined Keys from object
+ * @param obj
+ * @returns
+ */
+export const removeNillKeys =
(obj: T) =>
+ Object.fromEntries(Object.entries(obj ?? {}).filter(([key, value]) => key && !isNil(value)));
+
+/**
+ * This Method returns if the the grouped values are subGrouped
+ * @param groupedIssueIds
+ * @returns
+ */
+export const isSubGrouped = (groupedIssueIds: TGroupedIssues) => {
+ if (!groupedIssueIds || Array.isArray(groupedIssueIds)) {
+ return false;
+ }
+
+ if (Array.isArray(groupedIssueIds[Object.keys(groupedIssueIds)[0]])) {
+ return false;
+ }
+
+ return true;
+};
diff --git a/space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx b/space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx
new file mode 100644
index 00000000000..51ce71a7723
--- /dev/null
+++ b/space/core/components/issues/issue-layouts/with-display-properties-HOC.tsx
@@ -0,0 +1,26 @@
+import { ReactNode } from "react";
+import { observer } from "mobx-react";
+import { IIssueDisplayProperties } from "@plane/types";
+
+interface IWithDisplayPropertiesHOC {
+ displayProperties: IIssueDisplayProperties;
+ shouldRenderProperty?: (displayProperties: IIssueDisplayProperties) => boolean;
+ displayPropertyKey: keyof IIssueDisplayProperties | (keyof IIssueDisplayProperties)[];
+ children: ReactNode;
+}
+
+export const WithDisplayPropertiesHOC = observer(
+ ({ displayProperties, shouldRenderProperty, displayPropertyKey, children }: IWithDisplayPropertiesHOC) => {
+ let shouldDisplayPropertyFromFilters = false;
+ if (Array.isArray(displayPropertyKey))
+ shouldDisplayPropertyFromFilters = displayPropertyKey.every((key) => !!displayProperties[key]);
+ else shouldDisplayPropertyFromFilters = !!displayProperties[displayPropertyKey];
+
+ const renderProperty =
+ shouldDisplayPropertyFromFilters && (shouldRenderProperty ? shouldRenderProperty(displayProperties) : true);
+
+ if (!renderProperty) return null;
+
+ return <>{children}>;
+ }
+);
diff --git a/space/core/components/issues/peek-overview/layout.tsx b/space/core/components/issues/peek-overview/layout.tsx
index fa138382759..e649a3279a9 100644
--- a/space/core/components/issues/peek-overview/layout.tsx
+++ b/space/core/components/issues/peek-overview/layout.tsx
@@ -6,16 +6,17 @@ import { useRouter, useSearchParams } from "next/navigation";
import { Dialog, Transition } from "@headlessui/react";
// components
import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview";
-// store
+// hooks
import { useIssue, useIssueDetails } from "@/hooks/store";
type TIssuePeekOverview = {
anchor: string;
peekId: string;
+ handlePeekClose?: () => void;
};
export const IssuePeekOverview: FC = observer((props) => {
- const { anchor, peekId } = props;
+ const { anchor, peekId, handlePeekClose } = props;
const router = useRouter();
const searchParams = useSearchParams();
// query params
@@ -34,13 +35,17 @@ export const IssuePeekOverview: FC = observer((props) => {
useEffect(() => {
if (anchor && peekId && issueStore.groupedIssueIds) {
- if (!issueDetails) {
- issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
- }
+ issueDetailStore.fetchIssueDetails(anchor, peekId.toString());
}
- }, [anchor, issueDetailStore, issueDetails, peekId, issueStore.groupedIssueIds]);
+ }, [anchor, issueDetailStore, peekId, issueStore.groupedIssueIds]);
const handleClose = () => {
+ // if close logic is passed down, call that instead of the below logic
+ if (handlePeekClose) {
+ handlePeekClose();
+ return;
+ }
+
issueDetailStore.setPeekId(null);
let queryParams: any = {
board,
diff --git a/space/core/components/ui/not-found.tsx b/space/core/components/ui/not-found.tsx
new file mode 100644
index 00000000000..a2535616d80
--- /dev/null
+++ b/space/core/components/ui/not-found.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import React from "react";
+import Image from "next/image";
+// ui
+// images
+import Image404 from "@/public/404.svg";
+
+export const PageNotFound = () => (
+
+
+
+
+
+
+
+
Oops! Something went wrong.
+
+ Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
+ temporarily unavailable.
+
+
+
+
+
+);
diff --git a/space/core/constants/issue.ts b/space/core/constants/issue.ts
index e518e9ebb70..1d9ebbb19c6 100644
--- a/space/core/constants/issue.ts
+++ b/space/core/constants/issue.ts
@@ -75,4 +75,15 @@ export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilter
if (currentIssuePriority) return currentIssuePriority;
return undefined;
-};
\ No newline at end of file
+};
+
+export const ISSUE_PRIORITIES: {
+ key: TIssuePriorities;
+ title: string;
+}[] = [
+ { key: "urgent", title: "Urgent" },
+ { key: "high", title: "High" },
+ { key: "medium", title: "Medium" },
+ { key: "low", title: "Low" },
+ { key: "none", title: "None" },
+];
\ No newline at end of file
diff --git a/space/core/hooks/store/index.ts b/space/core/hooks/store/index.ts
index 87b9d7317df..f6f46eccbc2 100644
--- a/space/core/hooks/store/index.ts
+++ b/space/core/hooks/store/index.ts
@@ -7,3 +7,6 @@ export * from "./use-issue-details";
export * from "./use-issue-filter";
export * from "./use-state";
export * from "./use-label";
+export * from "./use-cycle";
+export * from "./use-module";
+export * from "./use-member";
diff --git a/space/core/hooks/store/use-cycle.ts b/space/core/hooks/store/use-cycle.ts
new file mode 100644
index 00000000000..554a93c4664
--- /dev/null
+++ b/space/core/hooks/store/use-cycle.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+// lib
+import { StoreContext } from "@/lib/store-provider";
+// store
+import { ICycleStore } from "@/store/cycle.store";
+
+export const useCycle = (): ICycleStore => {
+ const context = useContext(StoreContext);
+ if (context === undefined) throw new Error("useCycle must be used within StoreProvider");
+ return context.cycle;
+};
diff --git a/space/core/hooks/store/use-member.ts b/space/core/hooks/store/use-member.ts
new file mode 100644
index 00000000000..80aca3cfb6e
--- /dev/null
+++ b/space/core/hooks/store/use-member.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+// lib
+import { StoreContext } from "@/lib/store-provider";
+// store
+import { IIssueMemberStore } from "@/store/members.store";
+
+export const useMember = (): IIssueMemberStore => {
+ const context = useContext(StoreContext);
+ if (context === undefined) throw new Error("useMember must be used within StoreProvider");
+ return context.member;
+};
diff --git a/space/core/hooks/store/use-module.ts b/space/core/hooks/store/use-module.ts
new file mode 100644
index 00000000000..1749ca9ab07
--- /dev/null
+++ b/space/core/hooks/store/use-module.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+// lib
+import { StoreContext } from "@/lib/store-provider";
+// store
+import { IIssueModuleStore } from "@/store/module.store";
+
+export const useModule = (): IIssueModuleStore => {
+ const context = useContext(StoreContext);
+ if (context === undefined) throw new Error("useModule must be used within StoreProvider");
+ return context.module;
+};
diff --git a/space/core/services/cycle.service.ts b/space/core/services/cycle.service.ts
new file mode 100644
index 00000000000..a556800f8e3
--- /dev/null
+++ b/space/core/services/cycle.service.ts
@@ -0,0 +1,17 @@
+import { API_BASE_URL } from "@/helpers/common.helper";
+import { APIService } from "@/services/api.service";
+import { TPublicCycle } from "@/types/cycle";
+
+export class CycleService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async getCycles(anchor: string): Promise {
+ return this.get(`api/public/anchor/${anchor}/cycles/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
diff --git a/space/core/services/member.service.ts b/space/core/services/member.service.ts
new file mode 100644
index 00000000000..1c4758e42fd
--- /dev/null
+++ b/space/core/services/member.service.ts
@@ -0,0 +1,17 @@
+import { API_BASE_URL } from "@/helpers/common.helper";
+import { APIService } from "@/services/api.service";
+import { TPublicMember } from "@/types/member";
+
+export class MemberService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async getAnchorMembers(anchor: string): Promise {
+ return this.get(`api/public/anchor/${anchor}/members/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
diff --git a/space/core/services/module.service.ts b/space/core/services/module.service.ts
new file mode 100644
index 00000000000..153f3f67c47
--- /dev/null
+++ b/space/core/services/module.service.ts
@@ -0,0 +1,17 @@
+import { API_BASE_URL } from "@/helpers/common.helper";
+import { APIService } from "@/services/api.service";
+import { TPublicModule } from "@/types/modules";
+
+export class ModuleService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async getModules(anchor: string): Promise {
+ return this.get(`api/public/anchor/${anchor}/modules/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
diff --git a/space/core/store/cycle.store.ts b/space/core/store/cycle.store.ts
new file mode 100644
index 00000000000..a7310290bf4
--- /dev/null
+++ b/space/core/store/cycle.store.ts
@@ -0,0 +1,40 @@
+import { action, makeObservable, observable, runInAction } from "mobx";
+import { TPublicCycle } from "@/types/cycle";
+import { CycleService } from "../services/cycle.service";
+import { CoreRootStore } from "./root.store";
+
+export interface ICycleStore {
+ // observables
+ cycles: TPublicCycle[] | undefined;
+ // computed actions
+ getCycleById: (cycleId: string | undefined) => TPublicCycle | undefined;
+ // fetch actions
+ fetchCycles: (anchor: string) => Promise;
+}
+
+export class CycleStore implements ICycleStore {
+ cycles: TPublicCycle[] | undefined = undefined;
+ cycleService: CycleService;
+ rootStore: CoreRootStore;
+
+ constructor(_rootStore: CoreRootStore) {
+ makeObservable(this, {
+ // observables
+ cycles: observable,
+ // fetch action
+ fetchCycles: action,
+ });
+ this.cycleService = new CycleService();
+ this.rootStore = _rootStore;
+ }
+
+ getCycleById = (cycleId: string | undefined) => this.cycles?.find((cycle) => cycle.id === cycleId);
+
+ fetchCycles = async (anchor: string) => {
+ const cyclesResponse = await this.cycleService.getCycles(anchor);
+ runInAction(() => {
+ this.cycles = cyclesResponse;
+ });
+ return cyclesResponse;
+ };
+}
diff --git a/space/core/store/helpers/base-issues.store.ts b/space/core/store/helpers/base-issues.store.ts
index c61a6702337..004aa06c630 100644
--- a/space/core/store/helpers/base-issues.store.ts
+++ b/space/core/store/helpers/base-issues.store.ts
@@ -1,6 +1,5 @@
import concat from "lodash/concat";
import get from "lodash/get";
-import isEmpty from "lodash/isEmpty";
import set from "lodash/set";
import uniq from "lodash/uniq";
import update from "lodash/update";
@@ -23,7 +22,6 @@ import {
// services
import IssueService from "@/services/issue.service";
import { IIssue, TIssuesResponse } from "@/types/issue";
-import { IIssueFilterStore } from "../issue-filters.store";
import { CoreRootStore } from "../root.store";
// constants
// helpers
@@ -39,14 +37,9 @@ export enum EIssueGroupedAction {
export interface IBaseIssuesStore {
// observable
loader: Record;
- issuesMap: Record; // Record defines issue_id as key and IIssue as value
// actions
addIssue(issues: IIssue[], shouldReplace?: boolean): void;
// helper methods
- getIssueById(issueId: string): undefined | IIssue;
-
- fetchIssueById(anchorId: string, issueId: string): Promise;
-
groupedIssueIds: TGroupedIssues | TSubGroupedIssues | undefined; // object to store Issue Ids based on group or subgroup
groupedIssueCount: TGroupedIssueCount; // map of groupId/subgroup and issue count of that particular group/subgroup
issuePaginationData: TIssuePaginationData; // map of groupId/subgroup and pagination Data of that particular group/subgroup
@@ -79,7 +72,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
loader: Record = {};
groupedIssueIds: TIssues | undefined = undefined;
issuePaginationData: TIssuePaginationData = {};
- issuesMap: Record = {}; // Record defines issue_id as key and TIssue as value
groupedIssueCount: TGroupedIssueCount = {};
//
paginationOptions: IssuePaginationOptions | undefined = undefined;
@@ -87,9 +79,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
issueService;
// root store
rootIssueStore;
- issueFilterStore;
- constructor(_rootStore: CoreRootStore, issueFilterStore: IIssueFilterStore) {
+ constructor(_rootStore: CoreRootStore) {
makeObservable(this, {
// observable
loader: observable,
@@ -107,7 +98,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
setLoader: action.bound,
});
this.rootIssueStore = _rootStore;
- this.issueFilterStore = issueFilterStore;
this.issueService = new IssueService();
}
@@ -141,35 +131,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
if (issues && issues.length <= 0) return;
runInAction(() => {
issues.forEach((issue) => {
- if (!this.issuesMap[issue.id] || shouldReplace) set(this.issuesMap, issue.id, issue);
+ if (!this.rootIssueStore.issueDetail.getIssueById(issue.id) || shouldReplace)
+ set(this.rootIssueStore.issueDetail.details, issue.id, issue);
});
});
};
- /**
- * @description This method will return the issue from the issuesMap
- * @param {string} issueId
- * @returns {IIssue | undefined}
- */
- getIssueById = computedFn((issueId: string) => {
- if (!issueId || isEmpty(this.issuesMap) || !this.issuesMap[issueId]) return undefined;
- return this.issuesMap[issueId];
- });
-
- fetchIssueById = async (anchorId: string, issueId: string) => {
- try {
- const issueDetails = await this.issueService.getIssueById(anchorId, issueId);
-
- runInAction(() => {
- set(this.issuesMap, [issueId], issueDetails);
- });
-
- return issueDetails;
- } catch (e) {
- console.error("error fetching issue details");
- }
- };
-
/**
* Store the pagination data required for next subsequent issue pagination calls
* @param prevCursor cursor value of previous page
diff --git a/space/core/store/helpers/filter.helpers.ts b/space/core/store/helpers/filter.helpers.ts
index 001f08024fb..fd949efefd9 100644
--- a/space/core/store/helpers/filter.helpers.ts
+++ b/space/core/store/helpers/filter.helpers.ts
@@ -32,6 +32,16 @@ export const getPaginationParams = (
paginationParams.group_by = EIssueGroupByToServerOptions[options.groupedBy];
}
+ // If group by is specifically sent through options, like that for calendar layout, use that to group
+ if (options.subGroupedBy) {
+ paginationParams.sub_group_by = EIssueGroupByToServerOptions[options.subGroupedBy];
+ }
+
+ // If group by is specifically sent through options, like that for calendar layout, use that to group
+ if (options.orderBy) {
+ paginationParams.order_by = options.orderBy;
+ }
+
// If before and after dates are sent from option to filter by then, add them to filter the options
if (options.after && options.before) {
paginationParams["target_date"] = `${options.after};after,${options.before};before`;
diff --git a/space/core/store/issue-detail.store.ts b/space/core/store/issue-detail.store.ts
index f17e689caca..aa106b98a23 100644
--- a/space/core/store/issue-detail.store.ts
+++ b/space/core/store/issue-detail.store.ts
@@ -1,5 +1,7 @@
+import isEmpty from "lodash/isEmpty";
import set from "lodash/set";
import { makeObservable, observable, action, runInAction } from "mobx";
+import { computedFn } from "mobx-utils";
import { v4 as uuidv4 } from "uuid";
// services
import IssueService from "@/services/issue.service";
@@ -17,7 +19,10 @@ export interface IIssueDetailStore {
details: {
[key: string]: IIssue;
};
+ // computed actions
+ getIsIssuePeeked: (issueID: string) => boolean;
// actions
+ getIssueById: (issueId: string) => IIssue | undefined;
setPeekId: (issueID: string | null) => void;
setPeekMode: (mode: IPeekMode) => void;
// issue actions
@@ -88,6 +93,38 @@ export class IssueDetailStore implements IIssueDetailStore {
this.peekMode = mode;
};
+ getIsIssuePeeked = (issueID: string) => this.peekId === issueID;
+
+ /**
+ * @description This method will return the issue from the issuesMap
+ * @param {string} issueId
+ * @returns {IIssue | undefined}
+ */
+ getIssueById = computedFn((issueId: string) => {
+ if (!issueId || isEmpty(this.details) || !this.details[issueId]) return undefined;
+ return this.details[issueId];
+ });
+
+ /**
+ * Retrieves issue from API
+ * @param anchorId ]
+ * @param issueId
+ * @returns
+ */
+ fetchIssueById = async (anchorId: string, issueId: string) => {
+ try {
+ const issueDetails = await this.issueService.getIssueById(anchorId, issueId);
+
+ runInAction(() => {
+ set(this.details, [issueId], issueDetails);
+ });
+
+ return issueDetails;
+ } catch (e) {
+ console.error(`Error fetching issue details for issueId ${issueId}: `, e);
+ }
+ };
+
/**
* @description fetc
* @param {string} anchor
@@ -98,7 +135,7 @@ export class IssueDetailStore implements IIssueDetailStore {
this.loader = true;
this.error = null;
- const issueDetails = await this.rootStore.issue.fetchIssueById(anchor, issueID);
+ const issueDetails = await this.fetchIssueById(anchor, issueID);
const commentsResponse = await this.issueService.getIssueComments(anchor, issueID);
if (issueDetails) {
@@ -120,11 +157,11 @@ export class IssueDetailStore implements IIssueDetailStore {
addIssueComment = async (anchor: string, issueID: string, data: any) => {
try {
- const issueDetails = this.rootStore.issue.getIssueById(issueID);
+ const issueDetails = this.getIssueById(issueID);
const issueCommentResponse = await this.issueService.createIssueComment(anchor, issueID, data);
if (issueDetails) {
runInAction(() => {
- set(this.details, [issueID, "comments"], [...this.details[issueID].comments, issueCommentResponse]);
+ set(this.details, [issueID, "comments"], [...(issueDetails?.comments ?? []), issueCommentResponse]);
});
}
return issueCommentResponse;
diff --git a/space/core/store/issue.store.ts b/space/core/store/issue.store.ts
index 087ea392d10..ca5154df7a3 100644
--- a/space/core/store/issue.store.ts
+++ b/space/core/store/issue.store.ts
@@ -27,7 +27,7 @@ export class IssueStore extends BaseIssuesStore implements IIssueStore {
issueService: IssueService;
constructor(_rootStore: CoreRootStore) {
- super(_rootStore, _rootStore.issueFilter);
+ super(_rootStore);
makeObservable(this, {
// actions
fetchPublicIssues: action,
diff --git a/space/core/store/members.store.ts b/space/core/store/members.store.ts
new file mode 100644
index 00000000000..3de021e2c7e
--- /dev/null
+++ b/space/core/store/members.store.ts
@@ -0,0 +1,68 @@
+import set from "lodash/set";
+import { action, computed, makeObservable, observable, runInAction } from "mobx";
+import { TPublicMember } from "@/types/member";
+import { MemberService } from "../services/member.service";
+import { CoreRootStore } from "./root.store";
+
+export interface IIssueMemberStore {
+ // observables
+ members: TPublicMember[] | undefined;
+ // computed actions
+ getMemberById: (memberId: string | undefined) => TPublicMember | undefined;
+ getMembersByIds: (memberIds: string[]) => TPublicMember[];
+ // fetch actions
+ fetchMembers: (anchor: string) => Promise;
+}
+
+export class MemberStore implements IIssueMemberStore {
+ memberMap: Record = {};
+ memberService: MemberService;
+ rootStore: CoreRootStore;
+
+ constructor(_rootStore: CoreRootStore) {
+ makeObservable(this, {
+ // observables
+ memberMap: observable,
+ // computed
+ members: computed,
+ // fetch action
+ fetchMembers: action,
+ });
+ this.memberService = new MemberService();
+ this.rootStore = _rootStore;
+ }
+
+ get members() {
+ return Object.values(this.memberMap);
+ }
+
+ getMemberById = (memberId: string | undefined) => (memberId ? this.memberMap[memberId] : undefined);
+
+ getMembersByIds = (memberIds: string[]) => {
+ const currMembers = [];
+ for (const memberId of memberIds) {
+ const member = this.getMemberById(memberId);
+ if (member) {
+ currMembers.push(member);
+ }
+ }
+
+ return currMembers;
+ };
+
+ fetchMembers = async (anchor: string) => {
+ try {
+ const membersResponse = await this.memberService.getAnchorMembers(anchor);
+ runInAction(() => {
+ this.memberMap = {};
+ for (const member of membersResponse) {
+ set(this.memberMap, [member.member], member);
+ }
+ });
+ return membersResponse;
+ } catch (error) {
+ console.error("Failed to fetch members:", error);
+ return [];
+ }
+ };
+}
diff --git a/space/core/store/module.store.ts b/space/core/store/module.store.ts
new file mode 100644
index 00000000000..6da1ab1f80e
--- /dev/null
+++ b/space/core/store/module.store.ts
@@ -0,0 +1,68 @@
+import set from "lodash/set";
+import { action, computed, makeObservable, observable, runInAction } from "mobx";
+import { TPublicModule } from "@/types/modules";
+import { ModuleService } from "../services/module.service";
+import { CoreRootStore } from "./root.store";
+
+export interface IIssueModuleStore {
+ // observables
+ modules: TPublicModule[] | undefined;
+ // computed actions
+ getModuleById: (moduleId: string | undefined) => TPublicModule | undefined;
+ getModulesByIds: (moduleIds: string[]) => TPublicModule[];
+ // fetch actions
+ fetchModules: (anchor: string) => Promise;
+}
+
+export class ModuleStore implements IIssueModuleStore {
+ moduleMap: Record = {};
+ moduleService: ModuleService;
+ rootStore: CoreRootStore;
+
+ constructor(_rootStore: CoreRootStore) {
+ makeObservable(this, {
+ // observables
+ moduleMap: observable,
+ // computed
+ modules: computed,
+ // fetch action
+ fetchModules: action,
+ });
+ this.moduleService = new ModuleService();
+ this.rootStore = _rootStore;
+ }
+
+ get modules() {
+ return Object.values(this.moduleMap);
+ }
+
+ getModuleById = (moduleId: string | undefined) => (moduleId ? this.moduleMap[moduleId] : undefined);
+
+ getModulesByIds = (moduleIds: string[]) => {
+ const currModules = [];
+ for (const moduleId of moduleIds) {
+ const issueModule = this.getModuleById(moduleId);
+ if (issueModule) {
+ currModules.push(issueModule);
+ }
+ }
+
+ return currModules;
+ };
+
+ fetchModules = async (anchor: string) => {
+ try {
+ const modulesResponse = await this.moduleService.getModules(anchor);
+ runInAction(() => {
+ this.moduleMap = {};
+ for (const issueModule of modulesResponse) {
+ set(this.moduleMap, [issueModule.id], issueModule);
+ }
+ });
+ return modulesResponse;
+ } catch (error) {
+ console.error("Failed to fetch members:", error);
+ return [];
+ }
+ };
+}
diff --git a/space/core/store/root.store.ts b/space/core/store/root.store.ts
index 3f3ad5bb558..de43001d2c9 100644
--- a/space/core/store/root.store.ts
+++ b/space/core/store/root.store.ts
@@ -4,9 +4,12 @@ import { IInstanceStore, InstanceStore } from "@/store/instance.store";
import { IssueDetailStore, IIssueDetailStore } from "@/store/issue-detail.store";
import { IssueStore, IIssueStore } from "@/store/issue.store";
import { IUserStore, UserStore } from "@/store/user.store";
+import { CycleStore, ICycleStore } from "./cycle.store";
import { IssueFilterStore, IIssueFilterStore } from "./issue-filters.store";
import { IIssueLabelStore, LabelStore } from "./label.store";
+import { IIssueMemberStore, MemberStore } from "./members.store";
import { IMentionsStore, MentionsStore } from "./mentions.store";
+import { IIssueModuleStore, ModuleStore } from "./module.store";
import { IPublishListStore, PublishListStore } from "./publish/publish_list.store";
import { IStateStore, StateStore } from "./state.store";
@@ -20,6 +23,9 @@ export class CoreRootStore {
mentionStore: IMentionsStore;
state: IStateStore;
label: IIssueLabelStore;
+ module: IIssueModuleStore;
+ member: IIssueMemberStore;
+ cycle: ICycleStore;
issueFilter: IIssueFilterStore;
publishList: IPublishListStore;
@@ -31,6 +37,9 @@ export class CoreRootStore {
this.mentionStore = new MentionsStore(this);
this.state = new StateStore(this);
this.label = new LabelStore(this);
+ this.module = new ModuleStore(this);
+ this.member = new MemberStore(this);
+ this.cycle = new CycleStore(this);
this.issueFilter = new IssueFilterStore(this);
this.publishList = new PublishListStore(this);
}
@@ -51,6 +60,9 @@ export class CoreRootStore {
this.mentionStore = new MentionsStore(this);
this.state = new StateStore(this);
this.label = new LabelStore(this);
+ this.module = new ModuleStore(this);
+ this.member = new MemberStore(this);
+ this.cycle = new CycleStore(this);
this.issueFilter = new IssueFilterStore(this);
this.publishList = new PublishListStore(this);
}
diff --git a/space/core/types/cycle.d.ts b/space/core/types/cycle.d.ts
new file mode 100644
index 00000000000..edf8f31a8a1
--- /dev/null
+++ b/space/core/types/cycle.d.ts
@@ -0,0 +1,5 @@
+export type TPublicCycle = {
+ id: string;
+ name: string;
+ status: string;
+};
diff --git a/space/core/types/issue.d.ts b/space/core/types/issue.d.ts
index 00d6d505ebf..79c6257d5af 100644
--- a/space/core/types/issue.d.ts
+++ b/space/core/types/issue.d.ts
@@ -37,6 +37,8 @@ export interface IIssue
extends Pick<
TIssue,
| "description_html"
+ | "created_at"
+ | "updated_at"
| "created_by"
| "id"
| "name"
@@ -51,6 +53,10 @@ export interface IIssue
| "module_ids"
| "label_ids"
| "assignee_ids"
+ | "attachment_count"
+ | "sub_issues_count"
+ | "link_count"
+ | "estimate_point"
> {
comments: Comment[];
reaction_items: IIssueReaction[];
diff --git a/space/core/types/member.d.ts b/space/core/types/member.d.ts
new file mode 100644
index 00000000000..721ccd98fc5
--- /dev/null
+++ b/space/core/types/member.d.ts
@@ -0,0 +1,10 @@
+export type TPublicMember = {
+ id: string;
+ member: string;
+ member__avatar: string;
+ member__first_name: string;
+ member__last_name: string;
+ member__display_name: string;
+ project: string;
+ workspace: string;
+};
diff --git a/space/core/types/modules.d.ts b/space/core/types/modules.d.ts
new file mode 100644
index 00000000000..8bc35ce6ff8
--- /dev/null
+++ b/space/core/types/modules.d.ts
@@ -0,0 +1,4 @@
+export type TPublicModule = {
+ id: string;
+ name: string;
+};
diff --git a/space/ee/components/issue-layouts/root.tsx b/space/ee/components/issue-layouts/root.tsx
new file mode 100644
index 00000000000..d785c5c11c2
--- /dev/null
+++ b/space/ee/components/issue-layouts/root.tsx
@@ -0,0 +1 @@
+export * from "ce/components/issue-layouts/root";
diff --git a/space/ee/components/navbar/index.tsx b/space/ee/components/navbar/index.tsx
new file mode 100644
index 00000000000..960fa250745
--- /dev/null
+++ b/space/ee/components/navbar/index.tsx
@@ -0,0 +1 @@
+export * from "ce/components/navbar";
diff --git a/space/ee/hooks/store/index.ts b/space/ee/hooks/store/index.ts
new file mode 100644
index 00000000000..6ce80b4fb5a
--- /dev/null
+++ b/space/ee/hooks/store/index.ts
@@ -0,0 +1 @@
+export * from "ce/hooks/store";
diff --git a/space/helpers/string.helper.ts b/space/helpers/string.helper.ts
index f6319bc7507..fe7df96dae3 100644
--- a/space/helpers/string.helper.ts
+++ b/space/helpers/string.helper.ts
@@ -56,3 +56,7 @@ export const isEmptyHtmlString = (htmlString: string) => {
// Trim the string and check if it's empty
return cleanText.trim() === "";
};
+
+export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
+
+export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
\ No newline at end of file
diff --git a/space/styles/globals.css b/space/styles/globals.css
index 0b41d84811b..511f6ad1fdf 100644
--- a/space/styles/globals.css
+++ b/space/styles/globals.css
@@ -354,3 +354,90 @@ body {
.disable-autofill-style:-webkit-autofill:active {
-webkit-background-clip: text;
}
+
+
+@-moz-document url-prefix() {
+ * {
+ scrollbar-width: none;
+ }
+ .vertical-scrollbar,
+ .horizontal-scrollbar {
+ scrollbar-width: initial;
+ scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
+ }
+ .vertical-scrollbar:hover,
+ .horizontal-scrollbar:hover {
+ scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
+ }
+ .vertical-scrollbar:active,
+ .horizontal-scrollbar:active {
+ scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
+ }
+}
+
+.vertical-scrollbar {
+ overflow-y: auto;
+}
+.horizontal-scrollbar {
+ overflow-x: auto;
+}
+.vertical-scrollbar::-webkit-scrollbar,
+.horizontal-scrollbar::-webkit-scrollbar {
+ display: block;
+}
+.vertical-scrollbar::-webkit-scrollbar-track,
+.horizontal-scrollbar::-webkit-scrollbar-track {
+ background-color: transparent;
+ border-radius: 9999px;
+}
+.vertical-scrollbar::-webkit-scrollbar-thumb,
+.horizontal-scrollbar::-webkit-scrollbar-thumb {
+ background-clip: padding-box;
+ background-color: rgba(96, 100, 108, 0.1);
+ border-radius: 9999px;
+}
+.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
+.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
+ background-color: rgba(96, 100, 108, 0.25);
+}
+.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
+.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(96, 100, 108, 0.5);
+}
+.vertical-scrollbar::-webkit-scrollbar-thumb:active,
+.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
+ background-color: rgba(96, 100, 108, 0.7);
+}
+.vertical-scrollbar::-webkit-scrollbar-corner,
+.horizontal-scrollbar::-webkit-scrollbar-corner {
+ background-color: transparent;
+}
+.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
+ margin-top: 44px;
+}
+
+/* scrollbar sm size */
+.scrollbar-sm::-webkit-scrollbar {
+ height: 12px;
+ width: 12px;
+}
+.scrollbar-sm::-webkit-scrollbar-thumb {
+ border: 3px solid rgba(0, 0, 0, 0);
+}
+/* scrollbar md size */
+.scrollbar-md::-webkit-scrollbar {
+ height: 14px;
+ width: 14px;
+}
+.scrollbar-md::-webkit-scrollbar-thumb {
+ border: 3px solid rgba(0, 0, 0, 0);
+}
+/* scrollbar lg size */
+
+.scrollbar-lg::-webkit-scrollbar {
+ height: 16px;
+ width: 16px;
+}
+.scrollbar-lg::-webkit-scrollbar-thumb {
+ border: 4px solid rgba(0, 0, 0, 0);
+}
diff --git a/web/ce/components/views/publish/index.ts b/web/ce/components/views/publish/index.ts
new file mode 100644
index 00000000000..8c04a4e3d8e
--- /dev/null
+++ b/web/ce/components/views/publish/index.ts
@@ -0,0 +1,2 @@
+export * from "./modal";
+export * from "./use-view-publish";
diff --git a/web/ce/components/views/publish/modal.tsx b/web/ce/components/views/publish/modal.tsx
new file mode 100644
index 00000000000..0951de0930d
--- /dev/null
+++ b/web/ce/components/views/publish/modal.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import { IProjectView } from "@plane/types";
+
+type Props = {
+ isOpen: boolean;
+ view: IProjectView;
+ onClose: () => void;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const PublishViewModal = (props: Props) => <>>;
diff --git a/web/ce/components/views/publish/use-view-publish.tsx b/web/ce/components/views/publish/use-view-publish.tsx
new file mode 100644
index 00000000000..687a79ed762
--- /dev/null
+++ b/web/ce/components/views/publish/use-view-publish.tsx
@@ -0,0 +1,7 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useViewPublish = (isPublished: boolean, isAuthorized: boolean) => ({
+ isPublishModalOpen: false,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ setPublishModalOpen: (value: boolean) => {},
+ publishContextMenu: undefined,
+});
diff --git a/web/ce/services/project/view.service.ts b/web/ce/services/project/view.service.ts
index 07872394a39..6cb76222add 100644
--- a/web/ce/services/project/view.service.ts
+++ b/web/ce/services/project/view.service.ts
@@ -1,3 +1,4 @@
+import { TPublishViewSettings } from "@plane/types";
import { EViewAccess } from "@/constants/views";
import { API_BASE_URL } from "@/helpers/common.helper";
import { ViewService as CoreViewService } from "@/services/view.service";
@@ -21,4 +22,40 @@ export class ViewService extends CoreViewService {
async unLockView(workspaceSlug: string, projectId: string, viewId: string) {
return Promise.resolve();
}
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async getPublishDetails(workspaceSlug: string, projectId: string, viewId: string): Promise {
+ return Promise.resolve({});
+ }
+
+ async publishView(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ workspaceSlug: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ projectId: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ viewId: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ data: TPublishViewSettings
+ ): Promise {
+ return Promise.resolve();
+ }
+
+ async updatePublishedView(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ workspaceSlug: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ projectId: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ viewId: string,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ data: Partial
+ ): Promise {
+ return Promise.resolve();
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async unPublishView(workspaceSlug: string, projectId: string, viewId: string): Promise {
+ return Promise.resolve();
+ }
}
diff --git a/web/core/components/views/quick-actions.tsx b/web/core/components/views/quick-actions.tsx
index b2d8680b712..287fcad91d8 100644
--- a/web/core/components/views/quick-actions.tsx
+++ b/web/core/components/views/quick-actions.tsx
@@ -16,6 +16,7 @@ import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useUser } from "@/hooks/store";
+import { PublishViewModal, useViewPublish } from "@/plane-web/components/views/publish";
type Props = {
parentRef: React.RefObject;
@@ -38,6 +39,11 @@ export const ViewQuickActions: React.FC = observer((props) => {
const isOwner = view?.owned_by === data?.id;
const isAdmin = !!currentProjectRole && currentProjectRole == EUserProjectRoles.ADMIN;
+ const { isPublishModalOpen, setPublishModalOpen, publishContextMenu } = useViewPublish(
+ !!view.anchor,
+ isAdmin || isOwner
+ );
+
const viewLink = `${workspaceSlug}/projects/${projectId}/views/${view.id}`;
const handleCopyText = () =>
copyUrlToClipboard(viewLink).then(() => {
@@ -78,6 +84,8 @@ export const ViewQuickActions: React.FC = observer((props) => {
},
];
+ if (publishContextMenu) MENU_ITEMS.splice(2, 0, publishContextMenu);
+
return (
<>
= observer((props) => {
data={view}
/>
setDeleteViewModal(false)} />
+ setPublishModalOpen(false)} view={view} />
{MENU_ITEMS.map((item) => {
diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts
index f76bb45fefa..207eb70930a 100644
--- a/web/core/store/project-view.store.ts
+++ b/web/core/store/project-view.store.ts
@@ -2,7 +2,7 @@ import { set } from "lodash";
import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
// types
-import { IProjectView, TViewFilters } from "@plane/types";
+import { IProjectView, TPublishViewDetails, TPublishViewSettings, TViewFilters } from "@plane/types";
// constants
import { EViewAccess } from "@/constants/views";
// helpers
@@ -42,6 +42,25 @@ export interface IProjectViewStore {
// favorites actions
addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
+ // publish
+ publishView: (
+ workspaceSlug: string,
+ projectId: string,
+ viewId: string,
+ data: TPublishViewSettings
+ ) => Promise;
+ fetchPublishDetails: (
+ workspaceSlug: string,
+ projectId: string,
+ viewId: string
+ ) => Promise;
+ updatePublishedView: (
+ workspaceSlug: string,
+ projectId: string,
+ viewId: string,
+ data: Partial
+ ) => Promise;
+ unPublishView: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
}
export class ProjectViewStore implements IProjectViewStore {
@@ -372,4 +391,91 @@ export class ProjectViewStore implements IProjectViewStore {
});
}
};
+
+ /**
+ * Publishes View to the Public
+ * @param workspaceSlug
+ * @param projectId
+ * @param viewId
+ * @returns
+ */
+ publishView = async (workspaceSlug: string, projectId: string, viewId: string, data: TPublishViewSettings) => {
+ try {
+ const response = (await this.viewService.publishView(
+ workspaceSlug,
+ projectId,
+ viewId,
+ data
+ )) as TPublishViewDetails;
+ runInAction(() => {
+ set(this.viewMap, [viewId, "anchor"], response?.anchor);
+ });
+
+ return response;
+ } catch (error) {
+ console.error("Failed to publish view", error);
+ }
+ };
+
+ /**
+ * fetches Published Details
+ * @param workspaceSlug
+ * @param projectId
+ * @param viewId
+ * @returns
+ */
+ fetchPublishDetails = async (workspaceSlug: string, projectId: string, viewId: string) => {
+ try {
+ const response = (await this.viewService.getPublishDetails(
+ workspaceSlug,
+ projectId,
+ viewId
+ )) as TPublishViewDetails;
+ runInAction(() => {
+ set(this.viewMap, [viewId, "anchor"], response?.anchor);
+ });
+ return response;
+ } catch (error) {
+ console.error("Failed to fetch published view details", error);
+ }
+ };
+
+ /**
+ * updates already published view
+ * @param workspaceSlug
+ * @param projectId
+ * @param viewId
+ * @returns
+ */
+ updatePublishedView = async (
+ workspaceSlug: string,
+ projectId: string,
+ viewId: string,
+ data: Partial
+ ) => {
+ try {
+ return await this.viewService.updatePublishedView(workspaceSlug, projectId, viewId, data);
+ } catch (error) {
+ console.error("Failed to update published view details", error);
+ }
+ };
+
+ /**
+ * un publishes the view
+ * @param workspaceSlug
+ * @param projectId
+ * @param viewId
+ * @returns
+ */
+ unPublishView = async (workspaceSlug: string, projectId: string, viewId: string) => {
+ try {
+ const response = await this.viewService.unPublishView(workspaceSlug, projectId, viewId);
+ runInAction(() => {
+ set(this.viewMap, [viewId, "anchor"], null);
+ });
+ return response;
+ } catch (error) {
+ console.error("Failed to unPublish view", error);
+ }
+ };
}
diff --git a/web/ee/components/views/publish/index.ts b/web/ee/components/views/publish/index.ts
new file mode 100644
index 00000000000..d2680523a80
--- /dev/null
+++ b/web/ee/components/views/publish/index.ts
@@ -0,0 +1 @@
+export * from "ce/components/views/publish";