mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'refactor/space-app' of github.com:makeplane/plane into refactor/space-app
This commit is contained in:
commit
62d669ee95
@ -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;
|
@ -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: [],
|
||||
|
@ -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 (
|
||||
|
@ -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 */}
|
||||
|
@ -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 (
|
||||
|
@ -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" />
|
||||
|
@ -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">
|
||||
|
@ -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 }) => (
|
||||
<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>" && (
|
||||
<RichTextReadOnlyEditor
|
||||
initialValue={
|
||||
!issueDetails.description_html ||
|
||||
issueDetails.description_html === "" ||
|
||||
(typeof issueDetails.description_html === "object" && Object.keys(issueDetails.description_html).length === 0)
|
||||
? "<p></p>"
|
||||
: issueDetails.description_html
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<IssueReactions />
|
||||
</div>
|
||||
);
|
||||
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>
|
||||
{description !== "" && description !== "<p></p>" && (
|
||||
<RichTextReadOnlyEditor
|
||||
initialValue={
|
||||
!description ||
|
||||
description === "" ||
|
||||
(typeof description === "object" && Object.keys(description).length === 0)
|
||||
? "<p></p>"
|
||||
: description
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<IssueReactions anchor={anchor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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} />
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueVotes anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
{!isInIframe && canReact && (
|
||||
<div className="flex items-center gap-2">
|
||||
<IssueEmojiReactions workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<IssueEmojiReactions anchor={anchor} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user