diff --git a/src/editor/CreateConnection.tsx b/src/editor/CreateConnection.tsx index 1b1d674..e286f2d 100644 --- a/src/editor/CreateConnection.tsx +++ b/src/editor/CreateConnection.tsx @@ -19,31 +19,29 @@ const CreateConnection: FC = ({ setConnection, error, isLoading }) => { } = useForm(); return ( -
+

Create Redmine Connection

{error ? (

Error {error.message}

) : null} - -
- - - -
-
-
+
+ + + +
+ ); }; diff --git a/src/editor/CreateIssue.tsx b/src/editor/CreateIssue.tsx new file mode 100644 index 0000000..6598881 --- /dev/null +++ b/src/editor/CreateIssue.tsx @@ -0,0 +1,45 @@ +import React, { FC } from "react"; +import { useForm } from "react-hook-form"; +import useCreateIssue, { Issue } from "./useCreateIssue"; +import { Screenshot } from "./Screenshot"; +import InputField from "./InputField"; +import Button from "./Button"; + +type Props = { + screenshot: Screenshot; +}; + +const CreateIssue: FC = ({ screenshot }) => { + const { create, isLoading, error } = useCreateIssue(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + return ( +
create(issue, screenshot))}> +

Create Issue

+ {error ? ( +

+ Error {error.message} +

+ ) : null} +
+ + + +
+
+ ); +}; + +export default CreateIssue; diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 42585ad..0654528 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -1,17 +1,35 @@ -import React, { FC } from "react"; +import React, { FC, MutableRefObject, useRef } from "react"; import ImageEditor from "./ImageEditor"; import IssueEditor from "./IssueEditor"; import "twin.macro"; +import { Screenshot } from "./Screenshot"; -const Editor: FC = () => ( -
-
- -
- -
-); +const createScreenshot = (stageRef: MutableRefObject): Screenshot => { + return { + toBlob: () => { + const canvas = stageRef.current.clearAndToCanvas({ + pixelRatio: stageRef.current._pixelRatio, + }) as HTMLCanvasElement; + return new Promise((resolve) => { + canvas.toBlob(resolve); + }); + }, + }; +}; + +const Editor: FC = () => { + const stageRef = useRef(null); + const screenshot = createScreenshot(stageRef); + return ( +
+
+ +
+ +
+ ); +}; export default Editor; diff --git a/src/editor/ImageEditor.tsx b/src/editor/ImageEditor.tsx index 5dc4608..74e54b9 100644 --- a/src/editor/ImageEditor.tsx +++ b/src/editor/ImageEditor.tsx @@ -1,13 +1,16 @@ -import React, { FC, useRef } from "react"; +import React, { FC, MutableRefObject, useRef } from "react"; import ReactImgEditor from "react-img-editor"; import "react-img-editor/assets/index.css"; import useBugShot from "./useBugShot"; import "twin.macro"; import useDimension from "./useDimension"; -const ImageEditor: FC = () => { +type Props = { + stageRef: MutableRefObject; +}; + +const ImageEditor: FC = ({ stageRef }) => { const image = useBugShot(); - const stageRef = useRef(null); const { ref, width, height } = useDimension(); const setStage = (stage: any) => { diff --git a/src/editor/IssueEditor.tsx b/src/editor/IssueEditor.tsx index c478c3a..f0da381 100644 --- a/src/editor/IssueEditor.tsx +++ b/src/editor/IssueEditor.tsx @@ -1,8 +1,15 @@ -import React from "react"; +import React, { FC } from "react"; import useConnection from "./useConnection"; import CreateConnection from "./CreateConnection"; +import { Screenshot } from "./Screenshot"; +import CreateIssue from "./CreateIssue"; +import "twin.macro"; -const IssueEditor = () => { +type Props = { + screenshot: Screenshot; +}; + +const Editor: FC = ({screenshot}) => { const { connection, isLoading, update } = useConnection(); if (isLoading) { @@ -13,12 +20,13 @@ const IssueEditor = () => { return ; } - return ( - <> -

Was geht?

-

{JSON.stringify(connection)}

- - ); + return ; }; +const IssueEditor: FC = ({screenshot}) => ( +
+ +
+); + export default IssueEditor; diff --git a/src/editor/Screenshot.ts b/src/editor/Screenshot.ts new file mode 100644 index 0000000..f48cc1a --- /dev/null +++ b/src/editor/Screenshot.ts @@ -0,0 +1,3 @@ +export type Screenshot = { + toBlob: () => Promise; +}; diff --git a/src/editor/useCreateIssue.ts b/src/editor/useCreateIssue.ts new file mode 100644 index 0000000..8e05c4b --- /dev/null +++ b/src/editor/useCreateIssue.ts @@ -0,0 +1,107 @@ +import { useState } from "react"; +import { Screenshot } from "./Screenshot"; +import useConnection from "./useConnection"; + +export type Issue = { + subject: string; + description: string; +}; + +type UploadResponse = { + upload: { + token: string; + }; +}; + +const useCreateIssue = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const { connection } = useConnection(); + + const create = (issue: Issue, screenshot: Screenshot) => { + setIsLoading(true); + + let baseUrl = connection.url; + if (!baseUrl.endsWith("/")) { + baseUrl += "/"; + } + + const filename = `bugshot-${new Date().toISOString()}.png`; + screenshot + .toBlob() + .then((blob) => + fetch(`${baseUrl}uploads.json?filename=${filename}`, { + headers: { + "X-Redmine-API-Key": connection.apiKey, + "Content-Type": "application/octet-stream", + }, + // do not prompt for basic auth if key authentication failed + credentials: "omit", + method: "POST", + body: blob, + }) + ) + .then((response) => { + if (!response.ok) { + throw new Error("failed to upload"); + } + return response; + }) + .then((response) => response.json()) + .then((upload: UploadResponse) => ({ + issue: { + project_id: 1, + tracker_id: 1, + status_id: 1, + priority_id: 1, + category_id: 57, + subject: issue.subject, + description: issue.description, + custom_fields: [ + { + id: 1, + value: "ITZ TAM", + }, + { + id: 36, + value: "Team SCM", + }, + { + id: 38, + value: "Created with bugshot!", + }, + ], + uploads: [ + { token: upload.upload.token, filename, content_type: "image/png" }, + ], + }, + })) + .then((body) => + fetch(`${baseUrl}issues.json`, { + method: "POST", + headers: { + "X-Redmine-API-Key": connection.apiKey, + "Content-Type": "application/json" + }, + // do not prompt for basic auth if key authentication failed + credentials: "omit", + body: JSON.stringify(body), + }) + ) + .then((resp) => { + if (!resp.ok) { + throw new Error("failed to create issue"); + } + }) + .catch(setError) + .finally(() => setIsLoading(false)); + }; + + return { + create, + isLoading, + error, + }; +}; + +export default useCreateIssue;