Focusing on managing products, variants, and collections through the API.
Hey, I'm trying to upload the media with graphql. I read all the docs and this article:
https://shopify.dev/docs/apps/online-store/media/products#step-1-upload-media-to-shopify .
There's my file.server.js:
export const uploadFile = async (graphql) => { const result = await graphql(` mutation generateStagedUploads { stagedUploadsCreate(input: [ { filename: "watches_comparison.mp4", mimeType: "video/mp4", resource: VIDEO, fileSize: "899765" }, { filename: "another_watch.glb", mimeType: "model/gltf-binary", resource: MODEL_3D, fileSize: "456" } ]) { stagedTargets { url resourceUrl parameters { name value } } userErrors { field, message } } } `); console.log('result', result); console.log('result.data', result.data); console.log('result.data?.stagedUploadsCreate', result.data?.stagedUploadsCreate); console.log('result.data?.stagedUploadsCreate.stagedTargets', result.data?.stagedUploadsCreate.stagedTargets); return result; }
And I call it in the loader:
const { admin } = await authenticate.admin(request); await uploadFile(admin.graphql)
This response returns right data, when i use it in Shopify GraphiQL App.
But when i do it on the server, i have an empty object in response. What am I doing wrong?
Solved! Go to the solution
This is an accepted solution.
Ok, this code works. I hope it will be usefull:
// sopify.app.toml
scopes = "read_products,write_products,write_files,read_files,read_themes,write_themes"
// components/filesUploader.jsx
const handleDropZoneDrop = useCallback(async (_dropFiles, acceptedFiles, _rejectedFiles) => {
const formData = new FormData();
if (acceptedFiles.length) {
setFiles(acceptedFiles);
}
acceptedFiles.forEach((file) => {
formData.append('files', file)
});
const result = await fetch('/file', {
'method': 'POST',
headers: {
contentType: 'multipart/form-data',
},
body: formData,
});
}, []);
// routes/file.jsx
import { unstable_parseMultipartFormData, unstable_createMemoryUploadHandler } from "@remix-run/node";
import { authenticate } from "~/shopify.server";
import { uploadFile } from "~/models/file.server";
export const action = async ({request}) => {
const { admin } = await authenticate.admin(request);
const uploadHandler = unstable_createMemoryUploadHandler({
maxPartSize: 20_000_000,
});
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const files = formData.getAll('files');
const result = await uploadFile(files, admin.graphql);
return {
data: result,
}
}
// files.server.js
import {fetch, FormData} from "@remix-run/node";
const prepareFiles = (files) => files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes('image') ? 'IMAGE' : 'FILE',
fileSize: file.size.toString(),
httpMethod: 'POST',
}));
const prepareFilesToCreate = (stagedTargets, files, contentType) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes('image') ? 'IMAGE' : 'FILE',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const promises = [];
files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();
params.forEach((param) => {
formData.append(param.name, param.value)
})
formData.append('file', file);
const promise = fetch(url, {
method: 'POST',
body: formData,
});
promises.push(promise);
});
await Promise.all(promises);
await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
Hi EvilGranny,
It's possible that there's something happening here that's related to permissions or authentication. With the GraphiQL app, scopes and auth is already set up so since it's working with this method, it's likely your app isn't set up 100% correctly for it's scopes and auth. Just to confirm, does your app the `read_products` and `write_products` access scopes?
Liam | Developer Advocate @ Shopify
- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
Hey, thank you for your attention to my topic!
here's my scope:
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_products,write_products,write_files,read_files"
When I didn't have some permissions, I've got an error, but now only empty object
Is there any way you can test the mutation itself (eg by using something like Postman) to rule out if it's the mutation that's causing this vs if the app configuration is causing this?
Liam | Developer Advocate @ Shopify
- Was my reply helpful? Click Like to let me know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
I'd like to use postman, but I have no idea what I need to put in the headers, and which endpoint I should use ((
There's my shopify config, did I miss something
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
name = "shop-reviews"
client_id = "b4d0dc605b0d03899187a08ec64fb24f"
application_url = "https://household-kruger-forms-blond.trycloudflare.com"
embedded = true
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_products,write_products,read_files,write_files"
[auth]
redirect_urls = [
"https://household-kruger-forms-blond.trycloudflare.com/auth/callback",
"https://household-kruger-forms-blond.trycloudflare.com/auth/shopify/callback",
"https://household-kruger-forms-blond.trycloudflare.com/api/auth/callback"
]
[webhooks]
api_version = "2023-07"
[pos]
embedded = false
[build]
automatically_update_urls_on_dev = true
dev_store_url = "mmg-development-store.myshopify.com"
And here's response:
00:20:58 │ remix │ result NodeResponse [Response] {
00:20:58 │ remix │ size: 0,
00:20:58 │ remix │ [Symbol(Body internals)]: {
00:20:58 │ remix │ body: ReadableStream {
00:20:58 │ remix │ _state: 'readable',
00:20:58 │ remix │ _reader: undefined,
00:20:58 │ remix │ _storedError: undefined,
00:20:58 │ remix │ _disturbed: false,
00:20:58 │ remix │ _readableStreamController: [ReadableStreamDefaultController]
00:20:58 │ remix │ },
00:20:58 │ remix │ type: 'text/plain;charset=UTF-8',
00:20:58 │ remix │ size: 2436,
00:20:58 │ remix │ boundary: null,
00:20:58 │ remix │ disturbed: false,
00:20:58 │ remix │ error: null
00:20:58 │ remix │ },
00:20:58 │ remix │ [Symbol(Response internals)]: {
00:20:58 │ remix │ url: undefined,
00:20:58 │ remix │ status: 200,
00:20:58 │ remix │ statusText: '',
00:20:58 │ remix │ headers: {
00:20:58 │ remix │ 'alt-svc': 'h3=":443"; ma=86400',
00:20:58 │ remix │ 'cf-cache-status': 'DYNAMIC',
00:20:58 │ remix │ 'cf-ray': '817bee8bbc143515-WAW',
00:20:58 │ remix │ connection: 'close',
00:20:58 │ remix │ 'content-language': 'en',
00:20:58 │ remix │ 'content-security-policy': "default-src 'self' data: blob: 'unsafe-inline' 'unsafe-eval' https://* shopify-pos://*; block-all-mixed-content; child-src 'self' https://*
shopify-pos://*; connect-src 'self' wss://* https://*; frame-ancestors 'none'; img-src 'self' data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net
https://checkout.shopifycs.com https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com
https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com 'self' 'unsafe-inline' 'unsafe-eval'; upgrade-insecure-requests; report-uri
/csp-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=36b53ab3-532b-404c-9613-e1202d8b750a",
00:20:58 │ remix │ 'content-type': 'application/json; charset=utf-8',
00:20:58 │ remix │ date: 'Tue, 17 Oct 2023 22:20:58 GMT',
00:20:58 │ remix │ nel: '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}',
00:20:58 │ remix │ 'referrer-policy': 'origin-when-cross-origin',
00:20:58 │ remix │ 'report-to': '{"endpoints":[{"url":"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=U%2BnjgGzqnsUgTLlqdB78%2BP0lWiwU1MybCVj2sM%2Fk68dBTI9HUAx0tq2LZTjYY4xoiedpKaG7T7L6qw
mYyvPGQcGG1HOxE8%2FrwBr%2BqkJk2jssGUuWbZD7aAV7QLWRi3FGhQAI7%2BMqB%2F3R7MpP6Ofj1BP54KfR"}],"group":"cf-nel","max_age":604800}',
00:20:58 │ remix │ server: 'cloudflare',
00:20:58 │ remix │ 'server-timing': 'processing;dur=486, graphql;desc="admin/mutation/other", cfRequestDuration;dur=636.999846',
00:20:58 │ remix │ 'strict-transport-security': 'max-age=7889238',
00:20:58 │ remix │ 'transfer-encoding': 'chunked',
00:20:58 │ remix │ vary: 'Accept-Encoding',
00:20:58 │ remix │ 'x-content-type-options': 'nosniff',
00:20:58 │ remix │ 'x-dc': 'gcp-europe-west3,gcp-europe-west3,gcp-us-central1,gcp-us-central1',
00:20:58 │ remix │ 'x-download-options': 'noopen',
00:20:58 │ remix │ 'x-frame-options': 'DENY',
00:20:58 │ remix │ 'x-permitted-cross-domain-policies': 'none',
00:20:58 │ remix │ 'x-request-id': '36b53ab3-532b-404c-9613-e1202d8b750a',
00:20:58 │ remix │ 'x-shardid': '300',
00:20:58 │ remix │ 'x-shopid': '79825731885',
00:20:58 │ remix │ 'x-shopify-api-version': '2023-07',
00:20:58 │ remix │ 'x-shopify-stage': 'production',
00:20:58 │ remix │ 'x-sorting-hat-podid': '300',
00:20:58 │ remix │ 'x-sorting-hat-shopid': '79825731885',
00:20:58 │ remix │ 'x-stats-apiclientid': '66100330497',
00:20:58 │ remix │ 'x-stats-apipermissionid': '656712794413',
00:20:58 │ remix │ 'x-stats-userid': '',
00:20:58 │ remix │ 'x-xss-protection': '1; mode=block;
report=/xss-report?source%5Baction%5D=query&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Fgraphql&source%5Bsection%5D=admin_api&source%5Buuid%5D=36b53ab3-532b-404c-9613-e1202d8b750a'
00:20:58 │ remix │ },
00:20:58 │ remix │ counter: 0,
00:20:58 │ remix │ highWaterMark: undefined
00:20:58 │ remix │ }
00:20:58 │ remix │ }
Hi @EvilGranny,
If you want to use Postman to make HTTP requests while testing your app you can format them like these examples from our documentation on using access tokens.
If your app uses OAuth then you will need to use the session token that was issued when you installed the app on the shop.
All the best!
- James
Developer Support @ Shopify
- Was this reply helpful? Click Like to let us know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
Thanks! I have no idea how, but I've fifxed it. But now I have a second problem.
Here's my code:
const prepareFiles = (files) => files.map(file => ({
filename: file.name,
mimeType: file.type,
resource: 'IMAGE',
fileSize: file.size.toString(),
}));
const prepareFilesToCreate = (stagedTargets, files) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: 'IMAGE',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const filesSS = await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
const ll = await filesSS.json()
console.log(1111, ll.data.fileCreate.files)
console.log(5555, ll.data.fileCreate.userErrors)
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
And here's console.log() results:
image is null.
I tried to use different file formats, different sizes and so on...
On the screenshot result of jpg 400kb.
And when I go to content=>files I see errors. What am I doing wrong?
I've found the solution. This code works, but not for video files. When I'm uploading video files, I have an error: "No content"
import {fetch, FormData} from "@remix-run/node";
const prepareFiles = (files) => files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes('image') ? 'IMAGE' : 'VIDEO',
fileSize: file.size.toString(),
httpMethod: 'POST',
}));
const prepareFilesToCreate = (stagedTargets, files, contentType) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes('image') ? 'IMAGE' : 'VIDEO',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const promises = [];
files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();
params.forEach((param) => {
formData.append(param.name, param.value)
})
formData.append('file', file);
const promise = fetch(url, {
method: 'POST',
body: formData,
});
promises.push(promise);
});
await Promise.all(promises);
await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
This is an accepted solution.
Ok, this code works. I hope it will be usefull:
// sopify.app.toml
scopes = "read_products,write_products,write_files,read_files,read_themes,write_themes"
// components/filesUploader.jsx
const handleDropZoneDrop = useCallback(async (_dropFiles, acceptedFiles, _rejectedFiles) => {
const formData = new FormData();
if (acceptedFiles.length) {
setFiles(acceptedFiles);
}
acceptedFiles.forEach((file) => {
formData.append('files', file)
});
const result = await fetch('/file', {
'method': 'POST',
headers: {
contentType: 'multipart/form-data',
},
body: formData,
});
}, []);
// routes/file.jsx
import { unstable_parseMultipartFormData, unstable_createMemoryUploadHandler } from "@remix-run/node";
import { authenticate } from "~/shopify.server";
import { uploadFile } from "~/models/file.server";
export const action = async ({request}) => {
const { admin } = await authenticate.admin(request);
const uploadHandler = unstable_createMemoryUploadHandler({
maxPartSize: 20_000_000,
});
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const files = formData.getAll('files');
const result = await uploadFile(files, admin.graphql);
return {
data: result,
}
}
// files.server.js
import {fetch, FormData} from "@remix-run/node";
const prepareFiles = (files) => files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes('image') ? 'IMAGE' : 'FILE',
fileSize: file.size.toString(),
httpMethod: 'POST',
}));
const prepareFilesToCreate = (stagedTargets, files, contentType) => stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes('image') ? 'IMAGE' : 'FILE',
filename: files[index].name,
};
});
export const uploadFile = async (files, graphql) => {
const preparedFiles = prepareFiles(files);
const result = await graphql(`
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field,
message
}
}
}
`, { variables: { input: preparedFiles }});
const response = await result.json();
const promises = [];
files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();
params.forEach((param) => {
formData.append(param.name, param.value)
})
formData.append('file', file);
const promise = fetch(url, {
method: 'POST',
body: formData,
});
promises.push(promise);
});
await Promise.all(promises);
await graphql(`
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
id,
preview {
image {
url
}
}
}
userErrors {
field
message
}
}
}
`, {
variables: {
files: prepareFilesToCreate(response.data.stagedUploadsCreate.stagedTargets, files),
}
});
return {
stagedTargets: response.data.stagedUploadsCreate.stagedTargets,
errors: response.data.stagedUploadsCreate.userErrors
}
}
Thank you for your sharing
Hii can you please see my code and say what i am doing wrong hear the product is creating but the image is not uploading and show
import React, { useCallback, useEffect, useState } from "react";
import { json } from "@remix-run/node";
import { useActionData, useNavigation, useSubmit } from "@remix-run/react";
import {
Page,
Layout,
Card,
Button,
BlockStack,
Box,
InlineStack,
DropZone,
LegacyStack,
Thumbnail,
Text,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
await authenticate.admin(request);
return null;
};
export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const color = ["Red", "Orange", "Yellow", "Green"][
Math.floor(Math.random() * 4)
];
const requestBody = await request.text();
console.log(
"start:----------------------------------------------------------------------------------",
);
const formData = new URLSearchParams(requestBody);
const name = formData.get("filename");
const type = formData.get("filetype");
const size = formData.get("filesize");
const files = [
{
name: name,
type: type,
size: size,
},
];
console.log("File Details:", files);
const prepareFiles = (files) =>
files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes("image") ? "IMAGE" : "FILE",
fileSize: file.size.toString(),
httpMethod: "POST",
}));
const preparedFiles = prepareFiles(files);
console.log("Prepared Files for Upload:", preparedFiles);
const uploadFileResponse = await admin.graphql(
`#graphql
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field
message
}
}
}
`,
{ variables: { input: preparedFiles } },
);
console.log(
"Upload File Response:",
JSON.stringify(uploadFileResponse, null, 2),
);
const uplodeFileJson = await uploadFileResponse.json();
if (uplodeFileJson.data.stagedUploadsCreate?.userErrors?.length) {
console.log(
"Upload Errors:",
uplodeFileJson.data.stagedUploadsCreate.userErrors,
);
}
const resourceurl =
uplodeFileJson.data.stagedUploadsCreate?.stagedTargets[0]?.resourceUrl;
const productResponse = await admin.graphql(
`#graphql
mutation populateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
status
}
}
}`,
{
variables: {
input: {
title: `${color} Snowboard`,
},
},
},
);
console.log(
"Product Creation Response:",
JSON.stringify(productResponse, null, 2),
);
const productResponseJson = await productResponse.json();
const productId = productResponseJson.data?.productCreate?.product?.id;
// Add image to the product
const imageResponse = await admin.graphql(
`mutation productCreateMedia($media: [CreateMediaInput!]!, $productId: ID!) {
productCreateMedia(media: $media, productId: $productId) {
media {
alt
mediaContentType
status
}
mediaUserErrors {
field
message
}
product {
id
title
}
}
}`,
{
variables: {
media: {
alt: "Image",
mediaContentType: "IMAGE",
originalSource: resourceurl,
},
productId: productId,
},
},
);
console.log("Image Response:", JSON.stringify(imageResponse, null, 2));
const imageResponseJson = await imageResponse.json();
console.log(
"end:----------------------------------------------------------------------------------",
);
return json({
product: {
id: productId,
imageResponseJson: imageResponseJson,
},
});
};
const Index = () => {
const nav = useNavigation();
const actionData = useActionData();
const submit = useSubmit();
const isLoading =
["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";
const productId = actionData?.product?.id.replace(
"gid://shopify/Product/",
"",
);
useEffect(() => {
if (productId) {
shopify.toast.show("Product created");
}
}, [productId]);
const [files, setFiles] = useState([]);
const handleDropZoneDrop = useCallback(
async (_dropFiles, acceptedFiles, _rejectedFiles) => {
if (acceptedFiles.length) {
setFiles(acceptedFiles);
}
},
[],
);
const validImageTypes = ["image/gif", "image/jpeg", "image/png"];
const fileUpload = !files.length && <DropZone.FileUpload />;
const uploadedFiles = files.length > 0 && (
<div style={{ padding: "0" }}>
<LegacyStack vertical>
{files.map((file, index) => (
<LegacyStack alignment="center" key={index}>
<Thumbnail
size="small"
alt={file.name}
source={
validImageTypes.includes(file.type)
? window.URL.createObjectURL(file)
: ""
}
/>
<div>
{file.name}{" "}
<Text variant="bodySm" as="p">
{file.size} bytes
</Text>
</div>
</LegacyStack>
))}
</LegacyStack>
</div>
);
console.log(files);
const generateProduct = () => {
const filename = files[0]?.name;
const filetype = files[0]?.type;
const filesize = files[0]?.size;
submit({ filename, filetype, filesize }, { replace: true, method: "POST" });
};
return (
<Page>
<BlockStack gap="500">
<Layout>
<Layout.Section>
<Card>
<InlineStack gap="300">
<DropZone onDrop={handleDropZoneDrop}>
{uploadedFiles}
{fileUpload}
</DropZone>
<Button loading={isLoading} onClick={generateProduct}>
Generate a product
</Button>
{actionData?.product && (
<Button
url={`shopify:admin/products/${productId}`}
target="_blank"
variant="plain"
>
View product
</Button>
)}
</InlineStack>
{actionData?.imageUserErrors && (
<Box>
<p>Errors occurred while adding image:</p>
<ul>
{actionData.imageUserErrors.map((error, index) => (
<li key={index}>
<strong>{error.field}: </strong>
{error.message}
</li>
))}
</ul>
</Box>
)}
</Card>
</Layout.Section>
</Layout>
</BlockStack>
</Page>
);
};
export default Index;
hello wakil, I need my custom app file uploaded. this file URL saves a file in contend. I have not idea for this. so please give full code with image upload and save graphql storage manager.
Hi @EvilGranny
I'm not using your code but I'm trying to do the same thing using Postman and C# just for test purposes. I use the
hello EvilGranny, I will share my is working without any error but when I see the browser in the network check then image status upload shows but content in the file does not upload my images. so please let me know of any issues
import React, { useCallback, useEffect, useState, useRef } from "react";
import { json } from "@remix-run/node";
import { useActionData, useNavigation, useSubmit } from "@remix-run/react";
import {
Page,
Layout,
Card,
Button,
BlockStack,
Box,
InlineStack,
DropZone,
LegacyStack,
Thumbnail,
Text,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
export const loader = async ({ request }: LoaderFunctionArgs) => {
await authenticate.admin(request);
return null;
};
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const color = ["Red", "Orange", "Yellow", "Green"][
Math.floor(Math.random() * 4)
];
const requestBody = await request.text();
const formData = new URLSearchParams(requestBody);
const name = formData.get("filename");
const type = formData.get("filetype");
const size = formData.get("filesize");
const files = [
{
name: name,
type: type,
size: size,
},
];
const prepareFiles = (files: { name: string | null; type: string | null; size: string | null; }[]) =>
files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type?.includes("image") ? "IMAGE" : "FILE",
fileSize: file.size?.toString(),
httpMethod: "PUT",
}));
const preparedFiles = prepareFiles(files);
const uploadFileResponse = await admin.graphql(
`#graphql
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
stagedUploadsCreate(input: $input) {
stagedTargets {
resourceUrl
url
parameters {
name
value
}
}
userErrors {
field
message
}
}
}
`,
{ variables: { input: preparedFiles } },
);
const uplodeFileJson = await uploadFileResponse.json();
const resourceurl = uplodeFileJson.data.stagedUploadsCreate.stagedTargets[0].resourceUrl;
const fileCreateResponse = await admin.graphql(
`#graphql
mutation fileCreate($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
alt
createdAt
fileErrors {
code
details
message
}
fileStatus
preview {
image {
url
}
status
}
}
userErrors {
field
message
}
}
}`,
{
variables: {
files: {
alt: "Image",
contentType: "IMAGE",
originalSource: resourceurl,
},
},
},
);
const fileCreateJson = await fileCreateResponse.json();
return ({
stagedUpload: uplodeFileJson,
fileCreate: fileCreateJson,
resourceurl: resourceurl
});
};
export default function Index() {
const nav = useNavigation();
const actionData = useActionData();
const submit = useSubmit();
const isLoading = ["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";
useEffect(() => {
if (actionData) {
shopify.toast.show("Product created");
}
}, [actionData]);
const [files, setFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const handleDropZoneDrop = useCallback(
async (dropFiles: File[], acceptedFiles: File[], rejectedFiles: File[]) => {
if (acceptedFiles.length) {
setFiles((prevFiles) => [...prevFiles, ...acceptedFiles]);
for (const file of acceptedFiles) {
console.log(file.name);
console.log(file.size);
console.log(file.type);
}
}
},
[],
);
const validImageTypes = ["image/gif", "image/jpeg", "image/png"];
const fileUpload = !files.length && <DropZone.FileUpload />;
const uploadedFiles = files.length > 0 && (
<div style={{ padding: "0" }}>
<LegacyStack vertical>
{files.map((file, index) => (
<LegacyStack alignment="center" key={index}>
<Thumbnail
size="small"
alt={file.name}
source={
validImageTypes.includes(file.type)
? window.URL.createObjectURL(file)
: ""
}
/>
<div>
{file.name}{" "}
<Text variant="bodySm" as="p">
{file.size} bytes
</Text>
</div>
</LegacyStack>
))}
</LegacyStack>
</div>
);
const generateProduct = () => {
const filename = files[0]?.name;
const filetype = files[0]?.type;
const filesize = files[0]?.size;
submit({ filename, filetype, filesize }, { replace: true, method: "POST" });
};
return (
<Page>
<BlockStack gap="500">
<Layout>
<Layout.Section>
<Card>
<InlineStack gap="300">
<DropZone onDrop={handleDropZoneDrop}>
{uploadedFiles}
{fileUpload}
</DropZone>
<Button loading={isLoading} onClick={generateProduct}>
Generate a product
</Button>
</InlineStack>
</Card>
</Layout.Section>
</Layout>
</BlockStack>
</Page>
);
};
.
i will share some screenshots.
I have some requirements for a custom app for file upload using typescript. I am fresher level for created Shopify app. so please give me full code on how to file upload and this upload file show in file/content.