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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -152,14 +152,16 @@ export const SingleEstimate: React.FC<Props> = ({
</CustomMenu> </CustomMenu>
</div> </div>
{estimatePoints && estimatePoints.length > 0 ? ( {estimatePoints && estimatePoints.length > 0 ? (
<div className="flex gap-2 text-sm text-gray-400"> <div className="flex text-sm text-gray-400">
Estimate points ( Estimate points (
<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>
) : ( ) : (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@ const MyIssuesPage: NextPage = () => {
<BreadcrumbItem title="My Issues" /> <BreadcrumbItem title="My Issues" />
</Breadcrumbs> </Breadcrumbs>
} }
noPadding
right={ right={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{myIssues && myIssues.length > 0 && ( {myIssues && myIssues.length > 0 && (
@ -72,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>

View File

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

View File

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

View File

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

View File

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