feat: init
This commit is contained in:
127
apps/space/core/components/issues/navbar/controls.tsx
Normal file
127
apps/space/core/components/issues/navbar/controls.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
// components
|
||||
import { IssueFiltersDropdown } from "@/components/issues/filters";
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// store
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
// types
|
||||
import type { TIssueLayout } from "@/types/issue";
|
||||
// local imports
|
||||
import { IssuesLayoutSelection } from "./layout-selection";
|
||||
import { NavbarTheme } from "./theme";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
|
||||
export type NavbarControlsProps = {
|
||||
publishSettings: PublishStore;
|
||||
};
|
||||
|
||||
export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
||||
// props
|
||||
const { publishSettings } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
// query params
|
||||
const board = searchParams.get("board") || undefined;
|
||||
const labels = searchParams.get("labels") || undefined;
|
||||
const state = searchParams.get("state") || undefined;
|
||||
const priority = searchParams.get("priority") || undefined;
|
||||
const peekId = searchParams.get("peekId") || undefined;
|
||||
// hooks
|
||||
const { getIssueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter();
|
||||
const { setPeekId } = useIssueDetails();
|
||||
// derived values
|
||||
const { anchor, view_props, workspace_detail } = publishSettings;
|
||||
const issueFilters = anchor ? getIssueFilters(anchor) : undefined;
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor && workspace_detail) {
|
||||
const viewsAcceptable: string[] = [];
|
||||
let currentBoard: TIssueLayout | null = null;
|
||||
|
||||
if (view_props?.list) viewsAcceptable.push("list");
|
||||
if (view_props?.kanban) viewsAcceptable.push("kanban");
|
||||
if (view_props?.calendar) viewsAcceptable.push("calendar");
|
||||
if (view_props?.gantt) viewsAcceptable.push("gantt");
|
||||
if (view_props?.spreadsheet) viewsAcceptable.push("spreadsheet");
|
||||
|
||||
if (board) {
|
||||
if (viewsAcceptable.includes(board.toString())) currentBoard = board.toString() as TIssueLayout;
|
||||
else {
|
||||
if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout;
|
||||
}
|
||||
} else {
|
||||
if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout;
|
||||
}
|
||||
|
||||
if (currentBoard) {
|
||||
if (activeLayout === undefined || activeLayout !== currentBoard) {
|
||||
const { query, queryParam } = queryParamGenerator({ board: currentBoard, peekId, priority, state, labels });
|
||||
const params: any = {
|
||||
display_filters: { layout: (query?.board as string[])[0] },
|
||||
filters: {
|
||||
priority: query?.priority ?? undefined,
|
||||
state: query?.state ?? undefined,
|
||||
labels: query?.labels ?? undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (!isIssueFiltersUpdated(anchor, params)) {
|
||||
initIssueFilters(anchor, params);
|
||||
router.push(`/issues/${anchor}?${queryParam}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
anchor,
|
||||
board,
|
||||
labels,
|
||||
state,
|
||||
priority,
|
||||
peekId,
|
||||
activeLayout,
|
||||
router,
|
||||
initIssueFilters,
|
||||
setPeekId,
|
||||
isIssueFiltersUpdated,
|
||||
view_props,
|
||||
workspace_detail,
|
||||
]);
|
||||
|
||||
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">
|
||||
<IssuesLayoutSelection anchor={anchor} />
|
||||
</div>
|
||||
|
||||
{/* issue filters */}
|
||||
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
|
||||
<IssueFiltersDropdown anchor={anchor} />
|
||||
</div>
|
||||
|
||||
{/* theming */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<NavbarTheme />
|
||||
</div>
|
||||
|
||||
{!isInIframe && <UserAvatar />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
1
apps/space/core/components/issues/navbar/index.ts
Normal file
1
apps/space/core/components/issues/navbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
14
apps/space/core/components/issues/navbar/layout-icon.tsx
Normal file
14
apps/space/core/components/issues/navbar/layout-icon.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { LucideProps } from "lucide-react";
|
||||
import { List, Kanban } from "lucide-react";
|
||||
import type { TIssueLayout } from "@plane/constants";
|
||||
|
||||
export const IssueLayoutIcon = ({ layout, ...props }: { layout: TIssueLayout } & LucideProps) => {
|
||||
switch (layout) {
|
||||
case "list":
|
||||
return <List {...props} />;
|
||||
case "kanban":
|
||||
return <Kanban {...props} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
// ui
|
||||
import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
// mobx
|
||||
import type { TIssueLayout } from "@/types/issue";
|
||||
import { IssueLayoutIcon } from "./layout-icon";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
};
|
||||
|
||||
export const IssuesLayoutSelection: FC<Props> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
// query params
|
||||
const labels = searchParams.get("labels");
|
||||
const state = searchParams.get("state");
|
||||
const priority = searchParams.get("priority");
|
||||
const peekId = searchParams.get("peekId");
|
||||
// hooks
|
||||
const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter();
|
||||
// derived values
|
||||
const issueFilters = getIssueFilters(anchor);
|
||||
const activeLayout = issueFilters?.display_filters?.layout || undefined;
|
||||
|
||||
const handleCurrentBoardView = (boardView: TIssueLayout) => {
|
||||
updateIssueFilters(anchor, "display_filters", "layout", boardView);
|
||||
const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels });
|
||||
router.push(`/issues/${anchor}?${queryParam}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
|
||||
{SITES_ISSUE_LAYOUTS.map((layout) => {
|
||||
if (!layoutOptions[layout.key]) return;
|
||||
|
||||
return (
|
||||
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
|
||||
<button
|
||||
type="button"
|
||||
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
|
||||
activeLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||
}`}
|
||||
onClick={() => handleCurrentBoardView(layout.key)}
|
||||
>
|
||||
<IssueLayoutIcon
|
||||
layout={layout.key}
|
||||
className={`size-3.5 ${activeLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
45
apps/space/core/components/issues/navbar/root.tsx
Normal file
45
apps/space/core/components/issues/navbar/root.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ProjectIcon } from "@plane/propel/icons";
|
||||
// components
|
||||
import { ProjectLogo } from "@/components/common/project-logo";
|
||||
// store
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
// local imports
|
||||
import { NavbarControls } from "./controls";
|
||||
|
||||
type Props = {
|
||||
publishSettings: PublishStore;
|
||||
};
|
||||
|
||||
export const IssuesNavbarRoot: FC<Props> = observer((props) => {
|
||||
const { publishSettings } = props;
|
||||
// hooks
|
||||
const { project_details } = publishSettings;
|
||||
|
||||
return (
|
||||
<div className="relative flex justify-between w-full gap-4 px-5">
|
||||
{/* project detail */}
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
{project_details ? (
|
||||
<span className="h-7 w-7 flex-shrink-0 grid place-items-center">
|
||||
<ProjectLogo logo={project_details.logo_props} className="text-lg" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
|
||||
<ProjectIcon className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
|
||||
{project_details?.name || `...`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<NavbarControls publishSettings={publishSettings} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
33
apps/space/core/components/issues/navbar/theme.tsx
Normal file
33
apps/space/core/components/issues/navbar/theme.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
// next theme
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// mobx react lite
|
||||
|
||||
export const NavbarTheme = observer(() => {
|
||||
const [appTheme, setAppTheme] = useState("light");
|
||||
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const handleTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
setAppTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTheme}
|
||||
className="relative grid h-7 w-7 place-items-center rounded bg-custom-background-100 text-custom-text-100 hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="material-symbols-rounded text-sm">{appTheme === "light" ? "dark_mode" : "light_mode"}</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
128
apps/space/core/components/issues/navbar/user-avatar.tsx
Normal file
128
apps/space/core/components/issues/navbar/user-avatar.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { usePopper } from "react-popper";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const UserAvatar: FC = observer(() => {
|
||||
const pathName = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
// query params
|
||||
const board = searchParams.get("board") || undefined;
|
||||
const labels = searchParams.get("labels") || undefined;
|
||||
const state = searchParams.get("state") || undefined;
|
||||
const priority = searchParams.get("priority") || undefined;
|
||||
const peekId = searchParams.get("peekId") || undefined;
|
||||
// hooks
|
||||
const { data: currentUser, signOut } = useUser();
|
||||
// states
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-end",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 40],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
|
||||
return (
|
||||
<div className="relative mr-2">
|
||||
{currentUser?.id ? (
|
||||
<div>
|
||||
<Popover as="div">
|
||||
<Popover.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
className="flex items-center gap-2 rounded border border-custom-border-200 p-2"
|
||||
>
|
||||
<Avatar
|
||||
name={currentUser?.display_name}
|
||||
src={getFileURL(currentUser?.avatar_url)}
|
||||
shape="square"
|
||||
size="sm"
|
||||
showTooltip={false}
|
||||
/>
|
||||
<h6 className="text-xs font-medium">
|
||||
{currentUser?.display_name ||
|
||||
`${currentUser?.first_name} ${currentUser?.first_name}` ||
|
||||
currentUser?.email ||
|
||||
"User"}
|
||||
</h6>
|
||||
</button>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel>
|
||||
<div
|
||||
className="z-10 overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg p-1"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{csrfToken && (
|
||||
<form method="POST" action={`${API_BASE_URL}/auth/spaces/sign-out/`} onSubmit={signOut}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<input type="hidden" name="next_path" value={`${pathName}?${queryParam}`} />
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-2 rounded p-2 whitespace-nowrap hover:bg-custom-background-80 text-sm min-w-36 cursor-pointer"
|
||||
>
|
||||
<LogOut size={12} className="flex-shrink-0 text-red-500" />
|
||||
<div>Sign out</div>
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0">
|
||||
<Link href={`/?next_path=${pathName}?${queryParam}`}>
|
||||
<Button variant="outline-primary">Sign in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user