forked from github/plane
Merge branch 'develop' into self-hosting-tweaks
This commit is contained in:
commit
e22f552ea0
@ -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):
|
|
||||||
raise serializers.ValidationError(
|
|
||||||
{"query_data": ["Query data field cannot be empty"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
validated_data["query"] = issue_filters(query_params, "POST")
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
|
else:
|
||||||
|
validated_data["query"] = dict()
|
||||||
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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -241,10 +241,11 @@ LOGGER_BASE_URL = os.environ.get("LOGGER_BASE_URL", False)
|
|||||||
|
|
||||||
broker_ssl = os.environ.get("REDIS_BROKER_SSL", "1") == "1"
|
broker_ssl = os.environ.get("REDIS_BROKER_SSL", "1") == "1"
|
||||||
redis_url = os.environ.get("REDIS_URL")
|
redis_url = os.environ.get("REDIS_URL")
|
||||||
if broker_ssl:
|
|
||||||
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||||
else:
|
|
||||||
broker_url = redis_url
|
|
||||||
|
|
||||||
|
if DOCKERIZED:
|
||||||
|
CELERY_BROKER_URL = REDIS_URL
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URL
|
||||||
|
else:
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
CELERY_BROKER_URL = broker_url
|
CELERY_BROKER_URL = broker_url
|
||||||
|
@ -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);
|
||||||
|
@ -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") {
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
<>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between gap-2 ${
|
||||||
|
issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<FilterList filters={filters} setFilters={setFilters} />
|
<FilterList filters={filters} setFilters={setFilters} />
|
||||||
{Object.keys(filters).length > 0 &&
|
{areFiltersApplied && (
|
||||||
nullFilters.length !== Object.keys(filters).length && (
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (viewId) {
|
if (viewId) {
|
||||||
@ -414,19 +421,18 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
</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>
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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 (
|
||||||
|
<span className="flex gap-1">
|
||||||
{estimatePoints.map((point, index) => (
|
{estimatePoints.map((point, index) => (
|
||||||
<h6 key={point.id}>
|
<h6 key={point.id}>
|
||||||
{point.value}
|
{point.value}
|
||||||
{index !== estimatePoints.length - 1 && ","}{" "}
|
{index !== estimatePoints.length - 1 && ","}{" "}
|
||||||
</h6>
|
</h6>
|
||||||
))}
|
))}
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -45,7 +45,7 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
IntegrationService.deleteImporterService(workspaceSlug as string, data.id)
|
IntegrationService.deleteImporterService(workspaceSlug as string, data.service, data.id)
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -53,7 +53,10 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
|
|||||||
message: "Something went wrong. Please try again.",
|
message: "Something went wrong. Please try again.",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.finally(() => setDeleteLoading(false));
|
.finally(() => {
|
||||||
|
setDeleteLoading(false);
|
||||||
|
handleClose();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data) return <></>;
|
if (!data) return <></>;
|
||||||
|
@ -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>
|
||||||
|
@ -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 =
|
||||||
|
@ -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") {
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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">
|
||||||
|
@ -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" />
|
||||||
|
@ -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={
|
||||||
|
@ -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" />
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -58,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>
|
||||||
}
|
}
|
||||||
@ -67,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>
|
||||||
|
@ -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) => {
|
||||||
|
@ -199,7 +199,9 @@ 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) &&
|
||||||
|
!(typeof value === "string" && value.includes("<")) &&
|
||||||
|
placeholder && (
|
||||||
<p className="absolute pointer-events-none top-4 left-4 text-gray-300">{placeholder}</p>
|
<p className="absolute pointer-events-none top-4 left-4 text-gray-300">{placeholder}</p>
|
||||||
)}
|
)}
|
||||||
<EditorComponent />
|
<EditorComponent />
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -34,7 +34,7 @@ export const ProjectMemberProvider: React.FC<Props> = (props) => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { data: memberDetails, error } = useSWR(
|
const { data: memberDetails, error } = useSWR(
|
||||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(workspaceSlug.toString()) : null,
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
|
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
|
||||||
: null,
|
: null,
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -24,14 +22,6 @@ const useProjectDetails = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectDetailsError?.status === 404) {
|
|
||||||
router.push("/404");
|
|
||||||
} else if (projectDetailsError) {
|
|
||||||
router.push("/error");
|
|
||||||
}
|
|
||||||
}, [projectDetailsError, router]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectDetails,
|
projectDetails,
|
||||||
projectDetailsError,
|
projectDetailsError,
|
||||||
|
@ -21,7 +21,6 @@ import { PrimaryButton, Spinner } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
|
|
||||||
|
|
||||||
type Meta = {
|
type Meta = {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
@ -61,9 +60,7 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const {
|
const { issueView } = useIssuesView();
|
||||||
issueView,
|
|
||||||
} = useIssuesView();
|
|
||||||
|
|
||||||
const { loading, error, memberRole: memberType } = useProjectMyMembership();
|
const { loading, error, memberRole: memberType } = useProjectMyMembership();
|
||||||
|
|
||||||
@ -97,22 +94,6 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
|||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : error?.status === 401 || error?.status === 403 ? (
|
|
||||||
<JoinProject />
|
|
||||||
) : error?.status === 404 ? (
|
|
||||||
<div className="container h-screen grid place-items-center">
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<p className="text-2xl font-semibold">No such project exist. Create one?</p>
|
|
||||||
<PrimaryButton
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "p" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create project
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
) : settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
||||||
<NotAuthorizedView
|
<NotAuthorizedView
|
||||||
actionButton={
|
actionButton={
|
||||||
|
@ -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">
|
||||||
|
@ -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,7 +73,10 @@ 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) => {
|
||||||
|
if (key === "estimate") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
@ -85,7 +89,8 @@ const MyIssuesPage: NextPage = () => {
|
|||||||
>
|
>
|
||||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||||
</button>
|
</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>
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
@ -43,8 +43,12 @@ class IntegrationService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteImporterService(workspaceSlug: string, importerId: string): Promise<any> {
|
async deleteImporterService(
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${importerId}/`)
|
workspaceSlug: string,
|
||||||
|
service: string,
|
||||||
|
importerId: string
|
||||||
|
): Promise<any> {
|
||||||
|
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
|
||||||
.then((res) => res?.data)
|
.then((res) => res?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user