-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
223 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Props> = ({ screenshot }) => { | ||
const { create, isLoading, error } = useCreateIssue(); | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<Issue>(); | ||
return ( | ||
<form onSubmit={handleSubmit((issue) => create(issue, screenshot))}> | ||
<h2 tw="text-2xl font-bold">Create Issue</h2> | ||
{error ? ( | ||
<p tw="text-red-700"> | ||
<strong>Error</strong> {error.message} | ||
</p> | ||
) : null} | ||
<div tw="mt-8 grid grid-cols-1 gap-6"> | ||
<InputField | ||
label="Subject" | ||
{...register("subject", { required: true })} | ||
error={errors.subject ? "Subject is required" : null} | ||
/> | ||
<label tw="block"> | ||
<span tw="text-gray-700">Description</span> | ||
<textarea tw="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" {...register("description")}></textarea> | ||
</label> | ||
<Button type="submit" isLoading={isLoading}> | ||
Save | ||
</Button> | ||
</div> | ||
</form> | ||
); | ||
}; | ||
|
||
export default CreateIssue; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = () => ( | ||
<div tw="flex flex-row flex-wrap h-screen"> | ||
<main tw="w-3/4 h-full shadow-md border-2"> | ||
<ImageEditor /> | ||
</main> | ||
<aside tw="w-1/4 h-full"> | ||
<IssueEditor /> | ||
</aside> | ||
</div> | ||
); | ||
const createScreenshot = (stageRef: MutableRefObject<any>): Screenshot => { | ||
return { | ||
toBlob: () => { | ||
const canvas = stageRef.current.clearAndToCanvas({ | ||
pixelRatio: stageRef.current._pixelRatio, | ||
}) as HTMLCanvasElement; | ||
return new Promise<Blob>((resolve) => { | ||
canvas.toBlob(resolve); | ||
}); | ||
}, | ||
}; | ||
}; | ||
|
||
const Editor: FC = () => { | ||
const stageRef = useRef<unknown>(null); | ||
const screenshot = createScreenshot(stageRef); | ||
return ( | ||
<div tw="flex flex-row flex-wrap h-screen"> | ||
<main tw="w-3/4 h-full shadow-md border-2"> | ||
<ImageEditor stageRef={stageRef} /> | ||
</main> | ||
<aside tw="w-1/4 h-full"> | ||
<IssueEditor screenshot={screenshot} /> | ||
</aside> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Editor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export type Screenshot = { | ||
toBlob: () => Promise<Blob>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Error>(); | ||
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; |