Merge branch 'develop' into self-hosting-tweaks

This commit is contained in:
Kyle Lacy 2023-04-17 12:30:24 -07:00
commit e22f552ea0
No known key found for this signature in database
GPG Key ID: 82616D2392FB6605
37 changed files with 299 additions and 170 deletions

View File

@ -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"]}
)
validated_data["query"] = issue_filters(query_params, "POST")
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)

View File

@ -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)

View File

@ -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
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
if DOCKERIZED:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
else:
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url

View File

@ -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);

View File

@ -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") {

View File

@ -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,

View File

@ -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">
<FilterList filters={filters} setFilters={setFilters} />
{Object.keys(filters).length > 0 &&
nullFilters.length !== Object.keys(filters).length && (
{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} />
{areFiltersApplied && (
<PrimaryButton
onClick={() => {
if (viewId) {
@ -413,20 +420,19 @@ export const IssuesView: React.FC<Props> = ({
{viewId ? "Update" : "Save"} view
</PrimaryButton>
)}
</div>
</div>
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
<div className="mb-5 border-t" />
</div>
{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>

View File

@ -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;

View File

@ -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);

View File

@ -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">
Estimate points(
{estimatePoints.map((point, index) => (
<h6 key={point.id}>
{point.value}
{index !== estimatePoints.length - 1 && ","}{" "}
</h6>
))}
<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>
) : (

View File

@ -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 <></>;

View File

@ -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>

View File

@ -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 =

View File

@ -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") {

View File

@ -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 ? (

View File

@ -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">

View File

@ -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" />

View File

@ -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={

View File

@ -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" />

View File

@ -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}
/>
)}

View File

@ -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>

View File

@ -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) => {

View File

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

View File

@ -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) {

View File

@ -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>

View File

@ -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

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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={

View File

@ -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">

View File

@ -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,20 +73,24 @@ 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) => (
<button
key={key}
type="button"
className={`rounded border border-theme px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-theme bg-theme text-white"
: ""
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
</button>
))}
{Object.keys(properties).map((key) => {
if (key === "estimate") return null;
return (
<button
key={key}
type="button"
className={`rounded border border-theme px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
? "border-theme bg-theme text-white"
: ""
}`}
onClick={() => setProperties(key as keyof Properties)}
>
{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>

View File

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

View File

@ -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(

View File

@ -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;

View File

@ -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: {