Merge branch 'refactor/space-app' of github.com:makeplane/plane into refactor/space-app

This commit is contained in:
NarayanBavisetti 2024-06-04 19:18:25 +05:30
commit 62d669ee95
18 changed files with 196 additions and 314 deletions

View File

@ -0,0 +1,27 @@
"use client";
import useSWR from "swr";
// hooks
import { usePublish, usePublishList } from "@/hooks/store";
type Props = {
params: {
anchor: string;
};
};
const PageDetailsPage = (props: Props) => {
const { params } = props;
const { anchor } = params;
// store hooks
const { fetchPublishSettings } = usePublishList();
const publishSettings = usePublish(anchor);
useSWR(anchor ? `PUBLISH_SETTINGS_${anchor}` : null, anchor ? () => fetchPublishSettings(anchor) : null);
if (!publishSettings) return null;
return <>Page details</>;
};
export default PageDetailsPage;

View File

@ -5,7 +5,7 @@ import cloneDeep from "lodash/cloneDeep";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/navigation";
// hooks
import { useIssue, useIssueFilter, usePublish } from "@/hooks/store";
import { useIssue, useIssueFilter } from "@/hooks/store";
// store
import { TIssueQueryFilters } from "@/types/issue";
// components
@ -20,10 +20,10 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
// router
const router = useRouter();
// store hooks
const { issueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter();
const { getIssueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter();
const { states, labels } = useIssue();
const { project: projectID } = usePublish(anchor);
// derived values
const issueFilters = getIssueFilters(anchor);
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const userFilters = issueFilters?.filters || {};
@ -53,23 +53,19 @@ export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) =>
const handleFilters = useCallback(
(key: keyof TIssueQueryFilters, value: string | null) => {
if (!projectID) return;
let newValues = cloneDeep(issueFilters?.filters?.[key]) ?? [];
if (value === null) newValues = [];
else if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1);
updateIssueFilters(projectID, "filters", key, newValues);
updateIssueFilters(anchor, "filters", key, newValues);
updateRouteParams(key, newValues);
},
[projectID, issueFilters, updateIssueFilters, updateRouteParams]
[anchor, issueFilters, updateIssueFilters, updateRouteParams]
);
const handleRemoveAllFilters = () => {
if (!projectID) return;
initIssueFilters(projectID, {
initIssueFilters(anchor, {
display_filters: { layout: activeLayout || "list" },
filters: {
state: [],

View File

@ -17,17 +17,18 @@ import { useIssue, useIssueFilter } from "@/hooks/store";
import { TIssueQueryFilters } from "@/types/issue";
type IssueFiltersDropdownProps = {
workspaceSlug: string;
projectId: string;
anchor: string;
};
export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((props) => {
const { anchor } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = props;
// hooks
const { issueFilters, updateIssueFilters } = useIssueFilter();
const { getIssueFilters, updateIssueFilters } = useIssueFilter();
const { states, labels } = useIssue();
// derived values
const issueFilters = getIssueFilters(anchor);
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const updateRouteParams = useCallback(
@ -37,24 +38,24 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
const { queryParam } = queryParamGenerator({ board: activeLayout, priority, state, labels });
router.push(`/${workspaceSlug}/${projectId}?${queryParam}`);
router.push(`/issues/${anchor}?${queryParam}`);
},
[workspaceSlug, projectId, activeLayout, issueFilters, router]
[anchor, activeLayout, issueFilters, router]
);
const handleFilters = useCallback(
(key: keyof TIssueQueryFilters, value: string) => {
if (!projectId || !value) return;
if (!value) return;
const newValues = cloneDeep(issueFilters?.filters?.[key]) ?? [];
if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
updateIssueFilters(projectId, "filters", key, newValues);
updateIssueFilters(anchor, "filters", key, newValues);
updateRouteParams(key, newValues);
},
[projectId, issueFilters, updateIssueFilters, updateRouteParams]
[anchor, issueFilters, updateIssueFilters, updateRouteParams]
);
return (

View File

@ -35,16 +35,17 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
const priority = searchParams.get("priority") || undefined;
const peekId = searchParams.get("peekId") || undefined;
// hooks
const { issueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter();
const { getIssueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter();
const { setPeekId } = useIssueDetails();
// derived values
const { anchor, views, workspace_detail } = publishSettings;
const issueFilters = anchor ? getIssueFilters(anchor) : undefined;
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const { project, views, workspace_detail } = publishSettings;
const isInIframe = useIsInIframe();
useEffect(() => {
if (project && workspace_detail) {
if (anchor && workspace_detail) {
const viewsAcceptable: string[] = [];
let currentBoard: TIssueLayout | null = null;
@ -75,14 +76,15 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
},
};
if (!isIssueFiltersUpdated(params)) {
initIssueFilters(project, params);
router.push(`/${workspace_detail.slug}/${project}?${queryParam}`);
if (!isIssueFiltersUpdated(anchor, params)) {
initIssueFilters(anchor, params);
router.push(`/issues/${anchor}?${queryParam}`);
}
}
}
}
}, [
anchor,
board,
labels,
state,
@ -94,22 +96,21 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
setPeekId,
isIssueFiltersUpdated,
views,
project,
workspace_detail,
]);
if (!workspace_detail || !project) return;
if (!anchor) return null;
return (
<>
{/* issue views */}
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
<NavbarIssueBoardView workspaceSlug={workspace_detail.slug} projectId={project} />
<NavbarIssueBoardView anchor={anchor} />
</div>
{/* issue filters */}
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
<IssueFiltersDropdown workspaceSlug={workspace_detail.slug} projectId={project} />
<IssueFiltersDropdown anchor={anchor} />
</div>
{/* theming */}

View File

@ -13,11 +13,12 @@ import { useIssueFilter } from "@/hooks/store";
import { TIssueLayout } from "@/types/issue";
type NavbarIssueBoardViewProps = {
workspaceSlug: string;
projectId: string;
anchor: string;
};
export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((props) => {
const { anchor } = props;
// router
const router = useRouter();
const searchParams = useSearchParams();
// query params
@ -25,18 +26,16 @@ export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((pro
const state = searchParams.get("state") || undefined;
const priority = searchParams.get("priority") || undefined;
const peekId = searchParams.get("peekId") || undefined;
// props
const { workspaceSlug, projectId } = props;
// hooks
const { layoutOptions, issueFilters, updateIssueFilters } = useIssueFilter();
const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter();
// derived values
const issueFilters = getIssueFilters(anchor);
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const handleCurrentBoardView = (boardView: TIssueLayout) => {
updateIssueFilters(projectId, "display_filters", "layout", boardView);
updateIssueFilters(anchor, "display_filters", "layout", boardView);
const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels });
router.push(`/${workspaceSlug}/${projectId}?${queryParam}`);
router.push(`/issues/${anchor}?${queryParam}`);
};
return (

View File

@ -29,7 +29,7 @@ export const FullScreenPeekView: React.FC<Props> = observer((props) => {
<div className="h-full w-full overflow-y-auto px-6">
{/* issue title and description */}
<div className="w-full">
<PeekOverviewIssueDetails issueDetails={issueDetails} />
<PeekOverviewIssueDetails anchor={anchor} issueDetails={issueDetails} />
</div>
{/* divider */}
<div className="my-5 h-[1] w-full border-t border-custom-border-200" />

View File

@ -7,7 +7,7 @@ import { Button } from "@plane/ui";
import { CommentCard, AddComment } from "@/components/issues/peek-overview";
import { Icon } from "@/components/ui";
// hooks
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
import useIsInIframe from "@/hooks/use-is-in-iframe";
// types
import { IIssue } from "@/types/issue";
@ -21,13 +21,13 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
const { anchor } = props;
// router
const pathname = usePathname();
// store
const { canComment } = useProject();
// store hooks
const { details, peekId } = useIssueDetails();
const { data: currentUser } = useUser();
const isInIframe = useIsInIframe();
const { canComment } = usePublish(anchor);
// derived values
const comments = details[peekId || ""]?.comments || [];
const isInIframe = useIsInIframe();
return (
<div className="pb-10">

View File

@ -5,26 +5,33 @@ import { IssueReactions } from "@/components/issues/peek-overview";
import { IIssue } from "@/types/issue";
type Props = {
anchor: string;
issueDetails: IIssue;
};
export const PeekOverviewIssueDetails: React.FC<Props> = ({ issueDetails }) => (
export const PeekOverviewIssueDetails: React.FC<Props> = (props) => {
const { anchor, issueDetails } = props;
const description = issueDetails.description_html;
return (
<div className="space-y-2">
<h6 className="font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h6>
<h4 className="break-words text-2xl font-semibold">{issueDetails.name}</h4>
{issueDetails.description_html !== "" && issueDetails.description_html !== "<p></p>" && (
{description !== "" && description !== "<p></p>" && (
<RichTextReadOnlyEditor
initialValue={
!issueDetails.description_html ||
issueDetails.description_html === "" ||
(typeof issueDetails.description_html === "object" && Object.keys(issueDetails.description_html).length === 0)
!description ||
description === "" ||
(typeof description === "object" && Object.keys(description).length === 0)
? "<p></p>"
: issueDetails.description_html
: description
}
/>
)}
<IssueReactions />
<IssueReactions anchor={anchor} />
</div>
);
};

View File

@ -10,11 +10,12 @@ import { queryParamGenerator } from "@/helpers/query-param-generator";
import { useIssueDetails, useUser } from "@/hooks/store";
type IssueEmojiReactionsProps = {
workspaceSlug: string;
projectId: string;
anchor: string;
};
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
const { anchor } = props;
// router
const router = useRouter();
const pathName = usePathname();
const searchParams = useSearchParams();
@ -24,9 +25,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
const state = searchParams.get("state") || undefined;
const priority = searchParams.get("priority") || undefined;
const labels = searchParams.get("labels") || undefined;
const { workspaceSlug, projectId } = props;
// store
// store hooks
const issueDetailsStore = useIssueDetails();
const { data: user } = useUser();
@ -37,13 +36,13 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
const handleAddReaction = (reactionHex: string) => {
if (!workspaceSlug || !projectId || !issueId) return;
issueDetailsStore.addIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex);
if (!issueId) return;
issueDetailsStore.addIssueReaction(anchor, issueId, reactionHex);
};
const handleRemoveReaction = (reactionHex: string) => {
if (!workspaceSlug || !projectId || !issueId) return;
issueDetailsStore.removeIssueReaction(workspaceSlug.toString(), projectId.toString(), issueId, reactionHex);
if (!issueId) return;
issueDetailsStore.removeIssueReaction(anchor, issueId, reactionHex);
};
const handleReactionClick = (reactionHex: string) => {

View File

@ -1,33 +1,31 @@
import { useParams } from "next/navigation";
import { observer } from "mobx-react-lite";
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
import { useProject } from "@/hooks/store";
// hooks
import { usePublish } from "@/hooks/store";
import useIsInIframe from "@/hooks/use-is-in-iframe";
// type IssueReactionsProps = {
// workspaceSlug: string;
// projectId: string;
// };
type Props = {
anchor: string;
};
export const IssueReactions: React.FC = () => {
const { workspaceSlug, projectId } = useParams<any>();
const { canVote, canReact } = useProject();
export const IssueReactions: React.FC<Props> = observer((props) => {
const { anchor } = props;
// store hooks
const { canVote, canReact } = usePublish(anchor);
const isInIframe = useIsInIframe();
return (
<div className="mt-4 flex items-center gap-3">
{canVote && (
<>
<div className="flex items-center gap-2">
<IssueVotes workspaceSlug={workspaceSlug} projectId={projectId} />
<IssueVotes anchor={anchor} />
</div>
</>
)}
{!isInIframe && canReact && (
<div className="flex items-center gap-2">
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
<IssueEmojiReactions anchor={anchor} />
</div>
)}
</div>
);
};
});

View File

@ -12,11 +12,14 @@ import { useIssueDetails, useUser } from "@/hooks/store";
import useIsInIframe from "@/hooks/use-is-in-iframe";
type TIssueVotes = {
workspaceSlug: string;
projectId: string;
anchor: string;
};
export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
const { anchor } = props;
// states
const [isSubmitting, setIsSubmitting] = useState(false);
// router
const router = useRouter();
const pathName = usePathname();
const searchParams = useSearchParams();
@ -26,11 +29,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
const state = searchParams.get("state") || undefined;
const priority = searchParams.get("priority") || undefined;
const labels = searchParams.get("labels") || undefined;
const { workspaceSlug, projectId } = props;
// states
const [isSubmitting, setIsSubmitting] = useState(false);
// store hooks
const issueDetailsStore = useIssueDetails();
const { data: user } = useUser();
@ -47,18 +46,18 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
const isDownVotedByUser = allDownVotes?.some((vote) => vote.actor === user?.id);
const handleVote = async (e: any, voteValue: 1 | -1) => {
if (!workspaceSlug || !projectId || !issueId) return;
if (!issueId) return;
setIsSubmitting(true);
const actionPerformed = votes?.find((vote) => vote.actor === user?.id && vote.vote === voteValue);
if (actionPerformed)
await issueDetailsStore.removeIssueVote(workspaceSlug.toString(), projectId.toString(), issueId);
else
await issueDetailsStore.addIssueVote(workspaceSlug.toString(), projectId.toString(), issueId, {
if (actionPerformed) await issueDetailsStore.removeIssueVote(anchor, issueId);
else {
await issueDetailsStore.addIssueVote(anchor, issueId, {
vote: voteValue,
});
}
setIsSubmitting(false);
};

View File

@ -32,7 +32,7 @@ export const SidePeekView: React.FC<Props> = observer((props) => {
<div className="h-full w-full overflow-y-auto px-6">
{/* issue title and description */}
<div className="w-full">
<PeekOverviewIssueDetails issueDetails={issueDetails} />
<PeekOverviewIssueDetails anchor={anchor} issueDetails={issueDetails} />
</div>
{/* issue properties */}
<div className="mt-6 w-full">

View File

@ -33,11 +33,12 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
const priority = searchParams.get("priority") || undefined;
const labels = searchParams.get("labels") || undefined;
// store hooks
const { issueFilters } = useIssueFilter();
const { getIssueFilters } = useIssueFilter();
const { loader, issues, error, fetchPublicIssues } = useIssue();
const issueDetailStore = useIssueDetails();
// derived values
const { anchor } = publishSettings;
const issueFilters = anchor ? getIssueFilters(anchor) : undefined;
useSWR(
anchor ? `PUBLIC_ISSUES_${anchor}` : null,

View File

@ -1,11 +0,0 @@
import { useContext } from "react";
// lib
import { StoreContext } from "@/lib/store-provider";
// store
import { IProjectStore } from "@/store/project.store";
export const useProject = (): IProjectStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
return context.project;
};

View File

@ -4,30 +4,6 @@ import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
interface UnSplashImage {
id: string;
created_at: Date;
updated_at: Date;
promoted_at: Date;
width: number;
height: number;
color: string;
blur_hash: string;
description: null;
alt_description: string;
urls: UnSplashImageUrls;
[key: string]: any;
}
interface UnSplashImageUrls {
raw: string;
full: string;
regular: string;
small: string;
thumb: string;
small_s3: string;
}
class FileService extends APIService {
private cancelSource: any;
@ -123,40 +99,6 @@ class FileService extends APIService {
throw error?.response?.data;
});
}
async deleteFile(workspaceId: string, assetUrl: string): Promise<any> {
const lastIndex = assetUrl.lastIndexOf("/");
const assetId = assetUrl.substring(lastIndex + 1);
return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async uploadUserFile(file: FormData): Promise<any> {
return this.post(`/api/users/file-assets/`, file, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async deleteUserFile(assetUrl: string): Promise<any> {
const lastIndex = assetUrl.lastIndexOf("/");
const assetId = assetUrl.substring(lastIndex + 1);
return this.delete(`/api/users/file-assets/${assetId}`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
const fileService = new FileService();

View File

@ -1,7 +1,7 @@
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import set from "lodash/set";
import { action, makeObservable, observable, runInAction, computed } from "mobx";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
@ -19,16 +19,17 @@ import {
export interface IIssueFilterStore {
// observables
layoutOptions: TIssueLayoutOptions;
filters: { [projectId: string]: TIssueFilters } | undefined;
filters: { [anchor: string]: TIssueFilters } | undefined;
// computed
issueFilters: TIssueFilters | undefined;
appliedFilters: TIssueQueryFiltersParams | undefined;
isIssueFiltersUpdated: (filters: TIssueFilters) => boolean;
isIssueFiltersUpdated: (anchor: string, filters: TIssueFilters) => boolean;
// helpers
getIssueFilters: (anchor: string) => TIssueFilters | undefined;
getAppliedFilters: (anchor: string) => TIssueQueryFiltersParams | undefined;
// actions
updateLayoutOptions: (layout: TIssueLayoutOptions) => void;
initIssueFilters: (projectId: string, filters: TIssueFilters) => void;
initIssueFilters: (anchor: string, filters: TIssueFilters) => void;
updateIssueFilters: <K extends keyof TIssueFilters>(
projectId: string,
anchor: string,
filterKind: K,
filterKey: keyof TIssueFilters[K],
filters: TIssueFilters[K][typeof filterKey]
@ -44,16 +45,13 @@ export class IssueFilterStore implements IIssueFilterStore {
gantt: false,
spreadsheet: false,
};
filters: { [projectId: string]: TIssueFilters } | undefined = undefined;
filters: { [anchor: string]: TIssueFilters } | undefined = undefined;
constructor(private store: RootStore) {
makeObservable(this, {
// observables
layoutOptions: observable,
filters: observable,
// computed
issueFilters: computed,
appliedFilters: computed,
// actions
updateLayoutOptions: action,
initIssueFilters: action,
@ -82,79 +80,70 @@ export class IssueFilterStore implements IIssueFilterStore {
};
// computed
get issueFilters() {
const projectId = this.store.project.project?.id;
if (!projectId) return undefined;
const currentFilters = this.filters?.[projectId];
if (!currentFilters) return undefined;
getIssueFilters = computedFn((anchor: string) => {
const currentFilters = this.filters?.[anchor];
return currentFilters;
}
});
get appliedFilters() {
const currentIssueFilters = this.issueFilters;
if (!currentIssueFilters) return undefined;
getAppliedFilters = computedFn((anchor: string) => {
const issueFilters = this.getIssueFilters(anchor);
if (!issueFilters) return undefined;
const currentLayout = currentIssueFilters?.display_filters?.layout;
const currentLayout = issueFilters?.display_filters?.layout;
if (!currentLayout) return undefined;
const currentFilters: TIssueQueryFilters = {
priority: currentIssueFilters?.filters?.priority || undefined,
state: currentIssueFilters?.filters?.state || undefined,
labels: currentIssueFilters?.filters?.labels || undefined,
priority: issueFilters?.filters?.priority || undefined,
state: issueFilters?.filters?.state || undefined,
labels: issueFilters?.filters?.labels || undefined,
};
const filteredParams = ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[currentLayout]?.filters || [];
const currentFilterQueryParams: TIssueQueryFiltersParams = this.computedFilter(currentFilters, filteredParams);
return currentFilterQueryParams;
}
});
isIssueFiltersUpdated = computedFn((userFilters: TIssueFilters) => {
if (!this.issueFilters) return false;
isIssueFiltersUpdated = computedFn((anchor: string, userFilters: TIssueFilters) => {
const issueFilters = this.getIssueFilters(anchor);
if (!issueFilters) return false;
const currentUserFilters = cloneDeep(userFilters?.filters || {});
const currentIssueFilters = cloneDeep(this.issueFilters?.filters || {});
const currentIssueFilters = cloneDeep(issueFilters?.filters || {});
return isEqual(currentUserFilters, currentIssueFilters);
});
// actions
updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options);
initIssueFilters = async (projectId: string, initFilters: TIssueFilters) => {
initIssueFilters = async (anchor: string, initFilters: TIssueFilters) => {
try {
if (!projectId) return;
if (this.filters === undefined) runInAction(() => (this.filters = {}));
if (this.filters && initFilters) set(this.filters, [projectId], initFilters);
if (this.filters && initFilters) set(this.filters, [anchor], initFilters);
const workspaceSlug = this.store.project.workspace?.slug;
const currentAppliedFilters = this.appliedFilters;
const appliedFilters = this.getAppliedFilters(anchor);
if (!workspaceSlug) return;
await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters);
await this.store.issue.fetchPublicIssues(anchor, appliedFilters);
} catch (error) {
throw error;
}
};
updateIssueFilters = async <K extends keyof TIssueFilters>(
projectId: string,
anchor: string,
filterKind: K,
filterKey: keyof TIssueFilters[K],
filterValue: TIssueFilters[K][typeof filterKey]
) => {
try {
if (!projectId || !filterKind || !filterKey || !filterValue) return;
if (!filterKind || !filterKey || !filterValue) return;
if (this.filters === undefined) runInAction(() => (this.filters = {}));
runInAction(() => {
if (this.filters) set(this.filters, [projectId, filterKind, filterKey], filterValue);
if (this.filters) set(this.filters, [anchor, filterKind, filterKey], filterValue);
});
const workspaceSlug = this.store.project.workspace?.slug;
const currentAppliedFilters = this.appliedFilters;
const appliedFilters = this.getAppliedFilters(anchor);
if (!workspaceSlug) return;
await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters);
await this.store.issue.fetchPublicIssues(anchor, appliedFilters);
} catch (error) {
throw error;
}

View File

@ -1,96 +0,0 @@
// mobx
import { observable, action, makeObservable, runInAction, computed } from "mobx";
// service
import ProjectService from "@/services/project.service";
// store types
import { RootStore } from "@/store/root.store";
// types
import { TWorkspaceDetails, TProjectDetails, TProjectSettings } from "@/types/project";
export interface IProjectStore {
// observables
loader: boolean;
error: any | undefined;
settings: TProjectSettings | undefined;
workspace: TWorkspaceDetails | undefined;
projectMap: Record<string, TProjectDetails>; // { [projectID]: TProjectDetails }
canReact: boolean;
canComment: boolean;
canVote: boolean;
// actions
fetchProjectSettings: (workspaceSlug: string, project_slug: string) => Promise<void>;
hydrate: (projectSettings: any) => void;
}
export class ProjectStore implements IProjectStore {
// observables
loader: boolean = false;
error: any | undefined = undefined;
settings: TProjectSettings | undefined = undefined;
workspace: TWorkspaceDetails | undefined = undefined;
projectMap: Record<string, TProjectDetails> = {};
// service
projectService;
constructor(private store: RootStore) {
makeObservable(this, {
// loaders and error observables
loader: observable,
error: observable.ref,
// observable
workspace: observable,
projectMap: observable,
settings: observable,
// computed
canReact: computed,
canComment: computed,
canVote: computed,
// actions
fetchProjectSettings: action,
hydrate: action,
});
// services
this.projectService = new ProjectService();
}
// computed
get canReact() {
return this.settings?.reactions ?? false;
}
get canComment() {
return this.settings?.comments ?? false;
}
get canVote() {
return this.settings?.votes ?? false;
}
fetchProjectSettings = async (workspaceSlug: string, project_slug: string) => {
try {
this.loader = true;
this.error = null;
const response = await this.projectService.getProjectSettings(workspaceSlug, project_slug);
if (response) {
this.store.issueFilter.updateLayoutOptions(response?.views);
runInAction(() => {
this.project = response?.project_details;
this.workspace = response?.workspace_detail;
this.settings = response;
this.loader = false;
});
}
return response;
} catch (error) {
this.loader = false;
this.error = error;
return error;
}
};
hydrate = (projectSettings: TProjectSettings) => {
const { workspace_detail, project_details } = projectSettings;
this.workspace = workspace_detail;
this.project = project_details;
};
}

View File

@ -8,6 +8,9 @@ import { TPublishSettings } from "@/types/publish";
export interface IPublishStore extends TPublishSettings {
// computed
workspaceSlug: string | undefined;
canComment: boolean;
canReact: boolean;
canVote: boolean;
}
export class PublishStore implements IPublishStore {
@ -67,10 +70,37 @@ export class PublishStore implements IPublishStore {
workspace_detail: observable,
// computed
workspaceSlug: computed,
canComment: computed,
canReact: computed,
canVote: computed,
});
}
/**
* @description returns the workspace slug from the workspace details
*/
get workspaceSlug() {
return this?.workspace_detail?.slug ?? undefined;
}
/**
* @description returns whether commenting is enabled or not
*/
get canComment() {
return !!this.comments;
}
/**
* @description returns whether reacting is enabled or not
*/
get canReact() {
return !!this.reactions;
}
/**
* @description returns whether voting is enabled or not
*/
get canVote() {
return !!this.votes;
}
}