forked from github/plane
feat: made emoji-icon-picker
fix: google prompt coming up after leaving sign in, refractor: saving views data to db instead of local-storage
This commit is contained in:
parent
f52724fd86
commit
13985df860
@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
// swr
|
// swr
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
// headless
|
// headless
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
@ -18,7 +18,7 @@ import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
|||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TextArea, Select } from "ui";
|
import { Button, Input, TextArea, Select, EmojiIconPicker } from "ui";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
|
||||||
@ -32,6 +32,7 @@ const defaultValues: Partial<IProject> = {
|
|||||||
identifier: "",
|
identifier: "",
|
||||||
description: "",
|
description: "",
|
||||||
network: 0,
|
network: 0,
|
||||||
|
icon: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const IsGuestCondition: React.FC<{
|
const IsGuestCondition: React.FC<{
|
||||||
@ -83,6 +84,7 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
|
control,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm<IProject>({
|
} = useForm<IProject>({
|
||||||
@ -201,6 +203,22 @@ const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="icon" className="text-gray-500 mb-2">
|
||||||
|
Icon
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="icon"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<EmojiIconPicker
|
||||||
|
label={value ? String.fromCodePoint(parseInt(value)) : "Select Icon"}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
// react
|
// react
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { UseFormRegister, UseFormSetError } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
import type { Control, UseFormRegister, UseFormSetError } from "react-hook-form";
|
||||||
// services
|
// services
|
||||||
import projectServices from "lib/services/project.service";
|
import projectServices from "lib/services/project.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, Select, TextArea } from "ui";
|
import { Button, Input, Select, TextArea, EmojiIconPicker } from "ui";
|
||||||
// types
|
// types
|
||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
// constants
|
// constants
|
||||||
@ -18,11 +19,18 @@ type Props = {
|
|||||||
errors: any;
|
errors: any;
|
||||||
setError: UseFormSetError<IProject>;
|
setError: UseFormSetError<IProject>;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
control: Control<IProject, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmitting }) => {
|
const GeneralSettings: React.FC<Props> = ({
|
||||||
|
register,
|
||||||
|
errors,
|
||||||
|
setError,
|
||||||
|
isSubmitting,
|
||||||
|
control,
|
||||||
|
}) => {
|
||||||
const { activeWorkspace } = useUser();
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
const checkIdentifier = (slug: string, value: string) => {
|
const checkIdentifier = (slug: string, value: string) => {
|
||||||
@ -44,8 +52,26 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmit
|
|||||||
This information will be displayed to every member of the project.
|
This information will be displayed to every member of the project.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-10 gap-3 items-center">
|
||||||
<div className="col-span-2">
|
<div className="col-span-1">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="icon" className="text-gray-500 mb-2">
|
||||||
|
Icon
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="icon"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<EmojiIconPicker
|
||||||
|
label={value ? String.fromCodePoint(parseInt(value)) : "Select Icon"}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-5">
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
@ -58,7 +84,7 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmit
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="col-span-2">
|
||||||
<Select
|
<Select
|
||||||
name="network"
|
name="network"
|
||||||
id="network"
|
id="network"
|
||||||
@ -73,7 +99,7 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmit
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="col-span-2">
|
||||||
<Input
|
<Input
|
||||||
id="identifier"
|
id="identifier"
|
||||||
name="identifier"
|
name="identifier"
|
||||||
|
@ -62,9 +62,16 @@ const ProjectsList: React.FC<Props> = ({ navigation, sidebarCollapse }) => {
|
|||||||
sidebarCollapse ? "justify-center" : ""
|
sidebarCollapse ? "justify-center" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{project.icon ? (
|
||||||
|
<span className="text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
|
||||||
|
{String.fromCodePoint(parseInt(project.icon))}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span className="bg-gray-700 text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
|
<span className="bg-gray-700 text-white rounded h-7 w-7 grid place-items-center uppercase flex-shrink-0">
|
||||||
{project?.name.charAt(0)}
|
{project?.name.charAt(0)}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<span className="flex items-center justify-between w-full">
|
<span className="flex items-center justify-between w-full">
|
||||||
{project?.name}
|
{project?.name}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, CSSProperties, useEffect, useRef, useCallback } from "react";
|
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
@ -11,9 +11,10 @@ export interface IGoogleLoginButton {
|
|||||||
|
|
||||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
||||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||||
|
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||||
|
|
||||||
const loadScript = useCallback(() => {
|
const loadScript = useCallback(() => {
|
||||||
if (!googleSignInButton.current) return;
|
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||||
window?.google?.accounts.id.initialize({
|
window?.google?.accounts.id.initialize({
|
||||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||||
callback: props.onSuccess as any,
|
callback: props.onSuccess as any,
|
||||||
@ -30,12 +31,16 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
} as GsiButtonConfiguration // customization attributes
|
} as GsiButtonConfiguration // customization attributes
|
||||||
);
|
);
|
||||||
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
||||||
}, [props.onSuccess]);
|
setGsiScriptLoaded(true);
|
||||||
|
}, [props.onSuccess, gsiScriptLoaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window?.google?.accounts?.id) {
|
if (window?.google?.accounts?.id) {
|
||||||
loadScript();
|
loadScript();
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
window?.google?.accounts.id.cancel();
|
||||||
|
};
|
||||||
}, [loadScript]);
|
}, [loadScript]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -64,6 +64,8 @@ export const PROJECT_MEMBERS = (workspaceSlug: string, projectId: string) =>
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`;
|
||||||
export const PROJECT_MEMBER_DETAIL = (workspaceSlug: string, projectId: string, memberId: string) =>
|
export const PROJECT_MEMBER_DETAIL = (workspaceSlug: string, projectId: string, memberId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`;
|
||||||
|
export const PROJECT_MEMBER_ME = (workspaceSlug: string, projectId: string) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`;
|
||||||
export const PROJECT_VIEW_ENDPOINT = (workspaceSlug: string, projectId: string) =>
|
export const PROJECT_VIEW_ENDPOINT = (workspaceSlug: string, projectId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-views/`;
|
||||||
|
|
||||||
|
@ -68,13 +68,9 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
collapsed: !state.collapsed,
|
collapsed: !state.collapsed,
|
||||||
};
|
};
|
||||||
localStorage.setItem("theme", JSON.stringify(newState));
|
|
||||||
return newState;
|
return newState;
|
||||||
case REHYDRATE_THEME: {
|
case REHYDRATE_THEME: {
|
||||||
let newState: any = localStorage.getItem("theme");
|
const newState = payload;
|
||||||
if (newState !== null) {
|
|
||||||
newState = JSON.parse(newState);
|
|
||||||
}
|
|
||||||
return { ...initialState, ...newState };
|
return { ...initialState, ...newState };
|
||||||
}
|
}
|
||||||
case SET_ISSUE_VIEW: {
|
case SET_ISSUE_VIEW: {
|
||||||
@ -82,7 +78,6 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
issueView: payload?.issueView || "list",
|
issueView: payload?.issueView || "list",
|
||||||
};
|
};
|
||||||
localStorage.setItem("theme", JSON.stringify(newState));
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -93,7 +88,6 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
groupByProperty: payload?.groupByProperty || null,
|
groupByProperty: payload?.groupByProperty || null,
|
||||||
};
|
};
|
||||||
localStorage.setItem("theme", JSON.stringify(newState));
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -104,7 +98,6 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
orderBy: payload?.orderBy || null,
|
orderBy: payload?.orderBy || null,
|
||||||
};
|
};
|
||||||
localStorage.setItem("theme", JSON.stringify(newState));
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -115,7 +108,6 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
...state,
|
...state,
|
||||||
filterIssue: payload?.filterIssue || null,
|
filterIssue: payload?.filterIssue || null,
|
||||||
};
|
};
|
||||||
localStorage.setItem("theme", JSON.stringify(newState));
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...newState,
|
...newState,
|
||||||
@ -127,6 +119,10 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveDataToServer = async (workspaceSlug: string, projectID: string, state: any) => {
|
||||||
|
await projectService.setProjectView(workspaceSlug, projectID, state);
|
||||||
|
};
|
||||||
|
|
||||||
export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
@ -145,16 +141,6 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const saveDataToServer = useCallback(() => {
|
|
||||||
if (!activeProject || !activeWorkspace) return;
|
|
||||||
projectService
|
|
||||||
.setProjectView(activeWorkspace.slug, activeProject.id, state)
|
|
||||||
.then((res) => {
|
|
||||||
console.log("saved", res);
|
|
||||||
})
|
|
||||||
.catch((error) => {});
|
|
||||||
}, [activeProject, activeWorkspace, state]);
|
|
||||||
|
|
||||||
const setIssueView = useCallback(
|
const setIssueView = useCallback(
|
||||||
(display: "list" | "kanban") => {
|
(display: "list" | "kanban") => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -163,9 +149,14 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
issueView: display,
|
issueView: display,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
saveDataToServer();
|
|
||||||
|
if (!activeWorkspace || !activeProject) return;
|
||||||
|
saveDataToServer(activeWorkspace.slug, activeProject.id, {
|
||||||
|
...state,
|
||||||
|
issueView: display,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[saveDataToServer]
|
[activeProject, activeWorkspace, state]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setGroupByProperty = useCallback(
|
const setGroupByProperty = useCallback(
|
||||||
@ -176,9 +167,14 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
groupByProperty: property,
|
groupByProperty: property,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
saveDataToServer();
|
|
||||||
|
if (!activeWorkspace || !activeProject) return;
|
||||||
|
saveDataToServer(activeWorkspace.slug, activeProject.id, {
|
||||||
|
...state,
|
||||||
|
groupByProperty: property,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[saveDataToServer]
|
[activeProject, activeWorkspace, state]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setOrderBy = useCallback(
|
const setOrderBy = useCallback(
|
||||||
@ -189,11 +185,12 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
orderBy: property,
|
orderBy: property,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
saveDataToServer();
|
|
||||||
},
|
|
||||||
[saveDataToServer]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (!activeWorkspace || !activeProject) return;
|
||||||
|
saveDataToServer(activeWorkspace.slug, activeProject.id, state);
|
||||||
|
},
|
||||||
|
[activeProject, activeWorkspace, state]
|
||||||
|
);
|
||||||
const setFilterIssue = useCallback(
|
const setFilterIssue = useCallback(
|
||||||
(property: "activeIssue" | "backlogIssue" | null) => {
|
(property: "activeIssue" | "backlogIssue" | null) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -202,9 +199,14 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
filterIssue: property,
|
filterIssue: property,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
saveDataToServer();
|
|
||||||
|
if (!activeWorkspace || !activeProject) return;
|
||||||
|
saveDataToServer(activeWorkspace.slug, activeProject.id, {
|
||||||
|
...state,
|
||||||
|
filterIssue: property,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[saveDataToServer]
|
[activeProject, activeWorkspace, state]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// hooks
|
|
||||||
import useTheme from "./useTheme";
|
import useTheme from "./useTheme";
|
||||||
import useUser from "./useUser";
|
import useUser from "./useUser";
|
||||||
// commons
|
// commons
|
||||||
|
@ -18,6 +18,7 @@ const initialValues: Properties = {
|
|||||||
start_date: false,
|
start_date: false,
|
||||||
target_date: false,
|
target_date: false,
|
||||||
cycle: false,
|
cycle: false,
|
||||||
|
children_count: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
||||||
|
@ -18,6 +18,7 @@ const initialValues: Properties = {
|
|||||||
start_date: false,
|
start_date: false,
|
||||||
target_date: false,
|
target_date: false,
|
||||||
cycle: false,
|
cycle: false,
|
||||||
|
children_count: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const useMyIssuesProperties = (issues?: IIssue[]) => {
|
const useMyIssuesProperties = (issues?: IIssue[]) => {
|
||||||
|
19
apps/app/lib/hooks/useOutsideClickDetector.tsx
Normal file
19
apps/app/lib/hooks/useOutsideClickDetector.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener("click", handleClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClick);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOutsideClickDetector;
|
@ -11,6 +11,7 @@ import {
|
|||||||
PROJECT_MEMBER_DETAIL,
|
PROJECT_MEMBER_DETAIL,
|
||||||
USER_PROJECT_INVITATIONS,
|
USER_PROJECT_INVITATIONS,
|
||||||
PROJECT_VIEW_ENDPOINT,
|
PROJECT_VIEW_ENDPOINT,
|
||||||
|
PROJECT_MEMBER_ME,
|
||||||
} from "constants/api-routes";
|
} from "constants/api-routes";
|
||||||
// services
|
// services
|
||||||
import APIService from "lib/services/api.service";
|
import APIService from "lib/services/api.service";
|
||||||
@ -132,6 +133,16 @@ class ProjectServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async projectMemberMe(workspacSlug: string, projectId: string): Promise<IProjectMember> {
|
||||||
|
return this.get(PROJECT_MEMBER_ME(workspacSlug, projectId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getProjectMember(
|
async getProjectMember(
|
||||||
workspacSlug: string,
|
workspacSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -142,7 +142,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
setFilterIssue,
|
setFilterIssue,
|
||||||
orderBy,
|
orderBy,
|
||||||
filterIssue,
|
filterIssue,
|
||||||
} = useIssuesFilter(projectIssues?.results ?? []);
|
} = useIssuesFilter(projectIssues?.results.filter((p) => p.parent === null) ?? []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
@ -100,6 +100,7 @@ const ProjectSettings: NextPage = () => {
|
|||||||
description: formData.description,
|
description: formData.description,
|
||||||
default_assignee: formData.default_assignee,
|
default_assignee: formData.default_assignee,
|
||||||
project_lead: formData.project_lead,
|
project_lead: formData.project_lead,
|
||||||
|
icon: formData.icon,
|
||||||
};
|
};
|
||||||
await projectServices
|
await projectServices
|
||||||
.updateProject(activeWorkspace.slug, projectId as string, payload)
|
.updateProject(activeWorkspace.slug, projectId as string, payload)
|
||||||
@ -186,6 +187,7 @@ const ProjectSettings: NextPage = () => {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<GeneralSettings
|
<GeneralSettings
|
||||||
|
control={control}
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
setError={setError}
|
setError={setError}
|
||||||
|
1
apps/app/types/issues.d.ts
vendored
1
apps/app/types/issues.d.ts
vendored
@ -133,6 +133,7 @@ export type Properties = {
|
|||||||
start_date: boolean;
|
start_date: boolean;
|
||||||
target_date: boolean;
|
target_date: boolean;
|
||||||
cycle: boolean;
|
cycle: boolean;
|
||||||
|
children_count: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IIssueLabels {
|
export interface IIssueLabels {
|
||||||
|
1
apps/app/types/projects.d.ts
vendored
1
apps/app/types/projects.d.ts
vendored
@ -14,6 +14,7 @@ export interface IProject {
|
|||||||
slug: string;
|
slug: string;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectViewTheme = {
|
type ProjectViewTheme = {
|
||||||
|
1090
apps/app/ui/emoji-icon-picker/emojis.json
Normal file
1090
apps/app/ui/emoji-icon-picker/emojis.json
Normal file
File diff suppressed because it is too large
Load Diff
26
apps/app/ui/emoji-icon-picker/helpers.ts
Normal file
26
apps/app/ui/emoji-icon-picker/helpers.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export const saveRecentEmoji = (emoji: string) => {
|
||||||
|
const recentEmojis = localStorage.getItem("recentEmojis");
|
||||||
|
if (recentEmojis) {
|
||||||
|
const recentEmojisArray = recentEmojis.split(",");
|
||||||
|
if (recentEmojisArray.includes(emoji)) {
|
||||||
|
const index = recentEmojisArray.indexOf(emoji);
|
||||||
|
recentEmojisArray.splice(index, 1);
|
||||||
|
}
|
||||||
|
recentEmojisArray.unshift(emoji);
|
||||||
|
if (recentEmojisArray.length > 18) {
|
||||||
|
recentEmojisArray.pop();
|
||||||
|
}
|
||||||
|
localStorage.setItem("recentEmojis", recentEmojisArray.join(","));
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("recentEmojis", emoji);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecentEmojis = () => {
|
||||||
|
const recentEmojis = localStorage.getItem("recentEmojis");
|
||||||
|
if (recentEmojis) {
|
||||||
|
const recentEmojisArray = recentEmojis.split(",");
|
||||||
|
return recentEmojisArray;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
125
apps/app/ui/emoji-icon-picker/index.tsx
Normal file
125
apps/app/ui/emoji-icon-picker/index.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
// headless ui
|
||||||
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "lib/hooks/useOutsideClickDetector";
|
||||||
|
// emoji
|
||||||
|
import emojis from "./emojis.json";
|
||||||
|
// helpers
|
||||||
|
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
|
||||||
|
// types
|
||||||
|
import { Props } from "./types";
|
||||||
|
|
||||||
|
const tabOptions = [
|
||||||
|
{
|
||||||
|
key: "emoji",
|
||||||
|
title: "Emoji",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "icon",
|
||||||
|
title: "Icon",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRecentEmojis(getRecentEmojis());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useOutsideClickDetector(ref, () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover className="relative" ref={ref}>
|
||||||
|
<Popover.Button
|
||||||
|
className="bg-gray-100 px-3 py-1 rounded-full outline-none"
|
||||||
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Popover.Button>
|
||||||
|
<Transition
|
||||||
|
show={isOpen}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute z-10 w-80 bg-white rounded-md shadow-lg mt-2">
|
||||||
|
<div className="w-80 h-80 p-2 bg-white overflow-auto rounded shadow-2xl border">
|
||||||
|
<Tab.Group as="div" className="w-full h-full flex flex-col">
|
||||||
|
<Tab.List className="-mx-2 flex justify-around p-1 gap-1 border-b rounded flex-0">
|
||||||
|
{tabOptions.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.key}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`w-1/2 py-2 transition-colors border-b text-sm font-medium text-center outline-none -my-1 ${
|
||||||
|
selected ? "border-theme" : "border-transparent"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels className="w-full h-full flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
|
<Tab.Panel className="w-full h-full">
|
||||||
|
{recentEmojis.length > 0 && (
|
||||||
|
<div className="py-2 w-full">
|
||||||
|
<h3 className="text-lg mb-2">Recent Emojis</h3>
|
||||||
|
<div className="grid grid-cols-9 gap-2">
|
||||||
|
{recentEmojis.map((emoji) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="select-none text-xl"
|
||||||
|
key={emoji}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(emoji);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{String.fromCodePoint(parseInt(emoji))}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="py-3">
|
||||||
|
<h3 className="text-lg mb-2">All Emojis</h3>
|
||||||
|
<div className="grid grid-cols-9 gap-2">
|
||||||
|
{emojis.map((emoji) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="select-none text-xl"
|
||||||
|
key={emoji}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(emoji);
|
||||||
|
saveRecentEmoji(emoji);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{String.fromCodePoint(parseInt(emoji))}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="w-full h-full flex flex-col justify-center items-center">
|
||||||
|
<p>Coming Soon...</p>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiIconPicker;
|
5
apps/app/ui/emoji-icon-picker/types.d.ts
vendored
Normal file
5
apps/app/ui/emoji-icon-picker/types.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type Props = {
|
||||||
|
label: string | React.ReactNode;
|
||||||
|
value: any;
|
||||||
|
onChange: (data: any) => void;
|
||||||
|
};
|
@ -10,3 +10,4 @@ export { default as SearchListbox } from "./search-listbox";
|
|||||||
export { default as HeaderButton } from "./HeaderButton";
|
export { default as HeaderButton } from "./HeaderButton";
|
||||||
export * from "./Breadcrumbs";
|
export * from "./Breadcrumbs";
|
||||||
export * from "./EmptySpace";
|
export * from "./EmptySpace";
|
||||||
|
export { default as EmojiIconPicker } from "./emoji-icon-picker";
|
||||||
|
Loading…
Reference in New Issue
Block a user