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):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
|
||||
if not bool(query_params):
|
||||
raise serializers.ValidationError(
|
||||
{"query_data": ["Query data field cannot be empty"]}
|
||||
)
|
||||
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = dict()
|
||||
return IssueView.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if not bool(query_params):
|
||||
raise serializers.ValidationError(
|
||||
{"query_data": ["Query data field cannot be empty"]}
|
||||
)
|
||||
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = dict()
|
||||
validated_data["query"] = issue_filters(query_params, "PATCH")
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
@ -246,6 +246,20 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.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)
|
||||
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"
|
||||
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()}"
|
||||
else:
|
||||
broker_url = redis_url
|
||||
|
||||
if DOCKERIZED:
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
else:
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
|
@ -5,15 +5,16 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// ui
|
||||
import { PrimaryButton } from "components/ui";
|
||||
// icon
|
||||
// icons
|
||||
import { AssignmentClipboardIcon } from "components/icons";
|
||||
// img
|
||||
// images
|
||||
import JoinProjectImg from "public/auth/project-not-authorized.svg";
|
||||
import projectService from "services/project.service";
|
||||
// fetch-keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
|
||||
|
||||
export const JoinProject: React.FC = () => {
|
||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||
@ -22,13 +23,16 @@ export const JoinProject: React.FC = () => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const handleJoin = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
setIsJoiningProject(true);
|
||||
projectService
|
||||
.joinProject(workspaceSlug as string, {
|
||||
project_ids: [projectId as string],
|
||||
})
|
||||
.then(() => {
|
||||
mutate(PROJECT_MEMBERS(projectId as string));
|
||||
.then(async () => {
|
||||
await mutate(USER_PROJECT_VIEW(workspaceSlug.toString()));
|
||||
setIsJoiningProject(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
@ -73,6 +73,10 @@ const activityDetails: {
|
||||
message: "updated the description.",
|
||||
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: {
|
||||
message: "set the due date to",
|
||||
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`;
|
||||
} else if (activity.field === "link") {
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "estimate") {
|
||||
action = "updated the";
|
||||
}
|
||||
// for values that are after the action clause
|
||||
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";
|
||||
} else if (activity.field === "link") {
|
||||
value = "link";
|
||||
} else if (activity.field === "estimate") {
|
||||
value = "estimate";
|
||||
} else if (activity.field === "estimate_point") {
|
||||
value = activity.new_value
|
||||
? activity.new_value + ` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}`
|
||||
: "None";
|
||||
}
|
||||
|
||||
if (activity.field === "comment") {
|
||||
|
@ -309,7 +309,19 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
@ -319,6 +331,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
type: null,
|
||||
state: null,
|
||||
priority: null,
|
||||
assignees: null,
|
||||
|
@ -359,12 +359,15 @@ export const IssuesView: React.FC<Props> = ({
|
||||
(key) => filters[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
|
||||
const areFiltersApplied =
|
||||
Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateViewModal
|
||||
isOpen={createViewModal !== null}
|
||||
handleClose={() => setCreateViewModal(null)}
|
||||
data={createViewModal}
|
||||
preLoadedData={createViewModal}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createIssueModal && preloadedData?.actionType === "createIssue"}
|
||||
@ -388,11 +391,15 @@ export const IssuesView: React.FC<Props> = ({
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 ${
|
||||
issueView === "list" && areFiltersApplied ? "px-8 mt-6" : "-mt-2"
|
||||
}`}
|
||||
>
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
{Object.keys(filters).length > 0 &&
|
||||
nullFilters.length !== Object.keys(filters).length && (
|
||||
{areFiltersApplied && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
@ -414,19 +421,18 @@ export const IssuesView: React.FC<Props> = ({
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||
<div className="mb-5 border-t" />
|
||||
{areFiltersApplied && (
|
||||
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`${
|
||||
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" : ""
|
||||
} duration-200`}
|
||||
ref={provided.innerRef}
|
||||
@ -434,7 +440,6 @@ export const IssuesView: React.FC<Props> = ({
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
Drop issue here to delete
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
|
@ -36,7 +36,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
{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) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
@ -216,9 +216,9 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
</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
|
||||
className="flex items-center justify-between gap-2 px-4 py-3"
|
||||
className="flex items-center justify-between gap-2 py-3"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
|
@ -152,14 +152,16 @@ export const SingleEstimate: React.FC<Props> = ({
|
||||
</CustomMenu>
|
||||
</div>
|
||||
{estimatePoints && estimatePoints.length > 0 ? (
|
||||
<div className="flex gap-2 text-sm text-gray-400">
|
||||
<div className="flex text-sm text-gray-400">
|
||||
Estimate points (
|
||||
<span className="flex gap-1">
|
||||
{estimatePoints.map((point, index) => (
|
||||
<h6 key={point.id}>
|
||||
{point.value}
|
||||
{index !== estimatePoints.length - 1 && ","}{" "}
|
||||
</h6>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
</div>
|
||||
) : (
|
||||
|
@ -45,7 +45,7 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
|
||||
false
|
||||
);
|
||||
|
||||
IntegrationService.deleteImporterService(workspaceSlug as string, data.id)
|
||||
IntegrationService.deleteImporterService(workspaceSlug as string, data.service, data.id)
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
@ -53,7 +53,10 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data }
|
||||
message: "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setDeleteLoading(false));
|
||||
.finally(() => {
|
||||
setDeleteLoading(false);
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
if (!data) return <></>;
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { UseFormWatch } from "react-hook-form";
|
||||
// ui
|
||||
@ -22,8 +20,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers, watch }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0;
|
||||
|
||||
return (
|
||||
@ -44,7 +40,6 @@ export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers
|
||||
index={index}
|
||||
users={users}
|
||||
setUsers={setUsers}
|
||||
project={watch("project")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -4,21 +4,20 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// ui
|
||||
import { Avatar, CustomSearchSelect, CustomSelect, Input } from "components/ui";
|
||||
// types
|
||||
import { IGithubRepoCollaborator } from "types";
|
||||
import { IUserDetails } from "./root";
|
||||
// fetch-keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
collaborator: IGithubRepoCollaborator;
|
||||
index: number;
|
||||
users: IUserDetails[];
|
||||
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
|
||||
project: string | null;
|
||||
};
|
||||
|
||||
const importOptions = [
|
||||
@ -36,21 +35,13 @@ const importOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const SingleUserSelect: React.FC<Props> = ({
|
||||
collaborator,
|
||||
index,
|
||||
users,
|
||||
setUsers,
|
||||
project,
|
||||
}) => {
|
||||
export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users, setUsers }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && project ? PROJECT_MEMBERS(project) : null,
|
||||
workspaceSlug && project
|
||||
? () => projectService.projectMembers(workspaceSlug as string, project)
|
||||
: null
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const options =
|
||||
|
@ -29,6 +29,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueComment, IIssueLabels } from "types";
|
||||
import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
@ -56,6 +57,10 @@ const activityDetails: {
|
||||
message: "set the cycle to",
|
||||
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: {
|
||||
icon: <TagIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
@ -107,6 +112,8 @@ export const IssueActivitySection: React.FC<Props> = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||
|
||||
const { data: issueActivities, mutate: mutateIssueActivities } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
@ -278,8 +285,14 @@ export const IssueActivitySection: React.FC<Props> = () => {
|
||||
value = "attachment";
|
||||
} else if (activityItem.field === "link") {
|
||||
value = "link";
|
||||
} else if (activityItem.field === "estimate") {
|
||||
value = "estimate";
|
||||
} else if (activityItem.field === "estimate_point") {
|
||||
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") {
|
||||
|
@ -46,9 +46,9 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
project: "",
|
||||
name: "",
|
||||
description: "",
|
||||
description: { type: "doc", content: [] },
|
||||
description_html: "<p></p>",
|
||||
estimate_point: 0,
|
||||
estimate_point: null,
|
||||
state: "",
|
||||
cycle: null,
|
||||
priority: null,
|
||||
@ -137,7 +137,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
|
||||
};
|
||||
|
||||
const handelAutoGenerateDescription = async () => {
|
||||
const handleAutoGenerateDescription = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
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 ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handelAutoGenerateDescription}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
>
|
||||
{iAmFeelingLucky ? (
|
||||
|
@ -5,6 +5,8 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// components
|
||||
@ -13,16 +15,17 @@ import {
|
||||
ViewPrioritySelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues/view-select";
|
||||
// icon
|
||||
import { LinkIcon, PaperClipIcon } from "@heroicons/react/24/outline";
|
||||
// ui
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
// types
|
||||
import { IIssue, Properties } from "types";
|
||||
// helper
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// 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 = {
|
||||
issue: IIssue;
|
||||
@ -79,8 +82,8 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
const isNotAllowed = false;
|
||||
|
||||
return (
|
||||
<div className="border-b border-gray-300 last:border-b-0">
|
||||
<div key={issue.id} className="flex items-center justify-between gap-2 px-4 py-3">
|
||||
<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 py-3">
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties?.key && (
|
||||
@ -167,6 +170,26 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
|
||||
</div>
|
||||
</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.MenuItem onClick={handleCopyText}>
|
||||
<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";
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
value: number | null;
|
||||
onChange: (value: number | null) => void;
|
||||
};
|
||||
|
||||
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">
|
||||
<PlayIcon className="h-4 w-4 text-gray-500 -rotate-90" />
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
position="right"
|
||||
width="w-full min-w-[6rem]"
|
||||
width="w-full min-w-[8rem]"
|
||||
noChevron
|
||||
>
|
||||
<CustomSelect.Option value={null}>
|
||||
<>
|
||||
<span>
|
||||
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||
</span>
|
||||
None
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
{estimatePoints &&
|
||||
estimatePoints.map((point) => (
|
||||
<CustomSelect.Option className="w-full " key={point.key} value={point.key}>
|
||||
<CustomSelect.Option key={point.key} value={point.key}>
|
||||
<>
|
||||
<span>
|
||||
<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 currentDefaultState = states.find((s) => s.default);
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
@ -58,11 +59,12 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
{selectedOption ? (
|
||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
|
||||
) : currentDefaultState ? (
|
||||
getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color)
|
||||
) : (
|
||||
<Squares2X2Icon className="h-4 w-4" />
|
||||
)}
|
||||
|
||||
{selectedOption?.name ?? "State"}
|
||||
{selectedOption?.name ? selectedOption.name : currentDefaultState?.name ?? "State"}
|
||||
</div>
|
||||
}
|
||||
footerOption={
|
||||
|
@ -1,17 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
// hooks
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { BanknotesIcon, PlayIcon } from "@heroicons/react/24/outline";
|
||||
import { PlayIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { UserAuth } from "types";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
onChange: (val: number) => void;
|
||||
value: number | null;
|
||||
onChange: (val: number | null) => void;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
@ -35,7 +35,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAu
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<PlayIcon className="h-4 w-4 text-gray-700 -rotate-90" />
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
@ -44,9 +44,17 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAu
|
||||
width="w-full"
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
<CustomSelect.Option value={null}>
|
||||
<>
|
||||
<span>
|
||||
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||
</span>
|
||||
None
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
{estimatePoints &&
|
||||
estimatePoints.map((point) => (
|
||||
<CustomSelect.Option className="w-full " key={point.key} value={point.key}>
|
||||
<CustomSelect.Option key={point.key} value={point.key}>
|
||||
<>
|
||||
<span>
|
||||
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||
|
@ -294,7 +294,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
render={({ field: { value } }) => (
|
||||
<SidebarEstimateSelect
|
||||
value={value}
|
||||
onChange={(val: number) => submitChanges({ estimate_point: val })}
|
||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
)}
|
||||
|
@ -58,7 +58,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
||||
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
|
||||
<div className="flex items-center gap-1 text-gray-500">
|
||||
<PlayIcon className="h-3.5 w-3.5 -rotate-90" />
|
||||
{estimateValue}
|
||||
{estimateValue ?? "Estimate"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
@ -67,11 +67,24 @@ export const ViewEstimateSelect: React.FC<Props> = ({
|
||||
disabled={isNotAllowed}
|
||||
position={position}
|
||||
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) => (
|
||||
<CustomSelect.Option key={estimate.id} value={estimate.key} className="capitalize">
|
||||
<>{estimate.value}</>
|
||||
<CustomSelect.Option key={estimate.id} value={estimate.key}>
|
||||
<>
|
||||
<span>
|
||||
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||
</span>
|
||||
{estimate.value}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
|
@ -32,7 +32,8 @@ type Props = {
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
description: "<p></p>",
|
||||
description: { type: "doc", content: [] },
|
||||
description_html: "<p></p>",
|
||||
};
|
||||
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
@ -167,7 +168,7 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
|
||||
});
|
||||
else {
|
||||
setValue("description", {});
|
||||
setValue("description_html", `${watch("description_html")}<p>${res.response}</p>`);
|
||||
setValue("description_html", `${watch("description_html") ?? ""}<p>${res.response}</p>`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -199,7 +199,9 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
|
||||
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>
|
||||
)}
|
||||
<EditorComponent />
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
@ -16,6 +15,7 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
status: boolean;
|
||||
data?: IView | null;
|
||||
preLoadedData?: Partial<IView> | null;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IView> = {
|
||||
@ -23,7 +23,13 @@ const defaultValues: Partial<IView> = {
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
|
||||
export const ViewForm: React.FC<Props> = ({
|
||||
handleFormSubmit,
|
||||
handleClose,
|
||||
status,
|
||||
data,
|
||||
preLoadedData,
|
||||
}) => {
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
@ -47,9 +53,10 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
});
|
||||
}, [data, reset]);
|
||||
}, [data, preLoadedData, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status && data) {
|
||||
|
@ -21,9 +21,10 @@ type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
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 { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -133,6 +134,7 @@ export const CreateUpdateViewModal: React.FC<Props> = ({ isOpen, handleClose, da
|
||||
handleClose={handleClose}
|
||||
status={data ? true : false}
|
||||
data={data}
|
||||
preLoadedData={preLoadedData}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
|
@ -439,12 +439,11 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
payload: {
|
||||
...myViewProps?.view_props,
|
||||
filters: {
|
||||
...myViewProps?.view_props?.filters,
|
||||
...viewDetails?.query_data,
|
||||
...(viewId ? viewDetails?.query_data : myViewProps?.view_props?.filters),
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
}, [myViewProps, viewDetails]);
|
||||
}, [myViewProps, viewDetails, viewId]);
|
||||
|
||||
return (
|
||||
<issueViewContext.Provider
|
||||
|
@ -34,7 +34,7 @@ export const ProjectMemberProvider: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: memberDetails, error } = useSWR(
|
||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString())
|
||||
: null,
|
||||
|
@ -13,7 +13,7 @@ import { orderArrayBy } from "helpers/array.helper";
|
||||
// fetch-keys
|
||||
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
|
||||
|
||||
const useEstimateOption = (estimateKey?: number) => {
|
||||
const useEstimateOption = (estimateKey?: number | null) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
const getValueFromLocalStorage = (key: string, defaultValue: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return defaultValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -24,14 +22,6 @@ const useProjectDetails = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectDetailsError?.status === 404) {
|
||||
router.push("/404");
|
||||
} else if (projectDetailsError) {
|
||||
router.push("/error");
|
||||
}
|
||||
}, [projectDetailsError, router]);
|
||||
|
||||
return {
|
||||
projectDetails,
|
||||
projectDetailsError,
|
||||
|
@ -21,7 +21,6 @@ import { PrimaryButton, Spinner } from "components/ui";
|
||||
// icons
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
|
||||
|
||||
type Meta = {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
@ -61,9 +60,7 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
issueView,
|
||||
} = useIssuesView();
|
||||
const { issueView } = useIssuesView();
|
||||
|
||||
const { loading, error, memberRole: memberType } = useProjectMyMembership();
|
||||
|
||||
@ -97,22 +94,6 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
</PrimaryButton>
|
||||
</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) ? (
|
||||
<NotAuthorizedView
|
||||
actionButton={
|
||||
|
@ -46,7 +46,7 @@ const WorkspacePage: NextPage = () => {
|
||||
// style={{ background: "linear-gradient(90deg, #8e2de2 0%, #4a00e0 100%)" }}
|
||||
>
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <a href="#" target="_blank" rel="noopener noreferrer">
|
||||
|
@ -43,6 +43,7 @@ const MyIssuesPage: NextPage = () => {
|
||||
<BreadcrumbItem title="My Issues" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
noPadding
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
{myIssues && myIssues.length > 0 && (
|
||||
@ -72,7 +73,10 @@ const MyIssuesPage: NextPage = () => {
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<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
|
||||
key={key}
|
||||
type="button"
|
||||
@ -85,7 +89,8 @@ const MyIssuesPage: NextPage = () => {
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,10 +120,10 @@ const MyIssuesPage: NextPage = () => {
|
||||
<div className="flex flex-col space-y-5">
|
||||
<Disclosure as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div className="rounded-[10px] border border-gray-300 bg-white">
|
||||
<div className="bg-white">
|
||||
<div
|
||||
className={`flex items-center justify-start bg-gray-100 px-5 py-3 ${
|
||||
open ? "rounded-t-[10px]" : "rounded-[10px]"
|
||||
open ? "" : "rounded-[10px]"
|
||||
}`}
|
||||
>
|
||||
<Disclosure.Button>
|
||||
|
@ -34,6 +34,7 @@ const defaultValues = {
|
||||
name: "",
|
||||
description: "",
|
||||
description_html: "",
|
||||
estimate_point: null,
|
||||
state: "",
|
||||
assignees_list: [],
|
||||
priority: "low",
|
||||
|
@ -14,6 +14,7 @@ import projectService from "services/project.service";
|
||||
import pagesService from "services/pages.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// icons
|
||||
import { PlusIcon } from "components/icons";
|
||||
// layouts
|
||||
@ -74,6 +75,8 @@ const ProjectPages: NextPage = () => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { storedValue: pageTab, setValue: setPageTab } = useLocalStorage("pageTab", "Recent");
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
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 (
|
||||
<>
|
||||
<CreateUpdatePageModal
|
||||
@ -193,7 +215,26 @@ const ProjectPages: NextPage = () => {
|
||||
)}
|
||||
</form>
|
||||
<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">
|
||||
<div className="flex gap-4">
|
||||
{["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> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${importerId}/`)
|
||||
async deleteImporterService(
|
||||
workspaceSlug: string,
|
||||
service: string,
|
||||
importerId: string
|
||||
): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
|
||||
.then((res) => res?.data)
|
||||
.catch((error) => {
|
||||
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_html: any;
|
||||
description_stripped: any;
|
||||
estimate_point: number;
|
||||
estimate_point: number | null;
|
||||
id: string;
|
||||
issue_cycle: IIssueCycle | null;
|
||||
issue_link: {
|
||||
|
Loading…
Reference in New Issue
Block a user