@workday/graphiql-plugin-multipart-requests 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md ADDED
@@ -0,0 +1,89 @@
1
+ # Multipart Requests Plugin
2
+
3
+ This plugin allows GraphiQL to include files as part of a request to a GraphQL server
4
+ implementing [GraphQL Multipart Request: V3](https://workday.github.io/graphql-multipart-request-spec/graphql-multipart-request-v3.html).
5
+
6
+ ![Demo](./demo.gif)
7
+
8
+ ## Usage
9
+
10
+ ### Using `MultipartFetcher`
11
+ ```tsx
12
+ import {
13
+ MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN,
14
+ MultipartAttachmentsContext,
15
+ MultipartAttachmentsProvider,
16
+ } from '@workday/graphiql-plugin-multipart-requests';
17
+
18
+ function GraphiQLWrapper() {
19
+ const attachments = useContext(MultipartAttachmentsContext);
20
+
21
+ const fetcher = useMemo(() => {
22
+ // The decoratedFetcher will automatically pass all necessary FormData to the Fetcher
23
+ return attachments.decoratedFetcher(
24
+ async (graphQLParams: FetcherParams, formData: FormData | null) => {
25
+ console.log('Attachments:', formData);
26
+
27
+ // Send the request to the server
28
+ }
29
+ );
30
+ }, [attachments]);
31
+
32
+ return (
33
+ <GraphiQL
34
+ fetcher={fetcher}
35
+ plugins={[MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN]}
36
+ />
37
+ );
38
+ }
39
+
40
+ const App = () => (
41
+ <MultipartAttachmentsProvider>
42
+ <GraphiQLWrapper />
43
+ </MultipartAttachmentsProvider>
44
+ );
45
+ ```
46
+
47
+ ### Using `fetch` Override
48
+
49
+ ```tsx
50
+ import {
51
+ MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN,
52
+ MultipartAttachmentsContext,
53
+ MultipartAttachmentsProvider
54
+ } from '@workday/graphiql-plugin-multipart-requests';
55
+
56
+ function GraphiQLWrapper() {
57
+ const attachments = useContext(MultipartAttachmentsContext);
58
+
59
+ const fetcher = useMemo(() => {
60
+ // attachmentsFetch will automatically include all necessary FormData in the downstream request.
61
+ const attachmentsFetch = attachments.decoratedFetch();
62
+ async (graphQLParams: Fetcher) => {
63
+ const response = await attachmentsFetch('https://example.com/my-graphql', {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json'
67
+ },
68
+ body: JSON.stringify(graphQLParams)
69
+ }
70
+ );
71
+
72
+ // Return the response to GraphiQL
73
+ };
74
+ }, [attachments]);
75
+
76
+ return (
77
+ <GraphiQL
78
+ fetcher={fetcher}
79
+ plugins={[MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN]}
80
+ />
81
+ );
82
+ }
83
+
84
+ const App = () => (
85
+ <MultipartAttachmentsProvider>
86
+ <GraphiQLWrapper />
87
+ </MultipartAttachmentsProvider>
88
+ );
89
+ ```
@@ -0,0 +1,2 @@
1
+ declare const _default: import("react/jsx-runtime").JSX.Element;
2
+ export default _default;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("react/jsx-runtime");
4
+ exports.default = ((0, jsx_runtime_1.jsx)("svg", { height: '1em', viewBox: '0 0 14 14', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: (0, jsx_runtime_1.jsx)("path", { d: 'M 9 4 L 4.5 8.5 C 3.5 9.5 5 11 6 10 L 11.5 4.5 C 13.5 2.5 11 -0.5 8.5 2 L 2.5 8 C 0 10.5 4 14.5 6.5 12 L 12.5 6', stroke: 'currentColor', strokeWidth: '0.8' }) }));
5
+ //# sourceMappingURL=attachment-icon.svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-icon.svg.js","sourceRoot":"","sources":["../../src/attachment-icon.svg.tsx"],"names":[],"mappings":";;;AAAA,kBAAe,CACb,gCAAK,MAAM,EAAC,KAAK,EAAC,OAAO,EAAC,WAAW,EAAC,IAAI,EAAC,MAAM,EAAC,KAAK,EAAC,4BAA4B,YAClF,iCACE,CAAC,EAAC,iHAAiH,EACnH,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,KAAK,GACjB,GACE,CACP,CAAC","sourcesContent":["export default (\n <svg height='1em' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'>\n <path\n d='M 9 4 L 4.5 8.5 C 3.5 9.5 5 11 6 10 L 11.5 4.5 C 13.5 2.5 11 -0.5 8.5 2 L 2.5 8 C 0 10.5 4 14.5 6.5 12 L 12.5 6'\n stroke='currentColor'\n strokeWidth='0.8'\n />\n </svg>\n);\n"]}
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { AttachmentEntry, MultipartAttachments } from './multipart-attachments.js';
3
+ export type AttachmentAddAction = {
4
+ action: 'add';
5
+ name?: string;
6
+ };
7
+ export type AttachmentUpdateAction = {
8
+ action: 'update';
9
+ id: number;
10
+ name?: string;
11
+ value?: File;
12
+ };
13
+ export type AttachmentDeleteAction = {
14
+ action: 'delete';
15
+ id: number;
16
+ };
17
+ export type AttachmentsAction = AttachmentAddAction | AttachmentUpdateAction | AttachmentDeleteAction;
18
+ export type InternalAttachmentEntry = {
19
+ id: number;
20
+ } & (({
21
+ error?: string;
22
+ } & AttachmentEntry) | {
23
+ name: string;
24
+ value: null;
25
+ error: string;
26
+ });
27
+ export declare const NO_FILE_ERROR = "No File Attached";
28
+ export declare const DUPLICATE_NAME_ERROR = "Duplicate Name";
29
+ export declare class InternalAttachmentState {
30
+ private readonly attachmentId;
31
+ readonly attachments: InternalAttachmentEntry[];
32
+ constructor(attachments?: InternalAttachmentEntry[], attachmentId?: number);
33
+ get validAttachments(): AttachmentEntry[];
34
+ map<T>(callback: (value: InternalAttachmentEntry, index: number, array: InternalAttachmentEntry[]) => T): T[];
35
+ reduce(action: AttachmentsAction): InternalAttachmentState;
36
+ }
37
+ export declare const MultipartAttachmentsContext: React.Context<MultipartAttachments>;
38
+ export declare const MultipartAttachmentsInternalContext: React.Context<[InternalAttachmentState, React.Dispatch<AttachmentsAction>]>;
39
+ export declare function MultipartAttachmentsProvider({ children }: {
40
+ children: React.ReactNode;
41
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultipartAttachmentsProvider = exports.MultipartAttachmentsInternalContext = exports.MultipartAttachmentsContext = exports.InternalAttachmentState = exports.DUPLICATE_NAME_ERROR = exports.NO_FILE_ERROR = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const multipart_attachments_js_1 = require("./multipart-attachments.js");
7
+ exports.NO_FILE_ERROR = 'No File Attached';
8
+ exports.DUPLICATE_NAME_ERROR = 'Duplicate Name';
9
+ class InternalAttachmentState {
10
+ attachmentId;
11
+ attachments;
12
+ constructor(attachments = [], attachmentId = 0) {
13
+ this.attachments = attachments;
14
+ this.attachmentId = attachmentId;
15
+ }
16
+ get validAttachments() {
17
+ return this.attachments.filter((attachment) => attachment.value !== null && !attachment.error);
18
+ }
19
+ map(callback) {
20
+ return this.attachments.map(callback);
21
+ }
22
+ reduce(action) {
23
+ let nextAttachments;
24
+ let nextAttachmentId = this.attachmentId;
25
+ switch (action.action) {
26
+ case 'add': {
27
+ nextAttachments = [
28
+ ...this.attachments,
29
+ {
30
+ id: this.attachmentId,
31
+ name: action.name ?? `part${this.attachmentId}`,
32
+ value: null,
33
+ },
34
+ ];
35
+ nextAttachmentId++;
36
+ break;
37
+ }
38
+ case 'update': {
39
+ nextAttachments = this.attachments.map((attachment) => {
40
+ if (attachment.id === action.id) {
41
+ return {
42
+ ...attachment,
43
+ name: action.name ?? attachment.name,
44
+ value: action.value !== undefined ? action.value : attachment.value,
45
+ };
46
+ }
47
+ else {
48
+ return attachment;
49
+ }
50
+ });
51
+ break;
52
+ }
53
+ case 'delete': {
54
+ nextAttachments = this.attachments.filter((attachment) => attachment.id !== action.id);
55
+ break;
56
+ }
57
+ default: {
58
+ throw new Error(`Unknown action ${action}`);
59
+ }
60
+ }
61
+ for (const attachment of nextAttachments) {
62
+ if (nextAttachments.filter(({ name }) => name === attachment.name).length > 1) {
63
+ attachment.error = exports.DUPLICATE_NAME_ERROR;
64
+ }
65
+ else if (attachment.value === null) {
66
+ attachment.error = exports.NO_FILE_ERROR;
67
+ }
68
+ else {
69
+ delete attachment.error;
70
+ }
71
+ }
72
+ return new InternalAttachmentState(nextAttachments, nextAttachmentId);
73
+ }
74
+ }
75
+ exports.InternalAttachmentState = InternalAttachmentState;
76
+ exports.MultipartAttachmentsContext = (0, react_1.createContext)(new multipart_attachments_js_1.MultipartAttachments([]));
77
+ exports.MultipartAttachmentsInternalContext = (0, react_1.createContext)([
78
+ new InternalAttachmentState(),
79
+ () => {
80
+ throw new Error('MultipartAttachmentsProvider not found. Make sure to render it higher up in the tree');
81
+ },
82
+ ]);
83
+ function MultipartAttachmentsProvider({ children }) {
84
+ const [attachments, setAttachmentStates] = (0, react_1.useReducer)((attachments, action) => attachments.reduce(action), new InternalAttachmentState());
85
+ return ((0, jsx_runtime_1.jsx)(exports.MultipartAttachmentsContext.Provider, { value: new multipart_attachments_js_1.MultipartAttachments(attachments.validAttachments), children: (0, jsx_runtime_1.jsx)(exports.MultipartAttachmentsInternalContext.Provider, { value: [attachments, setAttachmentStates], children: children }) }));
86
+ }
87
+ exports.MultipartAttachmentsProvider = MultipartAttachmentsProvider;
88
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/context.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { createContext, Reducer, useReducer } from 'react';\nimport { AttachmentEntry, MultipartAttachments } from './multipart-attachments.js';\n\nexport type AttachmentAddAction = {\n action: 'add';\n name?: string;\n};\n\nexport type AttachmentUpdateAction = {\n action: 'update';\n id: number;\n name?: string;\n value?: File;\n};\n\nexport type AttachmentDeleteAction = {\n action: 'delete';\n id: number;\n};\n\nexport type AttachmentsAction = AttachmentAddAction | AttachmentUpdateAction | AttachmentDeleteAction;\n\nexport type InternalAttachmentEntry = { id: number } & (\n | ({ error?: string } & AttachmentEntry)\n | { name: string; value: null; error: string }\n);\n\nexport const NO_FILE_ERROR = 'No File Attached';\nexport const DUPLICATE_NAME_ERROR = 'Duplicate Name';\n\nexport class InternalAttachmentState {\n private readonly attachmentId;\n public readonly attachments: InternalAttachmentEntry[];\n\n constructor(attachments: InternalAttachmentEntry[] = [], attachmentId = 0) {\n this.attachments = attachments;\n this.attachmentId = attachmentId;\n }\n\n public get validAttachments(): AttachmentEntry[] {\n return this.attachments.filter(\n (\n attachment: InternalAttachmentEntry\n ): attachment is AttachmentEntry & {\n id: number;\n } => attachment.value !== null && !attachment.error\n );\n }\n\n public map<T>(callback: (value: InternalAttachmentEntry, index: number, array: InternalAttachmentEntry[]) => T): T[] {\n return this.attachments.map(callback);\n }\n\n public reduce(action: AttachmentsAction): InternalAttachmentState {\n let nextAttachments;\n let nextAttachmentId = this.attachmentId;\n switch (action.action) {\n case 'add': {\n nextAttachments = [\n ...this.attachments,\n {\n id: this.attachmentId,\n name: action.name ?? `part${this.attachmentId}`,\n value: null,\n },\n ];\n nextAttachmentId++;\n break;\n }\n case 'update': {\n nextAttachments = this.attachments.map((attachment) => {\n if (attachment.id === action.id) {\n return {\n ...attachment,\n name: action.name ?? attachment.name,\n value: action.value !== undefined ? action.value : attachment.value,\n };\n } else {\n return attachment;\n }\n });\n break;\n }\n case 'delete': {\n nextAttachments = this.attachments.filter((attachment) => attachment.id !== action.id);\n break;\n }\n default: {\n throw new Error(`Unknown action ${action}`);\n }\n }\n\n for (const attachment of nextAttachments) {\n if (nextAttachments.filter(({ name }) => name === attachment.name).length > 1) {\n attachment.error = DUPLICATE_NAME_ERROR;\n } else if (attachment.value === null) {\n attachment.error = NO_FILE_ERROR;\n } else {\n delete attachment.error;\n }\n }\n\n return new InternalAttachmentState(nextAttachments as InternalAttachmentEntry[], nextAttachmentId);\n }\n}\n\nexport const MultipartAttachmentsContext = createContext<MultipartAttachments>(new MultipartAttachments([]));\nexport const MultipartAttachmentsInternalContext = createContext<\n [InternalAttachmentState, React.Dispatch<AttachmentsAction>]\n>([\n new InternalAttachmentState(),\n () => {\n throw new Error('MultipartAttachmentsProvider not found. Make sure to render it higher up in the tree');\n },\n]);\n\nexport function MultipartAttachmentsProvider({ children }: { children: React.ReactNode }) {\n const [attachments, setAttachmentStates] = useReducer<Reducer<InternalAttachmentState, AttachmentsAction>>(\n (attachments, action) => attachments.reduce(action),\n new InternalAttachmentState()\n );\n\n return (\n <MultipartAttachmentsContext.Provider value={new MultipartAttachments(attachments.validAttachments)}>\n <MultipartAttachmentsInternalContext.Provider value={[attachments, setAttachmentStates]}>\n {children}\n </MultipartAttachmentsInternalContext.Provider>\n </MultipartAttachmentsContext.Provider>\n );\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export type { MultipartAttachments, MultipartFetcher } from './multipart-attachments.js';
2
+ export { default as MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN } from './plugin.js';
3
+ export { MultipartAttachmentsContext, MultipartAttachmentsProvider } from './context.js';
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MultipartAttachmentsProvider = exports.MultipartAttachmentsContext = exports.MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN = void 0;
7
+ var plugin_js_1 = require("./plugin.js");
8
+ Object.defineProperty(exports, "MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN", { enumerable: true, get: function () { return __importDefault(plugin_js_1).default; } });
9
+ var context_js_1 = require("./context.js");
10
+ Object.defineProperty(exports, "MultipartAttachmentsContext", { enumerable: true, get: function () { return context_js_1.MultipartAttachmentsContext; } });
11
+ Object.defineProperty(exports, "MultipartAttachmentsProvider", { enumerable: true, get: function () { return context_js_1.MultipartAttachmentsProvider; } });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;AAEA,yCAA+E;AAAtE,mJAAA,OAAO,OAAyC;AACzD,2CAAyF;AAAhF,yHAAA,2BAA2B,OAAA;AAAE,0HAAA,4BAA4B,OAAA","sourcesContent":["export type { MultipartAttachments, MultipartFetcher } from './multipart-attachments.js';\n\nexport { default as MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN } from './plugin.js';\nexport { MultipartAttachmentsContext, MultipartAttachmentsProvider } from './context.js';\n"]}
@@ -0,0 +1,20 @@
1
+ import { Fetcher, FetcherOpts, FetcherParams, FetcherReturnType } from '@graphiql/toolkit';
2
+ export type AttachmentEntry = {
3
+ name: string;
4
+ value: File;
5
+ };
6
+ export type MultipartFetcher = (graphQLParams: FetcherParams, multipartFormData: FormData | null, opts?: FetcherOpts) => FetcherReturnType;
7
+ export declare class MultipartAttachments {
8
+ private _attachments;
9
+ constructor(attachments: AttachmentEntry[]);
10
+ /**
11
+ * Returns an implementation of fetch that will transform the request to a multipart request if any files are found
12
+ */
13
+ decoratedFetch(fetchOverride?: typeof fetch): typeof fetch;
14
+ /**
15
+ * Constructs a GraphiQL Fetcher that provides FormData to the passed in MultipartFetcher
16
+ */
17
+ decoratedFetcher(fetcher: MultipartFetcher): Fetcher;
18
+ get attachments(): Map<string, File>;
19
+ private asFormData;
20
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultipartAttachments = void 0;
4
+ const OPERATIONS_KEY = 'operations';
5
+ const MAP_KEY = 'map';
6
+ class MultipartAttachments {
7
+ _attachments;
8
+ constructor(attachments) {
9
+ this._attachments = attachments;
10
+ }
11
+ /**
12
+ * Returns an implementation of fetch that will transform the request to a multipart request if any files are found
13
+ */
14
+ decoratedFetch(fetchOverride = fetch) {
15
+ if (this._attachments.length > 0) {
16
+ return async (input, init) => {
17
+ const originalRequest = new Request(input, init);
18
+ // we're going to overwrite the body but firefox complains if we don't clone it
19
+ const originalBody = await originalRequest.clone().text();
20
+ if (JSON.parse(originalBody)?.operationName !== 'IntrospectionQuery') {
21
+ const body = this.asFormData(originalBody);
22
+ const headers = new Headers(originalRequest.headers);
23
+ // make sure form data sets the correct content type
24
+ headers.delete('content-type');
25
+ return await fetchOverride(originalRequest, { body, headers });
26
+ }
27
+ else {
28
+ // just pass through for an introspection query
29
+ return fetchOverride(originalRequest);
30
+ }
31
+ };
32
+ }
33
+ else {
34
+ return fetchOverride;
35
+ }
36
+ }
37
+ /**
38
+ * Constructs a GraphiQL Fetcher that provides FormData to the passed in MultipartFetcher
39
+ */
40
+ decoratedFetcher(fetcher) {
41
+ return (graphQLParams, opts) => {
42
+ return fetcher(graphQLParams, this.asFormData(graphQLParams), opts);
43
+ };
44
+ }
45
+ get attachments() {
46
+ return new Map(this._attachments.map(({ name, value }) => [name, value]));
47
+ }
48
+ asFormData(operations) {
49
+ const attachments = this.attachments;
50
+ // verify our attachments
51
+ for (const invalidKey of [OPERATIONS_KEY, MAP_KEY]) {
52
+ if (attachments.has(invalidKey)) {
53
+ console.error(`Cannot have an attachment named "${invalidKey}"`);
54
+ return null;
55
+ }
56
+ }
57
+ if (attachments.size > 0) {
58
+ const formData = new FormData();
59
+ const encodedOperations = typeof operations === 'string' ? operations : JSON.stringify(operations);
60
+ formData.set(OPERATIONS_KEY, encodedOperations);
61
+ attachments.forEach((value, key) => {
62
+ formData.append(key, value);
63
+ });
64
+ return formData;
65
+ }
66
+ else {
67
+ return null;
68
+ }
69
+ }
70
+ }
71
+ exports.MultipartAttachments = MultipartAttachments;
72
+ //# sourceMappingURL=multipart-attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multipart-attachments.js","sourceRoot":"","sources":["../../src/multipart-attachments.ts"],"names":[],"mappings":"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","sourcesContent":["import { Fetcher, FetcherOpts, FetcherParams, FetcherReturnType } from '@graphiql/toolkit';\n\nconst OPERATIONS_KEY = 'operations';\nconst MAP_KEY = 'map';\n\nexport type AttachmentEntry = {\n name: string;\n value: File;\n};\n\nexport type MultipartFetcher = (\n graphQLParams: FetcherParams,\n multipartFormData: FormData | null,\n opts?: FetcherOpts\n) => FetcherReturnType;\n\nexport class MultipartAttachments {\n private _attachments: AttachmentEntry[];\n\n constructor(attachments: AttachmentEntry[]) {\n this._attachments = attachments;\n }\n\n /**\n * Returns an implementation of fetch that will transform the request to a multipart request if any files are found\n */\n public decoratedFetch(fetchOverride: typeof fetch = fetch): typeof fetch {\n if (this._attachments.length > 0) {\n return async (input: RequestInfo | URL, init?: RequestInit) => {\n const originalRequest = new Request(input, init);\n // we're going to overwrite the body but firefox complains if we don't clone it\n const originalBody = await originalRequest.clone().text();\n\n if (JSON.parse(originalBody)?.operationName !== 'IntrospectionQuery') {\n const body = this.asFormData(originalBody);\n const headers = new Headers(originalRequest.headers);\n\n // make sure form data sets the correct content type\n headers.delete('content-type');\n\n return await fetchOverride(originalRequest, { body, headers });\n } else {\n // just pass through for an introspection query\n return fetchOverride(originalRequest);\n }\n };\n } else {\n return fetchOverride;\n }\n }\n\n /**\n * Constructs a GraphiQL Fetcher that provides FormData to the passed in MultipartFetcher\n */\n public decoratedFetcher(fetcher: MultipartFetcher): Fetcher {\n return (graphQLParams: FetcherParams, opts?: FetcherOpts) => {\n return fetcher(graphQLParams, this.asFormData(graphQLParams), opts);\n };\n }\n\n get attachments(): Map<string, File> {\n return new Map(this._attachments.map(({ name, value }) => [name, value]));\n }\n\n private asFormData(operations: FetcherParams | string): FormData | null {\n const attachments = this.attachments;\n\n // verify our attachments\n for (const invalidKey of [OPERATIONS_KEY, MAP_KEY]) {\n if (attachments.has(invalidKey)) {\n console.error(`Cannot have an attachment named \"${invalidKey}\"`);\n\n return null;\n }\n }\n\n if (attachments.size > 0) {\n const formData = new FormData();\n\n const encodedOperations = typeof operations === 'string' ? operations : JSON.stringify(operations);\n formData.set(OPERATIONS_KEY, encodedOperations);\n\n attachments.forEach((value, key) => {\n formData.append(key, value);\n });\n\n return formData;\n } else {\n return null;\n }\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,21 @@
1
+ import { GraphiQLPlugin } from '@graphiql/react';
2
+ import React from 'react';
3
+ import { AttachmentDeleteAction, AttachmentUpdateAction } from './context.js';
4
+ import '../../style.css';
5
+ type AttachmentContentProps = {
6
+ name: string;
7
+ value: File | null;
8
+ updateAttachment: (action: Omit<AttachmentUpdateAction, 'id'> | Omit<AttachmentDeleteAction, 'id'>) => void;
9
+ error?: string;
10
+ };
11
+ declare const GRAPHIQL_PLUGIN: GraphiQLPlugin;
12
+ export declare function AttachmentContent({ name, value, updateAttachment, error }: AttachmentContentProps): React.JSX.Element;
13
+ export declare function PartNameComponent({ name, setName }: {
14
+ name: string;
15
+ setName: (name: string) => void;
16
+ }): import("react/jsx-runtime").JSX.Element;
17
+ export declare function PartHeadersComponent({ name, value }: {
18
+ name: string;
19
+ value: File;
20
+ }): import("react/jsx-runtime").JSX.Element;
21
+ export default GRAPHIQL_PLUGIN;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PartHeadersComponent = exports.PartNameComponent = exports.AttachmentContent = void 0;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("@graphiql/react");
9
+ const react_2 = require("react");
10
+ const attachment_icon_svg_js_1 = __importDefault(require("./attachment-icon.svg.js"));
11
+ const context_js_1 = require("./context.js");
12
+ require("../../style.css");
13
+ const GRAPHIQL_PLUGIN = {
14
+ title: 'Multipart Attachments',
15
+ icon: () => {
16
+ const [attachments] = (0, react_2.useContext)(context_js_1.MultipartAttachmentsInternalContext);
17
+ const pluginContext = (0, react_1.usePluginContext)();
18
+ const style = {
19
+ fontFamily: 'var(--font-family-mono)',
20
+ fontSize: 'var(--font-size-inline-code)',
21
+ textAlign: 'center',
22
+ width: 'auto',
23
+ };
24
+ if (pluginContext?.visiblePlugin === GRAPHIQL_PLUGIN) {
25
+ style.fontWeight = 'bold';
26
+ }
27
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'attachments-icon', children: [attachment_icon_svg_js_1.default, attachments.validAttachments.length > 0 ? ((0, jsx_runtime_1.jsx)("span", { className: 'attachments-icon-badge', children: attachments.validAttachments.length })) : ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}))] }));
28
+ },
29
+ content: () => {
30
+ const [attachments, attachmentsDispatch] = (0, react_2.useContext)(context_js_1.MultipartAttachmentsInternalContext);
31
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'attachments-container', children: [(0, jsx_runtime_1.jsx)("h2", { children: "Multipart Attachments" }), attachments.map(({ id, name, value, error }) => {
32
+ return ((0, jsx_runtime_1.jsx)(AttachmentContent, { name: name, value: value, updateAttachment: (action) => attachmentsDispatch({ ...action, id }), error: error }, id));
33
+ }), (0, jsx_runtime_1.jsx)("div", { style: { textAlign: 'center' }, children: (0, jsx_runtime_1.jsx)("button", { onClick: () => attachmentsDispatch({ action: 'add' }), children: "Add Part" }) })] }));
34
+ },
35
+ };
36
+ function AttachmentContent({ name, value, updateAttachment, error }) {
37
+ const fileInputRef = (0, react_2.useRef)(null);
38
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'attachment-content', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'attachment-title', children: [(0, jsx_runtime_1.jsx)(PartNameComponent, { name: name, setName: (name) => updateAttachment({ action: 'update', name }) }), (0, jsx_runtime_1.jsx)("button", { onClick: () => updateAttachment({ action: 'delete' }), className: 'attachment-delete', children: (0, jsx_runtime_1.jsx)(react_1.TrashIcon, {}) })] }), error ? (0, jsx_runtime_1.jsx)("div", { className: 'graphiql-doc-explorer-error', children: error }) : (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), value ? (0, jsx_runtime_1.jsx)(PartHeadersComponent, { name: name, value: value }) : (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { style: { marginTop: '0.5em' }, onClick: () => fileInputRef.current?.click(), children: "Attach File" }), (0, jsx_runtime_1.jsx)("input", { type: 'file', ref: fileInputRef, onChange: (event) => {
39
+ const file = event.target.files?.[0];
40
+ if (file) {
41
+ updateAttachment({ action: 'update', value: file });
42
+ }
43
+ }, style: { display: 'none' } })] })] }));
44
+ }
45
+ exports.AttachmentContent = AttachmentContent;
46
+ function PartNameComponent({ name, setName }) {
47
+ const [isEditing, setIsEditing] = (0, react_2.useState)(false);
48
+ const inputRef = (0, react_2.useRef)(null);
49
+ (0, react_2.useEffect)(() => {
50
+ if (isEditing) {
51
+ inputRef.current?.focus();
52
+ }
53
+ }, [isEditing]);
54
+ function endEditing() {
55
+ setIsEditing(false);
56
+ if (inputRef.current) {
57
+ setName(inputRef.current.value);
58
+ }
59
+ }
60
+ function onSubmit(event) {
61
+ event.preventDefault();
62
+ event.stopPropagation();
63
+ endEditing();
64
+ }
65
+ if (isEditing) {
66
+ return ((0, jsx_runtime_1.jsx)("form", { onSubmit: onSubmit, children: (0, jsx_runtime_1.jsx)("input", { ref: inputRef, type: 'text', defaultValue: name, onBlur: endEditing, onKeyDown: (e) => {
67
+ if (e.key === 'Escape') {
68
+ onSubmit(e);
69
+ }
70
+ } }) }));
71
+ }
72
+ else {
73
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { onClick: () => setIsEditing(true), style: { cursor: 'pointer' }, children: [name, "\u00A0", (0, jsx_runtime_1.jsx)(react_1.PenIcon, {})] }) }));
74
+ }
75
+ }
76
+ exports.PartNameComponent = PartNameComponent;
77
+ function PartHeadersComponent({ name, value }) {
78
+ const baseContentDisposition = `form-data; name="${encodeURIComponent(name)}"`;
79
+ let headers;
80
+ if (typeof value !== 'string') {
81
+ headers = [
82
+ ['Content-Disposition', `${baseContentDisposition}; filename="${encodeURIComponent(value.name)}"`],
83
+ ['Content-Type', value.type.length > 0 ? value.type : 'application/octet-stream'],
84
+ ];
85
+ }
86
+ else {
87
+ headers = [['Content-Disposition', baseContentDisposition]];
88
+ }
89
+ return ((0, jsx_runtime_1.jsx)("div", { className: 'attachment-header', children: headers.map(([name, value]) => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("span", { style: { color: 'hsl(var(--color-info))' }, children: [name, ": "] }), (0, jsx_runtime_1.jsx)("span", { children: value })] }, name))) }));
90
+ }
91
+ exports.PartHeadersComponent = PartHeadersComponent;
92
+ exports.default = GRAPHIQL_PLUGIN;
93
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/plugin.tsx"],"names":[],"mappings":"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","sourcesContent":["import { GraphiQLPlugin, PenIcon, TrashIcon, usePluginContext } from '@graphiql/react';\nimport React, { CSSProperties, useContext, useEffect, useRef, useState } from 'react';\nimport uploadClipSvg from './attachment-icon.svg.js';\nimport { AttachmentDeleteAction, AttachmentUpdateAction, MultipartAttachmentsInternalContext } from './context.js';\n\nimport '../../style.css';\n\ntype AttachmentContentProps = {\n name: string;\n value: File | null;\n updateAttachment: (action: Omit<AttachmentUpdateAction, 'id'> | Omit<AttachmentDeleteAction, 'id'>) => void;\n error?: string;\n};\n\nconst GRAPHIQL_PLUGIN: GraphiQLPlugin = {\n title: 'Multipart Attachments',\n icon: () => {\n const [attachments] = useContext(MultipartAttachmentsInternalContext);\n const pluginContext = usePluginContext();\n\n const style: CSSProperties = {\n fontFamily: 'var(--font-family-mono)',\n fontSize: 'var(--font-size-inline-code)',\n textAlign: 'center',\n width: 'auto',\n };\n if (pluginContext?.visiblePlugin === GRAPHIQL_PLUGIN) {\n style.fontWeight = 'bold';\n }\n\n return (\n <div className='attachments-icon'>\n {uploadClipSvg}\n {attachments.validAttachments.length > 0 ? (\n <span className='attachments-icon-badge'>{attachments.validAttachments.length}</span>\n ) : (\n <></>\n )}\n </div>\n );\n },\n content: () => {\n const [attachments, attachmentsDispatch] = useContext(MultipartAttachmentsInternalContext);\n\n return (\n <div className={'attachments-container'}>\n <h2>Multipart Attachments</h2>\n {attachments.map(({ id, name, value, error }) => {\n return (\n <AttachmentContent\n key={id}\n name={name}\n value={value}\n updateAttachment={(action) => attachmentsDispatch({ ...action, id })}\n error={error}\n />\n );\n })}\n <div style={{ textAlign: 'center' }}>\n <button onClick={() => attachmentsDispatch({ action: 'add' })}>Add Part</button>\n </div>\n </div>\n );\n },\n};\n\nexport function AttachmentContent({ name, value, updateAttachment, error }: AttachmentContentProps): React.JSX.Element {\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n\n return (\n <div className={'attachment-content'}>\n <div className={'attachment-title'}>\n <PartNameComponent name={name} setName={(name) => updateAttachment({ action: 'update', name })} />\n <button onClick={() => updateAttachment({ action: 'delete' })} className={'attachment-delete'}>\n <TrashIcon />\n </button>\n </div>\n {error ? <div className='graphiql-doc-explorer-error'>{error}</div> : <></>}\n {value ? <PartHeadersComponent name={name} value={value} /> : <></>}\n <div>\n <button style={{ marginTop: '0.5em' }} onClick={() => fileInputRef.current?.click()}>\n Attach File\n </button>\n <input\n type='file'\n ref={fileInputRef}\n onChange={(event) => {\n const file = event.target.files?.[0];\n if (file) {\n updateAttachment({ action: 'update', value: file });\n }\n }}\n style={{ display: 'none' }}\n />\n </div>\n </div>\n );\n}\n\nexport function PartNameComponent({ name, setName }: { name: string; setName: (name: string) => void }) {\n const [isEditing, setIsEditing] = useState(false);\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n useEffect(() => {\n if (isEditing) {\n inputRef.current?.focus();\n }\n }, [isEditing]);\n\n function endEditing() {\n setIsEditing(false);\n if (inputRef.current) {\n setName(inputRef.current.value);\n }\n }\n function onSubmit(event: React.BaseSyntheticEvent) {\n event.preventDefault();\n event.stopPropagation();\n endEditing();\n }\n\n if (isEditing) {\n return (\n <form onSubmit={onSubmit}>\n <input\n ref={inputRef}\n type='text'\n defaultValue={name}\n onBlur={endEditing}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n onSubmit(e);\n }\n }}\n />\n </form>\n );\n } else {\n return (\n <>\n <span onClick={() => setIsEditing(true)} style={{ cursor: 'pointer' }}>\n {name}\n &nbsp;\n <PenIcon />\n </span>\n </>\n );\n }\n}\n\nexport function PartHeadersComponent({ name, value }: { name: string; value: File }) {\n const baseContentDisposition = `form-data; name=\"${encodeURIComponent(name)}\"`;\n let headers;\n if (typeof value !== 'string') {\n headers = [\n ['Content-Disposition', `${baseContentDisposition}; filename=\"${encodeURIComponent(value.name)}\"`],\n ['Content-Type', value.type.length > 0 ? value.type : 'application/octet-stream'],\n ];\n } else {\n headers = [['Content-Disposition', baseContentDisposition]];\n }\n\n return (\n <div className={'attachment-header'}>\n {headers.map(([name, value]) => (\n <div key={name}>\n <span style={{ color: 'hsl(var(--color-info))' }}>{name}: </span>\n <span>{value}</span>\n </div>\n ))}\n </div>\n );\n}\n\nexport default GRAPHIQL_PLUGIN;\n"]}
@@ -0,0 +1,2 @@
1
+ declare const _default: import("react/jsx-runtime").JSX.Element;
2
+ export default _default;
@@ -0,0 +1,3 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default (_jsx("svg", { height: '1em', viewBox: '0 0 14 14', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', children: _jsx("path", { d: 'M 9 4 L 4.5 8.5 C 3.5 9.5 5 11 6 10 L 11.5 4.5 C 13.5 2.5 11 -0.5 8.5 2 L 2.5 8 C 0 10.5 4 14.5 6.5 12 L 12.5 6', stroke: 'currentColor', strokeWidth: '0.8' }) }));
3
+ //# sourceMappingURL=attachment-icon.svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-icon.svg.js","sourceRoot":"","sources":["../../src/attachment-icon.svg.tsx"],"names":[],"mappings":";AAAA,eAAe,CACb,cAAK,MAAM,EAAC,KAAK,EAAC,OAAO,EAAC,WAAW,EAAC,IAAI,EAAC,MAAM,EAAC,KAAK,EAAC,4BAA4B,YAClF,eACE,CAAC,EAAC,iHAAiH,EACnH,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,KAAK,GACjB,GACE,CACP,CAAC","sourcesContent":["export default (\n <svg height='1em' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'>\n <path\n d='M 9 4 L 4.5 8.5 C 3.5 9.5 5 11 6 10 L 11.5 4.5 C 13.5 2.5 11 -0.5 8.5 2 L 2.5 8 C 0 10.5 4 14.5 6.5 12 L 12.5 6'\n stroke='currentColor'\n strokeWidth='0.8'\n />\n </svg>\n);\n"]}
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { AttachmentEntry, MultipartAttachments } from './multipart-attachments.js';
3
+ export type AttachmentAddAction = {
4
+ action: 'add';
5
+ name?: string;
6
+ };
7
+ export type AttachmentUpdateAction = {
8
+ action: 'update';
9
+ id: number;
10
+ name?: string;
11
+ value?: File;
12
+ };
13
+ export type AttachmentDeleteAction = {
14
+ action: 'delete';
15
+ id: number;
16
+ };
17
+ export type AttachmentsAction = AttachmentAddAction | AttachmentUpdateAction | AttachmentDeleteAction;
18
+ export type InternalAttachmentEntry = {
19
+ id: number;
20
+ } & (({
21
+ error?: string;
22
+ } & AttachmentEntry) | {
23
+ name: string;
24
+ value: null;
25
+ error: string;
26
+ });
27
+ export declare const NO_FILE_ERROR = "No File Attached";
28
+ export declare const DUPLICATE_NAME_ERROR = "Duplicate Name";
29
+ export declare class InternalAttachmentState {
30
+ private readonly attachmentId;
31
+ readonly attachments: InternalAttachmentEntry[];
32
+ constructor(attachments?: InternalAttachmentEntry[], attachmentId?: number);
33
+ get validAttachments(): AttachmentEntry[];
34
+ map<T>(callback: (value: InternalAttachmentEntry, index: number, array: InternalAttachmentEntry[]) => T): T[];
35
+ reduce(action: AttachmentsAction): InternalAttachmentState;
36
+ }
37
+ export declare const MultipartAttachmentsContext: React.Context<MultipartAttachments>;
38
+ export declare const MultipartAttachmentsInternalContext: React.Context<[InternalAttachmentState, React.Dispatch<AttachmentsAction>]>;
39
+ export declare function MultipartAttachmentsProvider({ children }: {
40
+ children: React.ReactNode;
41
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useReducer } from 'react';
3
+ import { MultipartAttachments } from './multipart-attachments.js';
4
+ export const NO_FILE_ERROR = 'No File Attached';
5
+ export const DUPLICATE_NAME_ERROR = 'Duplicate Name';
6
+ export class InternalAttachmentState {
7
+ attachmentId;
8
+ attachments;
9
+ constructor(attachments = [], attachmentId = 0) {
10
+ this.attachments = attachments;
11
+ this.attachmentId = attachmentId;
12
+ }
13
+ get validAttachments() {
14
+ return this.attachments.filter((attachment) => attachment.value !== null && !attachment.error);
15
+ }
16
+ map(callback) {
17
+ return this.attachments.map(callback);
18
+ }
19
+ reduce(action) {
20
+ let nextAttachments;
21
+ let nextAttachmentId = this.attachmentId;
22
+ switch (action.action) {
23
+ case 'add': {
24
+ nextAttachments = [
25
+ ...this.attachments,
26
+ {
27
+ id: this.attachmentId,
28
+ name: action.name ?? `part${this.attachmentId}`,
29
+ value: null,
30
+ },
31
+ ];
32
+ nextAttachmentId++;
33
+ break;
34
+ }
35
+ case 'update': {
36
+ nextAttachments = this.attachments.map((attachment) => {
37
+ if (attachment.id === action.id) {
38
+ return {
39
+ ...attachment,
40
+ name: action.name ?? attachment.name,
41
+ value: action.value !== undefined ? action.value : attachment.value,
42
+ };
43
+ }
44
+ else {
45
+ return attachment;
46
+ }
47
+ });
48
+ break;
49
+ }
50
+ case 'delete': {
51
+ nextAttachments = this.attachments.filter((attachment) => attachment.id !== action.id);
52
+ break;
53
+ }
54
+ default: {
55
+ throw new Error(`Unknown action ${action}`);
56
+ }
57
+ }
58
+ for (const attachment of nextAttachments) {
59
+ if (nextAttachments.filter(({ name }) => name === attachment.name).length > 1) {
60
+ attachment.error = DUPLICATE_NAME_ERROR;
61
+ }
62
+ else if (attachment.value === null) {
63
+ attachment.error = NO_FILE_ERROR;
64
+ }
65
+ else {
66
+ delete attachment.error;
67
+ }
68
+ }
69
+ return new InternalAttachmentState(nextAttachments, nextAttachmentId);
70
+ }
71
+ }
72
+ export const MultipartAttachmentsContext = createContext(new MultipartAttachments([]));
73
+ export const MultipartAttachmentsInternalContext = createContext([
74
+ new InternalAttachmentState(),
75
+ () => {
76
+ throw new Error('MultipartAttachmentsProvider not found. Make sure to render it higher up in the tree');
77
+ },
78
+ ]);
79
+ export function MultipartAttachmentsProvider({ children }) {
80
+ const [attachments, setAttachmentStates] = useReducer((attachments, action) => attachments.reduce(action), new InternalAttachmentState());
81
+ return (_jsx(MultipartAttachmentsContext.Provider, { value: new MultipartAttachments(attachments.validAttachments), children: _jsx(MultipartAttachmentsInternalContext.Provider, { value: [attachments, setAttachmentStates], children: children }) }));
82
+ }
83
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/context.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { createContext, Reducer, useReducer } from 'react';\nimport { AttachmentEntry, MultipartAttachments } from './multipart-attachments.js';\n\nexport type AttachmentAddAction = {\n action: 'add';\n name?: string;\n};\n\nexport type AttachmentUpdateAction = {\n action: 'update';\n id: number;\n name?: string;\n value?: File;\n};\n\nexport type AttachmentDeleteAction = {\n action: 'delete';\n id: number;\n};\n\nexport type AttachmentsAction = AttachmentAddAction | AttachmentUpdateAction | AttachmentDeleteAction;\n\nexport type InternalAttachmentEntry = { id: number } & (\n | ({ error?: string } & AttachmentEntry)\n | { name: string; value: null; error: string }\n);\n\nexport const NO_FILE_ERROR = 'No File Attached';\nexport const DUPLICATE_NAME_ERROR = 'Duplicate Name';\n\nexport class InternalAttachmentState {\n private readonly attachmentId;\n public readonly attachments: InternalAttachmentEntry[];\n\n constructor(attachments: InternalAttachmentEntry[] = [], attachmentId = 0) {\n this.attachments = attachments;\n this.attachmentId = attachmentId;\n }\n\n public get validAttachments(): AttachmentEntry[] {\n return this.attachments.filter(\n (\n attachment: InternalAttachmentEntry\n ): attachment is AttachmentEntry & {\n id: number;\n } => attachment.value !== null && !attachment.error\n );\n }\n\n public map<T>(callback: (value: InternalAttachmentEntry, index: number, array: InternalAttachmentEntry[]) => T): T[] {\n return this.attachments.map(callback);\n }\n\n public reduce(action: AttachmentsAction): InternalAttachmentState {\n let nextAttachments;\n let nextAttachmentId = this.attachmentId;\n switch (action.action) {\n case 'add': {\n nextAttachments = [\n ...this.attachments,\n {\n id: this.attachmentId,\n name: action.name ?? `part${this.attachmentId}`,\n value: null,\n },\n ];\n nextAttachmentId++;\n break;\n }\n case 'update': {\n nextAttachments = this.attachments.map((attachment) => {\n if (attachment.id === action.id) {\n return {\n ...attachment,\n name: action.name ?? attachment.name,\n value: action.value !== undefined ? action.value : attachment.value,\n };\n } else {\n return attachment;\n }\n });\n break;\n }\n case 'delete': {\n nextAttachments = this.attachments.filter((attachment) => attachment.id !== action.id);\n break;\n }\n default: {\n throw new Error(`Unknown action ${action}`);\n }\n }\n\n for (const attachment of nextAttachments) {\n if (nextAttachments.filter(({ name }) => name === attachment.name).length > 1) {\n attachment.error = DUPLICATE_NAME_ERROR;\n } else if (attachment.value === null) {\n attachment.error = NO_FILE_ERROR;\n } else {\n delete attachment.error;\n }\n }\n\n return new InternalAttachmentState(nextAttachments as InternalAttachmentEntry[], nextAttachmentId);\n }\n}\n\nexport const MultipartAttachmentsContext = createContext<MultipartAttachments>(new MultipartAttachments([]));\nexport const MultipartAttachmentsInternalContext = createContext<\n [InternalAttachmentState, React.Dispatch<AttachmentsAction>]\n>([\n new InternalAttachmentState(),\n () => {\n throw new Error('MultipartAttachmentsProvider not found. Make sure to render it higher up in the tree');\n },\n]);\n\nexport function MultipartAttachmentsProvider({ children }: { children: React.ReactNode }) {\n const [attachments, setAttachmentStates] = useReducer<Reducer<InternalAttachmentState, AttachmentsAction>>(\n (attachments, action) => attachments.reduce(action),\n new InternalAttachmentState()\n );\n\n return (\n <MultipartAttachmentsContext.Provider value={new MultipartAttachments(attachments.validAttachments)}>\n <MultipartAttachmentsInternalContext.Provider value={[attachments, setAttachmentStates]}>\n {children}\n </MultipartAttachmentsInternalContext.Provider>\n </MultipartAttachmentsContext.Provider>\n );\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export type { MultipartAttachments, MultipartFetcher } from './multipart-attachments.js';
2
+ export { default as MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN } from './plugin.js';
3
+ export { MultipartAttachmentsContext, MultipartAttachmentsProvider } from './context.js';
@@ -0,0 +1,3 @@
1
+ export { default as MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN } from './plugin.js';
2
+ export { MultipartAttachmentsContext, MultipartAttachmentsProvider } from './context.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,qCAAqC,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC","sourcesContent":["export type { MultipartAttachments, MultipartFetcher } from './multipart-attachments.js';\n\nexport { default as MULTIPART_ATTACHMENTS_GRAPHIQL_PLUGIN } from './plugin.js';\nexport { MultipartAttachmentsContext, MultipartAttachmentsProvider } from './context.js';\n"]}
@@ -0,0 +1,20 @@
1
+ import { Fetcher, FetcherOpts, FetcherParams, FetcherReturnType } from '@graphiql/toolkit';
2
+ export type AttachmentEntry = {
3
+ name: string;
4
+ value: File;
5
+ };
6
+ export type MultipartFetcher = (graphQLParams: FetcherParams, multipartFormData: FormData | null, opts?: FetcherOpts) => FetcherReturnType;
7
+ export declare class MultipartAttachments {
8
+ private _attachments;
9
+ constructor(attachments: AttachmentEntry[]);
10
+ /**
11
+ * Returns an implementation of fetch that will transform the request to a multipart request if any files are found
12
+ */
13
+ decoratedFetch(fetchOverride?: typeof fetch): typeof fetch;
14
+ /**
15
+ * Constructs a GraphiQL Fetcher that provides FormData to the passed in MultipartFetcher
16
+ */
17
+ decoratedFetcher(fetcher: MultipartFetcher): Fetcher;
18
+ get attachments(): Map<string, File>;
19
+ private asFormData;
20
+ }
@@ -0,0 +1,68 @@
1
+ const OPERATIONS_KEY = 'operations';
2
+ const MAP_KEY = 'map';
3
+ export class MultipartAttachments {
4
+ _attachments;
5
+ constructor(attachments) {
6
+ this._attachments = attachments;
7
+ }
8
+ /**
9
+ * Returns an implementation of fetch that will transform the request to a multipart request if any files are found
10
+ */
11
+ decoratedFetch(fetchOverride = fetch) {
12
+ if (this._attachments.length > 0) {
13
+ return async (input, init) => {
14
+ const originalRequest = new Request(input, init);
15
+ // we're going to overwrite the body but firefox complains if we don't clone it
16
+ const originalBody = await originalRequest.clone().text();
17
+ if (JSON.parse(originalBody)?.operationName !== 'IntrospectionQuery') {
18
+ const body = this.asFormData(originalBody);
19
+ const headers = new Headers(originalRequest.headers);
20
+ // make sure form data sets the correct content type
21
+ headers.delete('content-type');
22
+ return await fetchOverride(originalRequest, { body, headers });
23
+ }
24
+ else {
25
+ // just pass through for an introspection query
26
+ return fetchOverride(originalRequest);
27
+ }
28
+ };
29
+ }
30
+ else {
31
+ return fetchOverride;
32
+ }
33
+ }
34
+ /**
35
+ * Constructs a GraphiQL Fetcher that provides FormData to the passed in MultipartFetcher
36
+ */
37
+ decoratedFetcher(fetcher) {
38
+ return (graphQLParams, opts) => {
39
+ return fetcher(graphQLParams, this.asFormData(graphQLParams), opts);
40
+ };
41
+ }
42
+ get attachments() {
43
+ return new Map(this._attachments.map(({ name, value }) => [name, value]));
44
+ }
45
+ asFormData(operations) {
46
+ const attachments = this.attachments;
47
+ // verify our attachments
48
+ for (const invalidKey of [OPERATIONS_KEY, MAP_KEY]) {
49
+ if (attachments.has(invalidKey)) {
50
+ console.error(`Cannot have an attachment named "${invalidKey}"`);
51
+ return null;
52
+ }
53
+ }
54
+ if (attachments.size > 0) {
55
+ const formData = new FormData();
56
+ const encodedOperations = typeof operations === 'string' ? operations : JSON.stringify(operations);
57
+ formData.set(OPERATIONS_KEY, encodedOperations);
58
+ attachments.forEach((value, key) => {
59
+ formData.append(key, value);
60
+ });
61
+ return formData;
62
+ }
63
+ else {
64
+ return null;
65
+ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=multipart-attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multipart-attachments.js","sourceRoot":"","sources":["../../src/multipart-attachments.ts"],"names":[],"mappings":"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","sourcesContent":["import { Fetcher, FetcherOpts, FetcherParams, FetcherReturnType } from '@graphiql/toolkit';\n\nconst OPERATIONS_KEY = 'operations';\nconst MAP_KEY = 'map';\n\nexport type AttachmentEntry = {\n name: string;\n value: File;\n};\n\nexport type MultipartFetcher = (\n graphQLParams: FetcherParams,\n multipartFormData: FormData | null,\n opts?: FetcherOpts\n) => FetcherReturnType;\n\nexport class MultipartAttachments {\n private _attachments: AttachmentEntry[];\n\n constructor(attachments: AttachmentEntry[]) {\n this._attachments = attachments;\n }\n\n /**\n * Returns an implementation of fetch that will transform the request to a multipart request if any files are found\n */\n public decoratedFetch(fetchOverride: typeof fetch = fetch): typeof fetch {\n if (this._attachments.length > 0) {\n return async (input: RequestInfo | URL, init?: RequestInit) => {\n const originalRequest = new Request(input, init);\n // we're going to overwrite the body but firefox complains if we don't clone it\n const originalBody = await originalRequest.clone().text();\n\n if (JSON.parse(originalBody)?.operationName !== 'IntrospectionQuery') {\n const body = this.asFormData(originalBody);\n const headers = new Headers(originalRequest.headers);\n\n // make sure form data sets the correct content type\n headers.delete('content-type');\n\n return await fetchOverride(originalRequest, { body, headers });\n } else {\n // just pass through for an introspection query\n return fetchOverride(originalRequest);\n }\n };\n } else {\n return fetchOverride;\n }\n }\n\n /**\n * Constructs a GraphiQL Fetcher that provides FormData to the passed in MultipartFetcher\n */\n public decoratedFetcher(fetcher: MultipartFetcher): Fetcher {\n return (graphQLParams: FetcherParams, opts?: FetcherOpts) => {\n return fetcher(graphQLParams, this.asFormData(graphQLParams), opts);\n };\n }\n\n get attachments(): Map<string, File> {\n return new Map(this._attachments.map(({ name, value }) => [name, value]));\n }\n\n private asFormData(operations: FetcherParams | string): FormData | null {\n const attachments = this.attachments;\n\n // verify our attachments\n for (const invalidKey of [OPERATIONS_KEY, MAP_KEY]) {\n if (attachments.has(invalidKey)) {\n console.error(`Cannot have an attachment named \"${invalidKey}\"`);\n\n return null;\n }\n }\n\n if (attachments.size > 0) {\n const formData = new FormData();\n\n const encodedOperations = typeof operations === 'string' ? operations : JSON.stringify(operations);\n formData.set(OPERATIONS_KEY, encodedOperations);\n\n attachments.forEach((value, key) => {\n formData.append(key, value);\n });\n\n return formData;\n } else {\n return null;\n }\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,21 @@
1
+ import { GraphiQLPlugin } from '@graphiql/react';
2
+ import React from 'react';
3
+ import { AttachmentDeleteAction, AttachmentUpdateAction } from './context.js';
4
+ import '../../style.css';
5
+ type AttachmentContentProps = {
6
+ name: string;
7
+ value: File | null;
8
+ updateAttachment: (action: Omit<AttachmentUpdateAction, 'id'> | Omit<AttachmentDeleteAction, 'id'>) => void;
9
+ error?: string;
10
+ };
11
+ declare const GRAPHIQL_PLUGIN: GraphiQLPlugin;
12
+ export declare function AttachmentContent({ name, value, updateAttachment, error }: AttachmentContentProps): React.JSX.Element;
13
+ export declare function PartNameComponent({ name, setName }: {
14
+ name: string;
15
+ setName: (name: string) => void;
16
+ }): import("react/jsx-runtime").JSX.Element;
17
+ export declare function PartHeadersComponent({ name, value }: {
18
+ name: string;
19
+ value: File;
20
+ }): import("react/jsx-runtime").JSX.Element;
21
+ export default GRAPHIQL_PLUGIN;
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PenIcon, TrashIcon, usePluginContext } from '@graphiql/react';
3
+ import { useContext, useEffect, useRef, useState } from 'react';
4
+ import uploadClipSvg from './attachment-icon.svg.js';
5
+ import { MultipartAttachmentsInternalContext } from './context.js';
6
+ import '../../style.css';
7
+ const GRAPHIQL_PLUGIN = {
8
+ title: 'Multipart Attachments',
9
+ icon: () => {
10
+ const [attachments] = useContext(MultipartAttachmentsInternalContext);
11
+ const pluginContext = usePluginContext();
12
+ const style = {
13
+ fontFamily: 'var(--font-family-mono)',
14
+ fontSize: 'var(--font-size-inline-code)',
15
+ textAlign: 'center',
16
+ width: 'auto',
17
+ };
18
+ if (pluginContext?.visiblePlugin === GRAPHIQL_PLUGIN) {
19
+ style.fontWeight = 'bold';
20
+ }
21
+ return (_jsxs("div", { className: 'attachments-icon', children: [uploadClipSvg, attachments.validAttachments.length > 0 ? (_jsx("span", { className: 'attachments-icon-badge', children: attachments.validAttachments.length })) : (_jsx(_Fragment, {}))] }));
22
+ },
23
+ content: () => {
24
+ const [attachments, attachmentsDispatch] = useContext(MultipartAttachmentsInternalContext);
25
+ return (_jsxs("div", { className: 'attachments-container', children: [_jsx("h2", { children: "Multipart Attachments" }), attachments.map(({ id, name, value, error }) => {
26
+ return (_jsx(AttachmentContent, { name: name, value: value, updateAttachment: (action) => attachmentsDispatch({ ...action, id }), error: error }, id));
27
+ }), _jsx("div", { style: { textAlign: 'center' }, children: _jsx("button", { onClick: () => attachmentsDispatch({ action: 'add' }), children: "Add Part" }) })] }));
28
+ },
29
+ };
30
+ export function AttachmentContent({ name, value, updateAttachment, error }) {
31
+ const fileInputRef = useRef(null);
32
+ return (_jsxs("div", { className: 'attachment-content', children: [_jsxs("div", { className: 'attachment-title', children: [_jsx(PartNameComponent, { name: name, setName: (name) => updateAttachment({ action: 'update', name }) }), _jsx("button", { onClick: () => updateAttachment({ action: 'delete' }), className: 'attachment-delete', children: _jsx(TrashIcon, {}) })] }), error ? _jsx("div", { className: 'graphiql-doc-explorer-error', children: error }) : _jsx(_Fragment, {}), value ? _jsx(PartHeadersComponent, { name: name, value: value }) : _jsx(_Fragment, {}), _jsxs("div", { children: [_jsx("button", { style: { marginTop: '0.5em' }, onClick: () => fileInputRef.current?.click(), children: "Attach File" }), _jsx("input", { type: 'file', ref: fileInputRef, onChange: (event) => {
33
+ const file = event.target.files?.[0];
34
+ if (file) {
35
+ updateAttachment({ action: 'update', value: file });
36
+ }
37
+ }, style: { display: 'none' } })] })] }));
38
+ }
39
+ export function PartNameComponent({ name, setName }) {
40
+ const [isEditing, setIsEditing] = useState(false);
41
+ const inputRef = useRef(null);
42
+ useEffect(() => {
43
+ if (isEditing) {
44
+ inputRef.current?.focus();
45
+ }
46
+ }, [isEditing]);
47
+ function endEditing() {
48
+ setIsEditing(false);
49
+ if (inputRef.current) {
50
+ setName(inputRef.current.value);
51
+ }
52
+ }
53
+ function onSubmit(event) {
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ endEditing();
57
+ }
58
+ if (isEditing) {
59
+ return (_jsx("form", { onSubmit: onSubmit, children: _jsx("input", { ref: inputRef, type: 'text', defaultValue: name, onBlur: endEditing, onKeyDown: (e) => {
60
+ if (e.key === 'Escape') {
61
+ onSubmit(e);
62
+ }
63
+ } }) }));
64
+ }
65
+ else {
66
+ return (_jsx(_Fragment, { children: _jsxs("span", { onClick: () => setIsEditing(true), style: { cursor: 'pointer' }, children: [name, "\u00A0", _jsx(PenIcon, {})] }) }));
67
+ }
68
+ }
69
+ export function PartHeadersComponent({ name, value }) {
70
+ const baseContentDisposition = `form-data; name="${encodeURIComponent(name)}"`;
71
+ let headers;
72
+ if (typeof value !== 'string') {
73
+ headers = [
74
+ ['Content-Disposition', `${baseContentDisposition}; filename="${encodeURIComponent(value.name)}"`],
75
+ ['Content-Type', value.type.length > 0 ? value.type : 'application/octet-stream'],
76
+ ];
77
+ }
78
+ else {
79
+ headers = [['Content-Disposition', baseContentDisposition]];
80
+ }
81
+ return (_jsx("div", { className: 'attachment-header', children: headers.map(([name, value]) => (_jsxs("div", { children: [_jsxs("span", { style: { color: 'hsl(var(--color-info))' }, children: [name, ": "] }), _jsx("span", { children: value })] }, name))) }));
82
+ }
83
+ export default GRAPHIQL_PLUGIN;
84
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/plugin.tsx"],"names":[],"mappings":"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","sourcesContent":["import { GraphiQLPlugin, PenIcon, TrashIcon, usePluginContext } from '@graphiql/react';\nimport React, { CSSProperties, useContext, useEffect, useRef, useState } from 'react';\nimport uploadClipSvg from './attachment-icon.svg.js';\nimport { AttachmentDeleteAction, AttachmentUpdateAction, MultipartAttachmentsInternalContext } from './context.js';\n\nimport '../../style.css';\n\ntype AttachmentContentProps = {\n name: string;\n value: File | null;\n updateAttachment: (action: Omit<AttachmentUpdateAction, 'id'> | Omit<AttachmentDeleteAction, 'id'>) => void;\n error?: string;\n};\n\nconst GRAPHIQL_PLUGIN: GraphiQLPlugin = {\n title: 'Multipart Attachments',\n icon: () => {\n const [attachments] = useContext(MultipartAttachmentsInternalContext);\n const pluginContext = usePluginContext();\n\n const style: CSSProperties = {\n fontFamily: 'var(--font-family-mono)',\n fontSize: 'var(--font-size-inline-code)',\n textAlign: 'center',\n width: 'auto',\n };\n if (pluginContext?.visiblePlugin === GRAPHIQL_PLUGIN) {\n style.fontWeight = 'bold';\n }\n\n return (\n <div className='attachments-icon'>\n {uploadClipSvg}\n {attachments.validAttachments.length > 0 ? (\n <span className='attachments-icon-badge'>{attachments.validAttachments.length}</span>\n ) : (\n <></>\n )}\n </div>\n );\n },\n content: () => {\n const [attachments, attachmentsDispatch] = useContext(MultipartAttachmentsInternalContext);\n\n return (\n <div className={'attachments-container'}>\n <h2>Multipart Attachments</h2>\n {attachments.map(({ id, name, value, error }) => {\n return (\n <AttachmentContent\n key={id}\n name={name}\n value={value}\n updateAttachment={(action) => attachmentsDispatch({ ...action, id })}\n error={error}\n />\n );\n })}\n <div style={{ textAlign: 'center' }}>\n <button onClick={() => attachmentsDispatch({ action: 'add' })}>Add Part</button>\n </div>\n </div>\n );\n },\n};\n\nexport function AttachmentContent({ name, value, updateAttachment, error }: AttachmentContentProps): React.JSX.Element {\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n\n return (\n <div className={'attachment-content'}>\n <div className={'attachment-title'}>\n <PartNameComponent name={name} setName={(name) => updateAttachment({ action: 'update', name })} />\n <button onClick={() => updateAttachment({ action: 'delete' })} className={'attachment-delete'}>\n <TrashIcon />\n </button>\n </div>\n {error ? <div className='graphiql-doc-explorer-error'>{error}</div> : <></>}\n {value ? <PartHeadersComponent name={name} value={value} /> : <></>}\n <div>\n <button style={{ marginTop: '0.5em' }} onClick={() => fileInputRef.current?.click()}>\n Attach File\n </button>\n <input\n type='file'\n ref={fileInputRef}\n onChange={(event) => {\n const file = event.target.files?.[0];\n if (file) {\n updateAttachment({ action: 'update', value: file });\n }\n }}\n style={{ display: 'none' }}\n />\n </div>\n </div>\n );\n}\n\nexport function PartNameComponent({ name, setName }: { name: string; setName: (name: string) => void }) {\n const [isEditing, setIsEditing] = useState(false);\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n useEffect(() => {\n if (isEditing) {\n inputRef.current?.focus();\n }\n }, [isEditing]);\n\n function endEditing() {\n setIsEditing(false);\n if (inputRef.current) {\n setName(inputRef.current.value);\n }\n }\n function onSubmit(event: React.BaseSyntheticEvent) {\n event.preventDefault();\n event.stopPropagation();\n endEditing();\n }\n\n if (isEditing) {\n return (\n <form onSubmit={onSubmit}>\n <input\n ref={inputRef}\n type='text'\n defaultValue={name}\n onBlur={endEditing}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n onSubmit(e);\n }\n }}\n />\n </form>\n );\n } else {\n return (\n <>\n <span onClick={() => setIsEditing(true)} style={{ cursor: 'pointer' }}>\n {name}\n &nbsp;\n <PenIcon />\n </span>\n </>\n );\n }\n}\n\nexport function PartHeadersComponent({ name, value }: { name: string; value: File }) {\n const baseContentDisposition = `form-data; name=\"${encodeURIComponent(name)}\"`;\n let headers;\n if (typeof value !== 'string') {\n headers = [\n ['Content-Disposition', `${baseContentDisposition}; filename=\"${encodeURIComponent(value.name)}\"`],\n ['Content-Type', value.type.length > 0 ? value.type : 'application/octet-stream'],\n ];\n } else {\n headers = [['Content-Disposition', baseContentDisposition]];\n }\n\n return (\n <div className={'attachment-header'}>\n {headers.map(([name, value]) => (\n <div key={name}>\n <span style={{ color: 'hsl(var(--color-info))' }}>{name}: </span>\n <span>{value}</span>\n </div>\n ))}\n </div>\n );\n}\n\nexport default GRAPHIQL_PLUGIN;\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@workday/graphiql-plugin-multipart-requests",
3
+ "collaborators": [
4
+ "Dylan Owen <dylan.owen@workday.com>"
5
+ ],
6
+ "version": "0.0.1",
7
+ "scripts": {
8
+ "dev": "tsc --project ./tsconfig.dev.json --watch --pretty --incremental --preserveWatchOutput",
9
+ "test:unit": "jest",
10
+ "build": "tsc --project ./tsconfig.json",
11
+ "release": "tshy",
12
+ "clean": "rimraf dist/"
13
+ },
14
+ "publishConfig": {
15
+ "registry": "https://registry.npmjs.org/",
16
+ "access": "public",
17
+ "dry-run": true
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ "./package.json": "./package.json",
22
+ ".": {
23
+ "import": {
24
+ "types": "./dist/esm/index.d.ts",
25
+ "default": "./dist/esm/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/commonjs/index.d.ts",
29
+ "default": "./dist/commonjs/index.js"
30
+ }
31
+ }
32
+ },
33
+ "types": "./dist/commonjs/index.d.ts",
34
+ "main": "./dist/commonjs/index.js",
35
+ "files": [
36
+ "dist/**/*.@(js|d.ts|js.map|json)",
37
+ "style.css"
38
+ ],
39
+ "directories": {
40
+ "src": "src",
41
+ "test": "tests"
42
+ },
43
+ "peerDependencies": {
44
+ "@graphiql/react": "^0.20.4",
45
+ "graphql": "^16.8.1",
46
+ "react": "^18.3.1",
47
+ "react-dom": "^18.3.1"
48
+ },
49
+ "tshy": {
50
+ "exports": {
51
+ "./package.json": "./package.json",
52
+ ".": "./src/index.ts"
53
+ }
54
+ }
55
+ }
package/style.css ADDED
@@ -0,0 +1,88 @@
1
+ .attachments-container {
2
+ display: contents;
3
+ }
4
+
5
+ .attachments-container h2 {
6
+ font-size: var(--font-size-h2);
7
+ font-family: var(--font-family);
8
+ font-weight: var(--font-weight-medium);
9
+ }
10
+
11
+ .attachments-container button {
12
+ all: unset;
13
+ border-radius: var(--border-radius-4);
14
+ color: hsla(var(--color-neutral), var(--alpha-secondary));
15
+ padding: var(--px-8) var(--px-12);
16
+ cursor: pointer;
17
+ }
18
+
19
+ .attachments-container button:hover {
20
+ background-color: hsla(var(--color-neutral), var(--alpha-background-light));
21
+ }
22
+
23
+ .attachments-container input[type="text"] {
24
+ color: hsla(var(--color-neutral));
25
+ background-color: hsla(var(--color-neutral), var(--alpha-background-light));
26
+ border-radius: var(--border-radius-4);
27
+ font-family: var(--font-family-mono);
28
+ }
29
+
30
+ .attachments-container svg {
31
+ height: var(--px-20);
32
+ width: var(--px-20);
33
+ }
34
+
35
+ .attachment-title {
36
+ display: flex;
37
+ justify-content: space-between;
38
+ font-size: var(--font-size-h3);
39
+ font-family: var(--font-family-mono);
40
+ }
41
+
42
+ .attachment-title > :first-child {
43
+ flex: 1 1 auto
44
+ }
45
+
46
+ .attachment-title > :last-child {
47
+ flex: 0 1 auto;
48
+ }
49
+
50
+ button.attachment-delete {
51
+ width: 2em;
52
+ font-size: 0.8em;
53
+ text-align: center;
54
+ padding: 0;
55
+ }
56
+
57
+ .attachment-delete:hover {
58
+ color: hsl(var(--color-error));
59
+ }
60
+
61
+ .attachment-header {
62
+ justify-content: start;
63
+ font-size: 1em;
64
+ font-family: var(--font-family-mono);
65
+ }
66
+
67
+ .attachments-icon {
68
+ display: inline-block;
69
+ position: relative;
70
+ }
71
+
72
+ .attachments-icon svg {
73
+ height: var(--px-20);
74
+ width: var(--px-20);
75
+ }
76
+
77
+ .attachments-icon-badge {
78
+ background-color: #fa3e3e;
79
+ border-radius: 2px;
80
+ color: white;
81
+
82
+ padding: 1px 3px;
83
+ font-size: 10px;
84
+
85
+ position: absolute; /* Position the badge within the relatively positioned button */
86
+ right: 80%;
87
+ bottom: 80%;
88
+ }