Upload media with remix and graphql

Solved

Upload media with remix and graphql

EvilGranny
Shopify Partner
15 2 4

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?



Accepted Solution (1)
EvilGranny
Shopify Partner
15 2 4

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

View solution in original post

Replies 15 (15)

Liam
Community Manager
3108 341 880

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

EvilGranny
Shopify Partner
15 2 4

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

Liam
Community Manager
3108 341 880

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

EvilGranny
Shopify Partner
15 2 4

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 │ }

 

ShopifyDevSup
Shopify Staff
1453 238 512

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. 

 

  • The Shopify Remix template app has a section in it's README outlining how the app's default session storage works as well.

 

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

EvilGranny
Shopify Partner
15 2 4

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:

EvilGranny_0-1697917117626.png


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?

EvilGranny_1-1697917227412.png

 

EvilGranny
Shopify Partner
15 2 4

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

 

EvilGranny
Shopify Partner
15 2 4

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
}
}
ThangCQ
Shopify Partner
31 3 7

Thank you for your sharing

mudasir07
Shopify Partner
3 0 1

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 

  • Image: Media failed to process because the image could not be processed

 

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;

 

this is my code please replay 

 

mudasir07_0-1708257393470.png

 

Wakil_eFoli
Shopify Partner
47 2 6

@mudasir07 Hi there!
Did you solve it? 

mohitpatel
Shopify Partner
15 0 0

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.

AndreFonseca
Shopify Partner
7 0 0

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 

stagedUploadsCreate mutation and I get the paramenters Content-Type, success_action_status, acl, key, x-goog-date, x-goog-credential, x-goog-algorithm, x-goog-signature, policy and resourceUrl.
 
Then I use this info to make a PUT request to the resourceUrl using the other parameters and the video file (in Base64 format). However, I'm getting this answer:

<Error><Code>AccessDenied</Code><Message>Access denied.</Message><Details>Anonymous caller does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist).</Details></Error>
 
Do you or anyone knows what am I doing wrong?
mohitpatel
Shopify Partner
15 0 0

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. 

my.png

mohitpatel
Shopify Partner
15 0 0

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.