fix: merge conflicts

This commit is contained in:
Aaryan Khandelwal 2023-09-19 17:25:04 +05:30
commit 47866fb511
29 changed files with 405 additions and 190 deletions

View File

@ -33,14 +33,9 @@ jobs:
deploy: deploy:
- space/** - space/**
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Build Plane's Main App - name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true' if: steps.changed-files.outputs.web_any_changed == 'true'
run: | run: |
mv ./.npmrc ./web
cd web cd web
yarn yarn
yarn build yarn build

View File

@ -22,10 +22,6 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release - name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaFrontend id: metaFrontend
uses: docker/metadata-action@v4.3.0 uses: docker/metadata-action@v4.3.0

View File

@ -59,17 +59,6 @@ chmod +x setup.sh
> If running in a cloud env replace localhost with public facing IP address of the VM > If running in a cloud env replace localhost with public facing IP address of the VM
- Setup Tiptap Pro
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
```
@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
```
- Run Docker compose up - Run Docker compose up
```bash ```bash

View File

@ -11,14 +11,3 @@ cp ./apiserver/.env.example ./apiserver/.env
# Generate the SECRET_KEY that will be used by django # Generate the SECRET_KEY that will be used by django
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
# Generate Prompt for taking tiptap auth key
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m"
echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n"
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc

View File

@ -18,7 +18,6 @@ import Gapcursor from "@tiptap/extension-gapcursor";
import ts from "highlight.js/lib/languages/typescript"; import ts from "highlight.js/lib/languages/typescript";
import "highlight.js/styles/github-dark.css"; import "highlight.js/styles/github-dark.css";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image"; import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator"; import isValidHttpUrl from "../bubble-menu/utils/link-validator";
import { CustomTableCell } from "./table/table-cell"; import { CustomTableCell } from "./table/table-cell";
@ -121,9 +120,6 @@ export const TiptapExtensions = (
}, },
includeChildren: true, includeChildren: true,
}), }),
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting), SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,

View File

@ -17,7 +17,6 @@
"@heroicons/react": "^2.0.12", "@heroicons/react": "^2.0.12",
"@mui/icons-material": "^5.14.1", "@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.1", "@mui/material": "^5.14.1",
"@tiptap-pro/extension-unique-id": "^2.1.0",
"@tiptap/extension-code-block-lowlight": "^2.0.4", "@tiptap/extension-code-block-lowlight": "^2.0.4",
"@tiptap/extension-color": "^2.0.4", "@tiptap/extension-color": "^2.0.4",
"@tiptap/extension-gapcursor": "^2.1.7", "@tiptap/extension-gapcursor": "^2.1.7",

View File

@ -41,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => {
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false); const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, issueId, inboxId } = router.query; const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query;
const { user } = useUser(); const { user } = useUser();
@ -183,6 +183,13 @@ export const CommandPalette: React.FC = observer(() => {
isOpen={isIssueModalOpen} isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)} handleClose={() => setIsIssueModalOpen(false)}
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]} fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
prePopulateData={
cycleId
? { cycle: cycleId.toString() }
: moduleId
? { module: moduleId.toString() }
: undefined
}
/> />
<BulkDeleteIssuesModal <BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen} isOpen={isBulkDeleteIssuesModalOpen}

View File

@ -2,8 +2,9 @@ import { useRouter } from "next/router";
// icons // icons
import { Icon, Tooltip } from "components/ui"; import { Icon, Tooltip } from "components/ui";
import { CopyPlus } from "lucide-react";
import { Squares2X2Icon } from "@heroicons/react/24/outline"; import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon } from "components/icons"; import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
// helpers // helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper"; import { capitalizeFirstLetter } from "helpers/string.helper";
@ -157,7 +158,7 @@ const activityDetails: {
}, },
icon: <BlockerIcon height="12" width="12" color="#6b7280" />, icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
}, },
blocks: { blocked_by: {
message: (activity) => { message: (activity) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
@ -176,6 +177,44 @@ const activityDetails: {
}, },
icon: <BlockedIcon height="12" width="12" color="#6b7280" />, icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
}, },
duplicate: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked this issue as duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed this issue as a duplicate of{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <CopyPlus size={12} color="#6b7280" />,
},
relates_to: {
message: (activity) => {
if (activity.old_value === "")
return (
<>
marked that this issue relates to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
</>
);
else
return (
<>
removed the relation from{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
</>
);
},
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
},
cycles: { cycles: {
message: (activity, showIssue, workspaceSlug) => { message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created") if (activity.verb === "created")

View File

@ -23,7 +23,6 @@ import {
CreateUpdateIssueModal, CreateUpdateIssueModal,
DeleteIssueModal, DeleteIssueModal,
DeleteDraftIssueModal, DeleteDraftIssueModal,
IssuePeekOverview,
CreateUpdateDraftIssueModal, CreateUpdateDraftIssueModal,
} from "components/issues"; } from "components/issues";
import { CreateUpdateViewModal } from "components/views"; import { CreateUpdateViewModal } from "components/views";
@ -484,15 +483,7 @@ export const IssuesView: React.FC<Props> = ({
} }
: null : null
} }
fieldsToShow={[ fieldsToShow={["all"]}
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
]}
/> />
<CreateUpdateIssueModal <CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"} isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}

View File

@ -50,12 +50,12 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
}; };
const handleDeletion = async () => { const handleDeletion = async () => {
if (!workspaceSlug || !data) return; if (!workspaceSlug || !data || !user) return;
setIsDeleteLoading(true); setIsDeleteLoading(true);
await issueServices await issueServices
.deleteDraftIssue(workspaceSlug as string, data.project, data.id) .deleteDraftIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => { .then(() => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
handleClose(); handleClose();

View File

@ -14,6 +14,7 @@ import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useLocalStorage from "hooks/use-local-storage";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues"; import useMyIssues from "hooks/my-issues/use-my-issues";
// components // components
@ -79,6 +80,8 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
const { user } = useUser(); const { user } = useUser();
const { projects } = useProjects(); const { projects } = useProjects();
const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {});
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -111,11 +114,14 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
return; return;
} }
if (prePopulateData && prePopulateData.project)
return setActiveProject(prePopulateData.project);
// if data is not present, set active project to the project // if data is not present, set active project to the project
// in the url. This has the least priority. // in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject) if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen]); }, [activeProject, data, projectId, projects, isOpen, prePopulateData]);
const calendarFetchKey = cycleId const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
@ -228,6 +234,8 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
if (!data) await createIssue(payload); if (!data) await createIssue(payload);
else await updateIssue(payload); else await updateIssue(payload);
clearDraftIssueLocalStorage();
if (onSubmit) await onSubmit(payload); if (onSubmit) await onSubmit(payload);
}; };

View File

@ -11,7 +11,6 @@ import { Controller, useForm } from "react-hook-form";
import aiService from "services/ai.service"; import aiService from "services/ai.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// components // components
import { GptAssistantModal } from "components/core"; import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal, TIssueFormAttributes } from "components/issues"; import { ParentIssuesListModal, TIssueFormAttributes } from "components/issues";
@ -59,11 +58,9 @@ export interface IssueFormProps {
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>; setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean; createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>; setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
handleDiscardClose: () => void; handleDiscardClose: () => void;
status: boolean; status: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleFormDirty: (payload: Partial<IIssue> | null) => void; handleFormDirty: (payload: Partial<IIssue> | null) => void;
customAttributesList: { [key: string]: string[] }; customAttributesList: { [key: string]: string[] };
handleCustomAttributesChange: (attributeId: string, val: string | string[] | undefined) => void; handleCustomAttributesChange: (attributeId: string, val: string | string[] | undefined) => void;
@ -95,8 +92,6 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
const [gptAssistantModal, setGptAssistantModal] = useState(false); const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const router = useRouter(); const router = useRouter();
@ -127,9 +122,11 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
state: getValues("state"), state: getValues("state"),
priority: getValues("priority"), priority: getValues("priority"),
assignees: getValues("assignees"), assignees: getValues("assignees"),
target_date: getValues("target_date"),
labels: getValues("labels"), labels: getValues("labels"),
start_date: getValues("start_date"),
target_date: getValues("target_date"),
project: getValues("project"), project: getValues("project"),
parent: getValues("parent"),
}; };
useEffect(() => { useEffect(() => {
@ -582,8 +579,6 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SecondaryButton <SecondaryButton
onClick={() => { onClick={() => {
const data = JSON.stringify(getValues());
setValueInLocalStorage(data);
handleDiscardClose(); handleDiscardClose();
}} }}
> >

View File

@ -22,6 +22,7 @@ import useInboxView from "hooks/use-inbox-view";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues"; import useMyIssues from "hooks/my-issues/use-my-issues";
import useLocalStorage from "hooks/use-local-storage";
// components // components
import { IssueForm, ConfirmIssueDiscard } from "components/issues"; import { IssueForm, ConfirmIssueDiscard } from "components/issues";
// types // types
@ -105,10 +106,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } =
useLocalStorage<any>("draftedIssue", {});
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
prePopulateData = { prePopulateData = {
...prePopulateData, ...prePopulateData,
@ -116,21 +118,21 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
}; };
const onClose = () => { const onClose = () => {
if (!showConfirmDiscard) handleClose();
if (formDirtyState === null) return setActiveProject(null);
const data = JSON.stringify(formDirtyState);
setValueInLocalStorage(data);
};
const onDiscardClose = () => {
if (formDirtyState !== null) { if (formDirtyState !== null) {
setShowConfirmDiscard(true); setShowConfirmDiscard(true);
} else { } else {
handleClose(); handleClose();
setActiveProject(null); setActiveProject(null);
setCustomAttributesList({});
} }
}; };
const onDiscardClose = () => {
handleClose();
setActiveProject(null);
setCustomAttributesList({});
};
const handleFormDirty = (data: any) => { const handleFormDirty = (data: any) => {
setFormDirtyState(data); setFormDirtyState(data);
}; };
@ -241,6 +243,27 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
}); });
}; };
useEffect(() => {
// if modal is closed, reset active project to null
// and return to avoid activeProject being set to some other project
if (!isOpen) {
setActiveProject(null);
return;
}
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) {
setActiveProject(data.project);
return;
}
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen]);
const calendarFetchKey = cycleId const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId : moduleId
@ -466,9 +489,9 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
setActiveProject(null); setActiveProject(null);
setFormDirtyState(null); setFormDirtyState(null);
setShowConfirmDiscard(false); setShowConfirmDiscard(false);
clearLocalStorageValue();
}} }}
/> />
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}> <Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child <Transition.Child
@ -500,16 +523,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
initialData={data ?? prePopulateData} initialData={data ?? prePopulateData}
createMore={createMore} createMore={createMore}
setCreateMore={setCreateMore} setCreateMore={setCreateMore}
handleClose={onClose}
handleDiscardClose={onDiscardClose} handleDiscardClose={onDiscardClose}
setIsConfirmDiscardOpen={setShowConfirmDiscard}
projectId={activeProject ?? ""} projectId={activeProject ?? ""}
setActiveProject={setActiveProject} setActiveProject={setActiveProject}
status={data ? true : false} status={data ? true : false}
user={user} user={user}
customAttributesList={customAttributesList}
handleCustomAttributesChange={handleCustomAttributesChange}
fieldsToShow={fieldsToShow} fieldsToShow={fieldsToShow}
handleCustomAttributesChange={handleCustomAttributesChange}
handleFormDirty={handleFormDirty} handleFormDirty={handleFormDirty}
/> />
</Dialog.Panel> </Dialog.Panel>

View File

@ -73,7 +73,7 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
...selectedIssues.map((issue) => ({ ...selectedIssues.map((issue) => ({
issue: issueId as string, issue: issueId as string,
relation_type: "blocked_by" as const, relation_type: "blocked_by" as const,
related_issue_detail: issue.blocked_issue_detail, issue_detail: issue.blocked_issue_detail,
related_issue: issue.blocked_issue_detail.id, related_issue: issue.blocked_issue_detail.id,
})), })),
], ],
@ -111,17 +111,17 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
{blockedByIssue && blockedByIssue.length > 0 {blockedByIssue && blockedByIssue.length > 0
? blockedByIssue.map((relation) => ( ? blockedByIssue.map((relation) => (
<div <div
key={relation.related_issue_detail?.id} key={relation?.id}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20" className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
> >
<a <a
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`} href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<BlockedIcon height={10} width={10} /> <BlockedIcon height={10} width={10} />
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`} {`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
</a> </a>
<button <button
type="button" type="button"

View File

@ -81,6 +81,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
related_issue_detail: issue.blocker_issue_detail, related_issue_detail: issue.blocker_issue_detail,
})), })),
], ],
relation: "blocking",
}) })
.then((response) => { .then((response) => {
submitChanges({ submitChanges({
@ -89,7 +90,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
...(response ?? []).map((i: any) => ({ ...(response ?? []).map((i: any) => ({
id: i.id, id: i.id,
relation_type: i.relation_type, relation_type: i.relation_type,
issue_detail: i.related_issue_detail, issue_detail: i.issue_detail,
issue: i.related_issue, issue: i.related_issue,
})), })),
], ],
@ -118,7 +119,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
{blockerIssue && blockerIssue.length > 0 {blockerIssue && blockerIssue.length > 0
? blockerIssue.map((relation) => ( ? blockerIssue.map((relation) => (
<div <div
key={relation.issue_detail?.id} key={relation.id}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20" className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
> >
<a <a
@ -134,9 +135,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
type="button" type="button"
className="opacity-0 duration-300 group-hover:opacity-100" className="opacity-0 duration-300 group-hover:opacity-100"
onClick={() => { onClick={() => {
const updatedBlockers = blockerIssue.filter( const updatedBlockers = blockerIssue.filter((i) => i.id !== relation.id);
(i) => i.issue_detail?.id !== relation.issue_detail?.id
);
submitChanges({ submitChanges({
issue_relations: updatedBlockers, issue_relations: updatedBlockers,

View File

@ -18,7 +18,7 @@ import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types";
type Props = { type Props = {
issueId?: string; issueId?: string;
submitChanges: (formData: Partial<IIssue>) => void; submitChanges: (formData?: Partial<IIssue>) => void;
watch: UseFormWatch<IIssue>; watch: UseFormWatch<IIssue>;
disabled?: boolean; disabled?: boolean;
}; };
@ -69,7 +69,7 @@ export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
related_list: [ related_list: [
...selectedIssues.map((issue) => ({ ...selectedIssues.map((issue) => ({
issue: issueId as string, issue: issueId as string,
related_issue_detail: issue.blocker_issue_detail, issue_detail: issue.blocker_issue_detail,
related_issue: issue.blocker_issue_detail.id, related_issue: issue.blocker_issue_detail.id,
relation_type: "duplicate" as const, relation_type: "duplicate" as const,
})), })),
@ -90,7 +90,7 @@ export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
?.filter((i) => i.relation_type === "duplicate") ?.filter((i) => i.relation_type === "duplicate")
.map((i) => ({ .map((i) => ({
...i, ...i,
related_issue_detail: i.issue_detail, issue_detail: i.issue_detail,
related_issue: i.issue_detail?.id, related_issue: i.issue_detail?.id,
})), })),
]; ];
@ -114,39 +114,35 @@ export const SidebarDuplicateSelect: React.FC<Props> = (props) => {
{duplicateIssuesRelation && duplicateIssuesRelation.length > 0 {duplicateIssuesRelation && duplicateIssuesRelation.length > 0
? duplicateIssuesRelation.map((relation) => ( ? duplicateIssuesRelation.map((relation) => (
<div <div
key={relation.related_issue_detail?.id} key={relation.issue_detail?.id}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20" className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
> >
<a <a
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`} href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<BlockerIcon height={10} width={10} /> <BlockerIcon height={10} width={10} />
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`} {`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
</a> </a>
<button <button
type="button" type="button"
className="opacity-0 duration-300 group-hover:opacity-100" className="opacity-0 duration-300 group-hover:opacity-100"
onClick={() => { onClick={() => {
const updatedBlockers = duplicateIssuesRelation.filter(
(i) => i.related_issue_detail?.id !== relation.related_issue_detail?.id
);
submitChanges({
related_issues: updatedBlockers,
});
if (!user) return; if (!user) return;
issuesService.deleteIssueRelation( issuesService
workspaceSlug as string, .deleteIssueRelation(
projectId as string, workspaceSlug as string,
issueId as string, projectId as string,
relation.id, issueId as string,
user relation.id,
); user
)
.then(() => {
submitChanges();
});
}} }}
> >
<X className="h-2 w-2" /> <X className="h-2 w-2" />

View File

@ -18,7 +18,7 @@ import { BlockeIssueDetail, IIssue, ISearchIssueResponse } from "types";
type Props = { type Props = {
issueId?: string; issueId?: string;
submitChanges: (formData: Partial<IIssue>) => void; submitChanges: (formData?: Partial<IIssue>) => void;
watch: UseFormWatch<IIssue>; watch: UseFormWatch<IIssue>;
disabled?: boolean; disabled?: boolean;
}; };
@ -69,7 +69,7 @@ export const SidebarRelatesSelect: React.FC<Props> = (props) => {
related_list: [ related_list: [
...selectedIssues.map((issue) => ({ ...selectedIssues.map((issue) => ({
issue: issueId as string, issue: issueId as string,
related_issue_detail: issue.blocker_issue_detail, issue_detail: issue.blocker_issue_detail,
related_issue: issue.blocker_issue_detail.id, related_issue: issue.blocker_issue_detail.id,
relation_type: "relates_to" as const, relation_type: "relates_to" as const,
})), })),
@ -90,7 +90,7 @@ export const SidebarRelatesSelect: React.FC<Props> = (props) => {
?.filter((i) => i.relation_type === "relates_to") ?.filter((i) => i.relation_type === "relates_to")
.map((i) => ({ .map((i) => ({
...i, ...i,
related_issue_detail: i.issue_detail, issue_detail: i.issue_detail,
related_issue: i.issue_detail?.id, related_issue: i.issue_detail?.id,
})), })),
]; ];
@ -114,39 +114,35 @@ export const SidebarRelatesSelect: React.FC<Props> = (props) => {
{relatedToIssueRelation && relatedToIssueRelation.length > 0 {relatedToIssueRelation && relatedToIssueRelation.length > 0
? relatedToIssueRelation.map((relation) => ( ? relatedToIssueRelation.map((relation) => (
<div <div
key={relation.related_issue_detail?.id} key={relation.issue_detail?.id}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20" className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
> >
<a <a
href={`/${workspaceSlug}/projects/${relation.related_issue_detail?.project_detail.id}/issues/${relation.related_issue_detail?.id}`} href={`/${workspaceSlug}/projects/${relation.issue_detail?.project_detail.id}/issues/${relation.issue_detail?.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<BlockerIcon height={10} width={10} /> <BlockerIcon height={10} width={10} />
{`${relation.related_issue_detail?.project_detail.identifier}-${relation.related_issue_detail?.sequence_id}`} {`${relation.issue_detail?.project_detail.identifier}-${relation.issue_detail?.sequence_id}`}
</a> </a>
<button <button
type="button" type="button"
className="opacity-0 duration-300 group-hover:opacity-100" className="opacity-0 duration-300 group-hover:opacity-100"
onClick={() => { onClick={() => {
const updatedBlockers = relatedToIssueRelation.filter(
(i) => i.related_issue_detail?.id !== relation.related_issue_detail?.id
);
submitChanges({
related_issues: updatedBlockers,
});
if (!user) return; if (!user) return;
issuesService.deleteIssueRelation( issuesService
workspaceSlug as string, .deleteIssueRelation(
projectId as string, workspaceSlug as string,
issueId as string, projectId as string,
relation.id, issueId as string,
user relation.id,
); user
)
.then(() => {
submitChanges();
});
}} }}
> >
<X className="h-2 w-2" /> <X className="h-2 w-2" />

View File

@ -514,17 +514,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<SidebarDuplicateSelect <SidebarDuplicateSelect
issueId={issueId as string} issueId={issueId as string}
submitChanges={(data: any) => { submitChanges={(data: any) => {
mutate<IIssue>( if (!data) return mutate(ISSUE_DETAILS(issueId as string));
ISSUE_DETAILS(issueId as string), mutate<IIssue>(ISSUE_DETAILS(issueId as string), (prevData) => {
(prevData) => { if (!prevData) return prevData;
if (!prevData) return prevData; return {
return { ...prevData,
...prevData, ...data,
...data, };
}; });
},
false
);
}} }}
watch={watchIssue} watch={watchIssue}
disabled={memberRole.isGuest || memberRole.isViewer || uneditable} disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
@ -534,17 +531,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<SidebarRelatesSelect <SidebarRelatesSelect
issueId={issueId as string} issueId={issueId as string}
submitChanges={(data: any) => { submitChanges={(data: any) => {
mutate<IIssue>( if (!data) return mutate(ISSUE_DETAILS(issueId as string));
ISSUE_DETAILS(issueId as string), mutate<IIssue>(ISSUE_DETAILS(issueId as string), (prevData) => {
(prevData) => { if (!prevData) return prevData;
if (!prevData) return prevData; return {
return { ...prevData,
...prevData, ...data,
...data, };
}; });
},
false
);
}} }}
watch={watchIssue} watch={watchIssue}
disabled={memberRole.isGuest || memberRole.isViewer || uneditable} disabled={memberRole.isGuest || memberRole.isViewer || uneditable}

View File

@ -18,7 +18,6 @@ import Gapcursor from "@tiptap/extension-gapcursor";
import ts from "highlight.js/lib/languages/typescript"; import ts from "highlight.js/lib/languages/typescript";
import "highlight.js/styles/github-dark.css"; import "highlight.js/styles/github-dark.css";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image"; import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator"; import isValidHttpUrl from "../bubble-menu/utils/link-validator";
import { CustomTableCell } from "./table/table-cell"; import { CustomTableCell } from "./table/table-cell";
@ -121,9 +120,6 @@ export const TiptapExtensions = (
}, },
includeChildren: true, includeChildren: true,
}), }),
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting), SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,

View File

@ -1,7 +1,7 @@
export type ButtonProps = { export type ButtonProps = {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
onClick?: () => void; onClick?: (e: any) => void;
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;

View File

@ -1,9 +1,10 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// icons // icons
import { CopyPlus } from "lucide-react";
import { Icon, Tooltip } from "components/ui"; import { Icon, Tooltip } from "components/ui";
import { Squares2X2Icon } from "@heroicons/react/24/outline"; import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon } from "components/icons"; import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
// helpers // helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper"; import { capitalizeFirstLetter } from "helpers/string.helper";
@ -117,7 +118,7 @@ const activityDetails: {
icon: <BlockerIcon height="12" width="12" color="#6b7280" />, icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
}, },
blocks: { blocked_by: {
message: (activity) => ( message: (activity) => (
<> <>
{activity.old_value === "" {activity.old_value === ""
@ -132,6 +133,36 @@ const activityDetails: {
icon: <BlockedIcon height="12" width="12" color="#6b7280" />, icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
}, },
duplicate: {
message: (activity) => (
<>
{activity.old_value === ""
? "marked this issue as duplicate of "
: "removed this issue as a duplicate of "}
<span className="font-medium text-custom-text-100">
{activity.verb === "created" ? activity.new_value : activity.old_value}
</span>
.
</>
),
icon: <CopyPlus size={12} color="#6b7280" />,
},
relates_to: {
message: (activity) => (
<>
{activity.old_value === ""
? "marked that this issue relates to "
: "removed the relation from "}
<span className="font-medium text-custom-text-100">
{activity.old_value === "" ? activity.new_value : activity.old_value}
</span>
.
</>
),
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
},
cycles: { cycles: {
message: (activity) => ( message: (activity) => (
<> <>

View File

@ -351,25 +351,23 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
{blockedIssue && {blockedIssue &&
blockedIssue.map((issue) => ( blockedIssue.map((issue) => (
<div <div
key={issue.related_issue_detail?.id} key={issue.issue_detail?.id}
className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20" className="group inline-flex mr-1 cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
> >
<a <a
href={`/${workspaceSlug}/projects/${issue.related_issue_detail?.project_detail.id}/issues/${issue.related_issue_detail?.id}`} href={`/${workspaceSlug}/projects/${issue.issue_detail?.project_detail.id}/issues/${issue.issue_detail?.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<BlockedIcon height={10} width={10} /> <BlockedIcon height={10} width={10} />
{`${issue?.related_issue_detail?.project_detail?.identifier}-${issue?.related_issue_detail?.sequence_id}`} {`${issue?.issue_detail?.project_detail?.identifier}-${issue?.issue_detail?.sequence_id}`}
</a> </a>
<button <button
type="button" type="button"
className="duration-300" className="duration-300"
onClick={() => { onClick={() => {
const updatedBlocked = blockedIssue.filter( const updatedBlocked = blockedIssue.filter((i) => i?.id !== issue?.id);
(i) => i.related_issue_detail?.id !== issue.related_issue_detail?.id
);
if (!user) return; if (!user) return;

View File

@ -17,7 +17,10 @@ export const WorkspaceSidebarQuickAction = () => {
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null); const { storedValue, clearValue } = useLocalStorage<any>(
"draftedIssue",
JSON.stringify(undefined)
);
return ( return (
<> <>
@ -30,18 +33,7 @@ export const WorkspaceSidebarQuickAction = () => {
clearValue(); clearValue();
setIsDraftIssueModalOpen(false); setIsDraftIssueModalOpen(false);
}} }}
fieldsToShow={[ fieldsToShow={["all"]}
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
"state",
"startDate",
"project",
]}
/> />
<div <div
@ -50,7 +42,7 @@ export const WorkspaceSidebarQuickAction = () => {
}`} }`}
> >
<div <div
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 ${ className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${
store?.theme?.sidebarCollapsed store?.theme?.sidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80" ? "px-2 hover:bg-custom-sidebar-background-80"
: "px-3 shadow border-[0.5px] border-custom-border-300" : "px-3 shadow border-[0.5px] border-custom-border-300"

View File

@ -26,7 +26,6 @@
"@nivo/pie": "0.80.0", "@nivo/pie": "0.80.0",
"@nivo/scatterplot": "0.80.0", "@nivo/scatterplot": "0.80.0",
"@sentry/nextjs": "^7.36.0", "@sentry/nextjs": "^7.36.0",
"@tiptap-pro/extension-unique-id": "^2.1.0",
"@tiptap/extension-code-block-lowlight": "^2.0.4", "@tiptap/extension-code-block-lowlight": "^2.0.4",
"@tiptap/extension-color": "^2.0.4", "@tiptap/extension-color": "^2.0.4",
"@tiptap/extension-gapcursor": "^2.1.7", "@tiptap/extension-gapcursor": "^2.1.7",

View File

@ -154,6 +154,7 @@ class ProjectIssuesServices extends APIService {
relation_type: "duplicate" | "relates_to" | "blocked_by"; relation_type: "duplicate" | "relates_to" | "blocked_by";
related_issue: string; related_issue: string;
}>; }>;
relation?: "blocking" | null;
} }
) { ) {
return this.post( return this.post(
@ -658,7 +659,12 @@ class ProjectIssuesServices extends APIService {
}); });
} }
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> { async deleteDraftIssue(
workspaceSlug: string,
projectId: string,
issueId: string,
user: ICurrentUserResponse
): Promise<any> {
return this.delete( return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/` `/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
) )

189
web/store/draft-issue.ts Normal file
View File

@ -0,0 +1,189 @@
// mobx
import { action, observable, runInAction, makeAutoObservable } from "mobx";
// services
import issueService from "services/issues.service";
// types
import type { ICurrentUserResponse, IIssue } from "types";
class DraftIssuesStore {
issues: { [key: string]: IIssue } = {};
isIssuesLoading: boolean = false;
rootStore: any | null = null;
constructor(_rootStore: any | null = null) {
makeAutoObservable(this, {
issues: observable.ref,
isIssuesLoading: observable.ref,
rootStore: observable.ref,
loadDraftIssues: action,
getIssueById: action,
createDraftIssue: action,
updateDraftIssue: action,
deleteDraftIssue: action,
});
this.rootStore = _rootStore;
}
/**
* @description Fetch all draft issues of a project and hydrate issues field
*/
loadDraftIssues = async (workspaceSlug: string, projectId: string, params?: any) => {
this.isIssuesLoading = true;
try {
const issuesResponse = await issueService.getDraftIssues(workspaceSlug, projectId, params);
const issues = Array.isArray(issuesResponse) ? { allIssues: issuesResponse } : issuesResponse;
runInAction(() => {
this.issues = issues;
this.isIssuesLoading = false;
});
} catch (error) {
this.isIssuesLoading = false;
console.error("Fetching issues error", error);
}
};
/**
* @description Fetch a single draft issue by id and hydrate issues field
* @param workspaceSlug
* @param projectId
* @param issueId
* @returns {IIssue}
*/
getIssueById = async (
workspaceSlug: string,
projectId: string,
issueId: string
): Promise<IIssue> => {
if (this.issues[issueId]) return this.issues[issueId];
try {
const issueResponse: IIssue = await issueService.getDraftIssueById(
workspaceSlug,
projectId,
issueId
);
const issues = {
...this.issues,
[issueId]: { ...issueResponse },
};
runInAction(() => {
this.issues = issues;
});
return issueResponse;
} catch (error) {
throw error;
}
};
/**
* @description Create a new draft issue and hydrate issues field
* @param workspaceSlug
* @param projectId
* @param issueForm
* @param user
* @returns {IIssue}
*/
createDraftIssue = async (
workspaceSlug: string,
projectId: string,
issueForm: IIssue,
user: ICurrentUserResponse
): Promise<IIssue> => {
try {
const issueResponse = await issueService.createDraftIssue(
workspaceSlug,
projectId,
issueForm,
user
);
const issues = {
...this.issues,
[issueResponse.id]: { ...issueResponse },
};
runInAction(() => {
this.issues = issues;
});
return issueResponse;
} catch (error) {
console.error("Creating issue error", error);
throw error;
}
};
updateDraftIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
issueForm: Partial<IIssue>,
user: ICurrentUserResponse
) => {
// keep a copy of the issue in the store
const originalIssue = { ...this.issues[issueId] };
// immediately update the issue in the store
const updatedIssue = { ...this.issues[issueId], ...issueForm };
if (updatedIssue.assignees_list) updatedIssue.assignees = updatedIssue.assignees_list;
try {
runInAction(() => {
this.issues[issueId] = { ...updatedIssue };
});
// make a patch request to update the issue
const issueResponse: IIssue = await issueService.updateDraftIssue(
workspaceSlug,
projectId,
issueId,
issueForm,
user
);
const updatedIssues = { ...this.issues };
updatedIssues[issueId] = { ...issueResponse };
runInAction(() => {
this.issues = updatedIssues;
});
} catch (error) {
// if there is an error, revert the changes
runInAction(() => {
this.issues[issueId] = originalIssue;
});
return error;
}
};
deleteDraftIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
user: ICurrentUserResponse
) => {
const issues = { ...this.issues };
delete issues[issueId];
try {
runInAction(() => {
this.issues = issues;
});
issueService.deleteDraftIssue(workspaceSlug, projectId, issueId, user);
} catch (error) {
console.error("Deleting issue error", error);
}
};
}
export default DraftIssuesStore;

View File

@ -8,6 +8,7 @@ import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
import IssuesStore from "./issues"; import IssuesStore from "./issues";
import CustomAttributesStore from "./custom-attributes"; import CustomAttributesStore from "./custom-attributes";
import CustomAttributeValuesStore from "./custom-attribute-values"; import CustomAttributeValuesStore from "./custom-attribute-values";
import DraftIssuesStore from "./draft-issue";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -19,6 +20,7 @@ export class RootStore {
issues: IssuesStore; issues: IssuesStore;
customAttributes: CustomAttributesStore; customAttributes: CustomAttributesStore;
customAttributeValues: CustomAttributeValuesStore; customAttributeValues: CustomAttributeValuesStore;
draftIssuesStore: DraftIssuesStore;
constructor() { constructor() {
this.user = new UserStore(this); this.user = new UserStore(this);
@ -28,5 +30,6 @@ export class RootStore {
this.issues = new IssuesStore(this); this.issues = new IssuesStore(this);
this.customAttributes = new CustomAttributesStore(this); this.customAttributes = new CustomAttributesStore(this);
this.customAttributeValues = new CustomAttributeValuesStore(this); this.customAttributeValues = new CustomAttributeValuesStore(this);
this.draftIssuesStore = new DraftIssuesStore(this);
} }
} }

View File

@ -70,8 +70,10 @@ export type IssueRelationType = "duplicate" | "relates_to" | "blocked_by";
export interface IssueRelation { export interface IssueRelation {
id: string; id: string;
issue: string; issue: string;
related_issue: string; issue_detail: BlockeIssueDetail;
relation_type: IssueRelationType; relation_type: IssueRelationType;
related_issue: string;
relation: "blocking" | null;
} }
export interface IIssue { export interface IIssue {
@ -96,6 +98,8 @@ export interface IIssue {
relation_type: IssueRelationType; relation_type: IssueRelationType;
related_issue: string; related_issue: string;
}[]; }[];
issue_relations: IssueRelation[];
related_issues: IssueRelation[];
bridge_id?: string | null; bridge_id?: string | null;
completed_at: Date; completed_at: Date;
created_at: string; created_at: string;

View File

@ -2174,13 +2174,6 @@
lodash.merge "^4.6.2" lodash.merge "^4.6.2"
postcss-selector-parser "6.0.10" postcss-selector-parser "6.0.10"
"@tiptap-pro/extension-unique-id@^2.1.0":
version "2.2.3"
resolved "https://registry.tiptap.dev/@tiptap-pro%2fextension-unique-id/-/extension-unique-id-2.2.3.tgz#151a570ef8363bf460bf5b08dc0581fb182ebabc"
integrity sha512-Y1jM+6hebNltFZ+0fbC+NcOCU647KjRtqJ7jEZVFoP12ZMocZNqTTabsSIys0UXOkSaJy3/H6Z/ybygAHY/dBg==
dependencies:
uuid "^8.3.2"
"@tiptap/core@^2.1.8": "@tiptap/core@^2.1.8":
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.8.tgz#4555dc7d86580dee790d4aded1ce7fb79319da70" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.8.tgz#4555dc7d86580dee790d4aded1ce7fb79319da70"
@ -8188,11 +8181,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0: uuid@^9.0.0:
version "9.0.1" version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"