Merge pull request #857 from makeplane/develop

promote: develop to stage-release
This commit is contained in:
Vamsi Kurama 2023-04-17 17:47:19 +05:30 committed by GitHub
commit 718f62a898
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 432 additions and 246 deletions

View File

@ -25,22 +25,18 @@ class IssueViewSerializer(BaseSerializer):
def create(self, validated_data): def create(self, validated_data):
query_params = validated_data.get("query_data", {}) query_params = validated_data.get("query_data", {})
if bool(query_params):
if not bool(query_params): validated_data["query"] = issue_filters(query_params, "POST")
raise serializers.ValidationError( else:
{"query_data": ["Query data field cannot be empty"]} validated_data["query"] = dict()
)
validated_data["query"] = issue_filters(query_params, "POST")
return IssueView.objects.create(**validated_data) return IssueView.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {}) query_params = validated_data.get("query_data", {})
if not bool(query_params): if bool(query_params):
raise serializers.ValidationError( validated_data["query"] = issue_filters(query_params, "POST")
{"query_data": ["Query data field cannot be empty"]} else:
) validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH") validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@ -246,6 +246,20 @@ class UserWorkSpaceIssues(BaseAPIView):
.prefetch_related("assignees") .prefetch_related("assignees")
.prefetch_related("labels") .prefetch_related("labels")
.order_by("-created_at") .order_by("-created_at")
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
) )
serializer = IssueLiteSerializer(issues, many=True) serializer = IssueLiteSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -5,15 +5,16 @@ import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// services
import projectService from "services/project.service";
// ui // ui
import { PrimaryButton } from "components/ui"; import { PrimaryButton } from "components/ui";
// icon // icons
import { AssignmentClipboardIcon } from "components/icons"; import { AssignmentClipboardIcon } from "components/icons";
// img // images
import JoinProjectImg from "public/auth/project-not-authorized.svg"; import JoinProjectImg from "public/auth/project-not-authorized.svg";
import projectService from "services/project.service";
// fetch-keys // fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { USER_PROJECT_VIEW } from "constants/fetch-keys";
export const JoinProject: React.FC = () => { export const JoinProject: React.FC = () => {
const [isJoiningProject, setIsJoiningProject] = useState(false); const [isJoiningProject, setIsJoiningProject] = useState(false);
@ -22,13 +23,16 @@ export const JoinProject: React.FC = () => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const handleJoin = () => { const handleJoin = () => {
if (!workspaceSlug || !projectId) return;
setIsJoiningProject(true); setIsJoiningProject(true);
projectService projectService
.joinProject(workspaceSlug as string, { .joinProject(workspaceSlug as string, {
project_ids: [projectId as string], project_ids: [projectId as string],
}) })
.then(() => { .then(async () => {
mutate(PROJECT_MEMBERS(projectId as string)); await mutate(USER_PROJECT_VIEW(workspaceSlug.toString()));
setIsJoiningProject(false);
}) })
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);

View File

@ -19,7 +19,12 @@ import { LayerDiagonalIcon } from "components/icons";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// fetch-keys // fetch-keys
import { CYCLE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; import {
CYCLE_DETAILS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS,
} from "constants/fetch-keys";
type FormInput = { type FormInput = {
issues: string[]; issues: string[];
@ -76,8 +81,14 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
} }
await handleOnSubmit(data); await handleOnSubmit(data);
if (cycleId) mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); if (cycleId) {
if (moduleId) mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
mutate(CYCLE_DETAILS(cycleId as string));
}
if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
}
handleClose(); handleClose();

View File

@ -73,6 +73,10 @@ const activityDetails: {
message: "updated the description.", message: "updated the description.",
icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <ChatBubbleBottomCenterTextIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
}, },
estimate_point: {
message: "set the estimate point to",
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
},
target_date: { target_date: {
message: "set the due date to", message: "set the due date to",
icon: <CalendarDaysIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />, icon: <CalendarDaysIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />,
@ -136,8 +140,6 @@ export const Feeds: React.FC<any> = ({ activities }) => (
action = `${activity.verb} the`; action = `${activity.verb} the`;
} else if (activity.field === "link") { } else if (activity.field === "link") {
action = `${activity.verb} the`; action = `${activity.verb} the`;
} else if (activity.field === "estimate") {
action = "updated the";
} }
// for values that are after the action clause // for values that are after the action clause
let value: any = activity.new_value ? activity.new_value : activity.old_value; let value: any = activity.new_value ? activity.new_value : activity.old_value;
@ -188,8 +190,10 @@ export const Feeds: React.FC<any> = ({ activities }) => (
value = "attachment"; value = "attachment";
} else if (activity.field === "link") { } else if (activity.field === "link") {
value = "link"; value = "link";
} else if (activity.field === "estimate") { } else if (activity.field === "estimate_point") {
value = "estimate"; value = activity.new_value
? activity.new_value + ` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}`
: "None";
} }
if (activity.field === "comment") { if (activity.field === "comment") {

View File

@ -309,7 +309,19 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
)} )}
</div> </div>
) : ( ) : (
<span className="capitalize">{filters[key as keyof typeof filters]}</span> <div className="flex items-center gap-x-1 capitalize">
{filters[key as keyof typeof filters]}
<button
type="button"
onClick={() =>
setFilters({
[key]: null,
})
}
>
<XMarkIcon className="h-3 w-3" />
</button>
</div>
)} )}
</div> </div>
); );
@ -319,6 +331,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
type="button" type="button"
onClick={() => onClick={() =>
setFilters({ setFilters({
type: null,
state: null, state: null,
priority: null, priority: null,
assignees: null, assignees: null,

View File

@ -234,7 +234,7 @@ export const IssuesView: React.FC<Props> = ({
) )
trackEventServices.trackIssueMarkedAsDoneEvent({ trackEventServices.trackIssueMarkedAsDoneEvent({
workspaceSlug, workspaceSlug,
workspaceId: draggedItem.workspace_detail.id, workspaceId: draggedItem.workspace,
projectName: draggedItem.project_detail.name, projectName: draggedItem.project_detail.name,
projectIdentifier: draggedItem.project_detail.identifier, projectIdentifier: draggedItem.project_detail.identifier,
projectId, projectId,
@ -359,12 +359,15 @@ export const IssuesView: React.FC<Props> = ({
(key) => filters[key as keyof IIssueFilterOptions] === null (key) => filters[key as keyof IIssueFilterOptions] === null
); );
const areFiltersApplied =
Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
return ( return (
<> <>
<CreateUpdateViewModal <CreateUpdateViewModal
isOpen={createViewModal !== null} isOpen={createViewModal !== null}
handleClose={() => setCreateViewModal(null)} handleClose={() => setCreateViewModal(null)}
data={createViewModal} preLoadedData={createViewModal}
/> />
<CreateUpdateIssueModal <CreateUpdateIssueModal
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"} isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
@ -388,11 +391,15 @@ export const IssuesView: React.FC<Props> = ({
handleClose={() => setTransferIssuesModal(false)} handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal} isOpen={transferIssuesModal}
/> />
<div> {issueView !== "calendar" && (
<div className="flex items-center justify-between gap-2"> <>
<FilterList filters={filters} setFilters={setFilters} /> <div
{Object.keys(filters).length > 0 && className={`flex items-center justify-between gap-2 ${
nullFilters.length !== Object.keys(filters).length && ( issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2"
}`}
>
<FilterList filters={filters} setFilters={setFilters} />
{areFiltersApplied && (
<PrimaryButton <PrimaryButton
onClick={() => { onClick={() => {
if (viewId) { if (viewId) {
@ -413,20 +420,19 @@ export const IssuesView: React.FC<Props> = ({
{viewId ? "Update" : "Save"} view {viewId ? "Update" : "Save"} view
</PrimaryButton> </PrimaryButton>
)} )}
</div> </div>
</div> {areFiltersApplied && (
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && ( )}
<div className="mb-5 border-t" /> </>
)} )}
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<StrictModeDroppable droppableId="trashBox"> <StrictModeDroppable droppableId="trashBox">
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
className={`${ className={`${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0" trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-9 right-9 z-20 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${ } fixed top-9 right-9 z-30 flex h-28 w-96 flex-col items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 text-white" : "" snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
} duration-200`} } duration-200`}
ref={provided.innerRef} ref={provided.innerRef}
@ -434,7 +440,6 @@ export const IssuesView: React.FC<Props> = ({
> >
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
Drop issue here to delete Drop issue here to delete
{provided.placeholder}
</div> </div>
)} )}
</StrictModeDroppable> </StrictModeDroppable>

View File

@ -36,7 +36,7 @@ export const AllLists: React.FC<Props> = ({
return ( return (
<> <>
{groupedByIssues && ( {groupedByIssues && (
<div className="flex flex-col space-y-5 gap-4 bg-white"> <div className="flex flex-col space-y-5 bg-white">
{Object.keys(groupedByIssues).map((singleGroup) => { {Object.keys(groupedByIssues).map((singleGroup) => {
const currentState = const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null; selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;

View File

@ -216,9 +216,9 @@ export const SingleListIssue: React.FC<Props> = ({
</ContextMenu.Item> </ContextMenu.Item>
</a> </a>
</ContextMenu> </ContextMenu>
<div className="border-b mx-4 border-gray-300 last:border-b-0"> <div className="border-b mx-6 border-gray-300 last:border-b-0">
<div <div
className="flex items-center justify-between gap-2 px-4 py-3" className="flex items-center justify-between gap-2 py-3"
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
setContextMenu(true); setContextMenu(true);

View File

@ -152,14 +152,16 @@ export const SingleEstimate: React.FC<Props> = ({
</CustomMenu> </CustomMenu>
</div> </div>
{estimatePoints && estimatePoints.length > 0 ? ( {estimatePoints && estimatePoints.length > 0 ? (
<div className="flex gap-2 text-sm text-gray-400"> <div className="flex text-sm text-gray-400">
Estimate points( Estimate points (
{estimatePoints.map((point, index) => ( <span className="flex gap-1">
<h6 key={point.id}> {estimatePoints.map((point, index) => (
{point.value} <h6 key={point.id}>
{index !== estimatePoints.length - 1 && ","}{" "} {point.value}
</h6> {index !== estimatePoints.length - 1 && ","}{" "}
))} </h6>
))}
</span>
) )
</div> </div>
) : ( ) : (

View File

@ -1,7 +1,5 @@
import { FC } from "react"; import { FC } from "react";
import { useRouter } from "next/router";
// react-hook-form // react-hook-form
import { UseFormWatch } from "react-hook-form"; import { UseFormWatch } from "react-hook-form";
// ui // ui
@ -22,8 +20,6 @@ type Props = {
}; };
export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers, watch }) => { export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers, watch }) => {
const router = useRouter();
const isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0; const isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0;
return ( return (
@ -44,7 +40,6 @@ export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers
index={index} index={index}
users={users} users={users}
setUsers={setUsers} setUsers={setUsers}
project={watch("project")}
/> />
))} ))}
</div> </div>

View File

@ -4,21 +4,20 @@ import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// services // services
import projectService from "services/project.service"; import workspaceService from "services/workspace.service";
// ui // ui
import { Avatar, CustomSearchSelect, CustomSelect, Input } from "components/ui"; import { Avatar, CustomSearchSelect, CustomSelect, Input } from "components/ui";
// types // types
import { IGithubRepoCollaborator } from "types"; import { IGithubRepoCollaborator } from "types";
import { IUserDetails } from "./root"; import { IUserDetails } from "./root";
// fetch-keys // fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys"; import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
collaborator: IGithubRepoCollaborator; collaborator: IGithubRepoCollaborator;
index: number; index: number;
users: IUserDetails[]; users: IUserDetails[];
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>; setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
project: string | null;
}; };
const importOptions = [ const importOptions = [
@ -36,21 +35,13 @@ const importOptions = [
}, },
]; ];
export const SingleUserSelect: React.FC<Props> = ({ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users, setUsers }) => {
collaborator,
index,
users,
setUsers,
project,
}) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && project ? PROJECT_MEMBERS(project) : null, workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null,
workspaceSlug && project workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug.toString()) : null
? () => projectService.projectMembers(workspaceSlug as string, project)
: null
); );
const options = const options =

View File

@ -29,6 +29,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import { IIssueComment, IIssueLabels } from "types"; import { IIssueComment, IIssueLabels } from "types";
import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import useEstimateOption from "hooks/use-estimate-option";
const activityDetails: { const activityDetails: {
[key: string]: { [key: string]: {
@ -56,6 +57,10 @@ const activityDetails: {
message: "set the cycle to", message: "set the cycle to",
icon: <CyclesIcon height="12" width="12" color="#6b7280" />, icon: <CyclesIcon height="12" width="12" color="#6b7280" />,
}, },
estimate_point: {
message: "set the estimate point to",
icon: <PlayIcon className="h-3 w-3 text-gray-500 -rotate-90" aria-hidden="true" />,
},
labels: { labels: {
icon: <TagIcon height="12" width="12" color="#6b7280" />, icon: <TagIcon height="12" width="12" color="#6b7280" />,
}, },
@ -107,6 +112,8 @@ export const IssueActivitySection: React.FC<Props> = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
const { isEstimateActive, estimatePoints } = useEstimateOption();
const { data: issueActivities, mutate: mutateIssueActivities } = useSWR( const { data: issueActivities, mutate: mutateIssueActivities } = useSWR(
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId as string) : null, workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId as string) : null,
workspaceSlug && projectId && issueId workspaceSlug && projectId && issueId
@ -278,8 +285,14 @@ export const IssueActivitySection: React.FC<Props> = () => {
value = "attachment"; value = "attachment";
} else if (activityItem.field === "link") { } else if (activityItem.field === "link") {
value = "link"; value = "link";
} else if (activityItem.field === "estimate") { } else if (activityItem.field === "estimate_point") {
value = "estimate"; value = activityItem.new_value
? isEstimateActive
? estimatePoints.find((e) => e.key === parseInt(activityItem.new_value ?? "", 10))
?.value
: activityItem.new_value +
` Point${parseInt(activityItem.new_value ?? "", 10) > 1 ? "s" : ""}`
: "None";
} }
if ("field" in activityItem && activityItem.field !== "updated_by") { if ("field" in activityItem && activityItem.field !== "updated_by") {

View File

@ -46,9 +46,9 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
const defaultValues: Partial<IIssue> = { const defaultValues: Partial<IIssue> = {
project: "", project: "",
name: "", name: "",
description: "", description: { type: "doc", content: [] },
description_html: "<p></p>", description_html: "<p></p>",
estimate_point: 0, estimate_point: null,
state: "", state: "",
cycle: null, cycle: null,
priority: null, priority: null,
@ -137,7 +137,7 @@ export const IssueForm: FC<IssueFormProps> = ({
setValue("description_html", `${watch("description_html")}<p>${response}</p>`); setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
}; };
const handelAutoGenerateDescription = async () => { const handleAutoGenerateDescription = async () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
setIAmFeelingLucky(true); setIAmFeelingLucky(true);
@ -301,7 +301,7 @@ export const IssueForm: FC<IssueFormProps> = ({
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${ className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100 ${
iAmFeelingLucky ? "cursor-wait" : "" iAmFeelingLucky ? "cursor-wait" : ""
}`} }`}
onClick={handelAutoGenerateDescription} onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky} disabled={iAmFeelingLucky}
> >
{iAmFeelingLucky ? ( {iAmFeelingLucky ? (

View File

@ -29,6 +29,8 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
CYCLE_ISSUES_WITH_PARAMS, CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
CYCLE_DETAILS,
MODULE_DETAILS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
export interface IssuesModalProps { export interface IssuesModalProps {
@ -101,7 +103,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
issues: [issueId], issues: [issueId],
}) })
.then(() => { .then(() => {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params)); if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
mutate(CYCLE_DETAILS(cycleId as string));
}
}); });
}; };
@ -113,7 +118,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
issues: [issueId], issues: [issueId],
}) })
.then(() => { .then(() => {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
}
}); });
}; };

View File

@ -5,6 +5,8 @@ import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// hooks
import useToast from "hooks/use-toast";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// components // components
@ -13,16 +15,17 @@ import {
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues/view-select"; } from "components/issues/view-select";
// icon
import { LinkIcon, PaperClipIcon } from "@heroicons/react/24/outline";
// ui // ui
import { AssigneesList } from "components/ui/avatar"; import { AssigneesList } from "components/ui/avatar";
import { CustomMenu, Tooltip } from "components/ui"; import { CustomMenu, Tooltip } from "components/ui";
// types // types
import { IIssue, Properties } from "types"; import { IIssue, Properties } from "types";
// helper
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// fetch-keys // fetch-keys
import { USER_ISSUE } from "constants/fetch-keys"; import { USER_ISSUE } from "constants/fetch-keys";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import useToast from "hooks/use-toast";
import { LinkIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
@ -79,8 +82,8 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
const isNotAllowed = false; const isNotAllowed = false;
return ( return (
<div className="border-b border-gray-300 last:border-b-0"> <div className="border-b border-gray-300 last:border-b-0 mx-6">
<div key={issue.id} className="flex items-center justify-between gap-2 px-4 py-3"> <div key={issue.id} className="flex items-center justify-between gap-2 py-3">
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2"> <a className="group relative flex items-center gap-2">
{properties?.key && ( {properties?.key && (
@ -167,6 +170,26 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</div> </div>
</Tooltip> </Tooltip>
)} )}
{properties.link && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-gray-500">
<LinkIcon className="h-3.5 w-3.5 text-gray-500" />
{issue.link_count}
</div>
</Tooltip>
</div>
)}
{properties.attachment_count && (
<div className="flex items-center rounded-md shadow-sm px-2.5 py-1 cursor-default text-xs border border-gray-200">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-gray-500">
<PaperClipIcon className="h-3.5 w-3.5 text-gray-500 -rotate-45" />
{issue.attachment_count}
</div>
</Tooltip>
</div>
)}
<CustomMenu width="auto" ellipsis> <CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={handleCopyText}> <CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">

View File

@ -8,8 +8,8 @@ import { PlayIcon } from "@heroicons/react/24/outline";
import useEstimateOption from "hooks/use-estimate-option"; import useEstimateOption from "hooks/use-estimate-option";
type Props = { type Props = {
value: number; value: number | null;
onChange: (value: number) => void; onChange: (value: number | null) => void;
}; };
export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => { export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
@ -24,18 +24,26 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<PlayIcon className="h-4 w-4 text-gray-500 -rotate-90" /> <PlayIcon className="h-4 w-4 text-gray-500 -rotate-90" />
<span className={`${value ? "text-gray-600" : "text-gray-500"}`}> <span className={`${value ? "text-gray-600" : "text-gray-500"}`}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate points"} {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
</span> </span>
</div> </div>
} }
onChange={onChange} onChange={onChange}
position="right" position="right"
width="w-full min-w-[6rem]" width="w-full min-w-[8rem]"
noChevron noChevron
> >
<CustomSelect.Option value={null}>
<>
<span>
<PlayIcon className="h-4 w-4 -rotate-90" />
</span>
None
</>
</CustomSelect.Option>
{estimatePoints && {estimatePoints &&
estimatePoints.map((point) => ( estimatePoints.map((point) => (
<CustomSelect.Option className="w-full " key={point.key} value={point.key}> <CustomSelect.Option key={point.key} value={point.key}>
<> <>
<span> <span>
<PlayIcon className="h-4 w-4 -rotate-90" /> <PlayIcon className="h-4 w-4 -rotate-90" />

View File

@ -48,6 +48,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
})); }));
const selectedOption = states?.find((s) => s.id === value); const selectedOption = states?.find((s) => s.id === value);
const currentDefaultState = states.find((s) => s.default);
return ( return (
<CustomSearchSelect <CustomSearchSelect
@ -58,11 +59,12 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
<div className="flex items-center gap-2 text-gray-500"> <div className="flex items-center gap-2 text-gray-500">
{selectedOption ? ( {selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color) getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
) : currentDefaultState ? (
getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color)
) : ( ) : (
<Squares2X2Icon className="h-4 w-4" /> <Squares2X2Icon className="h-4 w-4" />
)} )}
{selectedOption?.name ? selectedOption.name : currentDefaultState?.name ?? "State"}
{selectedOption?.name ?? "State"}
</div> </div>
} }
footerOption={ footerOption={

View File

@ -1,17 +1,17 @@
import React from "react"; import React from "react";
// hooks
import useEstimateOption from "hooks/use-estimate-option";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// icons // icons
import { BanknotesIcon, PlayIcon } from "@heroicons/react/24/outline"; import { PlayIcon } from "@heroicons/react/24/outline";
// types // types
import { UserAuth } from "types"; import { UserAuth } from "types";
import useEstimateOption from "hooks/use-estimate-option";
// constants
type Props = { type Props = {
value: number; value: number | null;
onChange: (val: number) => void; onChange: (val: number | null) => void;
userAuth: UserAuth; userAuth: UserAuth;
}; };
@ -35,7 +35,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAu
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<PlayIcon className="h-4 w-4 text-gray-700 -rotate-90" /> <PlayIcon className="h-4 w-4 text-gray-700 -rotate-90" />
<span className={`${value ? "text-gray-600" : "text-gray-500"}`}> <span className={`${value ? "text-gray-600" : "text-gray-500"}`}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate points"} {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
</span> </span>
</div> </div>
} }
@ -44,9 +44,17 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAu
width="w-full" width="w-full"
disabled={isNotAllowed} disabled={isNotAllowed}
> >
<CustomSelect.Option value={null}>
<>
<span>
<PlayIcon className="h-4 w-4 -rotate-90" />
</span>
None
</>
</CustomSelect.Option>
{estimatePoints && {estimatePoints &&
estimatePoints.map((point) => ( estimatePoints.map((point) => (
<CustomSelect.Option className="w-full " key={point.key} value={point.key}> <CustomSelect.Option key={point.key} value={point.key}>
<> <>
<span> <span>
<PlayIcon className="h-4 w-4 -rotate-90" /> <PlayIcon className="h-4 w-4 -rotate-90" />

View File

@ -294,7 +294,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SidebarEstimateSelect <SidebarEstimateSelect
value={value} value={value}
onChange={(val: number) => submitChanges({ estimate_point: val })} onChange={(val: number | null) => submitChanges({ estimate_point: val })}
userAuth={memberRole} userAuth={memberRole}
/> />
)} )}

View File

@ -75,8 +75,8 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug: issue.workspace_detail.slug, workspaceSlug,
workspaceId: issue.workspace_detail.id, workspaceId: issue.workspace,
projectId: issue.project_detail.id, projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier, projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name, projectName: issue.project_detail.name,

View File

@ -1,3 +1,5 @@
import { useRouter } from "next/router";
// ui // ui
import { CustomDatePicker, Tooltip } from "components/ui"; import { CustomDatePicker, Tooltip } from "components/ui";
// helpers // helpers
@ -13,41 +15,46 @@ type Props = {
isNotAllowed: boolean; isNotAllowed: boolean;
}; };
export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => ( export const ViewDueDateSelect: React.FC<Props> = ({ issue, partialUpdateIssue, isNotAllowed }) => {
<Tooltip tooltipHeading="Due Date" tooltipContent={issue.target_date ?? "N/A"}> const router = useRouter();
<div const { workspaceSlug } = router.query;
className={`group relative ${
issue.target_date === null return (
? "" <Tooltip tooltipHeading="Due Date" tooltipContent={issue.target_date ?? "N/A"}>
: issue.target_date < new Date().toISOString() <div
? "text-red-600" className={`group relative ${
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400" issue.target_date === null
}`} ? ""
> : issue.target_date < new Date().toISOString()
<CustomDatePicker ? "text-red-600"
placeholder="N/A" : findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
value={issue?.target_date} }`}
onChange={(val) => { >
partialUpdateIssue({ <CustomDatePicker
target_date: val, placeholder="N/A"
priority: issue.priority, value={issue?.target_date}
state: issue.state, onChange={(val) => {
}); partialUpdateIssue({
trackEventServices.trackIssuePartialPropertyUpdateEvent( target_date: val,
{ priority: issue.priority,
workspaceSlug: issue.workspace_detail.slug, state: issue.state,
workspaceId: issue.workspace_detail.id, });
projectId: issue.project_detail.id, trackEventServices.trackIssuePartialPropertyUpdateEvent(
projectIdentifier: issue.project_detail.identifier, {
projectName: issue.project_detail.name, workspaceSlug,
issueId: issue.id, workspaceId: issue.workspace,
}, projectId: issue.project_detail.id,
"ISSUE_PROPERTY_UPDATE_DUE_DATE" projectIdentifier: issue.project_detail.identifier,
); projectName: issue.project_detail.name,
}} issueId: issue.id,
className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} },
disabled={isNotAllowed} "ISSUE_PROPERTY_UPDATE_DUE_DATE"
/> );
</div> }}
</Tooltip> className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"}
); disabled={isNotAllowed}
/>
</div>
</Tooltip>
);
};

View File

@ -1,17 +1,17 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// services
import trackEventServices from "services/track-event.service";
// hooks
import useEstimateOption from "hooks/use-estimate-option";
// ui // ui
import { CustomSelect, Tooltip } from "components/ui"; import { CustomSelect, Tooltip } from "components/ui";
// icons // icons
import { getPriorityIcon } from "components/icons/priority-icon"; import { PlayIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// constants
import { PRIORITIES } from "constants/project";
// services
import trackEventServices from "services/track-event.service";
import useEstimateOption from "hooks/use-estimate-option";
import { PlayIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
@ -28,6 +28,9 @@ export const ViewEstimateSelect: React.FC<Props> = ({
selfPositioned = false, selfPositioned = false,
isNotAllowed, isNotAllowed,
}) => { }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point); const { isEstimateActive, estimatePoints } = useEstimateOption(issue.estimate_point);
const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value; const estimateValue = estimatePoints?.find((e) => e.key === issue.estimate_point)?.value;
@ -41,8 +44,8 @@ export const ViewEstimateSelect: React.FC<Props> = ({
partialUpdateIssue({ estimate_point: val }); partialUpdateIssue({ estimate_point: val });
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug: issue.workspace_detail.slug, workspaceSlug,
workspaceId: issue.workspace_detail.id, workspaceId: issue.workspace,
projectId: issue.project_detail.id, projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier, projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name, projectName: issue.project_detail.name,
@ -55,7 +58,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}> <Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-gray-500">
<PlayIcon className="h-3.5 w-3.5 -rotate-90" /> <PlayIcon className="h-3.5 w-3.5 -rotate-90" />
{estimateValue} {estimateValue ?? "Estimate"}
</div> </div>
</Tooltip> </Tooltip>
} }
@ -64,11 +67,24 @@ export const ViewEstimateSelect: React.FC<Props> = ({
disabled={isNotAllowed} disabled={isNotAllowed}
position={position} position={position}
selfPositioned={selfPositioned} selfPositioned={selfPositioned}
width="w-full min-w-[6rem]" width="w-full min-w-[8rem]"
> >
<CustomSelect.Option value={null}>
<>
<span>
<PlayIcon className="h-4 w-4 -rotate-90" />
</span>
None
</>
</CustomSelect.Option>
{estimatePoints?.map((estimate) => ( {estimatePoints?.map((estimate) => (
<CustomSelect.Option key={estimate.id} value={estimate.key} className="capitalize"> <CustomSelect.Option key={estimate.id} value={estimate.key}>
<>{estimate.value}</> <>
<span>
<PlayIcon className="h-4 w-4 -rotate-90" />
</span>
{estimate.value}
</>
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
</CustomSelect> </CustomSelect>

View File

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// ui // ui
import { CustomSelect, Tooltip } from "components/ui"; import { CustomSelect, Tooltip } from "components/ui";
// icons // icons
@ -25,63 +27,68 @@ export const ViewPrioritySelect: React.FC<Props> = ({
position = "left", position = "left",
selfPositioned = false, selfPositioned = false,
isNotAllowed, isNotAllowed,
}) => ( }) => {
<CustomSelect const router = useRouter();
value={issue.priority} const { workspaceSlug } = router.query;
onChange={(data: string) => {
partialUpdateIssue({ priority: data }); return (
trackEventServices.trackIssuePartialPropertyUpdateEvent( <CustomSelect
{ value={issue.priority}
workspaceSlug: issue.workspace_detail.slug, onChange={(data: string) => {
workspaceId: issue.workspace_detail.id, partialUpdateIssue({ priority: data });
projectId: issue.project_detail.id, trackEventServices.trackIssuePartialPropertyUpdateEvent(
projectIdentifier: issue.project_detail.identifier, {
projectName: issue.project_detail.name, workspaceSlug,
issueId: issue.id, workspaceId: issue.workspace,
}, projectId: issue.project_detail.id,
"ISSUE_PROPERTY_UPDATE_PRIORITY" projectIdentifier: issue.project_detail.identifier,
); projectName: issue.project_detail.name,
}} issueId: issue.id,
maxHeight="md" },
customButton={ "ISSUE_PROPERTY_UPDATE_PRIORITY"
<button );
type="button" }}
className={`grid h-6 w-6 place-items-center rounded ${ maxHeight="md"
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer" customButton={
} items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${ <button
issue.priority === "urgent" type="button"
? "bg-red-100 text-red-600 hover:bg-red-100" className={`grid h-6 w-6 place-items-center rounded ${
: issue.priority === "high" isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
? "bg-orange-100 text-orange-500 hover:bg-orange-100" } items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
: issue.priority === "medium" issue.priority === "urgent"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100" ? "bg-red-100 text-red-600 hover:bg-red-100"
: issue.priority === "low" : issue.priority === "high"
? "bg-green-100 text-green-500 hover:bg-green-100" ? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: "bg-gray-100" : issue.priority === "medium"
} border-none`} ? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
> : issue.priority === "low"
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}> ? "bg-green-100 text-green-500 hover:bg-green-100"
<span> : "bg-gray-100"
{getPriorityIcon( } border-none`}
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", >
"text-sm" <Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
)} <span>
</span> {getPriorityIcon(
</Tooltip> issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
</button> "text-sm"
} )}
noChevron </span>
disabled={isNotAllowed} </Tooltip>
position={position} </button>
selfPositioned={selfPositioned} }
> noChevron
{PRIORITIES?.map((priority) => ( disabled={isNotAllowed}
<CustomSelect.Option key={priority} value={priority} className="capitalize"> position={position}
<> selfPositioned={selfPositioned}
{getPriorityIcon(priority, "text-sm")} >
{priority ?? "None"} {PRIORITIES?.map((priority) => (
</> <CustomSelect.Option key={priority} value={priority} className="capitalize">
</CustomSelect.Option> <>
))} {getPriorityIcon(priority, "text-sm")}
</CustomSelect> {priority ?? "None"}
); </>
</CustomSelect.Option>
))}
</CustomSelect>
);
};

View File

@ -67,8 +67,8 @@ export const ViewStateSelect: React.FC<Props> = ({
}); });
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug: issue.workspace_detail.slug, workspaceSlug,
workspaceId: issue.workspace_detail.id, workspaceId: issue.workspace,
projectId: issue.project_detail.id, projectId: issue.project_detail.id,
projectIdentifier: issue.project_detail.identifier, projectIdentifier: issue.project_detail.identifier,
projectName: issue.project_detail.name, projectName: issue.project_detail.name,

View File

@ -32,7 +32,8 @@ type Props = {
const defaultValues = { const defaultValues = {
name: "", name: "",
description: "<p></p>", description: { type: "doc", content: [] },
description_html: "<p></p>",
}; };
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
@ -167,7 +168,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
}); });
else { else {
setValue("description", {}); setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${res.response}</p>`); setValue("description_html", `${watch("description_html") ?? ""}<p>${res.response}</p>`);
} }
}) })
.catch((err) => { .catch((err) => {

View File

@ -199,9 +199,11 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
onBlur(jsonValue, htmlValue); onBlur(jsonValue, htmlValue);
}} }}
> >
{(!value || value === "" || value?.content?.[0]?.content === undefined) && placeholder && ( {(!value || value === "" || value?.content?.[0]?.content === undefined) &&
<p className="absolute pointer-events-none top-4 left-4 text-gray-300">{placeholder}</p> !(typeof value === "string" && value.includes("<")) &&
)} placeholder && (
<p className="absolute pointer-events-none top-4 left-4 text-gray-300">{placeholder}</p>
)}
<EditorComponent /> <EditorComponent />
{imageLoader && ( {imageLoader && (

View File

@ -1,6 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// ui // ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
@ -16,6 +15,7 @@ type Props = {
handleClose: () => void; handleClose: () => void;
status: boolean; status: boolean;
data?: IView | null; data?: IView | null;
preLoadedData?: Partial<IView> | null;
}; };
const defaultValues: Partial<IView> = { const defaultValues: Partial<IView> = {
@ -23,7 +23,13 @@ const defaultValues: Partial<IView> = {
description: "", description: "",
}; };
export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => { export const ViewForm: React.FC<Props> = ({
handleFormSubmit,
handleClose,
status,
data,
preLoadedData,
}) => {
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -47,9 +53,10 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
useEffect(() => { useEffect(() => {
reset({ reset({
...defaultValues, ...defaultValues,
...preLoadedData,
...data, ...data,
}); });
}, [data, reset]); }, [data, preLoadedData, reset]);
useEffect(() => { useEffect(() => {
if (status && data) { if (status && data) {

View File

@ -21,9 +21,10 @@ type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
data?: IView | null; data?: IView | null;
preLoadedData?: Partial<IView> | null ;
}; };
export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, data }) => { export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, data, preLoadedData }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -133,6 +134,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, da
handleClose={handleClose} handleClose={handleClose}
status={data ? true : false} status={data ? true : false}
data={data} data={data}
preLoadedData={preLoadedData}
/> />
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

@ -439,12 +439,11 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
payload: { payload: {
...myViewProps?.view_props, ...myViewProps?.view_props,
filters: { filters: {
...myViewProps?.view_props?.filters, ...(viewId ? viewDetails?.query_data : myViewProps?.view_props?.filters),
...viewDetails?.query_data,
} as any, } as any,
}, },
}); });
}, [myViewProps, viewDetails]); }, [myViewProps, viewDetails, viewId]);
return ( return (
<issueViewContext.Provider <issueViewContext.Provider

View File

@ -13,7 +13,7 @@ import { orderArrayBy } from "helpers/array.helper";
// fetch-keys // fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys"; import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
const useEstimateOption = (estimateKey?: number) => { const useEstimateOption = (estimateKey?: number | null) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
const getValueFromLocalStorage = (key: string, defaultValue: any) => { const getValueFromLocalStorage = (key: string, defaultValue: any) => {
if (typeof window === undefined || typeof window === "undefined") return defaultValue;
try { try {
const item = window.localStorage.getItem(key); const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue; return item ? JSON.parse(item) : defaultValue;

View File

@ -46,7 +46,7 @@ const WorkspacePage: NextPage = () => {
// style={{ background: "linear-gradient(90deg, #8e2de2 0%, #4a00e0 100%)" }} // style={{ background: "linear-gradient(90deg, #8e2de2 0%, #4a00e0 100%)" }}
> >
<p className="font-semibold"> <p className="font-semibold">
Plane is a open source application, to support us you can star us on GitHub! Plane is open source, support us by staring us on GitHub.
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* <a href="#" target="_blank" rel="noopener noreferrer"> {/* <a href="#" target="_blank" rel="noopener noreferrer">

View File

@ -43,6 +43,7 @@ const MyIssuesPage: NextPage = () => {
<BreadcrumbItem title="My Issues" /> <BreadcrumbItem title="My Issues" />
</Breadcrumbs> </Breadcrumbs>
} }
noPadding
right={ right={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{myIssues && myIssues.length > 0 && ( {myIssues && myIssues.length > 0 && (
@ -72,20 +73,24 @@ const MyIssuesPage: NextPage = () => {
<div className="relative flex flex-col gap-1"> <div className="relative flex flex-col gap-1">
<h4 className="text-base text-gray-600">Properties</h4> <h4 className="text-base text-gray-600">Properties</h4>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => ( {Object.keys(properties).map((key) => {
<button if (key === "estimate") return null;
key={key}
type="button" return (
className={`rounded border border-theme px-2 py-1 text-xs capitalize ${ <button
properties[key as keyof Properties] key={key}
? "border-theme bg-theme text-white" type="button"
: "" className={`rounded border border-theme px-2 py-1 text-xs capitalize ${
}`} properties[key as keyof Properties]
onClick={() => setProperties(key as keyof Properties)} ? "border-theme bg-theme text-white"
> : ""
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)} }`}
</button> onClick={() => setProperties(key as keyof Properties)}
))} >
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
);
})}
</div> </div>
</div> </div>
</div> </div>
@ -115,10 +120,10 @@ const MyIssuesPage: NextPage = () => {
<div className="flex flex-col space-y-5"> <div className="flex flex-col space-y-5">
<Disclosure as="div" defaultOpen> <Disclosure as="div" defaultOpen>
{({ open }) => ( {({ open }) => (
<div className="rounded-[10px] border border-gray-300 bg-white"> <div className="bg-white">
<div <div
className={`flex items-center justify-start bg-gray-100 px-5 py-3 ${ className={`flex items-center justify-start bg-gray-100 px-5 py-3 ${
open ? "rounded-t-[10px]" : "rounded-[10px]" open ? "" : "rounded-[10px]"
}`} }`}
> >
<Disclosure.Button> <Disclosure.Button>

View File

@ -34,6 +34,7 @@ const defaultValues = {
name: "", name: "",
description: "", description: "",
description_html: "", description_html: "",
estimate_point: null,
state: "", state: "",
assignees_list: [], assignees_list: [],
priority: "low", priority: "low",

View File

@ -14,6 +14,7 @@ import projectService from "services/project.service";
import pagesService from "services/pages.service"; import pagesService from "services/pages.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// icons // icons
import { PlusIcon } from "components/icons"; import { PlusIcon } from "components/icons";
// layouts // layouts
@ -74,6 +75,8 @@ const ProjectPages: NextPage = () => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent");
const { const {
handleSubmit, handleSubmit,
register, register,
@ -145,6 +148,25 @@ const ProjectPages: NextPage = () => {
}); });
}; };
const currentTabValue = (tab: string | null) => {
switch (tab) {
case "Recent":
return 0;
case "All":
return 1;
case "Favorites":
return 2;
case "Created by me":
return 3;
case "Created by others":
return 4;
default:
return 0;
}
};
return ( return (
<> <>
<CreateUpdatePageModal <CreateUpdatePageModal
@ -193,7 +215,26 @@ const ProjectPages: NextPage = () => {
)} )}
</form> </form>
<div> <div>
<Tab.Group> <Tab.Group
defaultIndex={currentTabValue(pageTab)}
onChange={(i) => {
switch (i) {
case 0:
return setPageTab("Recent");
case 1:
return setPageTab("All");
case 2:
return setPageTab("Favorites");
case 3:
return setPageTab("Created by me");
case 4:
return setPageTab("Created by others");
default:
return setPageTab("Recent");
}
}}
>
<Tab.List as="div" className="flex items-center justify-between mb-6"> <Tab.List as="div" className="flex items-center justify-between mb-6">
<div className="flex gap-4"> <div className="flex gap-4">
{["Recent", "All", "Favorites", "Created by me", "Created by others"].map( {["Recent", "All", "Favorites", "Created by me", "Created by others"].map(

View File

@ -87,7 +87,7 @@ export interface IIssue {
description: any; description: any;
description_html: any; description_html: any;
description_stripped: any; description_stripped: any;
estimate_point: number; estimate_point: number | null;
id: string; id: string;
issue_cycle: IIssueCycle | null; issue_cycle: IIssueCycle | null;
issue_link: { issue_link: {