diff --git a/docs/docs/02-Stash/03-get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx b/docs/docs/02-Stash/03-get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx index c627c1ff..53fa51dc 100644 --- a/docs/docs/02-Stash/03-get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx +++ b/docs/docs/02-Stash/03-get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx @@ -5,7 +5,7 @@ description: "This endpoint calculates the borrow quote for a liquidity transact sidebar_label: "Borrow Quote API" hide_title: true hide_table_of_contents: true -api: eJzNVktv2zgQ/isETynAWEXRveiWtEERYNHudrOnIAdaHEtsKZIhh04MQ/99MaTkqI6aNlkUqA+2TM3jmwe/mT1XEJugPWpnec2vOh0ZWOWdtsgaaZpkJEJk2AFbuxDcHbtNDoFtXGCSGX2btNK4YxikjbIhO4Jp25iktG3neo2LKFiA26QDKKatT1i5hD6hYNIqplKQpL/ignsZZA8IIfL6en+E8q/g0DXOMCt7YCewaleCnTXBxfiKC65JxkvsuOAkQf9GDS74BIDXGBIIHpsOesnrPcedJ9mIQduWD4M49vt3jlya1gWNXc9IYXJ/cS8bvLQ5mPz8KUf2HTzZ1f/C8h4iapvzxT4C3rnwlV2+ZycuC0gjmIKNTAYjQ5fLYEepTXB9PiDvEJGtndodcN4mCLsHoKMSX8CmLUILgQ/DTYkEIp47tSORxlkEi/QovTe6yUCrL5Gw75+IcxBHcZ71LlmkGEobLWTt/jS+caduC8E4qabzgQSjdzZCJE9vXr+mn2+t/5OaBmLcJGN2LAAGDVtQ33Q6Fz8fjQ/OQ0BdPCqJC6fF9jsXcSF+wadLsPhyijz32ZMSpfsWO2mWv+s5mpnvY0+P7N6Iya5bf4EGH9nNsS9IkdzbpUqcS3VoSJWACq7tVho9MgVzgfU6RiKVGTm8vDiwTu1iCiEEF36cuWJgEv9OqH8shXppEYKVhkUIWwisWPitIxkER9kSF/M/J8onyXL17mSwZLjmnzyUFmItYF0dxkM1EXC1n56GitxUe/oeqjJUOhlZnwxqbw70dO4UBSo43CPYqJ0tI+Er7Hh9BEDwrTQJfikQYrsesHOK17wFzMMKO17z53ohUs0dUAJKwfCad4g+1lUlvV5FH4hkw2qdtFF3Gru4a3u5alxfZRzUGp8fmPfiXvbezCpNnL5xEyHLhnqLSqkxS1G+IMTSldQw3kXsZe62kf8/AD5r9rO1jKCYs1mr3NzMBUe8PhsRv8/SMfY9wj1W3khtKSe5LPuxxtf84Jzq/rBV1LPncbjX42+p9Y3gXeb8a77fU5L+DWYY6LhM3Lzl6CjXhi7lRpoIT6TsF0z/xeDLJXtYAqbrlbtvK4MmvM/EfvJ5rMYr9oNFbhHSeCjtbo5ngnooQ96YXoLphUveC7Dmt8PNIHgHUkGY0dq7guz0qrTQpPpoLFCUReOsacDjk7Jz4vpwccUFX4/rWu8UqQSZ1yt5V1CWZsozJp/tuZG2TbIl2WKSPv8BqkZTiw== +api: eJzNV21v2zYQ/isEP6UAG1l+WRN9c9y0CDCgWdt9yoLhLJ0tthSpkJQdw9B/H46SHPkl7eZhwPzBpqnj8bm751605Rm61MrSS6N5wmeg0kqBR8d8jmxurDVr9lQZj0zqVFWZ1MtuOzXOC2bxqZIWMyZ1WfnIVL6svGCgM5ZVFkjxJRe8BAsFerSOJw/bg2u/mMqmyGbTu3uW5iA1u3vPLvByeSkYyjKeTJL4DRdcknAJPueCayiQJzyIc8E7GDzxtkLBXZpjATzZcr8pSdB5K/WSC47PUJSKtlrVV+PJiNe1OER1b403qVGMrurgTFNrnHsFTNmeOBNPARvQp5D8FgIAamms9HnBSEUH6PYZUn+ng9PD+lOIwCsIw+Xnoesp53X92ChB529MtqGTqdEetacllKWSaYh99M2RCdveFaU1JVov0QXZwlTNqX2bm31mFsyb76gdWxjL1rlMc7ZGBhZZe3+PkQ1RL6QO7HUFKIXOswy1KaQOeN7s2TS+vhpMxsOYiwPDa5JKVeXkCv9cSKWOAc6NUQiaWSwtOtQBiFywjalYDitknQLpN8wbRlqYz6VrYPZxLEA53EFoFRMGjX5t7Pfjy1OQJXmkAI8Za8WYzE7x+6R1wanHesM2gyyz6By7yPF532GD59lgCDAdXc+Hw9GH26v3g+kAJ7Pxh+E7nL6/HlyNZu8mv8zS4dGlgjd4ecJzfCaa92j40PHgyO8vTuhAP+40m/k3TImNtTiw46ZPiJYorISNMpAd8T8gcaXRrqHkcDA49syXKk3RuUWl1IZZ9FbiCrM95v3BqdKdmQcZ+BO7jfqZcf44OcnotsKefKjkUyUz6Tf3xqiTEhZL2BSo/SxU0WTLUVcFTx5iMR7Gv8QiHoh49E5QhRTjURyPRTwYiKvRaCTiOJ5M4jgW8fX1KHyNg9zwsRdoqf2oxwSpPS7RcsGf39JNb1dgqTBRT+C3PkeLVcEFn9q59DYsP5VeFtLR8t6ozdJQsb8BR/kzXYECnea0/qiNk46ejbjgX7A0SkL421uPe2tw2P177LviM6aylG0AT3isIU4ouD+UaCvlscgB73sR7sXzMHpHsTqJ+BDfEZoTqbOPJrDwVILVgo9PpcUNZLv8yiqkQif1CpRsBwJmLCukc1QcX2aAf5cpOK+WJ32P1hr7c5c3Yq9YOTll5Z32aDUo5tCu0LKg4X9rBJV3WIak+rVjEUkW6HOT8YQv0YeRzOc84dGOaVEYpqJt+KmjbpqJtt2qjuiyaEvfddS1scYnzVxXWUX13fvSJVEEpbx0paW0t5fzSqpsLX3uNssCLlNTRJzGCPLU55dR4rbrNS+jQb9NH3bltnXuGuVe02t63D9pWjVNTQvTzTOQUmDJn9KH/scFJ1sbVpB0aZwvIIS6HbE+oj8enml4AbbzNPMWtIOUqMLm4DBjphlamqShNCRy7bGwN2L9h4N6SyePzz4qFZWaWjRh3baMeXipTcT/thwl3aI3BCe9dTt5Ju1vQ55HwfPQ2x74dkt++N2quqbtpwrthicPj4KvwEqYk/vpxUE6Wme7yL/qoovPrcFv2N99vzhpe7sJmqxdgapaHnzHze79I4zs5yD7yTvGGYB2Lj8b05lvG2dgDU9rar85QoY2RLh5NGuQvf3a0KU7elRiycrmxDRNsfQ/lO3XwI+3X7ng8/blpTAZHbGwpqYJ6walCY4K9TrsbbkCvaxgSbKNSvr8BWhqNXM= sidebar_class_name: "get api-method" custom_edit_url: null --- @@ -54,16 +54,16 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- description: "destChainID must use capid format from our configuration
-eip155:8453(Base),
-eip155:10 (Optimism),
-eip155:42161 (Arbitrum)", in: "path", - name: "destChainId", + name: "chain", required: true, - schema: { type: "string" }, + schema: { type: "string", example: "eip155:8453" }, }, { description: "Protocol name - across, mayan", in: "path", name: "protocol", required: true, - schema: { type: "string" }, + schema: { type: "string", example: "mayan" }, }, { description: @@ -71,14 +71,7 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- in: "path", name: "type", required: true, - schema: { type: "string" }, - }, - { - description: - "Destination Network ID (optional, defaults to the network from the request body)", - in: "query", - name: "network", - schema: { type: "integer" }, + schema: { type: "string", example: "ExactOutput" }, }, ]} > @@ -86,10 +79,40 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- @@ -98,7 +121,7 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- label={undefined} responses={{ "200": { - description: "Successfully retrieved borrow quote", + description: 'Successfully retrieved borrow quote".', content: { "application/json": { schema: { @@ -107,10 +130,39 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- properties: { borrowCost: { type: "string" }, duration: { type: "string" }, + liquidityPool: { type: "string" }, + repaymentChain: { + enum: [1, 42161, 10, 137, 8453, 43114, 100, 8333, 11155111, 1993, 19934, 84532], + format: "int32", + type: "integer", + "x-enum-varnames": [ + "Ethereum", + "Arbitrum", + "Optimism", + "Polygon", + "Base", + "Avalanche", + "Gnosis", + "B3", + "Sepolia", + "B3Sepolia", + "B4Sepolia", + "BaseSepolia", + ], + }, + repaymentRecipient: { type: "string" }, requiredInput: { type: "string" }, requiredOutput: { type: "string" }, }, - required: ["borrowCost", "duration", "requiredInput", "requiredOutput"], + required: [ + "borrowCost", + "duration", + "liquidityPool", + "repaymentChain", + "repaymentRecipient", + "requiredInput", + "requiredOutput", + ], type: "object", }, }, @@ -121,7 +173,7 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- }, }, "400": { - description: "Bad request due to invalid input or missing parameters", + description: 'Bad request due to invalid input or missing parameters".', content: { "application/json": { schema: { @@ -129,14 +181,14 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- debug: { type: "string" }, error: { type: "string" }, }, - required: ["debug", "error"], + required: ["error"], type: "object", }, }, }, }, "500": { - description: "Internal server error", + description: 'Internal server error".', content: { "application/json": { schema: { @@ -144,7 +196,7 @@ See the [Request Final Borrow Quote example payload](/Stash/use-stash#3-request- debug: { type: "string" }, error: { type: "string" }, }, - required: ["debug", "error"], + required: ["error"], type: "object", }, }, diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 20fd2b5f..9ea4e27b 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -136,6 +136,33 @@ const config: Config = { theme: prismThemes.github, darkTheme: prismThemes.dracula, }, + languageTabs: [ + { + language: "curl", + highlight: "bash", + logoClass: "curl", + }, + { + language: "go", + highlight: "go", + logoClass: "go", + }, + { + language: "javascript", + highlight: "javascript", + logoClass: "javascript", + }, + { + language: "csharp", + highlight: "csharp", + logoClass: "csharp", + }, + { + language: "java", + highlight: "java", + logoClass: "java", + }, + ], } satisfies Preset.ThemeConfig, plugins: [ [ diff --git a/docs/src/api/generated/stash/get-the-borrow-cost-for-a-liquidity-transaction.api.mdx b/docs/src/api/generated/stash/get-signing-authorization-for-a-liquidity-transaction.api.mdx similarity index 54% rename from docs/src/api/generated/stash/get-the-borrow-cost-for-a-liquidity-transaction.api.mdx rename to docs/src/api/generated/stash/get-signing-authorization-for-a-liquidity-transaction.api.mdx index 73237f55..f82d2efd 100644 --- a/docs/src/api/generated/stash/get-the-borrow-cost-for-a-liquidity-transaction.api.mdx +++ b/docs/src/api/generated/stash/get-signing-authorization-for-a-liquidity-transaction.api.mdx @@ -1,11 +1,11 @@ --- -id: get-the-borrow-cost-for-a-liquidity-transaction -title: "Get the borrow cost for a liquidity transaction" -description: "Get the borrow cost for a liquidity transaction" -sidebar_label: "Get the borrow cost for a liquidity transaction" +id: get-signing-authorization-for-a-liquidity-transaction +title: "Get signing authorization for a liquidity transaction" +description: "This endpoint returns signed authorization for a liquidity transaction" +sidebar_label: "Get signing authorization for a liquidity transaction" hide_title: true hide_table_of_contents: true -api: eJzNVk2P2zgM/SsCTy2gGRdF9+Jbpyi6xRZoUcyeghwYm2OrK0saiZ4kMPzfC/pj4mS9s505LDaXyDJFPpJPj+6gpFREE9h4Bzl8IlZck9r5GP1eFT6xuvNRobLmvjWl4aPiiC5hMZzQEDBiQ0wxQb7pLtx9i5594S1oMPIckGvQ4LAheTq9jXTfmkgl5Bxb0pCKmhqEvAM+BrFNHI2roO/1ZYzbExxVY6oHvJJDScEnw+ux+fA7pvpZkbejMSW+8eVRLArvmBzLEkOwpkBBkf1IAqx7wlWvL5IwLrSsSmRUryJZPA7r1yv4Dlfprb/yDxStx3Le78UwBe8SJQn49s0b+TsP8vUP0L+O+fzszYIQiWNbcBtJ7Q3XCtXOVNefHatIIVIix1QqTGpKV0ujA0U2I7aRWx984pXSaHgk2jfv7aqF866g1TfJVA4F2SpzFrXcLFFcxpwjLP1t9ezP735QwdLEXsO7tTrfYKkmpqiyJcVeGfeA1pRq7LOPqjEpGVepxfV5Rm/OC1rSrq1W60Ex+vjvtRgdzOb/kOpva6l+dkzRoVWJ4gNFNXr4X2fSa2CsRK3gy9x2sRwv1h6jE8c5fA0UB9SqIs6zR4pks2xl3bzqs0lrsm7UlT6b+19jUk1r2QRL6iQfkq8GOjC5ZLwbtfMvOkJ+gUPDA9qW/gs8InANce1LyKEiHsSda8jhhcHkAg20GNNro4UcauaQ8izDYK5TiEYIdL1rjS1FTNKxavC68E02wBG+fD9p7scDNsEu2i/SfudnKcZCCCf9NTxYSfUoppGqwqLgEzc4UHCaA88feGcXYDEDXuBqIifTgbNg0TjBOJSpm0q/OUnTqKPzwMwX69Ogy8/GmrRgq6EelHYDXbfDRH9G2/eyfd9SPEK+2QrFosGdlEwmuEmyLiG/Q5voiYRffZ9u3mu1GPSrSU2b6I4LRoOeKP+YyzDdXxL/Fz4CXoBrqma/7TXUhCXFxT39MOK4uhUXp8N/kzvJaTzxvigo8JO2yyv46eMtaNhN3xqNL+VIxL10F/cjTj+UZdDOYa8Di65qsRLb0aX8fgImuHzF +api: eJzNVk1v3DYQ/SsETwkgW0HQXnSL0yA1WiBB4J4WPsyKY4mpRNLDkXe3gv57MaTW+1HVjd0A7V6WkoYzb4Zv3nDUBmNNNrD1Tlf6prVRoTPBW8eKkAdyUUXbODQKBm492T9AjNWdJwWqs/eDNZZ3iglchDo5KnQAgh4ZKepqNZ5F+Uyefe07XWgrzwG41YV20KM8Hb4S3g+W0OiKacBCx7rFHnQ1at4FsY1M1jV6morzGDcHOKqF2Ca83KIyGHy0vBybtz9DbP9d5J8wsnW5SHUL1qkabFDWLIdMJs+KeJuNMfKVNzuxqL1jdCxLCKGzdYpefo0CaHzC1VScgbcuDKwMMKhXhB3s0vr1Ar7tRXzrL/wDUufB7N9PYhiDdxGjBHz75o38nQb59Isuvh3z6d4rT+Q3qvaRVWQaah4I1cZyq0CtbXN5nYgbCCM6FtZGNadbCLUCEtuMbZ1cvev9kGGcFaeYDd77uPz5kfufve8WLZx3NS5+kZYCgb5IqaNir05hnoA6h7APeOz+tti79+uvWLMc+lToH5bO5QqMmpmlzICKvbLuATprVOaFJ9XbGK1r1FGDP+MsTw/A4HpoFsuDRJ7+uTTZ7G9S/HEpxWvHSA46FZEekFT28L/MYCo0QyP6qX/dH7NY5sbbADlxWOlPASmrTYNclY+UKJOylGP6uzZTuRfWctyvpnJWw3LMyjeV+/NvIap+6NiGDtVBbiTvQuOW0UXrXVb333GnqzNchX6AbsD/Ap8IZI/ceqMr3SCnccStrvR3Ci4NluiT0x+o05VumUOsyhKCvYyBrBDtcj3Yzog4xV3Tw2Xt+zLBE159OWj4hy30oTuiiUyKO7+XdqiFmMIHy8lKqosUM6WFbcFH7iFRdR4rH5HT3JZefc7gPmmXo8ny/a4GM9MZt1yGTobfVOQajvM5rQ66Jq05D8hqPqus4/srQnW0Poz26mSQy5HdFrpNQr7S47iGiL9RN03y+n5A2ulqdSuUJQtrKbHcWWyUtdHVHXQRnyjNqy9zR79WR1ebxTznl+B2Rx2ii7mFHnNJt4qXxP+Ga88LcM3VfDGqp65EL4CTGTHdToVuEQzSkQq9z/EvbsTDYe9fRF1yyTve1TUGftL2WFA+frhJUzjfvHpvZAvBRrgGmwzTp3KkCZHejboD1wzQiG12Kb8/Ac2uDZw= sidebar_class_name: "get api-method" custom_edit_url: null --- @@ -21,16 +21,18 @@ import Heading from "@theme/Heading"; -Get the borrow cost for a liquidity transaction +This endpoint returns signed authorization for a liquidity transaction @@ -80,12 +89,19 @@ Get the borrow cost for a liquidity transaction description: "Borrow cost structure with a big.Int represented as string", properties: { + borrowAmount: { type: "string" }, borrowCost: { type: "string" }, liquidityPool: { type: "string" }, nonce: { type: "string" }, signature: { type: "string" }, }, - required: ["borrowCost", "liquidityPool", "nonce", "signature"], + required: [ + "borrowAmount", + "borrowCost", + "liquidityPool", + "nonce", + "signature", + ], type: "object", }, }, @@ -100,7 +116,7 @@ Get the borrow cost for a liquidity transaction debug: { type: "string" }, error: { type: "string" }, }, - required: ["debug", "error"], + required: ["error"], type: "object", }, }, @@ -115,7 +131,7 @@ Get the borrow cost for a liquidity transaction debug: { type: "string" }, error: { type: "string" }, }, - required: ["debug", "error"], + required: ["error"], type: "object", }, }, diff --git a/docs/src/api/generated/stash/get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx b/docs/src/api/generated/stash/get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx index 904c2ba8..8b297d60 100644 --- a/docs/src/api/generated/stash/get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx +++ b/docs/src/api/generated/stash/get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data.api.mdx @@ -1,11 +1,11 @@ --- id: get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data -title: "Get the borrow quote for a liquidity transaction based on the input data" -description: "This endpoint calculates the borrow quote for a liquidity transaction, including the borrow cost, required input/output, and duration." -sidebar_label: "Get the borrow quote for a liquidity transaction based on the input data" +title: "Get the borrow quote for a liquidity transaction based on the input data." +description: "Calculates the borrow quote including borrow cost, required input/output, and duration." +sidebar_label: "Get the borrow quote for a liquidity transaction based on the input data." hide_title: true hide_table_of_contents: true -api: eJzNVktv2zgQ/isETynAWEXRveiWtEERYNHudrOnIAdaHEtsKZIhh04MQ/99MaTkqI6aNlkUqA+2TM3jmwe/mT1XEJugPWpnec2vOh0ZWOWdtsgaaZpkJEJk2AFbuxDcHbtNDoFtXGCSGX2btNK4YxikjbIhO4Jp25iktG3neo2LKFiA26QDKKatT1i5hD6hYNIqplKQpL/ignsZZA8IIfL6en+E8q/g0DXOMCt7YCewaleCnTXBxfiKC65JxkvsuOAkQf9GDS74BIDXGBIIHpsOesnrPcedJ9mIQduWD4M49vt3jlya1gWNXc9IYXJ/cS8bvLQ5mPz8KUf2HTzZ1f/C8h4iapvzxT4C3rnwlV2+ZycuC0gjmIKNTAYjQ5fLYEepTXB9PiDvEJGtndodcN4mCLsHoKMSX8CmLUILgQ/DTYkEIp47tSORxlkEi/QovTe6yUCrL5Gw75+IcxBHcZ71LlmkGEobLWTt/jS+caduC8E4qabzgQSjdzZCJE9vXr+mn2+t/5OaBmLcJGN2LAAGDVtQ33Q6Fz8fjQ/OQ0BdPCqJC6fF9jsXcSF+wadLsPhyijz32ZMSpfsWO2mWv+s5mpnvY0+P7N6Iya5bf4EGH9nNsS9IkdzbpUqcS3VoSJWACq7tVho9MgVzgfU6RiKVGTm8vDiwTu1iCiEEF36cuWJgEv9OqH8shXppEYKVhkUIWwisWPitIxkER9kSF/M/J8onyXL17mSwZLjmnzyUFmItYF0dxkM1EXC1n56GitxUe/oeqjJUOhlZnwxqbw70dO4UBSo43CPYqJ0tI+Er7Hh9BEDwrTQJfikQYrsesHOK17wFzMMKO17z53ohUs0dUAJKwfCad4g+1lUlvV5FH4hkw2qdtFF3Gru4a3u5alxfZRzUGp8fmPfiXvbezCpNnL5xEyHLhnqLSqkxS1G+IMTSldQw3kXsZe62kf8/AD5r9rO1jKCYs1mr3NzMBUe8PhsRv8/SMfY9wj1W3khtKSe5LPuxxtf84Jzq/rBV1LPncbjX42+p9Y3gXeb8a77fU5L+DWYY6LhM3Lzl6CjXhi7lRpoIT6TsF0z/xeDLJXtYAqbrlbtvK4MmvM/EfvJ5rMYr9oNFbhHSeCjtbo5ngnooQ96YXoLphUveC7Dmt8PNIHgHUkGY0dq7guz0qrTQpPpoLFCUReOsacDjk7Jz4vpwccUFX4/rWu8UqQSZ1yt5V1CWZsozJp/tuZG2TbIl2WKSPv8BqkZTiw== +api: eJzNV21v2zYQ/isEP6UAG1l+WRN9c9y0CDCgWdt9yoLhLJ0tthSpkJQdw9B/H46SHPkl7eZhwPzBpqnj8bm751605Rm61MrSS6N5wmeg0kqBR8d8jmxurDVr9lQZj0zqVFWZ1MtuOzXOC2bxqZIWMyZ1WfnIVL6svGCgM5ZVFkjxJRe8BAsFerSOJw/bg2u/mMqmyGbTu3uW5iA1u3vPLvByeSkYyjKeTJL4DRdcknAJPueCayiQJzyIc8E7GDzxtkLBXZpjATzZcr8pSdB5K/WSC47PUJSKtlrVV+PJiNe1OER1b403qVGMrurgTFNrnHsFTNmeOBNPARvQp5D8FgIAamms9HnBSEUH6PYZUn+ng9PD+lOIwCsIw+Xnoesp53X92ChB529MtqGTqdEetacllKWSaYh99M2RCdveFaU1JVov0QXZwlTNqX2bm31mFsyb76gdWxjL1rlMc7ZGBhZZe3+PkQ1RL6QO7HUFKIXOswy1KaQOeN7s2TS+vhpMxsOYiwPDa5JKVeXkCv9cSKWOAc6NUQiaWSwtOtQBiFywjalYDitknQLpN8wbRlqYz6VrYPZxLEA53EFoFRMGjX5t7Pfjy1OQJXmkAI8Za8WYzE7x+6R1wanHesM2gyyz6By7yPF532GD59lgCDAdXc+Hw9GH26v3g+kAJ7Pxh+E7nL6/HlyNZu8mv8zS4dGlgjd4ecJzfCaa92j40PHgyO8vTuhAP+40m/k3TImNtTiw46ZPiJYorISNMpAd8T8gcaXRrqHkcDA49syXKk3RuUWl1IZZ9FbiCrM95v3BqdKdmQcZ+BO7jfqZcf44OcnotsKefKjkUyUz6Tf3xqiTEhZL2BSo/SxU0WTLUVcFTx5iMR7Gv8QiHoh49E5QhRTjURyPRTwYiKvRaCTiOJ5M4jgW8fX1KHyNg9zwsRdoqf2oxwSpPS7RcsGf39JNb1dgqTBRT+C3PkeLVcEFn9q59DYsP5VeFtLR8t6ozdJQsb8BR/kzXYECnea0/qiNk46ejbjgX7A0SkL421uPe2tw2P177LviM6aylG0AT3isIU4ouD+UaCvlscgB73sR7sXzMHpHsTqJ+BDfEZoTqbOPJrDwVILVgo9PpcUNZLv8yiqkQif1CpRsBwJmLCukc1QcX2aAf5cpOK+WJ32P1hr7c5c3Yq9YOTll5Z32aDUo5tCu0LKg4X9rBJV3WIak+rVjEUkW6HOT8YQv0YeRzOc84dGOaVEYpqJt+KmjbpqJtt2qjuiyaEvfddS1scYnzVxXWUX13fvSJVEEpbx0paW0t5fzSqpsLX3uNssCLlNTRJzGCPLU55dR4rbrNS+jQb9NH3bltnXuGuVe02t63D9pWjVNTQvTzTOQUmDJn9KH/scFJ1sbVpB0aZwvIIS6HbE+oj8enml4AbbzNPMWtIOUqMLm4DBjphlamqShNCRy7bGwN2L9h4N6SyePzz4qFZWaWjRh3baMeXipTcT/thwl3aI3BCe9dTt5Ju1vQ55HwfPQ2x74dkt++N2quqbtpwrthicPj4KvwEqYk/vpxUE6Wme7yL/qoovPrcFv2N99vzhpe7sJmqxdgapaHnzHze79I4zs5yD7yTvGGYB2Lj8b05lvG2dgDU9rar85QoY2RLh5NGuQvf3a0KU7elRiycrmxDRNsfQ/lO3XwI+3X7ng8/blpTAZHbGwpqYJ6walCY4K9TrsbbkCvaxgSbKNSvr8BWhqNXM= sidebar_class_name: "get api-method" custom_edit_url: null --- @@ -22,17 +22,17 @@ import Heading from "@theme/Heading"; as={"h1"} className={"openapi__heading"} children={ - "Get the borrow quote for a liquidity transaction based on the input data" + "Get the borrow quote for a liquidity transaction based on the input data." } > -This endpoint calculates the borrow quote for a liquidity transaction, including the borrow cost, required input/output, and duration. +Calculates the borrow quote including borrow cost, required input/output, and duration. @@ -70,10 +70,41 @@ This endpoint calculates the borrow quote for a liquidity transaction, including @@ -82,7 +113,7 @@ This endpoint calculates the borrow quote for a liquidity transaction, including label={undefined} responses={{ "200": { - description: "Successfully retrieved borrow quote", + description: 'Successfully retrieved borrow quote".', content: { "application/json": { schema: { @@ -91,12 +122,39 @@ This endpoint calculates the borrow quote for a liquidity transaction, including properties: { borrowCost: { type: "string" }, duration: { type: "string" }, + liquidityPool: { type: "string" }, + repaymentChain: { + enum: [ + 1, 42161, 10, 137, 8453, 43114, 100, 8333, 11155111, 1993, + 19934, 84532, + ], + format: "int32", + type: "integer", + "x-enum-varnames": [ + "Ethereum", + "Arbitrum", + "Optimism", + "Polygon", + "Base", + "Avalanche", + "Gnosis", + "B3", + "Sepolia", + "B3Sepolia", + "B4Sepolia", + "BaseSepolia", + ], + }, + repaymentRecipient: { type: "string" }, requiredInput: { type: "string" }, requiredOutput: { type: "string" }, }, required: [ "borrowCost", "duration", + "liquidityPool", + "repaymentChain", + "repaymentRecipient", "requiredInput", "requiredOutput", ], @@ -110,7 +168,7 @@ This endpoint calculates the borrow quote for a liquidity transaction, including }, }, "400": { - description: "Bad request due to invalid input or missing parameters", + description: 'Bad request due to invalid input or missing parameters".', content: { "application/json": { schema: { @@ -118,14 +176,14 @@ This endpoint calculates the borrow quote for a liquidity transaction, including debug: { type: "string" }, error: { type: "string" }, }, - required: ["debug", "error"], + required: ["error"], type: "object", }, }, }, }, "500": { - description: "Internal server error", + description: 'Internal server error".', content: { "application/json": { schema: { @@ -133,7 +191,7 @@ This endpoint calculates the borrow quote for a liquidity transaction, including debug: { type: "string" }, error: { type: "string" }, }, - required: ["debug", "error"], + required: ["error"], type: "object", }, }, diff --git a/docs/src/api/generated/stash/sidebar.ts b/docs/src/api/generated/stash/sidebar.ts index 0f77141b..0629aaeb 100644 --- a/docs/src/api/generated/stash/sidebar.ts +++ b/docs/src/api/generated/stash/sidebar.ts @@ -8,15 +8,15 @@ const sidebar: SidebarsConfig = { items: [ { type: "doc", - id: "../src/api/generated/stash/get-the-borrow-cost-for-a-liquidity-transaction", - label: "Get the borrow cost for a liquidity transaction", + id: "../src/api/generated/stash/get-signing-authorization-for-a-liquidity-transaction", + label: "Get signing authorization for a liquidity transaction", className: "api-method get", }, { type: "doc", id: "../src/api/generated/stash/get-the-borrow-quote-for-a-liquidity-transaction-based-on-the-input-data", label: - "Get the borrow quote for a liquidity transaction based on the input data", + "Get the borrow quote for a liquidity transaction based on the input data.", className: "api-method get", }, ], diff --git a/docs/src/api/stash-openapi.yaml b/docs/src/api/stash-openapi.yaml index aabf3ff2..f9f21c3b 100644 --- a/docs/src/api/stash-openapi.yaml +++ b/docs/src/api/stash-openapi.yaml @@ -1,16 +1,13 @@ definitions: - entity.Aave: + config.Token: properties: - pool: - type: string - poolAddressProvider: - type: string - priceOracle: + address: type: string + decimals: + type: integer required: - - pool - - poolAddressProvider - - priceOracle + - address + - decimals type: object entity.Amount: properties: @@ -38,6 +35,7 @@ definitions: - 1993 - 19934 - 84532 + format: int32 type: integer x-enum-varnames: - Ethereum @@ -88,10 +86,6 @@ definitions: type: object entity.Network: properties: - aave: - $ref: "#/definitions/entity.Aave" - aaveLiquidityPool: - type: string blockTime: type: integer chainID: @@ -108,11 +102,7 @@ definitions: items: type: string type: array - usdcLiquidityPool: - type: string required: - - aave - - aaveLiquidityPool - blockTime - chainID - chainType @@ -120,7 +110,6 @@ definitions: - name - nativeToken - rpcURLs - - usdcLiquidityPool type: object entity.TokenTransferQuote: properties: @@ -265,7 +254,6 @@ definitions: - account - amount - destination - - destinationContractCall - threshold - token - type @@ -293,6 +281,8 @@ definitions: server.BorrowCost: description: Borrow cost structure with a big.Int represented as string properties: + borrowAmount: + type: string borrowCost: type: string liquidityPool: @@ -302,17 +292,38 @@ definitions: signature: type: string required: + - borrowAmount - borrowCost - liquidityPool - nonce - signature type: object - server.BorrowQuote.response: + server.BorrowQuoteRequest: properties: - data: - $ref: "#/definitions/server.Quote" + amount: + description: + amount of tokens for which we are requesting borrow quote (in + the smallest denomination) + example: "49805421" + type: string + exclusive_fill: + description: boolean representing if you have exclusivity to fill this quote + example: false + type: boolean + network: + description: caip formated network id + example: eip155:1 + type: string + token: + description: token address (hex) + example: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + type: string + format: hex required: - - data + - amount + - exclusive_fill + - network + - token type: object server.ErrorResponse: properties: @@ -321,7 +332,6 @@ definitions: error: type: string required: - - debug - error type: object server.GetFungibleTokensByChainHandler.response: @@ -342,6 +352,25 @@ definitions: required: - data type: object + server.GetSolverHandler.response: + properties: + duration: + type: integer + solver: + type: string + required: + - duration + - solver + type: object + server.GetStashSupportedTokensHandler.response: + properties: + data: + additionalProperties: + $ref: "#/definitions/config.Token" + type: object + required: + - data + type: object server.GetSupportedFungibleTokens.response: properties: data: @@ -373,6 +402,12 @@ definitions: type: string duration: type: string + liquidityPool: + type: string + repaymentChain: + $ref: "#/definitions/entity.ChainID" + repaymentRecipient: + type: string requiredInput: type: string requiredOutput: @@ -380,6 +415,9 @@ definitions: required: - borrowCost - duration + - liquidityPool + - repaymentChain + - repaymentRecipient - requiredInput - requiredOutput type: object @@ -432,7 +470,6 @@ definitions: - account - amount - destination - - destinationContractCall - enableSwaps - recipient - threshold @@ -465,17 +502,24 @@ definitions: - chainId - tokenDecimals type: object + server.response: + properties: + data: + $ref: "#/definitions/server.Quote" + required: + - data + type: object info: contact: {} schemes: - https host: api.sprinter.buildwithsygma.com/ paths: - /liquidity/protocol/{protocol}/deposit/{txHash}/request: + /liquidity/chain/{chainId}/protocol/{protocol}/deposit/{txHash}/request: get: consumes: - application/json - description: Get the borrow cost for a liquidity transaction + description: This endpoint returns signed authorization for a liquidity transaction parameters: - description: input data (relay data) in: body @@ -499,6 +543,11 @@ paths: name: txHash required: true type: string + - description: Destination chain caip id + in: path + name: chain + required: true + type: string produces: - application/json responses: @@ -514,69 +563,62 @@ paths: description: Internal server error schema: $ref: "#/definitions/server.ErrorResponse" - summary: Get the borrow cost for a liquidity transaction + summary: Get signing authorization for a liquidity transaction tags: - Liquidity - /liquidity/protocol/{protocol}/type/{type}/quote: + /liquidity/chain/{chain}/protocol/{protocol}/type/{type}/quote: get: consumes: - application/json description: - This endpoint calculates the borrow quote for a liquidity transaction, - including the borrow cost, required input/output, and duration. + Calculates the borrow quote including borrow cost, required input/output, + and duration. parameters: - - description: Amount to borrow - in: body - name: amount - required: true - schema: - type: string - - description: Token address (in Ethereum address format) - in: body - name: token + - description: Source CAIP chain ID (e.g., eip155:1) + in: path + name: chain required: true schema: type: string - - description: Network ID for the transaction - in: body - name: network - required: true - schema: - type: integer + example: eip155:8453 - description: Protocol name (e.g., Across) in: path name: protocol required: true - type: string + schema: + type: string + example: mayan - description: Quote algorithm type (e.g., ExactInput, ExactOutput) in: path name: type required: true - type: string - - description: - Destination Network ID (optional, defaults to the network from - the request body) - in: query - name: network - type: integer + schema: + type: string + example: ExactOutput + - description: Borrow quote request payload + in: body + name: body + required: true + schema: + $ref: "#/definitions/server.BorrowQuoteRequest" produces: - application/json responses: "200": - description: Successfully retrieved borrow quote + description: Successfully retrieved borrow quote". schema: - $ref: "#/definitions/server.BorrowQuote.response" + $ref: "#/definitions/server.response" "400": - description: Bad request due to invalid input or missing parameters + description: Bad request due to invalid input or missing parameters". schema: $ref: "#/definitions/server.ErrorResponse" "500": - description: Internal server error + description: Internal server error". schema: $ref: "#/definitions/server.ErrorResponse" summary: Get the borrow quote for a liquidity transaction based on the input - data + data. tags: - Liquidity swagger: "2.0" diff --git a/docs/src/theme/ApiExplorer/Accept/index.tsx b/docs/src/theme/ApiExplorer/Accept/index.tsx new file mode 100644 index 00000000..a8b06236 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Accept/index.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +import FormItem from "@theme/ApiExplorer/FormItem"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; + +import { setAccept } from "./slice"; + +function Accept() { + const value = useTypedSelector((state: any) => state.accept.value); + const options = useTypedSelector((state: any) => state.accept.options); + const dispatch = useTypedDispatch(); + + if (options.length <= 1) { + return null; + } + + return ( + + dispatch(setAccept(e.target.value))} + /> + + ); +} + +export default Accept; diff --git a/docs/src/theme/ApiExplorer/Accept/slice.ts b/docs/src/theme/ApiExplorer/Accept/slice.ts new file mode 100644 index 00000000..e6092392 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Accept/slice.ts @@ -0,0 +1,22 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface State { + value: string; + options: string[]; +} + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "accept", + initialState, + reducers: { + setAccept: (state, action: PayloadAction) => { + state.value = action.payload; + }, + }, +}); + +export const { setAccept } = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Container/_Container.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/Container/_Container.scss new file mode 100644 index 00000000..8e17c256 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Container/_Container.scss @@ -0,0 +1,7 @@ +.openapi-explorer__code-block-container { + height: 100%; + background: var(--prism-background-color); + color: var(--prism-color); + margin-bottom: unset; + box-shadow: var(--ifm-global-shadow-lw); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx new file mode 100644 index 00000000..886be6a8 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Container/index.tsx @@ -0,0 +1,25 @@ +import React, { ComponentProps } from "react"; + +import { ThemeClassNames, usePrismTheme } from "@docusaurus/theme-common"; +import { getPrismCssVariables } from "@docusaurus/theme-common/internal"; +import clsx from "clsx"; + +export default function CodeBlockContainer({ + as: As, + ...props +}: { as: T } & ComponentProps): React.JSX.Element { + const prismTheme = usePrismTheme(); + const prismCssVariables = getPrismCssVariables(prismTheme); + return ( + + ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/Element.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/Element.tsx new file mode 100644 index 00000000..f636ca0a --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/Element.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +import Container from "@theme/ApiExplorer/ApiCodeBlock/Container"; +import type { Props } from "@theme/CodeBlock/Content/Element"; +import clsx from "clsx"; + +//
 tags in markdown map to CodeBlocks. They may contain JSX children. When
+// the children is not a simple string, we just return a styled block without
+// actually highlighting.
+export default function CodeBlockJSX({
+  children,
+  className,
+}: Props): React.JSX.Element {
+  return (
+    
+      {children}
+    
+  );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
new file mode 100644
index 00000000..46042522
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
@@ -0,0 +1,127 @@
+import React from "react";
+
+import { useThemeConfig, usePrismTheme } from "@docusaurus/theme-common";
+import {
+  parseCodeBlockTitle,
+  parseLanguage,
+  parseLines,
+  containsLineNumbers,
+  useCodeWordWrap,
+} from "@docusaurus/theme-common/internal";
+import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
+import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton";
+import ExpandButton from "@theme/ApiExplorer/ApiCodeBlock/ExpandButton";
+import Line from "@theme/ApiExplorer/ApiCodeBlock/Line";
+import WordWrapButton from "@theme/ApiExplorer/ApiCodeBlock/WordWrapButton";
+import type { Props } from "@theme/CodeBlock/Content/String";
+import clsx from "clsx";
+import { Highlight, Language } from "prism-react-renderer";
+
+export default function CodeBlockString({
+  children,
+  className: blockClassName = "",
+  metastring,
+  title: titleProp,
+  showLineNumbers: showLineNumbersProp,
+  language: languageProp,
+}: Props): React.JSX.Element {
+  const {
+    prism: { defaultLanguage, magicComments },
+  } = useThemeConfig();
+  const language =
+    languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
+  const prismTheme = usePrismTheme();
+  const wordWrap = useCodeWordWrap();
+  // We still parse the metastring in case we want to support more syntax in the
+  // future. Note that MDX doesn't strip quotes when parsing metastring:
+  // "title=\"xyz\"" => title: "\"xyz\""
+  const title = parseCodeBlockTitle(metastring) || titleProp;
+  const { lineClassNames, code } = parseLines(children, {
+    metastring,
+    language,
+    magicComments,
+  });
+  const showLineNumbers =
+    showLineNumbersProp ?? containsLineNumbers(metastring);
+
+  return (
+    
+      {title && (
+        
{title}
+ )} +
+ + {({ className, tokens, getLineProps, getTokenProps }) => ( +
+              
+                {tokens.map((line, i) => (
+                  
+                ))}
+              
+            
+ )} +
+
+ {(wordWrap.isEnabled || wordWrap.isCodeScrollable) && ( + wordWrap.toggle()} + isEnabled={wordWrap.isEnabled} + /> + )} + + +
+
+
+ ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/_Content.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/_Content.scss new file mode 100644 index 00000000..acc774e1 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/_Content.scss @@ -0,0 +1,91 @@ +.openapi-explorer__code-block-content { + height: 100%; + position: relative; + /* rtl:ignore */ + direction: ltr; + border-radius: inherit; +} + +.openapi-explorer__code-block-title { + border-bottom: 1px solid var(--ifm-color-emphasis-300); + font-size: var(--ifm-code-font-size); + font-weight: 500; + padding: 0.75rem var(--ifm-pre-padding); + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.openapi-explorer__code-block { + height: 100%; + border-radius: var(--ifm-global-radius); + --ifm-pre-background: var(--prism-background-color); + margin: 0; + padding: 0; +} + +.openapi-explorer__code-block-title + + .openapi-explorer__code-block-content + .openapi-explorer__code-block { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.openapi-explorer__code-block-standalone { + padding: 0; +} + +.openapi-explorer__code-block-lines { + font: inherit; + /* rtl:ignore */ + float: left; + min-width: 100%; + padding: var(--ifm-pre-padding); +} + +.openapi-explorer__code-block-lines-numbering { + // This causes max-height to unset + // display: table; + padding: var(--ifm-pre-padding) 0; +} + +@media print { + .openapi-explorer__code-block-lines { + white-space: pre-wrap; + } +} + +.openapi-explorer__code-block-btn-group { + display: flex; + column-gap: 0.2rem; + position: absolute; + right: calc(var(--ifm-pre-padding) / 2); + top: calc(var(--ifm-pre-padding) / 2); +} + +.openapi-explorer__code-block-btn-group button { + display: flex; + align-items: center; + background: var(--prism-background-color); + color: var(--prism-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + padding: 0.4rem; + line-height: 0; + transition: opacity 200ms ease-in-out; + opacity: 0; +} + +.openapi-explorer__code-block-btn-group button:focus-visible, +.openapi-explorer__code-block-btn-group button:hover { + opacity: 1 !important; +} + +.theme-code-block:hover .openapi-explorer__code-block-btn-group button { + opacity: 0.4; +} + +@media screen and (max-width: 996px) { + .openapi-explorer__expand-btn { + display: none !important; + } +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/_CopyButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/_CopyButton.scss new file mode 100644 index 00000000..e245fe1b --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/_CopyButton.scss @@ -0,0 +1,44 @@ +.theme-code-block:hover { + .openapi-explorer__code-block-copy-btn--copied { + opacity: 1 !important; + } +} + +.openapi-explorer__code-block-copy-btn-icons { + position: relative; + width: 1.125rem; + height: 1.125rem; +} + +.openapi-explorer__code-block-copy-btn-icon, +.openapi-explorer__code-block-copy-btn-icon--success { + position: absolute; + top: 0; + left: 0; + fill: currentColor; + opacity: inherit; + width: inherit; + height: inherit; + transition: all 0.15s ease; +} + +.openapi-explorer__code-block-copy-btn-icon--success { + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.33); + opacity: 0; + color: #00d600; +} + +.openapi-explorer__code-block-copy-btn--copied { + .openapi-explorer__code-block-copy-btn-icon { + transform: scale(0.33); + opacity: 0; + } + + .openapi-explorer__code-block-copy-btn-icon--success { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + transition-delay: 0.075s; + } +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/index.tsx new file mode 100644 index 00000000..0431e508 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/index.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useState, useRef, useEffect } from "react"; + +import { CopyButtonProps } from "@docusaurus/theme-common/internal"; +import { translate } from "@docusaurus/Translate"; +import clsx from "clsx"; +import copy from "copy-text-to-clipboard"; + +export default function CopyButton({ + code, + className, +}: CopyButtonProps): React.JSX.Element { + const [isCopied, setIsCopied] = useState(false); + const copyTimeout = useRef(undefined); + const handleCopyCode = useCallback(() => { + copy(code); + setIsCopied(true); + copyTimeout.current = window.setTimeout(() => { + setIsCopied(false); + }, 1000); + }, [code]); + + useEffect(() => () => window.clearTimeout(copyTimeout.current), []); + + return ( + + ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/_ExitButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/_ExitButton.scss new file mode 100644 index 00000000..4aaf9602 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/_ExitButton.scss @@ -0,0 +1,16 @@ +.openapi-explorer__code-block-exit-btn-icons { + position: relative; + width: 1.125rem; + height: 1.125rem; +} + +.openapi-explorer__code-block-exit-btn-icon { + position: absolute; + top: 0; + left: 0; + fill: currentColor; + opacity: inherit; + width: inherit; + height: inherit; + transition: all 0.15s ease; +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/index.tsx new file mode 100644 index 00000000..95b33897 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +import { translate } from "@docusaurus/Translate"; +import clsx from "clsx"; + +export interface Props { + readonly className: string; + readonly handler: () => void; +} + +export default function ExitButton({ + className, + handler, +}: Props): React.JSX.Element { + return ( + + ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/_ExpandButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/_ExpandButton.scss new file mode 100644 index 00000000..dcf38279 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/_ExpandButton.scss @@ -0,0 +1,62 @@ +.openapi-explorer__expand-modal-content { + padding: none; + border: thin solid var(--ifm-toc-border-color); + border-radius: var(--ifm-global-radius); + max-width: 95%; + width: 65vw; + height: 65vh; + overflow: auto; +} + +.openapi-explorer__expand-modal-overlay { + display: flex; + align-items: center; + justify-content: center; + position: fixed; + inset: 0px; + background-color: rgba(0, 0, 0, 0.9); + z-index: 201; +} + +.theme-code-block:hover .openapi-explorer__code-block-expand-btn--copied { + opacity: 1 !important; +} + +.openapi-explorer__code-block-expand-btn-icons { + position: relative; + width: 1.125rem; + height: 1.125rem; +} + +.openapi-explorer__code-block-expand-btn-icon, +.openapi-explorer__code-block-expand-btn-icon--success { + position: absolute; + top: 0; + left: 0; + fill: currentColor; + opacity: inherit; + width: inherit; + height: inherit; + transition: all 0.15s ease; +} + +.openapi-explorer__code-block-expand-btn-icon--success { + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.33); + opacity: 0; + color: #00d600; +} + +.openapi-explorer__code-block-expand-btn--copied + .openapi-explorer__code-block-expand-btn-icon { + transform: scale(0.33); + opacity: 0; +} + +.openapi-explorer__code-block-expand-btn--copied + .openapi-explorer__code-block-expand-btn-icon--success { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + transition-delay: 0.075s; +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/index.tsx new file mode 100644 index 00000000..14db1367 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/index.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useState } from "react"; + +import { usePrismTheme } from "@docusaurus/theme-common"; +import { translate } from "@docusaurus/Translate"; +import Container from "@theme/ApiExplorer/ApiCodeBlock/Container"; +import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton"; +import ExitButton from "@theme/ApiExplorer/ApiCodeBlock/ExitButton"; +import Line from "@theme/ApiExplorer/ApiCodeBlock/Line"; +import clsx from "clsx"; +import { Highlight, Language } from "prism-react-renderer"; +import Modal from "react-modal"; + +export interface Props { + readonly code: string; + readonly className: string; + readonly language: Language; + readonly showLineNumbers: boolean; + readonly blockClassName: string; + readonly title: string | undefined; + readonly lineClassNames: { [lineIndex: number]: string[] }; +} + +export default function ExpandButton({ + code, + className, + language, + showLineNumbers, + blockClassName, + title, + lineClassNames, +}: Props): React.JSX.Element { + const [isModalOpen, setIsModalOpen] = useState(false); + const prismTheme = usePrismTheme(); + + useEffect(() => { + Modal.setAppElement("body"); + }, []); + + return ( + <> + + setIsModalOpen(false)} + contentLabel="Code Snippet" + > + + {title && ( +
{title}
+ )} +
+ + {({ className, tokens, getLineProps, getTokenProps }) => ( +
+                  
+                    {tokens.map((line, i) => (
+                      
+                    ))}
+                  
+                
+ )} +
+
+ + setIsModalOpen(false)} + /> +
+
+
+
+ + ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/_Line.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/_Line.scss new file mode 100644 index 00000000..8a83a0d0 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/_Line.scss @@ -0,0 +1,32 @@ +:where(:root) { + --docusaurus-highlighted-code-line-bg: rgb(72 77 91); +} + +:where([data-theme="dark"]) { + --docusaurus-highlighted-code-line-bg: rgb(100 100 100); +} + +.openapi-explorer__code-block-code-line { + display: table-row; + counter-increment: line-count; +} + +.openapi-explorer__code-block-code-line-number { + display: table-cell; + text-align: right; + width: 1%; + position: sticky; + left: 0; + padding: 0 var(--ifm-pre-padding); + background: var(--ifm-pre-background); + overflow-wrap: normal; +} + +.openapi-explorer__code-block-code-line-number::before { + content: counter(line-count); + opacity: 0.4; +} + +.openapi-explorer__code-block-code-line-number { + padding-right: var(--ifm-pre-padding); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx new file mode 100644 index 00000000..87e5de92 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +import { LineProps } from "@docusaurus/theme-common/internal"; +import clsx from "clsx"; + +export default function CodeBlockLine({ + line, + classNames, + showLineNumbers, + getLineProps, + getTokenProps, +}: LineProps): React.JSX.Element { + if (line.length === 1 && line[0].content === "\n") { + line[0].content = ""; + } + const lineProps = getLineProps({ + line, + className: clsx( + classNames, + showLineNumbers && "openapi-explorer__code-block-code-line", + ), + }); + const lineTokens = line.map((token, key) => ( + + )); + return ( + + {showLineNumbers ? ( + <> + + + {lineTokens} + + + ) : ( + lineTokens + )} +
+
+ ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/_WordWrapButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/_WordWrapButton.scss new file mode 100644 index 00000000..cf2c8aa9 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/_WordWrapButton.scss @@ -0,0 +1,10 @@ +.openapi-explorer__code-block-word-wrap-btn-icon { + width: 1.2rem; + height: 1.2rem; +} + +.openapi-explorer__code-block-word-wrap-btn--enabled { + .openapi-explorer__code-block-word-wrap-btn-icon { + color: var(--ifm-color-primary); + } +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/index.tsx new file mode 100644 index 00000000..5f0e60cb --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/index.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +import { translate } from "@docusaurus/Translate"; +import clsx from "clsx"; + +export interface Props { + readonly className?: string; + readonly onClick: React.MouseEventHandler; + readonly isEnabled: boolean; +} + +export default function WordWrapButton({ + className, + onClick, + isEnabled, +}: Props): React.JSX.Element | null { + const title = translate({ + id: "theme.CodeBlock.wordWrapToggle", + message: "Toggle word wrap", + description: + "The title attribute for toggle word wrapping button of code block lines", + }); + return ( + + ); +} diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx new file mode 100644 index 00000000..62dc4f00 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx @@ -0,0 +1,39 @@ +import React, { isValidElement, ReactNode } from "react"; + +import { CodeBlockProps } from "@docusaurus/theme-common/internal"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import ElementContent from "@theme/ApiExplorer/ApiCodeBlock/Content/Element"; +import StringContent from "@theme/ApiExplorer/ApiCodeBlock/Content/String"; + +/** + * Best attempt to make the children a plain string so it is copyable. If there + * are react elements, we will not be able to copy the content, and it will + * return `children` as-is; otherwise, it concatenates the string children + * together. + */ +function maybeStringifyChildren(children: ReactNode): ReactNode { + if (React.Children.toArray(children).some((el) => isValidElement(el))) { + return children; + } + // The children is now guaranteed to be one/more plain strings + return Array.isArray(children) ? children.join("") : (children as string); +} +export default function ApiCodeBlock({ + children: rawChildren, + ...props +}: CodeBlockProps) { + // The Prism theme on SSR is always the default theme but the site theme can + // be in a different mode. React hydration doesn't update DOM styles that come + // from SSR. Hence force a re-render after mounting to apply the current + // relevant styles. + const isBrowser = useIsBrowser(); + const children = maybeStringifyChildren(rawChildren); + const CodeBlockComp = + typeof children === "string" ? StringContent : ElementContent; + + return ( + + {children as string} + + ); +} diff --git a/docs/src/theme/ApiExplorer/Authorization/auth-types.ts b/docs/src/theme/ApiExplorer/Authorization/auth-types.ts new file mode 100644 index 00000000..125d18c9 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Authorization/auth-types.ts @@ -0,0 +1,23 @@ +export function getAuthDataKeys(security: { [key: string]: any }) { + // Bearer Auth + if (security.type === "http" && security.scheme === "bearer") { + return ["token"]; + } + + if (security.type === "oauth2") { + return ["token"]; + } + + // Basic Auth + if (security.type === "http" && security.scheme === "basic") { + return ["username", "password"]; + } + + // API Auth + if (security.type === "apiKey") { + return ["apiKey"]; + } + + // none + return []; +} diff --git a/docs/src/theme/ApiExplorer/Authorization/index.tsx b/docs/src/theme/ApiExplorer/Authorization/index.tsx new file mode 100644 index 00000000..42a7c61d --- /dev/null +++ b/docs/src/theme/ApiExplorer/Authorization/index.tsx @@ -0,0 +1,151 @@ +import React from "react"; + +import FormItem from "@theme/ApiExplorer/FormItem"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import FormTextInput from "@theme/ApiExplorer/FormTextInput"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; + +import { setAuthData, setSelectedAuth } from "./slice"; + +function Authorization() { + const data = useTypedSelector((state: any) => state.auth.data); + const options = useTypedSelector((state: any) => state.auth.options); + const selected = useTypedSelector((state: any) => state.auth.selected); + + const dispatch = useTypedDispatch(); + + if (selected === undefined) { + return null; + } + + const selectedAuth = options[selected]; + + const optionKeys = Object.keys(options); + + return ( +
+ {optionKeys.length > 1 && ( + + ) => { + dispatch(setSelectedAuth(e.target.value)); + }} + /> + + )} + {selectedAuth.map((a: any) => { + if (a.type === "http" && a.scheme === "bearer") { + return ( + + ) => { + const value = e.target.value; + dispatch( + setAuthData({ + scheme: a.key, + key: "token", + value: value ? value : undefined, + }), + ); + }} + /> + + ); + } + + if (a.type === "oauth2") { + return ( + + ) => { + const value = e.target.value; + dispatch( + setAuthData({ + scheme: a.key, + key: "token", + value: value ? value : undefined, + }), + ); + }} + /> + + ); + } + + if (a.type === "http" && a.scheme === "basic") { + return ( + + + ) => { + const value = e.target.value; + dispatch( + setAuthData({ + scheme: a.key, + key: "username", + value: value ? value : undefined, + }), + ); + }} + /> + + + ) => { + const value = e.target.value; + dispatch( + setAuthData({ + scheme: a.key, + key: "password", + value: value ? value : undefined, + }), + ); + }} + /> + + + ); + } + + if (a.type === "apiKey") { + return ( + + ) => { + const value = e.target.value; + dispatch( + setAuthData({ + scheme: a.key, + key: "apiKey", + value: value ? value : undefined, + }), + ); + }} + /> + + ); + } + + return null; + })} +
+ ); +} + +export default Authorization; diff --git a/docs/src/theme/ApiExplorer/Authorization/slice.ts b/docs/src/theme/ApiExplorer/Authorization/slice.ts new file mode 100644 index 00000000..64a0fa51 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Authorization/slice.ts @@ -0,0 +1,139 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { createStorage, hashArray } from "@theme/ApiExplorer/storage-utils"; +import { + SecurityRequirementObject, + SecuritySchemeObject, +} from "docusaurus-plugin-openapi-docs/src/openapi/types"; +/* eslint-disable import/no-extraneous-dependencies*/ +import { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types"; + +import { getAuthDataKeys } from "./auth-types"; + +// The global definitions +// "securitySchemes": { +// "BearerAuth": { "type": "http", "scheme": "BeAreR" }, +// "BasicAuth": { "type": "http", "scheme": "basic" } +// }, + +// The operation level requirements +// "security": [ +// { "BearerAuth": [] }, +// { "BearerAuth": [], "BasicAuth": [] } +// ], + +// SLICE_STATE +// data: +// BearerAuth: +// token=xxx +// BasicAuth: +// username=xxx +// password=xxx +// +// options: +// "BearerAuth": [{ key: "BearerAuth", scopes: [], ...rest }] +// "BearerAuth and BasicAuth": [{ key: "BearerAuth", scopes: [], ...rest }, { key: "BasicAuth", scopes: [], ...rest }] +// +// selected: "BearerAuth and BasicAuth" + +// LOCAL_STORAGE +// hash(SLICE_STATE.options) -> "BearerAuth and BasicAuth" +// BearerAuth -> { token: xxx } +// BasicAuth -> { username: xxx, password: xxx } + +export function createAuth({ + security, + securitySchemes, + options: opts, +}: { + security?: SecurityRequirementObject[]; + securitySchemes?: { + [key: string]: SecuritySchemeObject; + }; + options?: ThemeConfig["api"]; +}): AuthState { + const storage = createStorage("sessionStorage"); + + let data: AuthState["data"] = {}; + let options: AuthState["options"] = {}; + + for (const option of security ?? []) { + const id = Object.keys(option).join(" and "); + for (const [schemeID, scopes] of Object.entries(option)) { + const scheme = securitySchemes?.[schemeID]; + if (scheme) { + if (options[id] === undefined) { + options[id] = []; + } + const dataKeys = getAuthDataKeys(scheme); + for (const key of dataKeys) { + if (data[schemeID] === undefined) { + data[schemeID] = {}; + } + + let persisted = undefined; + try { + persisted = JSON.parse(storage.getItem(schemeID) ?? "")[key]; + } catch {} + + data[schemeID][key] = persisted; + } + options[id].push({ + ...scheme, + key: schemeID, + scopes, + }); + } + } + } + + let persisted = undefined; + try { + persisted = storage.getItem(hashArray(Object.keys(options))) ?? undefined; + } catch {} + + return { + data, + options, + selected: persisted ?? Object.keys(options)[0], + }; +} + +export type Scheme = { + key: string; + scopes: string[]; +} & SecuritySchemeObject; + +export interface AuthState { + data: { + [scheme: string]: { + [key: string]: string | undefined; + }; + }; + options: { + [key: string]: Scheme[]; + }; + selected?: string; +} + +const initialState: AuthState = {} as any; + +export const slice = createSlice({ + name: "auth", + initialState, + reducers: { + setAuthData: ( + state, + action: PayloadAction<{ scheme: string; key: string; value?: string }>, + ) => { + const { scheme, key, value } = action.payload; + state.data[scheme][key] = value; + }, + setSelectedAuth: (state, action: PayloadAction) => { + state.selected = action.payload; + }, + }, +}); + +export const { setAuthData, setSelectedAuth } = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/Body/index.tsx b/docs/src/theme/ApiExplorer/Body/index.tsx new file mode 100644 index 00000000..054fe7ae --- /dev/null +++ b/docs/src/theme/ApiExplorer/Body/index.tsx @@ -0,0 +1,369 @@ +import React from "react"; + +import json2xml from "@theme/ApiExplorer/Body/json2xml"; +import FormFileUpload from "@theme/ApiExplorer/FormFileUpload"; +import FormItem from "@theme/ApiExplorer/FormItem"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import FormTextInput from "@theme/ApiExplorer/FormTextInput"; +import LiveApp from "@theme/ApiExplorer/LiveEditor"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; +import Markdown from "@theme/Markdown"; +import SchemaTabs from "@theme/SchemaTabs"; +import TabItem from "@theme/TabItem"; +import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; +import format from "xml-formatter"; + +import { + clearFormBodyKey, + clearRawBody, + setFileFormBody, + setFileRawBody, + setStringFormBody, +} from "./slice"; + +export interface Props { + jsonRequestBodyExample: string; + requestBodyMetadata?: RequestBodyObject; + methods?: any; + required?: boolean; +} + +function BodyWrap({ + requestBodyMetadata, + jsonRequestBodyExample, + methods, + required, +}: Props) { + const contentType = useTypedSelector((state: any) => state.contentType.value); + + // NOTE: We used to check if body was required, but opted to always show the request body + // to reduce confusion, see: https://github.com/cloud-annotations/docusaurus-openapi/issues/145 + + // No body + if (contentType === undefined) { + return null; + } + + return ( + + ); +} + +function Body({ + requestBodyMetadata, + jsonRequestBodyExample, + methods, + required, +}: Props) { + const contentType = useTypedSelector((state: any) => state.contentType.value); + const dispatch = useTypedDispatch(); + + // Lot's of possible content-types: + // - application/json + // - application/xml + // - text/plain + // - text/css + // - text/html + // - text/javascript + // - application/javascript + // - multipart/form-data + // - application/x-www-form-urlencoded + // - image/svg+xml;charset=US-ASCII + + // Show editor: + // - application/json + // - application/xml + // - */* + + // Show form: + // - multipart/form-data + // - application/x-www-form-urlencoded + + const schema = requestBodyMetadata?.content?.[contentType]?.schema; + const example = requestBodyMetadata?.content?.[contentType]?.example; + const examples = requestBodyMetadata?.content?.[contentType]?.examples; + + if (schema?.format === "binary") { + return ( + + { + if (file === undefined) { + dispatch(clearRawBody()); + return; + } + dispatch( + setFileRawBody({ + src: `/path/to/${file.name}`, + content: file, + }), + ); + }} + /> + + ); + } + if ( + (contentType === "multipart/form-data" || + contentType === "application/x-www-form-urlencoded") && + schema?.type === "object" + ) { + return ( + +
+ {Object.entries(schema.properties ?? {}).map(([key, val]: any) => { + if (val.format === "binary") { + return ( + + { + if (file === undefined) { + dispatch(clearFormBodyKey(key)); + return; + } + dispatch( + setFileFormBody({ + key: key, + value: { + src: `/path/to/${file.name}`, + content: file, + }, + }), + ); + }} + /> + + ); + } + + if (val.enum) { + return ( + + ) => { + const val = e.target.value; + if (val === "---") { + dispatch(clearFormBodyKey(key)); + } else { + dispatch( + setStringFormBody({ + key: key, + value: val, + }), + ); + } + }} + /> + + ); + } + // TODO: support all the other types. + return ( + + ) => { + dispatch( + setStringFormBody({ key: key, value: e.target.value }), + ); + }} + /> + + ); + })} +
+
+ ); + } + + let language = "plaintext"; + let defaultBody = ""; //"body content"; + let exampleBody; + let examplesBodies = [] as any; + + if ( + contentType.includes("application/json") || + contentType.endsWith("+json") + ) { + if (jsonRequestBodyExample) { + defaultBody = JSON.stringify(jsonRequestBodyExample, null, 2); + } + if (example) { + exampleBody = JSON.stringify(example, null, 2); + } + if (examples) { + for (const [key, example] of Object.entries(examples)) { + let body = example.value; + try { + // If the value is already valid JSON we shouldn't double encode the value + JSON.parse(example.value); + } catch (e) { + body = JSON.stringify(example.value, null, 2); + } + + examplesBodies.push({ + label: key, + body, + summary: example.summary, + }); + } + } + language = "json"; + } + + if (contentType === "application/xml" || contentType.endsWith("+xml")) { + if (jsonRequestBodyExample) { + try { + defaultBody = format(json2xml(jsonRequestBodyExample, ""), { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + defaultBody = json2xml(jsonRequestBodyExample); + } + } + if (example) { + try { + exampleBody = format(json2xml(example, ""), { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + exampleBody = json2xml(example); + } + } + if (examples) { + for (const [key, example] of Object.entries(examples)) { + let formattedXmlBody; + try { + formattedXmlBody = format(example.value, { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + formattedXmlBody = example.value; + } + examplesBodies.push({ + label: key, + body: formattedXmlBody, + summary: example.summary, + }); + } + } + language = "xml"; + } + + if (exampleBody) { + return ( + + + {/* @ts-ignore */} + + + {defaultBody} + + + {/* @ts-ignore */} + + {example.summary && {example.summary}} + {exampleBody && ( + + {exampleBody} + + )} + + + + ); + } + + if (examplesBodies && examplesBodies.length > 0) { + return ( + + + {/* @ts-ignore */} + + + {defaultBody} + + + {examplesBodies.map((example: any) => { + return ( + // @ts-ignore + + {example.summary && {example.summary}} + {example.body && ( + + {example.body} + + )} + + ); + })} + + + ); + } + + return ( + + + {defaultBody} + + + ); +} + +export default BodyWrap; diff --git a/docs/src/theme/ApiExplorer/Body/json2xml.js b/docs/src/theme/ApiExplorer/Body/json2xml.js new file mode 100644 index 00000000..6092291b --- /dev/null +++ b/docs/src/theme/ApiExplorer/Body/json2xml.js @@ -0,0 +1,36 @@ +export default function json2xml(o, tab) { + var toXml = function (v, name, ind) { + var xml = ""; + if (v instanceof Array) { + for (var i = 0, n = v.length; i < n; i++) + xml += ind + toXml(v[i], name, ind + "\t") + "\n"; + } else if (typeof v == "object") { + var hasChild = false; + xml += ind + "<" + name; + for (var m in v) { + if (m.charAt(0) === "@") + xml += " " + m.substr(1) + '="' + v[m].toString() + '"'; + else hasChild = true; + } + xml += hasChild ? ">" : "/>"; + if (hasChild) { + for (var m2 in v) { + if (m2 === "#text") xml += v[m2]; + else if (m2 === "#cdata") xml += ""; + else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t"); + } + xml += + (xml.charAt(xml.length - 1) === "\n" ? ind : "") + + ""; + } + } else { + xml += ind + "<" + name + ">" + v.toString() + ""; + } + return xml; + }, + xml = ""; + for (var m3 in o) xml += toXml(o[m3], m3, ""); + return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); +} diff --git a/docs/src/theme/ApiExplorer/Body/slice.ts b/docs/src/theme/ApiExplorer/Body/slice.ts new file mode 100644 index 00000000..59c68416 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Body/slice.ts @@ -0,0 +1,126 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface FileContent { + type: "file"; + value: { + src: string; + content: Blob; + }; +} + +export interface StringContent { + type: "string"; + value?: string; +} + +export type Content = FileContent | StringContent | undefined; + +export interface FormBody { + type: "form"; + content: { + [key: string]: Content; + }; +} + +export interface RawBody { + type: "raw"; + content: Content; +} + +export interface EmptyBody { + type: "empty"; +} + +export type Body = EmptyBody | FormBody | RawBody; + +export type State = Body; + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "body", + initialState, + reducers: { + clearRawBody: (_state) => { + return { + type: "empty", + }; + }, + setStringRawBody: (_state, action: PayloadAction) => { + return { + type: "raw", + content: { + type: "string", + value: action.payload, + }, + }; + }, + setFileRawBody: (_state, action: PayloadAction) => { + return { + type: "raw", + content: { + type: "file", + value: action.payload, + }, + }; + }, + clearFormBodyKey: (state, action: PayloadAction) => { + if (state?.type === "form") { + delete state.content[action.payload]; + } + }, + setStringFormBody: ( + state, + action: PayloadAction<{ key: string; value: string }>, + ) => { + if (state?.type !== "form") { + return { + type: "form", + content: { + [action.payload.key]: { + type: "string", + value: action.payload.value, + }, + }, + }; + } + state.content[action.payload.key] = { + type: "string", + value: action.payload.value, + }; + return state; + }, + setFileFormBody: ( + state, + action: PayloadAction<{ key: string; value: FileContent["value"] }>, + ) => { + if (state?.type !== "form") { + return { + type: "form", + content: { + [action.payload.key]: { + type: "file", + value: action.payload.value, + }, + }, + }; + } + state.content[action.payload.key] = { + type: "file", + value: action.payload.value, + }; + return state; + }, + }, +}); + +export const { + clearRawBody, + setStringRawBody, + setFileRawBody, + clearFormBodyKey, + setStringFormBody, + setFileFormBody, +} = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts b/docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts new file mode 100644 index 00000000..09ae5c43 --- /dev/null +++ b/docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts @@ -0,0 +1,50 @@ +// https://github.com/github-linguist/linguist/blob/master/lib/linguist/popular.yml +export type CodeSampleLanguage = + | "C" + | "C#" + | "C++" + | "CoffeeScript" + | "CSS" + | "Dart" + | "DM" + | "Elixir" + | "Go" + | "Groovy" + | "HTML" + | "Java" + | "JavaScript" + | "Kotlin" + | "Objective-C" + | "OCaml" + | "Perl" + | "PHP" + | "PowerShell" + | "Python" + | "R" + | "Ruby" + | "Rust" + | "Scala" + | "Shell" + | "Swift" + | "TypeScript"; + +export interface Language { + highlight: string; + language: string; + codeSampleLanguage: CodeSampleLanguage; + logoClass: string; + variant: string; + variants: string[]; + options?: { [key: string]: boolean }; + sample?: string; + samples?: string[]; + samplesSources?: string[]; + samplesLabels?: string[]; +} + +// https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples +export interface CodeSample { + source: string; + lang: CodeSampleLanguage; + label?: string; +} diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/index.tsx b/docs/src/theme/ApiExplorer/CodeSnippets/index.tsx new file mode 100644 index 00000000..1ca03201 --- /dev/null +++ b/docs/src/theme/ApiExplorer/CodeSnippets/index.tsx @@ -0,0 +1,331 @@ +import React, { useState, useEffect, type JSX } from "react"; + +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock"; +import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest"; +import CodeTabs from "@theme/ApiExplorer/CodeTabs"; +import { useTypedSelector } from "@theme/ApiItem/hooks"; +import cloneDeep from "lodash/cloneDeep"; +import codegen from "postman-code-generators"; +import sdk from "postman-collection"; + +import { CodeSample, Language } from "./code-snippets-types"; +import { + getCodeSampleSourceFromLanguage, + mergeArraysbyLanguage, + mergeCodeSampleLanguage, + generateLanguageSet, +} from "./languages"; + +export const languageSet: Language[] = generateLanguageSet(); + +export interface Props { + postman: sdk.Request; + codeSamples: CodeSample[]; +} + +function CodeTab({ children, hidden, className }: any): JSX.Element { + return ( + + ); +} + +function CodeSnippets({ postman, codeSamples }: Props) { + const { siteConfig } = useDocusaurusContext(); + + const contentType = useTypedSelector((state: any) => state.contentType.value); + const accept = useTypedSelector((state: any) => state.accept.value); + const server = useTypedSelector((state: any) => state.server.value); + const body = useTypedSelector((state: any) => state.body); + + const pathParams = useTypedSelector((state: any) => state.params.path); + const queryParams = useTypedSelector((state: any) => state.params.query); + const cookieParams = useTypedSelector((state: any) => state.params.cookie); + const headerParams = useTypedSelector((state: any) => state.params.header); + + const auth = useTypedSelector((state: any) => state.auth); + const clonedAuth = cloneDeep(auth); + let placeholder: string; + + function cleanCredentials(obj: any) { + for (const key in obj) { + if (typeof obj[key] === "object" && obj[key] !== null) { + // use name as placeholder if exists + const comboAuthId = Object.keys(obj).join(" and "); + const authOptions = + clonedAuth?.options?.[key] ?? clonedAuth?.options?.[comboAuthId]; + placeholder = authOptions?.[0]?.name; + obj[key] = cleanCredentials(obj[key]); + } else { + obj[key] = `<${placeholder ?? key}>`; + } + } + + return obj; + } + + // scrub credentials from code snippets + const cleanedAuth = { + ...clonedAuth, + data: cleanCredentials(clonedAuth.data), + }; + + // Create a Postman request object using cleanedAuth + const cleanedPostmanRequest = buildPostmanRequest(postman, { + queryParams, + pathParams, + cookieParams, + contentType, + accept, + headerParams, + body, + server, + auth: cleanedAuth, + }); + + // User-defined languages array + // Can override languageSet, change order of langs, override options and variants + const userDefinedLanguageSet = + (siteConfig?.themeConfig?.languageTabs as Language[] | undefined) ?? + languageSet; + + // Filter languageSet by user-defined langs + const filteredLanguageSet = languageSet.filter((ls) => { + return userDefinedLanguageSet?.some((lang) => { + return lang.language === ls.language; + }); + }); + + // Merge user-defined langs into languageSet + const mergedLangs = mergeCodeSampleLanguage( + mergeArraysbyLanguage(userDefinedLanguageSet, filteredLanguageSet), + codeSamples, + ); + + // Read defaultLang from localStorage + const defaultLang: Language[] = mergedLangs.filter( + (lang) => + lang.language === localStorage.getItem("docusaurus.tab.code-samples"), + ); + const [selectedVariant, setSelectedVariant] = useState(); + const [selectedSample, setSelectedSample] = useState(); + const [language, setLanguage] = useState(() => { + // Return first index if only 1 user-defined language exists + if (mergedLangs.length === 1) { + return mergedLangs[0]; + } + // Fall back to language in localStorage or first user-defined language + return defaultLang[0] ?? mergedLangs[0]; + }); + const [codeText, setCodeText] = useState(""); + const [codeSampleCodeText, setCodeSampleCodeText] = useState< + string | (() => string) + >(() => getCodeSampleSourceFromLanguage(language)); + + useEffect(() => { + if (language && !!language.sample) { + setCodeSampleCodeText(getCodeSampleSourceFromLanguage(language)); + } + + if (language && !!language.options) { + codegen.convert( + language.language, + language.variant, + cleanedPostmanRequest, + language.options, + (error: any, snippet: string) => { + if (error) { + return; + } + setCodeText(snippet); + }, + ); + } else if (language && !language.options) { + const langSource = mergedLangs.filter( + (lang) => lang.language === language.language, + ); + + // Merges user-defined language with default languageSet + // This allows users to define only the minimal properties necessary in languageTabs + // User-defined properties should override languageSet properties + const mergedLanguage = { ...langSource[0], ...language }; + codegen.convert( + mergedLanguage.language, + mergedLanguage.variant, + cleanedPostmanRequest, + mergedLanguage.options, + (error: any, snippet: string) => { + if (error) { + return; + } + setCodeText(snippet); + }, + ); + } else { + setCodeText(""); + } + }, [ + accept, + body, + contentType, + cookieParams, + headerParams, + language, + pathParams, + postman, + queryParams, + server, + cleanedPostmanRequest, + mergedLangs, + ]); + // no dependencies was intentionally set for this particular hook. it's safe as long as if conditions are set + useEffect(function onSelectedVariantUpdate() { + if (selectedVariant && selectedVariant !== language?.variant) { + codegen.convert( + language.language, + selectedVariant, + cleanedPostmanRequest, + language.options, + (error: any, snippet: string) => { + if (error) { + return; + } + setCodeText(snippet); + }, + ); + } + }); + + // no dependencies was intentionally set for this particular hook. it's safe as long as if conditions are set + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(function onSelectedSampleUpdate() { + if ( + language && + language.samples && + language.samplesSources && + selectedSample && + selectedSample !== language.sample + ) { + const sampleIndex = language.samples.findIndex( + (smp) => smp === selectedSample, + ); + setCodeSampleCodeText(language.samplesSources[sampleIndex]); + } + }); + + if (language === undefined) { + return null; + } + + return ( + <> + {/* Outer language tabs */} + + {mergedLangs.map((lang) => { + return ( + + {/* Inner x-codeSamples tabs */} + {lang.samples && ( + + {lang.samples.map((sample, index) => { + return ( + + {/* @ts-ignore */} + + {codeSampleCodeText} + + + ); + })} + + )} + + {/* Inner generated code snippets */} + + {lang.variants.map((variant, index) => { + return ( + + {/* @ts-ignore */} + + {codeText} + + + ); + })} + + + ); + })} + + + ); +} + +export default CodeSnippets; diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/languages.json b/docs/src/theme/ApiExplorer/CodeSnippets/languages.json new file mode 100644 index 00000000..36c345a3 --- /dev/null +++ b/docs/src/theme/ApiExplorer/CodeSnippets/languages.json @@ -0,0 +1,1290 @@ +[ + { + "key": "csharp", + "label": "C#", + "syntax_mode": "csharp", + "variants": [ + { + "key": "RestSharp", + "options": [ + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + }, + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "curl", + "label": "cURL", + "syntax_mode": "powershell", + "variants": [ + { + "key": "cURL", + "options": [ + { + "name": "Generate multiline snippet", + "id": "multiLine", + "type": "boolean", + "default": true, + "description": "Split cURL command across multiple lines" + }, + { + "name": "Use long form options", + "id": "longFormat", + "type": "boolean", + "default": true, + "description": "Use the long form for cURL options (--header instead of -H)" + }, + { + "name": "Line continuation character", + "id": "lineContinuationCharacter", + "availableOptions": ["\\", "^", "`"], + "type": "enum", + "default": "\\", + "description": "Set a character used to mark the continuation of a statement on the next line (generally, \\ for OSX/Linux, ^ for Windows cmd and ` for Powershell)" + }, + { + "name": "Quote Type", + "id": "quoteType", + "availableOptions": ["single", "double"], + "type": "enum", + "default": "single", + "description": "String denoting the quote type to use (single or double) for URL (Use double quotes when running curl in cmd.exe and single quotes for the rest)" + }, + { + "name": "Set request timeout (in seconds)", + "id": "requestTimeoutInSeconds", + "type": "positiveInteger", + "default": 0, + "description": "Set number of seconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Use Silent Mode", + "id": "silent", + "type": "boolean", + "default": false, + "description": "Display the requested data without showing the cURL progress meter or error messages" + } + ] + } + ] + }, + { + "key": "dart", + "label": "Dart", + "syntax_mode": "dart", + "variants": [ + { + "key": "http", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + } + ] + } + ] + }, + { + "key": "go", + "label": "Go", + "syntax_mode": "golang", + "variants": [ + { + "key": "Native", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "http", + "label": "HTTP", + "syntax_mode": "text", + "variants": [ + { + "key": "HTTP", + "options": [ + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "java", + "label": "Java", + "syntax_mode": "java", + "variants": [ + { + "key": "OkHttp", + "options": [ + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + }, + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + }, + { + "key": "Unirest", + "options": [ + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + }, + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "javascript", + "label": "JavaScript", + "syntax_mode": "javascript", + "variants": [ + { + "key": "Fetch", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + }, + { + "key": "jQuery", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + }, + { + "key": "XHR", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "c", + "label": "C", + "syntax_mode": "c_cpp", + "variants": [ + { + "key": "libcurl", + "options": [ + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + }, + { + "name": "Protocol", + "id": "protocol", + "type": "enum", + "availableOptions": ["http", "https"], + "default": "https", + "description": "The protocol to be used to make the request" + }, + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Use curl_mime", + "id": "useMimeType", + "type": "boolean", + "default": true, + "description": "Use curl_mime to send multipart/form-data requests" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + } + ] + } + ] + }, + { + "key": "nodejs", + "label": "NodeJs", + "syntax_mode": "javascript", + "variants": [ + { + "key": "Axios", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Enable ES6 features", + "id": "ES6_enabled", + "type": "boolean", + "default": false, + "description": "Modifies code snippet to incorporate ES6 (EcmaScript) features" + } + ] + }, + { + "key": "Native", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Enable ES6 features", + "id": "ES6_enabled", + "type": "boolean", + "default": false, + "description": "Modifies code snippet to incorporate ES6 (EcmaScript) features" + } + ] + } + ] + }, + { + "key": "objective-c", + "label": "Objective-C", + "syntax_mode": "objectivec", + "variants": [ + { + "key": "NSURLSession", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 10000, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + } + ] + } + ] + }, + { + "key": "ocaml", + "label": "OCaml", + "syntax_mode": "ocaml", + "variants": [ + { + "key": "Cohttp", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + } + ] + } + ] + }, + { + "key": "php", + "label": "PHP", + "syntax_mode": "php", + "variants": [ + { + "key": "cURL", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + }, + { + "key": "Guzzle", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Set communication type", + "id": "asyncType", + "type": "enum", + "availableOptions": ["async", "sync"], + "default": "async", + "description": "Set if the requests will be asynchronous or synchronous" + }, + { + "name": "Include boilerplate", + "id": "includeBoilerplate", + "type": "boolean", + "default": false, + "description": "Include class definition and import statements in snippet" + } + ] + }, + { + "key": "HTTP_Request2", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "default": "Space", + "availableOptions": ["Tab", "Space"], + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + } + ] + }, + { + "key": "pecl_http", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "default": "Space", + "availableOptions": ["Tab", "Space"], + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + } + ] + } + ] + }, + { + "key": "powershell", + "label": "PowerShell", + "syntax_mode": "powershell", + "variants": [ + { + "key": "RestMethod", + "options": [ + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "python", + "label": "Python", + "syntax_mode": "python", + "variants": [ + { + "key": "http.client", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "default": "Space", + "availableOptions": ["Tab", "Space"], + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + }, + { + "key": "Requests", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "r", + "label": "R", + "syntax_mode": "r", + "variants": [ + { + "key": "httr", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + } + ] + }, + { + "key": "RCurl", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Ignore warnings", + "id": "ignoreWarnings", + "type": "boolean", + "default": false, + "description": "Ignore warnings from R" + } + ] + } + ] + }, + { + "key": "ruby", + "label": "Ruby", + "syntax_mode": "ruby", + "variants": [ + { + "key": "Net::HTTP", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "shell", + "label": "Shell", + "syntax_mode": "powershell", + "variants": [ + { + "key": "Httpie", + "options": [ + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + } + ] + }, + { + "key": "wget", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + } + ] + } + ] + }, + { + "key": "swift", + "label": "Swift", + "syntax_mode": "swift", + "variants": [ + { + "key": "URLSession", + "options": [ + { + "name": "Set indentation count", + "id": "indentCount", + "type": "positiveInteger", + "default": 2, + "description": "Set the number of indentation characters to add per code level" + }, + { + "name": "Set indentation type", + "id": "indentType", + "type": "enum", + "availableOptions": ["Tab", "Space"], + "default": "Space", + "description": "Select the character used to indent lines of code" + }, + { + "name": "Set request timeout", + "id": "requestTimeout", + "type": "positiveInteger", + "default": 0, + "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)" + }, + { + "name": "Trim request body fields", + "id": "trimRequestBody", + "type": "boolean", + "default": false, + "description": "Remove white space and additional lines that may affect the server's response" + }, + { + "name": "Follow redirects", + "id": "followRedirect", + "type": "boolean", + "default": true, + "description": "Automatically follow HTTP redirects" + } + ] + } + ] + } +] diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/languages.ts b/docs/src/theme/ApiExplorer/CodeSnippets/languages.ts new file mode 100644 index 00000000..2cc25fc2 --- /dev/null +++ b/docs/src/theme/ApiExplorer/CodeSnippets/languages.ts @@ -0,0 +1,89 @@ +import find from "lodash/find"; +import mergeWith from "lodash/mergeWith"; +import unionBy from "lodash/unionBy"; +import codegen from "postman-code-generators"; + +import { CodeSample, Language } from "./code-snippets-types"; + +export function mergeCodeSampleLanguage( + languages: Language[], + codeSamples: CodeSample[], +): Language[] { + return languages.map((language) => { + const languageCodeSamples = codeSamples.filter( + ({ lang }) => lang === language.codeSampleLanguage, + ); + + if (languageCodeSamples.length) { + const samples = languageCodeSamples.map(({ lang }) => lang); + const samplesLabels = languageCodeSamples.map( + ({ label, lang }) => label || lang, + ); + const samplesSources = languageCodeSamples.map(({ source }) => source); + + return { + ...language, + sample: samples[0], + samples, + samplesSources, + samplesLabels, + }; + } + + return language; + }); +} + +export const mergeArraysbyLanguage = (arr1: any, arr2: any) => { + const mergedArray = unionBy(arr1, arr2, "language"); + + return mergedArray.map((item: any) => { + const matchingItems = [ + find(arr1, ["language", item["language"]]), + find(arr2, ["language", item["language"]]), + ]; + return mergeWith({}, ...matchingItems, (objValue: any) => { + return objValue; + }); + }); +}; + +export function getCodeSampleSourceFromLanguage(language: Language) { + if ( + language && + language.sample && + language.samples && + language.samplesSources + ) { + const sampleIndex = language.samples.findIndex( + (smp) => smp === language.sample, + ); + return language.samplesSources[sampleIndex]; + } + + return ""; +} + +export function generateLanguageSet() { + const languageSet: Language[] = []; + codegen.getLanguageList().forEach((language: any) => { + const variants: any = []; + language.variants.forEach((variant: any) => { + variants.push(variant.key); + }); + languageSet.push({ + highlight: language.syntax_mode, + language: language.key, + codeSampleLanguage: language.label, + logoClass: language.key, + options: { + longFormat: false, + followRedirect: true, + trimRequestBody: true, + }, + variant: variants[0], + variants: variants, + }); + }); + return languageSet; +} diff --git a/docs/src/theme/ApiExplorer/CodeTabs/_CodeTabs.scss b/docs/src/theme/ApiExplorer/CodeTabs/_CodeTabs.scss new file mode 100644 index 00000000..fd2c84e1 --- /dev/null +++ b/docs/src/theme/ApiExplorer/CodeTabs/_CodeTabs.scss @@ -0,0 +1,501 @@ +:root { + --bash-background-color: transparent; + --bash-border-radius: none; + --code-tab-logo-width: 24px; + --code-tab-logo-height: 24px; +} + +[data-theme="dark"] { + --bash-background-color: lightgrey; + --bash-border-radius: 20px; +} + +.openapi-tabs__code-container { + margin-bottom: 1rem; + + &:not(.openapi-tabs__code-container-inner) { + padding: 1rem; + background-color: var(--ifm-pre-background); + border-radius: var(--ifm-global-radius); + border: 1px solid var(--openapi-explorer-border-color); + box-shadow: + 0 2px 3px hsla(222, 8%, 43%, 0.1), + 0 8px 16px -10px hsla(222, 8%, 43%, 0.2); + transition: 300ms; + + &:hover { + box-shadow: + 0 0 0 2px rgba(38, 53, 61, 0.15), + 0 2px 3px hsla(222, 8%, 43%, 0.15), + 0 16px 16px -10px hsla(222, 8%, 43%, 0.2); + } + } + + .openapi-tabs__code-item { + display: flex; + flex-direction: column-reverse; + flex: 0 0 80px; + align-items: center; + padding: 0.5rem 0 !important; + margin-top: 0 !important; + margin-right: 0.5rem; + border: 1px solid transparent; + transition: 300ms; + + &:not(.active):hover { + border: 1px solid var(--openapi-code-tab-border-color); + } + + &:hover { + background-color: transparent; + } + + span { + padding-top: 0.5rem; + color: var(--ifm-font-color-secondary); + font-size: 10px; + text-transform: uppercase; + } + } +} + +.openapi-tabs__code-list-container { + display: flex; + justify-content: flex-start; + padding: 0.25rem; + padding-bottom: 0.6rem; +} + +.openapi-tabs__code-content { + margin-top: unset !important; +} + +.openapi-explorer__code-block code { + max-height: 200px; + font-size: var(--openapi-explorer-font-size-code); + padding-top: var(--ifm-pre-padding); +} + +body[class="ReactModal__Body--open"] { + .openapi-explorer__code-block code { + max-height: 600px; + } +} + +.openapi-tabs__code-item--variant { + color: var(--ifm-color-secondary); + + &.active { + border-color: var(--ifm-toc-border-color); + } +} + +.openapi-tabs__code-item--variant > span { + padding-top: unset !important; + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; +} + +.openapi-tabs__code-item--sample { + color: var(--ifm-color-secondary); + + &.active { + border-color: var(--ifm-toc-border-color); + } +} + +.openapi-tabs__code-item--sample > span { + padding-top: unset !important; + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; +} + +.openapi-tabs__code-item--python { + color: var(--ifm-color-success); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-python); + border-color: var(--openapi-code-tab-border-color-python); + } +} + +.openapi-tabs__code-item--go { + color: var(--ifm-color-info); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/go/go-original-wordmark.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-go); + border-color: var(--openapi-code-tab-border-color-go); + } +} + +.openapi-tabs__code-item--dart { + color: var(--ifm-color-info); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/dart/dart-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-dart); + border-color: var(--openapi-code-tab-border-color-dart); + } +} + +.openapi-tabs__code-item--javascript { + color: var(--ifm-color-warning); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/javascript/javascript-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-js); + border-color: var(--openapi-code-tab-border-color-js); + } +} + +.openapi-tabs__code-item--curl { + color: var(--ifm-color-danger); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/bash/bash-plain.svg") + no-repeat; + margin-block: auto; + background-color: var(--bash-background-color); + border-radius: var(--bash-border-radius); + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-curl); + border-color: var(--ifm-color-danger); + } +} + +.openapi-tabs__code-item--ruby { + color: var(--ifm-color-danger); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/ruby/ruby-plain.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-ruby); + border-color: var(--openapi-code-tab-border-color-ruby); + } +} + +.openapi-tabs__code-item--csharp { + color: var(--ifm-color-gray-500); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/csharp/csharp-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-csharp); + border-color: var(--openapi-code-tab-border-color-csharp); + } +} + +.openapi-tabs__code-item--r { + color: var(--ifm-color-gray-500); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/r/r-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-r); + border-color: var(--openapi-code-tab-border-color-r); + } +} + +.openapi-tabs__code-item--swift { + color: var(--ifm-color-danger); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/swift/swift-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-swift); + border-color: var(--openapi-code-tab-border-color-swift); + } +} + +.openapi-tabs__code-item--c { + color: var(--ifm-color-info); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/c/c-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-c); + border-color: var(--openapi-code-tab-border-color-c); + } +} + +.openapi-tabs__code-item--objective-c { + color: var(--ifm-color-info); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/objectivec/objectivec-plain.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-objective-c); + border-color: var(--openapi-code-tab-border-color-objective-c); + } +} + +.openapi-tabs__code-item--ocaml { + color: var(--ifm-color-warning); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/ocaml/ocaml-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-objective-ocaml); + border-color: var(--openapi-code-tab-border-color-objective-ocaml); + } +} + +.openapi-tabs__code-item--nodejs { + color: var(--ifm-color-success); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/nodejs/nodejs-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-nodejs); + border-color: var(--openapi-code-tab-border-color-nodejs); + } +} + +.openapi-tabs__code-item--php { + color: var(--ifm-color-gray-500); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/php/php-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-php); + border-color: var(--openapi-code-tab-border-color-php); + } +} + +.openapi-tabs__code-item--kotlin { + color: var(--ifm-color-gray-500); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/kotlin/kotlin-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-kotlin); + border-color: var(--openapi-code-tab-border-color-kotlin); + } +} + +.openapi-tabs__code-item--rust { + color: var(--ifm-color-gray-500); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/rust/rust-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-rust); + border-color: var(--openapi-code-tab-border-color-rust); + } +} + +.openapi-tabs__code-item--java { + color: var(--ifm-color-warning); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/java/java-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-java); + border-color: var(--openapi-code-tab-border-color-java); + } +} + +.openapi-tabs__code-item--powershell { + color: var(--ifm-color-info); + + &::after { + content: ""; + width: var(--code-tab-logo-width); + height: var(--code-tab-logo-height); + background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/windows8/windows8-original.svg") + no-repeat; + margin-block: auto; + } + + &.active { + box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-powershell); + border-color: var(--openapi-code-tab-border-color-powershell); + } +} + +.openapi-tabs__code-item--http { + color: var(--ifm-color-gray-500); + display: flex; + align-items: center; + justify-content: center; + position: relative; + + &::after { + content: ""; + display: inline-block; + width: 32px; /* Explicitly setting width to 32 pixels */ + height: 32px; /* Explicitly setting height to 32 pixels */ + background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDIyTDggMTZMMTIgMTBNMjAgMjJMMjQgMTZMIDIwIDEwIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4="); + background-size: contain; + background-repeat: no-repeat; + background-position: center; /* Center the SVG */ + margin-top: 0.5rem; + } + + &.active { + box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-http); + border-color: var(--openapi-code-tab-border-color-http); + } +} + +.openapi-tabs__code-item--shell { + color: var(--ifm-color-gray-500); + display: flex; + align-items: center; + justify-content: center; + position: relative; + + &::after { + content: ""; + display: inline-block; + width: 32px; /* Explicitly setting width to 32 pixels */ + height: 32px; /* Explicitly setting height to 32 pixels */ + background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDIyTDggMTZMMTIgMTBNMjAgMjJMMjQgMTZMIDIwIDEwIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4="); + background-size: contain; + background-repeat: no-repeat; + background-position: center; /* Center the SVG */ + margin-top: 0.5rem; + } + + &.active { + box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-shell); + border-color: var(--openapi-code-tab-border-color-shell); + } +} + +@media only screen and (min-width: 768px) and (max-width: 996px) { + .openapi-tabs__code-list { + justify-content: space-around; + } +} + +.ReactModal__Body--open { + overflow: hidden !important; +} + +.openapi-modal--open { + background-color: rgba(0, 0, 0, 0.7) !important; +} diff --git a/docs/src/theme/ApiExplorer/CodeTabs/index.tsx b/docs/src/theme/ApiExplorer/CodeTabs/index.tsx new file mode 100644 index 00000000..1fc4e7e8 --- /dev/null +++ b/docs/src/theme/ApiExplorer/CodeTabs/index.tsx @@ -0,0 +1,241 @@ +import React, { cloneElement, ReactElement, useEffect, useRef } from "react"; + +import { + sanitizeTabsChildren, + type TabProps, + useScrollPositionBlocker, + useTabs, +} from "@docusaurus/theme-common/internal"; +import { TabItemProps } from "@docusaurus/theme-common/lib/utils/tabsUtils"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import { Language } from "@theme/ApiExplorer/CodeSnippets"; +import clsx from "clsx"; + +export interface Props { + action: { + [key: string]: React.Dispatch; + }; + currentLanguage: Language; + languageSet: Language[]; + includeVariant: boolean; +} + +export interface CodeTabsProps extends Props, TabProps { + includeSample?: boolean; +} + +function TabList({ + action, + currentLanguage, + languageSet, + includeVariant, + includeSample, + className, + block, + selectedValue, + selectValue, + tabValues, +}: CodeTabsProps & ReturnType) { + const tabRefs = useRef<(HTMLLIElement | null)[]>([]); + const tabsScrollContainerRef = useRef(null); + const { blockElementScrollPositionUntilNextRender } = + useScrollPositionBlocker(); + + useEffect(() => { + const activeTab = tabRefs.current.find( + (tab) => tab?.getAttribute("aria-selected") === "true", + ); + + if (activeTab && tabsScrollContainerRef.current) { + const container = tabsScrollContainerRef.current; + const containerRect = container.getBoundingClientRect(); + const activeTabRect = activeTab.getBoundingClientRect(); + + // Calculate the distance to scroll to align active tab to the left + const glowOffset = 3; + const scrollOffset = + activeTabRect.left - + containerRect.left + + container.scrollLeft - + glowOffset; + + // Check if the active tab is not already at the left position + + if (Math.abs(scrollOffset - container.scrollLeft) > 4) { + // Adjust the scroll of the container + container.scrollLeft = scrollOffset; + } + } + }, []); + + const handleTabChange = ( + event: + | React.FocusEvent + | React.MouseEvent + | React.KeyboardEvent, + ) => { + const newTab = event.currentTarget; + const newTabIndex = tabRefs.current.indexOf(newTab); + const newTabValue = tabValues[newTabIndex]!.value; + + if (newTabValue !== selectedValue) { + blockElementScrollPositionUntilNextRender(newTab); + selectValue(newTabValue); + } + + if (action) { + let newLanguage: Language; + if (currentLanguage && includeVariant) { + newLanguage = languageSet.filter( + (lang: Language) => lang.language === currentLanguage, + )[0]; + newLanguage.variant = newTabValue; + action.setSelectedVariant(newTabValue.toLowerCase()); + } else if (currentLanguage && includeSample) { + newLanguage = languageSet.filter( + (lang: Language) => lang.language === currentLanguage, + )[0]; + newLanguage.sample = newTabValue; + action.setSelectedSample(newTabValue); + } else { + newLanguage = languageSet.filter( + (lang: Language) => lang.language === newTabValue, + )[0]; + action.setSelectedVariant(newLanguage.variants[0].toLowerCase()); + action.setSelectedSample(newLanguage.sample); + } + action.setLanguage(newLanguage); + } + }; + + const handleKeydown = (event: React.KeyboardEvent) => { + let focusElement: HTMLLIElement | null = null; + + switch (event.key) { + case "Enter": { + handleTabChange(event); + break; + } + case "ArrowRight": { + const nextTab = tabRefs.current.indexOf(event.currentTarget) + 1; + focusElement = tabRefs.current[nextTab] ?? tabRefs.current[0]!; + break; + } + case "ArrowLeft": { + const prevTab = tabRefs.current.indexOf(event.currentTarget) - 1; + focusElement = + tabRefs.current[prevTab] ?? + tabRefs.current[tabRefs.current.length - 1]!; + break; + } + default: + break; + } + + focusElement?.focus(); + }; + + return ( +
    + {tabValues.map(({ value, label, attributes }) => ( +
  • { + if (tabControl) { + tabRefs.current.push(tabControl); + } + }} + onKeyDown={handleKeydown} + onClick={handleTabChange} + {...attributes} + className={clsx( + "tabs__item", + "openapi-tabs__code-item", + attributes?.className as string, + { + active: selectedValue === value, + }, + )} + > + {label ?? value} +
  • + ))} +
+ ); +} + +function TabContent({ + lazy, + children, + selectedValue, +}: CodeTabsProps & ReturnType): React.JSX.Element | null { + const childTabs = (Array.isArray(children) ? children : [children]).filter( + Boolean, + ) as ReactElement[]; + if (lazy) { + const selectedTabItem = childTabs.find( + (tabItem) => tabItem.props.value === selectedValue, + ); + if (!selectedTabItem) { + // fail-safe or fail-fast? not sure what's best here + return null; + } + return cloneElement(selectedTabItem, { className: "margin-top--md" }); + } + return ( +
+ {childTabs.map((tabItem, i) => + cloneElement(tabItem, { + key: i, + hidden: tabItem.props.value !== selectedValue, + }), + )} +
+ ); +} + +function TabsComponent(props: CodeTabsProps & Props): React.JSX.Element { + const tabs = useTabs(props); + const { className } = props; + + return ( +
+ + +
+ ); +} + +export default function CodeTabs( + props: CodeTabsProps & Props, +): React.JSX.Element { + const isBrowser = useIsBrowser(); + return ( + + {sanitizeTabsChildren(props.children)} + + ); +} diff --git a/docs/src/theme/ApiExplorer/ContentType/index.tsx b/docs/src/theme/ApiExplorer/ContentType/index.tsx new file mode 100644 index 00000000..ecc95b22 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ContentType/index.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import FormItem from "@theme/ApiExplorer/FormItem"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; + +import { setContentType } from "./slice"; + +function ContentType() { + const value = useTypedSelector((state: any) => state.contentType.value); + const options = useTypedSelector((state: any) => state.contentType.options); + const dispatch = useTypedDispatch(); + + if (options.length <= 1) { + return null; + } + + return ( + + ) => + dispatch(setContentType(e.target.value)) + } + /> + + ); +} + +export default ContentType; diff --git a/docs/src/theme/ApiExplorer/ContentType/slice.ts b/docs/src/theme/ApiExplorer/ContentType/slice.ts new file mode 100644 index 00000000..b0c3221a --- /dev/null +++ b/docs/src/theme/ApiExplorer/ContentType/slice.ts @@ -0,0 +1,22 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface State { + value: string; + options: string[]; +} + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "contentType", + initialState, + reducers: { + setContentType: (state, action: PayloadAction) => { + state.value = action.payload; + }, + }, +}); + +export const { setContentType } = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/Export/index.tsx b/docs/src/theme/ApiExplorer/Export/index.tsx new file mode 100644 index 00000000..c8a20102 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Export/index.tsx @@ -0,0 +1,40 @@ +import React from "react"; + +import fileSaver from "file-saver"; + +const saveFile = (url: string) => { + let fileName; + if (url.endsWith("json") || url.endsWith("yaml") || url.endsWith("yml")) { + fileName = url.substring(url.lastIndexOf("/") + 1); + } + fileSaver.saveAs(url, fileName ? fileName : "openapi.txt"); +}; + +function Export({ url, proxy }: any) { + return ( + + ); +} + +export default Export; diff --git a/docs/src/theme/ApiExplorer/FloatingButton/_FloatingButton.scss b/docs/src/theme/ApiExplorer/FloatingButton/_FloatingButton.scss new file mode 100644 index 00000000..73c9627c --- /dev/null +++ b/docs/src/theme/ApiExplorer/FloatingButton/_FloatingButton.scss @@ -0,0 +1,27 @@ +.openapi-explorer__floating-btn { + position: relative; + + button { + position: relative; + background: var(--ifm-color-emphasis-900); + border: none; + border-radius: var(--ifm-global-radius); + color: var(--ifm-color-emphasis-100); + cursor: pointer; + padding: 0.4rem 0.5rem; + opacity: 0; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out, + bottom 0.2s ease-in-out; + position: absolute; + right: calc(var(--ifm-pre-padding) / 2); + } +} +.openapi-explorer__floating-btn:hover button, +.openapi-explorer__floating-btn:focus-visible button, +.openapi-explorer__floating-btn button:focus-visible { + visibility: visible; + opacity: 1; +} diff --git a/docs/src/theme/ApiExplorer/FloatingButton/index.tsx b/docs/src/theme/ApiExplorer/FloatingButton/index.tsx new file mode 100644 index 00000000..c1bef2ea --- /dev/null +++ b/docs/src/theme/ApiExplorer/FloatingButton/index.tsx @@ -0,0 +1,22 @@ +import React from "react"; + +export interface Props { + label?: string; + onClick?: React.MouseEventHandler; + children?: React.ReactNode; +} + +function FloatingButton({ label, onClick, children }: Props) { + return ( +
+ {label && ( + + )} + {children} +
+ ); +} + +export default FloatingButton; diff --git a/docs/src/theme/ApiExplorer/FormFileUpload/_FormFileUpload.scss b/docs/src/theme/ApiExplorer/FormFileUpload/_FormFileUpload.scss new file mode 100644 index 00000000..0b0033c3 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormFileUpload/_FormFileUpload.scss @@ -0,0 +1,72 @@ +.openapi-explorer__dropzone { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px dashed var(--openapi-monaco-border-color); + background-color: var(--openapi-input-background); + + width: 100%; + border-radius: 4px; + padding: var(--ifm-pre-padding); + font-size: var(--ifm-code-font-size); + + &:hover { + border: 2px dashed var(--ifm-color-primary); + background: linear-gradient( + var(--openapi-dropzone-hover-shim), + var(--openapi-dropzone-hover-shim) + ), + linear-gradient(var(--ifm-color-primary), var(--ifm-color-primary)); + + .openapi-explorer__dropzone-content { + color: var(--ifm-pre-color); + } + } +} + +.openapi-explorer__dropzone-hover { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px dashed var(--openapi-monaco-border-color); + background-color: var(--openapi-input-background); + width: 100%; + border-radius: 4px; + padding: var(--ifm-pre-padding); + font-size: var(--ifm-code-font-size); + border: 2px dashed var(--ifm-color-primary); + + background: linear-gradient( + var(--openapi-dropzone-hover-shim), + var(--openapi-dropzone-hover-shim) + ), + linear-gradient(var(--ifm-color-primary), var(--ifm-color-primary)); + + .openapi-explorer__dropzone-content { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + margin: var(--ifm-pre-padding) 0; + color: var(--ifm-pre-color); + } + + .openapi-explorer__file-name { + margin: 0 calc(var(--ifm-pre-padding) * 1.5); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + } +} + +.openapi-explorer__dropzone-content { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + margin: var(--ifm-pre-padding) 0; + color: var(--openapi-dropzone-color); +} diff --git a/docs/src/theme/ApiExplorer/FormFileUpload/index.tsx b/docs/src/theme/ApiExplorer/FormFileUpload/index.tsx new file mode 100644 index 00000000..5d5831c4 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormFileUpload/index.tsx @@ -0,0 +1,112 @@ +import React, { useState } from "react"; + +import FloatingButton from "@theme/ApiExplorer/FloatingButton"; +import MagicDropzone from "react-magic-dropzone"; + +type PreviewFile = { preview: string } & File; + +interface RenderPreviewProps { + file: PreviewFile; +} + +function RenderPreview({ file }: RenderPreviewProps) { + switch (file.type) { + case "image/png": + case "image/jpeg": + case "image/jpg": + case "image/svg+xml": + return ( + + ); + default: + return ( +
+ + + + +
{file.name}
+
+ ); + } +} + +export interface Props { + placeholder: string; + onChange?(file?: File): any; +} + +function FormFileUpload({ placeholder, onChange }: Props) { + const [hover, setHover] = useState(false); + const [file, setFile] = useState(); + + function setAndNotifyFile(file?: PreviewFile) { + setFile(file); + onChange?.(file); + } + + function handleDrop(accepted: PreviewFile[]) { + const [file] = accepted; + setAndNotifyFile(file); + setHover(false); + } + + return ( + + setHover(true)} + onDragLeave={() => setHover(false)} + multiple={false} + style={{ marginTop: "calc(var(--ifm-pre-padding) / 2)" }} + > + {file ? ( + <> + + + + ) : ( +
+ {placeholder} +
+ )} +
+
+ ); +} + +export default FormFileUpload; diff --git a/docs/src/theme/ApiExplorer/FormItem/_FormItem.scss b/docs/src/theme/ApiExplorer/FormItem/_FormItem.scss new file mode 100644 index 00000000..25b59053 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormItem/_FormItem.scss @@ -0,0 +1,21 @@ +.openapi-explorer__form-item { + padding: var(--openapi-explorer-padding-input); + font-size: var(--openapi-explorer-font-size-input); + + &:first-child { + margin-top: 0; + } + + .required { + color: var(--openapi-required); + } +} + +.openapi-explorer__form-item-body-container { + padding: 0; +} + +.openapi-explorer__form-item-label { + font-family: var(--ifm-font-family-monospace); + font-weight: bold; +} diff --git a/docs/src/theme/ApiExplorer/FormItem/index.tsx b/docs/src/theme/ApiExplorer/FormItem/index.tsx new file mode 100644 index 00000000..992f6c60 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormItem/index.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +import clsx from "clsx"; + +export interface Props { + label?: string; + type?: string; + required?: boolean | undefined; + children?: React.ReactNode; + className?: string; +} + +function FormItem({ label, type, required, children, className }: Props) { + return ( +
+ {label && ( + + )} + {type && — {type}} + {required && required} +
{children}
+
+ ); +} + +export default FormItem; diff --git a/docs/src/theme/ApiExplorer/FormMultiSelect/_FormMultiSelect.scss b/docs/src/theme/ApiExplorer/FormMultiSelect/_FormMultiSelect.scss new file mode 100644 index 00000000..da855e0e --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormMultiSelect/_FormMultiSelect.scss @@ -0,0 +1,30 @@ +.openapi-explorer__multi-select-input { + width: 100%; + margin-top: calc(var(--ifm-pre-padding) / 2); + padding: 1rem; + border-radius: 4px; + border: 1px solid transparent; + background-color: var(--openapi-input-background); + outline: none; + font-size: var(--openapi-explorer-font-size-input); + color: var(--ifm-pre-color); + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + + &:focus { + border: 1px solid var(--openapi-input-border); + } + + &.error { + border: 1px solid var(--ifm-color-danger); + } + + option { + border-radius: 0.25rem; + color: var(--ifm-menu-color); + margin: 0.25rem 0; + padding: var(--ifm-menu-link-padding-vertical) + var(--ifm-menu-link-padding-horizontal); + } +} diff --git a/docs/src/theme/ApiExplorer/FormMultiSelect/index.tsx b/docs/src/theme/ApiExplorer/FormMultiSelect/index.tsx new file mode 100644 index 00000000..7678ab79 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormMultiSelect/index.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +import clsx from "clsx"; + +export interface Props { + value?: string; + options: string[]; + onChange?: React.ChangeEventHandler; + showErrors?: boolean; +} + +function FormMultiSelect({ value, options, onChange, showErrors }: Props) { + if (options.length === 0) { + return null; + } + + let height; + if (options.length < 6) { + const selectPadding = 12 * 2; + const rawHeight = options.length * 29; + const innerMargins = 4 * options.length - 1; + const outerMargins = 4 * 2; + const mysteryScroll = 1; + height = + rawHeight + innerMargins + outerMargins + selectPadding + mysteryScroll; + } + + return ( + + ); +} + +export default FormMultiSelect; diff --git a/docs/src/theme/ApiExplorer/FormSelect/_FormSelect.scss b/docs/src/theme/ApiExplorer/FormSelect/_FormSelect.scss new file mode 100644 index 00000000..86872613 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormSelect/_FormSelect.scss @@ -0,0 +1,43 @@ +html[data-theme="dark"] .openapi-explorer__select-input { + margin-top: calc(var(--ifm-pre-padding) / 2); + background-color: var(--openapi-input-background); + border: none; + outline: none; + width: 100%; + color: var(--ifm-pre-color); + + border-radius: 4px; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + + background-image: url('data:image/svg+xml;charset=US-ASCII,'); + background-repeat: no-repeat; + background-position: right var(--ifm-pre-padding) top 50%; + background-size: auto auto; +} + +.openapi-explorer__select-input { + width: 100%; + margin-top: calc(var(--ifm-pre-padding) / 2); + padding: var(--openapi-explorer-padding-input); + border: none; + outline: none; + border-radius: 4px; + background-color: var(--openapi-input-background); + font-size: var(--openapi-explorer-font-size-input); + font-family: var(--ifm-font-family-monospace); + color: var(--ifm-pre-color); + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + + background-image: url('data:image/svg+xml;charset=US-ASCII,'); + background-repeat: no-repeat; + background-position: right var(--ifm-pre-padding) top 50%; + background-size: auto auto; + + &:focus { + box-shadow: inset 0px 0px 0px 2px var(--openapi-input-border); + } +} diff --git a/docs/src/theme/ApiExplorer/FormSelect/index.tsx b/docs/src/theme/ApiExplorer/FormSelect/index.tsx new file mode 100644 index 00000000..f1933674 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormSelect/index.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +export interface Props { + value?: string; + options?: string[]; + onChange?: React.ChangeEventHandler; +} + +function FormSelect({ value, options, onChange }: Props) { + if (!Array.isArray(options) || options.length === 0) { + return null; + } + + return ( + + ); +} + +export default FormSelect; diff --git a/docs/src/theme/ApiExplorer/FormTextInput/_FormTextInput.scss b/docs/src/theme/ApiExplorer/FormTextInput/_FormTextInput.scss new file mode 100644 index 00000000..8c02e92a --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormTextInput/_FormTextInput.scss @@ -0,0 +1,34 @@ +.openapi-explorer__form-item-input { + margin-top: calc(var(--ifm-pre-padding) / 2); + background-color: var(--openapi-input-background); + border: 1px solid transparent; + outline: none; + width: 100%; + color: var(--ifm-pre-color); + padding: var(--openapi-explorer-padding-input); + border-radius: 4px; + + &:hover { + border: 1px solid var(--ifm-toc-border-color); + } + + &:focus { + border: 1px solid var(--ifm-color-primary); + box-shadow: none; + } + + &.error { + border: 1px solid var(--openapi-required); + } +} + +.openapi-explorer__input-error { + font-size: var(--openapi-explorer-font-size-input); + color: var(--openapi-required); + padding-top: var(--openapi-explorer-padding-input); + + &::before { + display: inline; + content: "⚠ "; + } +} diff --git a/docs/src/theme/ApiExplorer/FormTextInput/index.tsx b/docs/src/theme/ApiExplorer/FormTextInput/index.tsx new file mode 100644 index 00000000..e70736e0 --- /dev/null +++ b/docs/src/theme/ApiExplorer/FormTextInput/index.tsx @@ -0,0 +1,73 @@ +// @ts-nocheck +import React from "react"; + +import { ErrorMessage } from "@hookform/error-message"; +import clsx from "clsx"; +import { useFormContext } from "react-hook-form"; + +export interface Props { + value?: string; + placeholder?: string; + password?: boolean; + onChange?: React.ChangeEventHandler; +} + +function FormTextInput({ + isRequired, + value, + placeholder, + password, + onChange, + paramName, +}: Props) { + placeholder = placeholder?.split("\n")[0]; + + const { + register, + formState: { errors }, + } = useFormContext(); + + const showErrorMessage = errors?.[paramName]?.message; + + return ( + <> + {paramName ? ( + + ) : ( + + )} + {showErrorMessage && ( + ( +
{message}
+ )} + /> + )} + + ); +} + +export default FormTextInput; diff --git a/docs/src/theme/ApiExplorer/LiveEditor/_LiveEditor.scss b/docs/src/theme/ApiExplorer/LiveEditor/_LiveEditor.scss new file mode 100644 index 00000000..9dd7393c --- /dev/null +++ b/docs/src/theme/ApiExplorer/LiveEditor/_LiveEditor.scss @@ -0,0 +1,15 @@ +.openapi-explorer__playground-container { + margin-top: 1rem; + margin-bottom: var(--ifm-leading); + border-radius: var(--ifm-global-radius); + box-shadow: var(--ifm-global-shadow-lw); + overflow: auto; + max-height: 500px; +} + +.openapi-explorer__playground-editor { + font: var(--ifm-code-font-size) / var(--ifm-pre-line-height) + var(--ifm-font-family-monospace) !important; + /* rtl:ignore */ + direction: ltr; +} diff --git a/docs/src/theme/ApiExplorer/LiveEditor/index.tsx b/docs/src/theme/ApiExplorer/LiveEditor/index.tsx new file mode 100644 index 00000000..c3667566 --- /dev/null +++ b/docs/src/theme/ApiExplorer/LiveEditor/index.tsx @@ -0,0 +1,107 @@ +import React, { type JSX, useEffect, useState } from "react"; + +import { usePrismTheme } from "@docusaurus/theme-common"; +import useIsBrowser from "@docusaurus/useIsBrowser"; +import { ErrorMessage } from "@hookform/error-message"; +import { setStringRawBody } from "@theme/ApiExplorer/Body/slice"; +import clsx from "clsx"; +import { Controller, useFormContext } from "react-hook-form"; +import { LiveProvider, LiveEditor, withLive } from "react-live"; + +function Live({ onEdit, showErrors }: any) { + const isBrowser = useIsBrowser(); + const [editorDisabled, setEditorDisabled] = useState(false); + + return ( +
setEditorDisabled(false)} + onBlur={() => setEditorDisabled(true)} + > + +
+ ); +} + +const LiveComponent = withLive(Live); + +function App({ + children, + transformCode, + value, + language, + action, + required: isRequired, + ...props +}: any): JSX.Element { + const prismTheme = usePrismTheme(); + const [code, setCode] = React.useState(children.replace(/\n$/, "")); + + useEffect(() => { + action(setStringRawBody(code)); + }, [action, code]); + + const { + control, + formState: { errors }, + } = useFormContext(); + + const showErrorMessage = errors?.requestBody; + + const handleChange = (snippet: string, onChange: any) => { + setCode(snippet); + onChange(snippet); + }; + + return ( +
+ `${code};`)} + theme={prismTheme} + language={language} + {...props} + > + ( + handleChange(e, onChange)} + name={name} + showErrors={showErrorMessage} + /> + )} + /> + {showErrorMessage && ( + ( +
{message}
+ )} + /> + )} +
+
+ ); +} + +const LiveApp = withLive(App); +export default LiveApp; diff --git a/docs/src/theme/ApiExplorer/MethodEndpoint/_MethodEndpoint.scss b/docs/src/theme/ApiExplorer/MethodEndpoint/_MethodEndpoint.scss new file mode 100644 index 00000000..9d22e85f --- /dev/null +++ b/docs/src/theme/ApiExplorer/MethodEndpoint/_MethodEndpoint.scss @@ -0,0 +1,22 @@ +.openapi__method-endpoint { + display: flex; + align-items: center; + max-width: 100%; + width: fit-content; + padding: 0.65rem; + border: 1px solid var(--ifm-toc-border-color); +} + +.openapi__method-endpoint-path { + margin-bottom: 0; + margin-left: 0.5rem; + font-size: 12px; + font-weight: normal; + font-family: var(--ifm-font-family-monospace); +} + +.openapi__divider { + width: 100%; + margin: 1.5rem 0; + border-bottom: 1px solid var(--ifm-toc-border-color); +} diff --git a/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx b/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx new file mode 100644 index 00000000..e4098eb5 --- /dev/null +++ b/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx @@ -0,0 +1,84 @@ +import React from "react"; + +import BrowserOnly from "@docusaurus/BrowserOnly"; +import { useTypedSelector } from "@theme/ApiItem/hooks"; + +function colorForMethod(method: string) { + switch (method.toLowerCase()) { + case "get": + return "primary"; + case "post": + return "success"; + case "delete": + return "danger"; + case "put": + return "info"; + case "patch": + return "warning"; + case "head": + return "secondary"; + case "event": + return "secondary"; + default: + return undefined; + } +} + +export interface Props { + method: string; + path: string; + context?: "endpoint" | "callback"; +} + +function MethodEndpoint({ method, path, context }: Props) { + let serverValue = useTypedSelector((state: any) => state.server.value); + let serverUrlWithVariables = ""; + + const renderServerUrl = () => { + if (context === "callback") { + return ""; + } + + if (serverValue && serverValue.variables) { + serverUrlWithVariables = serverValue.url.replace(/\/$/, ""); + + Object.keys(serverValue.variables).forEach((variable) => { + serverUrlWithVariables = serverUrlWithVariables.replace( + `{${variable}}`, + serverValue.variables?.[variable].default ?? "", + ); + }); + } + + return ( + + {() => { + if (serverUrlWithVariables.length) { + return serverUrlWithVariables; + } else if (serverValue && serverValue.url) { + return serverValue.url; + } + }} + + ); + }; + + return ( + <> +
+        
+          {method === "event" ? "Webhook" : method.toUpperCase()}
+        {" "}
+        {method !== "event" && (
+          

+ {renderServerUrl()} + {`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`} +

+ )} +
+
+ + ); +} + +export default MethodEndpoint; diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx new file mode 100644 index 00000000..b6076a4b --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from "react"; + +import { ErrorMessage } from "@hookform/error-message"; +import { nanoid } from "@reduxjs/toolkit"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import FormTextInput from "@theme/ApiExplorer/FormTextInput"; +import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice"; +import { useTypedDispatch } from "@theme/ApiItem/hooks"; +import { Controller, useFormContext } from "react-hook-form"; + +export interface ParamProps { + param: Param; +} + +function ArrayItem({ + param, + onChange, + initialValue, +}: ParamProps & { onChange(value?: string): any; initialValue?: string }) { + const [value, setValue] = useState(initialValue || ""); + + if (param.schema?.items?.type === "boolean") { + return ( + ) => { + const val = e.target.value; + onChange(val === "---" ? undefined : val); + }} + /> + ); + } + + return ( + ) => { + setValue(e.target.value); + onChange(e.target.value); + }} + /> + ); +} + +export default function ParamArrayFormItem({ param }: ParamProps) { + const [items, setItems] = useState<{ id: string; value?: string }[]>([]); + const dispatch = useTypedDispatch(); + + const { + control, + formState: { errors }, + } = useFormContext(); + + const showErrorMessage = errors?.paramArray?.message; + + function handleAddItem(e: any) { + e.preventDefault(); // prevent form from submitting + setItems((i) => [ + ...i, + { + id: nanoid(), + }, + ]); + } + + useEffect(() => { + const values = items + .map((item) => item.value) + .filter((item): item is string => !!item); + + dispatch( + setParam({ + ...param, + value: values.length > 0 ? values : undefined, + }), + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items]); + + useEffect(() => { + if (param.schema?.example?.length > 0) { + const examplesWithIds = param.schema.example.map((item: any) => ({ + id: nanoid(), + value: item.toString(), + })); + + setItems(examplesWithIds); + } + }, [param.schema.example, param.schema.length]); + + function handleDeleteItem(itemToDelete: { id: string }) { + return () => { + const newItems = items.filter((i) => i.id !== itemToDelete.id); + setItems(newItems); + }; + } + + function handleChangeItem(itemToUpdate: { id: string }, onChange: any) { + return (value: string) => { + const newItems = items.map((i) => { + if (i.id === itemToUpdate.id) { + return { ...i, value: value }; + } + return i; + }); + setItems(newItems); + onChange(newItems); + }; + } + + return ( + <> + ( + <> + {items.map((item) => ( +
+ + +
+ ))} + + + )} + /> + {showErrorMessage && ( + ( +
{message}
+ )} + /> + )} + + ); +} diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx new file mode 100644 index 00000000..2fb3c315 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +import { ErrorMessage } from "@hookform/error-message"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice"; +import { useTypedDispatch } from "@theme/ApiItem/hooks"; +import { Controller, useFormContext } from "react-hook-form"; + +export interface ParamProps { + param: Param; +} + +export default function ParamBooleanFormItem({ param }: ParamProps) { + const dispatch = useTypedDispatch(); + + const { + control, + formState: { errors }, + } = useFormContext(); + + const showErrorMessage = errors?.paramBoolean; + + return ( + <> + ( + ) => { + const val = e.target.value; + dispatch( + setParam({ + ...param, + value: val === "---" ? undefined : val, + }), + ); + onChange(val); + }} + /> + )} + /> + {showErrorMessage && ( + ( +
{message}
+ )} + /> + )} + + ); +} diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx new file mode 100644 index 00000000..6c59f98d --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx @@ -0,0 +1,79 @@ +import React from "react"; + +import { ErrorMessage } from "@hookform/error-message"; +import FormMultiSelect from "@theme/ApiExplorer/FormMultiSelect"; +import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; +import { Controller, useFormContext } from "react-hook-form"; + +export interface ParamProps { + param: Param; +} + +export default function ParamMultiSelectFormItem({ param }: ParamProps) { + const { + control, + formState: { errors }, + } = useFormContext(); + + const showErrorMessage = errors?.paramMultiSelect; + + const dispatch = useTypedDispatch(); + + const options = param.schema?.items?.enum ?? []; + + const pathParams = useTypedSelector((state: any) => state.params.path); + const queryParams = useTypedSelector((state: any) => state.params.query); + const cookieParams = useTypedSelector((state: any) => state.params.cookie); + const headerParams = useTypedSelector((state: any) => state.params.header); + + const paramTypeToWatch = pathParams.length + ? pathParams + : queryParams.length + ? queryParams + : cookieParams.length + ? cookieParams + : headerParams; + + const handleChange = (e: any, onChange: any) => { + const values = Array.prototype.filter + .call(e.target.options, (o) => o.selected) + .map((o) => o.value); + + dispatch( + setParam({ + ...param, + value: values.length > 0 ? values : undefined, + }), + ); + + onChange(paramTypeToWatch); + }; + + return ( + <> + ( + handleChange(e, onChange)} + showErrors={showErrorMessage} + /> + )} + /> + {showErrorMessage && ( + ( +
{message}
+ )} + /> + )} + + ); +} diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx new file mode 100644 index 00000000..b86a4634 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx @@ -0,0 +1,58 @@ +import React from "react"; + +import { ErrorMessage } from "@hookform/error-message"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice"; +import { useTypedDispatch } from "@theme/ApiItem/hooks"; +import { Controller, useFormContext } from "react-hook-form"; + +export interface ParamProps { + param: Param; +} + +export default function ParamSelectFormItem({ param }: ParamProps) { + const { + control, + formState: { errors }, + } = useFormContext(); + + const showErrorMessage = errors?.paramSelect; + + const dispatch = useTypedDispatch(); + + const options = param.schema?.enum ?? []; + + return ( + <> + ( + ) => { + const val = e.target.value; + dispatch( + setParam({ + ...param, + value: val === "---" ? undefined : val, + }), + ); + onChange(val); + }} + /> + )} + /> + {showErrorMessage && ( + ( +
{message}
+ )} + /> + )} + + ); +} diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx new file mode 100644 index 00000000..8309b56e --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import FormTextInput from "@theme/ApiExplorer/FormTextInput"; +import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice"; +import { useTypedDispatch } from "@theme/ApiItem/hooks"; + +export interface ParamProps { + param: Param; +} + +export default function ParamTextFormItem({ param }: ParamProps) { + const dispatch = useTypedDispatch(); + return ( + ) => + dispatch( + setParam({ + ...param, + value: + param.in === "path" || param.in === "query" + ? e.target.value.replace(/\s/g, "%20") + : e.target.value, + }), + ) + } + /> + ); +} diff --git a/docs/src/theme/ApiExplorer/ParamOptions/_ParamOptions.scss b/docs/src/theme/ApiExplorer/ParamOptions/_ParamOptions.scss new file mode 100644 index 00000000..e90d5af2 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/_ParamOptions.scss @@ -0,0 +1,123 @@ +.openapi-explorer__plus-btn--expanded { + transition: transform 0.2s ease; + display: inline-block; + transform: rotate(0deg); + transform-origin: center; + margin-right: 6px; + transform: rotate(45deg); +} + +.openapi-explorer__show-more-btn { + width: 100%; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + margin-left: 0.25rem; + margin-bottom: 0.5rem; + padding: 0; + cursor: pointer; + font-size: var(--openapi-explorer-font-size-input); + user-select: none; + white-space: nowrap; + border: 0px solid transparent; + display: block; + background-color: transparent; + color: var(--ifm-color-primary); + text-align: left; + + &:hover { + color: var(--ifm-color-primary-hover); + } + + &:first-child { + margin-top: 0; + } +} + +.openapi-explorer__delete-btn { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + cursor: pointer; + font-size: calc(0.875rem * var(--ifm-button-size-multiplier)); + font-weight: normal; + line-height: 1.5; + + transition-property: color, background, border-color, box-shadow; + transition-duration: 100ms, 100ms, 100ms, + var(--ifm-button-transition-duration); + transition-timing-function: cubic-bezier(0.08, 0.52, 0.52, 1); + + -webkit-user-select: none; + user-select: none; + white-space: nowrap; + + display: flex; + + align-items: center; + justify-content: center; + + padding: 0 12px; + + margin-top: calc(var(--ifm-pre-padding) / 2); + background-color: var(--openapi-input-background); + border: none; + outline: none; + color: var(--ifm-pre-color); + border-radius: 4px; + margin-left: 4px; + + &:focus { + outline: 0; + } + + &:active { + box-shadow: inset 0px 0px 0px 2px var(--openapi-input-border); + } +} + +.openapi-explorer__thin-btn { + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; + font-weight: bold; + padding: 0.5rem 1rem; + font-size: 12px; + transition-property: color, background, border-color, box-shadow; + transition-duration: 100ms, 100ms, 100ms, + var(--ifm-button-transition-duration); + transition-timing-function: cubic-bezier(0.08, 0.52, 0.52, 1); + user-select: none; + white-space: nowrap; + background-color: transparent; + color: var(--openapi-input-border); + border: 1px solid var(--openapi-input-border); + border-radius: var(--ifm-pre-border-radius); + margin-top: calc(var(--ifm-pre-padding) / 2); + text-transform: uppercase; + + &:hover { + color: var(--openapi-inverse-color); + background-color: var(--openapi-input-border); + } + + &:focus { + outline: 0; + } + + &:active { + box-shadow: + inset 0 0 0 1px var(--openapi-input-border), + inset 0 0 0 2px var(--openapi-inverse-color); + } +} + +.openapi-explorer__show-options { + visibility: visible; +} + +.openapi-explorer__hide-options { + display: none; + visibility: hidden; +} diff --git a/docs/src/theme/ApiExplorer/ParamOptions/index.tsx b/docs/src/theme/ApiExplorer/ParamOptions/index.tsx new file mode 100644 index 00000000..d0780769 --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/index.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; + +import FormItem from "@theme/ApiExplorer/FormItem"; +import ParamArrayFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem"; +import ParamBooleanFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem"; +import ParamMultiSelectFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem"; +import ParamSelectFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem"; +import ParamTextFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem"; +import { useTypedSelector } from "@theme/ApiItem/hooks"; + +import { Param } from "./slice"; + +export interface ParamProps { + param: Param; +} + +function ParamOption({ param }: ParamProps) { + if (param.schema?.type === "array" && param.schema.items?.enum) { + return ; + } + + if (param.schema?.type === "array") { + return ; + } + + if (param.schema?.enum) { + return ; + } + + if (param.schema?.type === "boolean") { + return ; + } + + // integer, number, string, int32, int64, float, double, object, byte, binary, + // date-time, date, password + return ; +} + +function ParamOptionWrapper({ param }: ParamProps) { + return ( + + + + ); +} + +function ParamOptions() { + const [showOptional, setShowOptional] = useState(false); + + const pathParams = useTypedSelector((state: any) => state.params.path); + const queryParams = useTypedSelector((state: any) => state.params.query); + const cookieParams = useTypedSelector((state: any) => state.params.cookie); + const headerParams = useTypedSelector((state: any) => state.params.header); + + const allParams = [ + ...pathParams, + ...queryParams, + ...cookieParams, + ...headerParams, + ]; + + const requiredParams = allParams.filter((p) => p.required); + const optionalParams = allParams.filter((p) => !p.required); + + return ( + <> + {/* Required Parameters */} + {requiredParams.map((param) => ( + + ))} + + {/* Optional Parameters */} + {optionalParams.length > 0 && ( + <> + + +
+ {optionalParams.map((param) => ( + + ))} +
+ + )} + + ); +} + +export default ParamOptions; diff --git a/docs/src/theme/ApiExplorer/ParamOptions/slice.ts b/docs/src/theme/ApiExplorer/ParamOptions/slice.ts new file mode 100644 index 00000000..52c8a72e --- /dev/null +++ b/docs/src/theme/ApiExplorer/ParamOptions/slice.ts @@ -0,0 +1,30 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { ParameterObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; + +export type Param = ParameterObject & { value?: string[] | string }; + +export interface State { + path: Param[]; + query: Param[]; + header: Param[]; + cookie: Param[]; +} + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "params", + initialState, + reducers: { + setParam: (state, action: PayloadAction) => { + const newParam = action.payload; + const paramGroup = state[action.payload.in]; + const index = paramGroup.findIndex((p) => p.name === newParam.name); + paramGroup[index] = newParam; + }, + }, +}); + +export const { setParam } = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/Request/_Request.scss b/docs/src/theme/ApiExplorer/Request/_Request.scss new file mode 100644 index 00000000..8b1d135d --- /dev/null +++ b/docs/src/theme/ApiExplorer/Request/_Request.scss @@ -0,0 +1,129 @@ +.openapi-explorer__request-form { + background-color: var(--ifm-pre-background); + border-radius: var(--openapi-card-border-radius); + border: 1px solid var(--openapi-explorer-border-color); + box-shadow: + 0 2px 3px hsla(222, 8%, 43%, 0.1), + 0 8px 16px -10px hsla(222, 8%, 43%, 0.2); + color: var(--ifm-pre-color); + line-height: var(--ifm-pre-line-height); + margin-bottom: var(--ifm-spacing-vertical); + margin-top: 0; + overflow: auto; + transition: 300ms; + + /* hack for view calculation when monaco is hidden */ + position: relative; + + &:empty { + display: none; + } + + &:hover { + box-shadow: + 0 0 0 2px rgba(38, 53, 61, 0.15), + 0 2px 3px hsla(222, 8%, 43%, 0.15), + 0 16px 16px -10px hsla(222, 8%, 43%, 0.2); + } + + .required { + font-size: var(--ifm-code-font-size); + color: var(--openapi-required); + + &.request-body { + padding-left: 0.25rem; + } + } +} + +.openapi-explorer__request-header-container { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--openapi-explorer-border-color); + margin: 0; + padding: 0.75rem var(--ifm-pre-padding); + text-transform: uppercase; + font-size: 12px; + font-weight: bold; +} + +.openapi-explorer__expand-details-btn { + &:hover { + cursor: pointer; + } +} + +.openapi-explorer__details-outer-container { + padding: 1rem; +} + +.openapi-explorer__details-container[open] { + .openapi-explorer__details-summary::before { + transform: rotate(180deg); + margin-top: 0.25rem; + } +} + +.openapi-explorer__details-summary { + display: inline-flex; + align-items: center; + padding: 0.35rem 0; + font-size: 14px; + list-style: none; + + &:hover { + cursor: pointer; + } + + &::-webkit-details-marker { + display: none; + } + + &::before { + margin-right: 0.25rem; + margin-bottom: 0.25rem; + margin-top: 0.25rem; + background-image: var(--openapi-explorer-caret-bg); + border: none !important; + transform: rotate(90deg); + content: ""; + height: 1rem; + width: 1rem; + } +} + +.openapi-explorer__request-btn { + border: none; + border-radius: var(--ifm-global-radius); + padding: 0.5rem 1rem; + margin-top: 1rem; + background-color: var(--ifm-color-primary-light); + text-transform: uppercase; + font-weight: bold; + font-size: 12px; + color: white; + cursor: pointer; + transition: 300ms; + + &:hover { + background-color: var(--ifm-color-primary-lightest); + } + + &:active { + background-color: var(--ifm-color-primary-light); + } +} + +.openapi-security__summary-container { + background: var(--ifm-pre-background); + border-radius: var(--ifm-pre-border-radius); +} + +// Prevent auto zoom on mobile iOS devices when focusing on input elmenents +@media screen and (-webkit-min-device-pixel-ratio: 0) and (max-device-width: 1024px) { + .prism-code, + select, + input { + font-size: 1rem; + } +} diff --git a/docs/src/theme/ApiExplorer/Request/index.tsx b/docs/src/theme/ApiExplorer/Request/index.tsx new file mode 100644 index 00000000..1e971325 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Request/index.tsx @@ -0,0 +1,279 @@ +// @ts-nocheck +import React, { useState } from "react"; + +import { useDoc } from "@docusaurus/plugin-content-docs/client"; +import Accept from "@theme/ApiExplorer/Accept"; +import Authorization from "@theme/ApiExplorer/Authorization"; +import Body from "@theme/ApiExplorer/Body"; +import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest"; +import ContentType from "@theme/ApiExplorer/ContentType"; +import ParamOptions from "@theme/ApiExplorer/ParamOptions"; +import { + setResponse, + setCode, + clearCode, + setHeaders, + clearHeaders, +} from "@theme/ApiExplorer/Response/slice"; +import Server from "@theme/ApiExplorer/Server"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; +import { ParameterObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; +import { ApiItem } from "docusaurus-plugin-openapi-docs/src/types"; +import sdk from "postman-collection"; +import { FormProvider, useForm } from "react-hook-form"; + +import makeRequest from "./makeRequest"; + +function Request({ item }: { item: ApiItem }) { + const postman = new sdk.Request(item.postman); + const metadata = useDoc(); + const { proxy, hide_send_button: hideSendButton } = metadata.frontMatter; + + const pathParams = useTypedSelector((state: any) => state.params.path); + const queryParams = useTypedSelector((state: any) => state.params.query); + const cookieParams = useTypedSelector((state: any) => state.params.cookie); + const contentType = useTypedSelector((state: any) => state.contentType.value); + const headerParams = useTypedSelector((state: any) => state.params.header); + const body = useTypedSelector((state: any) => state.body); + const accept = useTypedSelector((state: any) => state.accept.value); + const acceptOptions = useTypedDispatch((state: any) => state.accept.options); + const authSelected = useTypedSelector((state: any) => state.auth.selected); + const server = useTypedSelector((state: any) => state.server.value); + const serverOptions = useTypedSelector((state: any) => state.server.options); + const auth = useTypedSelector((state: any) => state.auth); + const dispatch = useTypedDispatch(); + + const [expandAccept, setExpandAccept] = useState(true); + const [expandAuth, setExpandAuth] = useState(true); + const [expandBody, setExpandBody] = useState(true); + const [expandParams, setExpandParams] = useState(true); + const [expandServer, setExpandServer] = useState(true); + + const allParams = [ + ...pathParams, + ...queryParams, + ...cookieParams, + ...headerParams, + ]; + + const postmanRequest = buildPostmanRequest(postman, { + queryParams, + pathParams, + cookieParams, + contentType, + accept, + headerParams, + body, + server, + auth, + }); + + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + const paramsObject = { + path: [] as ParameterObject[], + query: [] as ParameterObject[], + header: [] as ParameterObject[], + cookie: [] as ParameterObject[], + }; + + item.parameters?.forEach( + (param: { in: "path" | "query" | "header" | "cookie" }) => { + const paramType = param.in; + const paramsArray: ParameterObject[] = paramsObject[paramType]; + paramsArray.push(param as ParameterObject); + }, + ); + + const methods = useForm({ shouldFocusError: false }); + + const onSubmit = async (data) => { + dispatch(setResponse("Fetching...")); + try { + await delay(1200); + const res = await makeRequest(postmanRequest, proxy, body); + dispatch(setResponse(await res.text())); + dispatch(setCode(res.status)); + res.headers && dispatch(setHeaders(Object.fromEntries(res.headers))); + } catch (e: any) { + console.log(e); + dispatch(setResponse("Connection failed")); + dispatch(clearCode()); + dispatch(clearHeaders()); + } + }; + + const showServerOptions = serverOptions.length > 0; + const showAcceptOptions = acceptOptions.length > 1; + const showRequestBody = contentType !== undefined; + const showRequestButton = item.servers && !hideSendButton; + const showAuth = authSelected !== undefined; + const showParams = allParams.length > 0; + const requestBodyRequired = item.requestBody?.required; + + if ( + !showAcceptOptions && + !showAuth && + !showParams && + !showRequestBody && + !showServerOptions + ) { + return null; + } + + const expandAllDetails = () => { + setExpandAccept(true); + setExpandAuth(true); + setExpandBody(true); + setExpandParams(true); + setExpandServer(true); + }; + + const collapseAllDetails = () => { + setExpandAccept(false); + setExpandAuth(false); + setExpandBody(false); + setExpandParams(false); + setExpandServer(false); + }; + + const allDetailsExpanded = + expandParams && expandBody && expandServer && expandAuth && expandAccept; + + return ( + +
+
+ Request + {allDetailsExpanded ? ( + + Collapse all + + ) : ( + + Expand all + + )} +
+
+ {showServerOptions && item.method !== "event" && ( +
+ { + e.preventDefault(); + setExpandServer(!expandServer); + }} + > + Base URL + + +
+ )} + {showAuth && ( +
+ { + e.preventDefault(); + setExpandAuth(!expandAuth); + }} + > + Auth + + +
+ )} + {showParams && ( +
+ { + e.preventDefault(); + setExpandParams(!expandParams); + }} + > + Parameters + + +
+ )} + {showRequestBody && ( +
+ { + e.preventDefault(); + setExpandBody(!expandBody); + }} + > + Body + {requestBodyRequired && ( + +  required + + )} + + <> + + + +
+ )} + {showAcceptOptions && ( +
+ { + e.preventDefault(); + setExpandAccept(!expandAccept); + }} + > + Accept + + +
+ )} + {showRequestButton && item.method !== "event" && ( + + )} +
+
+
+ ); +} + +export default Request; diff --git a/docs/src/theme/ApiExplorer/Request/makeRequest.ts b/docs/src/theme/ApiExplorer/Request/makeRequest.ts new file mode 100644 index 00000000..fec289e3 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Request/makeRequest.ts @@ -0,0 +1,247 @@ +import { Body } from "@theme/ApiExplorer/Body/slice"; +import sdk from "postman-collection"; + +function fetchWithtimeout( + url: string, + options: RequestInit, + timeout = 5000, +): any { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out")), timeout), + ), + ]); +} + +async function loadImage(content: Blob): Promise { + return new Promise((accept, reject) => { + const reader = new FileReader(); + + reader.onabort = () => { + console.log("file reading was aborted"); + reject(); + }; + + reader.onerror = () => { + console.log("file reading has failed"); + reject(); + }; + + reader.onload = () => { + // Do whatever you want with the file contents + const binaryStr = reader.result; + accept(binaryStr); + }; + reader.readAsArrayBuffer(content); + }); +} + +async function makeRequest( + request: sdk.Request, + proxy: string | undefined, + _body: Body, +) { + const headers = request.toJSON().header; + + let myHeaders = new Headers(); + if (headers) { + headers.forEach((header: any) => { + if (header.key && header.value) { + myHeaders.append(header.key, header.value); + } + }); + } + + // The following code handles multiple files in the same formdata param. + // It removes the form data params where the src property is an array of filepath strings + // Splits that array into different form data params with src set as a single filepath string + // TODO: + // if (request.body && request.body.mode === 'formdata') { + // let formdata = request.body.formdata, + // formdataArray = []; + // formdata.members.forEach((param) => { + // let key = param.key, + // type = param.type, + // disabled = param.disabled, + // contentType = param.contentType; + // // check if type is file or text + // if (type === 'file') { + // // if src is not of type string we check for array(multiple files) + // if (typeof param.src !== 'string') { + // // if src is an array(not empty), iterate over it and add files as separate form fields + // if (Array.isArray(param.src) && param.src.length) { + // param.src.forEach((filePath) => { + // addFormParam( + // formdataArray, + // key, + // param.type, + // filePath, + // disabled, + // contentType + // ); + // }); + // } + // // if src is not an array or string, or is an empty array, add a placeholder for file path(no files case) + // else { + // addFormParam( + // formdataArray, + // key, + // param.type, + // '/path/to/file', + // disabled, + // contentType + // ); + // } + // } + // // if src is string, directly add the param with src as filepath + // else { + // addFormParam( + // formdataArray, + // key, + // param.type, + // param.src, + // disabled, + // contentType + // ); + // } + // } + // // if type is text, directly add it to formdata array + // else { + // addFormParam( + // formdataArray, + // key, + // param.type, + // param.value, + // disabled, + // contentType + // ); + // } + // }); + // request.body.update({ + // mode: 'formdata', + // formdata: formdataArray, + // }); + // } + + const body = request.body?.toJSON(); + + let myBody: RequestInit["body"] = undefined; + if (body !== undefined && Object.keys(body).length > 0) { + switch (body.mode) { + case "urlencoded": { + myBody = new URLSearchParams(); + if (Array.isArray(body.urlencoded)) { + for (const data of body.urlencoded) { + if (data.key && data.value) { + myBody.append(data.key, data.value); + } + } + } + break; + } + case "raw": { + myBody = (body.raw ?? "").toString(); + break; + } + case "formdata": { + // The Content-Type header will be set automatically based on the type of body. + myHeaders.delete("Content-Type"); + + myBody = new FormData(); + if (Array.isArray(request.body.formdata.members)) { + for (const data of request.body.formdata.members) { + if (data.key && data.value.content) { + myBody.append(data.key, data.value.content); + } + // handle generic key-value payload + if (data.key && typeof data.value === "string") { + myBody.append(data.key, data.value); + } + } + } + break; + } + case "file": { + if (_body.type === "raw" && _body.content?.type === "file") { + myBody = await loadImage(_body.content.value.content); + } + break; + } + default: + break; + } + } + + const requestOptions: RequestInit = { + method: request.method, + headers: myHeaders, + body: myBody, + }; + + let finalUrl = request.url.toString(); + if (proxy) { + // Ensure the proxy ends with a slash. + let normalizedProxy = proxy.replace(/\/$/, "") + "/"; + finalUrl = normalizedProxy + request.url.toString(); + } + + return fetchWithtimeout(finalUrl, requestOptions).then((response: any) => { + const contentType = response.headers.get("content-type"); + let fileExtension = ""; + + if (contentType) { + if (contentType.includes("application/pdf")) { + fileExtension = ".pdf"; + } else if (contentType.includes("image/jpeg")) { + fileExtension = ".jpg"; + } else if (contentType.includes("image/png")) { + fileExtension = ".png"; + } else if (contentType.includes("image/gif")) { + fileExtension = ".gif"; + } else if (contentType.includes("image/webp")) { + fileExtension = ".webp"; + } else if (contentType.includes("video/mpeg")) { + fileExtension = ".mpeg"; + } else if (contentType.includes("video/mp4")) { + fileExtension = ".mp4"; + } else if (contentType.includes("audio/mpeg")) { + fileExtension = ".mp3"; + } else if (contentType.includes("audio/ogg")) { + fileExtension = ".ogg"; + } else if (contentType.includes("application/octet-stream")) { + fileExtension = ".bin"; + } else if (contentType.includes("application/zip")) { + fileExtension = ".zip"; + } + + if (fileExtension) { + return response.blob().then((blob: any) => { + const url = window.URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + // Now the file name includes the extension + link.setAttribute("download", `file${fileExtension}`); + + // These two lines are necessary to make the link click in Firefox + link.style.display = "none"; + document.body.appendChild(link); + + link.click(); + + // After link is clicked, it's safe to remove it. + setTimeout(() => document.body.removeChild(link), 0); + + return response; + }); + } else { + return response; + } + } + + return response; + }); +} + +export default makeRequest; diff --git a/docs/src/theme/ApiExplorer/Response/_Response.scss b/docs/src/theme/ApiExplorer/Response/_Response.scss new file mode 100644 index 00000000..70b3ec01 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Response/_Response.scss @@ -0,0 +1,120 @@ +.openapi-explorer__response-container { + background-color: var(--ifm-pre-background); + border-radius: var(--openapi-card-border-radius); + border: 1px solid var(--openapi-explorer-border-color); + box-shadow: + 0 2px 3px hsla(222, 8%, 43%, 0.1), + 0 8px 16px -10px hsla(222, 8%, 43%, 0.2); + color: var(--ifm-pre-color); + line-height: var(--ifm-pre-line-height); + margin-bottom: var(--ifm-spacing-vertical); + margin-top: 0; + overflow: auto; + transition: 300ms; + + &:hover { + box-shadow: + 0 0 0 2px rgba(38, 53, 61, 0.15), + 0 2px 3px hsla(222, 8%, 43%, 0.15), + 0 16px 16px -10px hsla(222, 8%, 43%, 0.2); + } + + .openapi-explorer__code-block code { + padding-top: 0; + } +} + +.openapi-explorer__response-title-container { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--openapi-explorer-border-color); + margin: 0; + padding: 0.75rem var(--ifm-pre-padding); + text-transform: uppercase; + font-size: 12px; + font-weight: bold; +} + +.openapi-explorer__response-placeholder-message { + font-size: 12px; + padding: 1.25rem; + margin-bottom: 0; + text-align: center; +} + +.openapi-explorer__response-clear-btn { + &:hover { + cursor: pointer; + } +} + +.openapi-explorer__loading-container { + width: 100%; + display: flex; + justify-content: center; +} + +.openapi-response__dot::before { + margin-right: 0.2rem; + margin-bottom: 0.15rem; + content: "⬤"; + color: var(--ifm-color-primary); + font-size: 8px; +} + +.openapi-response__dot--danger::before { + color: var(--ifm-color-danger); +} + +.openapi-response__dot--success::before { + color: var(--ifm-color-success); +} + +.openapi-response__dot--info::before { + color: var(--ifm-color-info); +} + +.openapi-response__status-code { + margin-left: -1rem; +} + +.openapi-response__status-headers { + margin-left: -1rem; +} + +.openapi-response__lds-ring { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.openapi-response__lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 64px; + height: 64px; + margin: 8px; + border: 5px solid #dfc; + border-radius: 50%; + animation: openapi-response__lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) + infinite; + border-color: var(--ifm-color-primary) transparent transparent transparent; +} +.openapi-response__lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.openapi-response__lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.openapi-response__lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes openapi-response__lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/docs/src/theme/ApiExplorer/Response/index.tsx b/docs/src/theme/ApiExplorer/Response/index.tsx new file mode 100644 index 00000000..cf031c83 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Response/index.tsx @@ -0,0 +1,150 @@ +import React from "react"; + +import { useDoc } from "@docusaurus/plugin-content-docs/client"; +import { usePrismTheme } from "@docusaurus/theme-common"; +import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; +import SchemaTabs from "@theme/SchemaTabs"; +import TabItem from "@theme/TabItem"; +import clsx from "clsx"; +import { ApiItem } from "docusaurus-plugin-openapi-docs/src/types"; + +import { clearResponse, clearCode, clearHeaders } from "./slice"; + +// TODO: We probably shouldn't attempt to format XML... +function formatXml(xml: string) { + const tab = " "; + let formatted = ""; + let indent = ""; + + xml.split(/>\s* { + if (node.match(/^\/\w/)) { + // decrease indent by one 'tab' + indent = indent.substring(tab.length); + } + formatted += indent + "<" + node + ">\r\n"; + if (node.match(/^]*[^/]$/)) { + // increase indent + indent += tab; + } + }); + return formatted.substring(1, formatted.length - 3); +} + +function Response({ item }: { item: ApiItem }) { + const metadata = useDoc(); + const hideSendButton = metadata.frontMatter.hide_send_button; + const prismTheme = usePrismTheme(); + const code = useTypedSelector((state: any) => state.response.code); + const headers = useTypedSelector((state: any) => state.response.headers); + const response = useTypedSelector((state: any) => state.response.value); + const dispatch = useTypedDispatch(); + const responseStatusClass = + code && + "openapi-response__dot " + + (parseInt(code) >= 400 + ? "openapi-response__dot--danger" + : parseInt(code) >= 200 && parseInt(code) < 300 + ? "openapi-response__dot--success" + : "openapi-response__dot--info"); + + if (!item.servers || hideSendButton) { + return null; + } + + let prettyResponse: string = response; + + if (prettyResponse) { + try { + prettyResponse = JSON.stringify(JSON.parse(response), null, 2); + } catch { + if (response.startsWith("<")) { + prettyResponse = formatXml(response); + } + } + } + + return ( +
+
+ Response + { + dispatch(clearResponse()); + dispatch(clearCode()); + dispatch(clearHeaders()); + }} + > + Clear + +
+
+ {code && prettyResponse !== "Fetching..." ? ( + + {/* @ts-ignore */} + + {/* @ts-ignore */} + + {prettyResponse || ( +

+ Click the Send API Request button above and see + the response here! +

+ )} +
+
+ {/* @ts-ignore */} + + {/* @ts-ignore */} + + {JSON.stringify(headers, undefined, 2)} + + +
+ ) : prettyResponse === "Fetching..." ? ( +
+
+
+
+
+
+
+
+ ) : ( +

+ Click the Send API Request button above and see the + response here! +

+ )} +
+
+ ); +} + +export default Response; diff --git a/docs/src/theme/ApiExplorer/Response/slice.ts b/docs/src/theme/ApiExplorer/Response/slice.ts new file mode 100644 index 00000000..d58f0b05 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Response/slice.ts @@ -0,0 +1,45 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface State { + value?: string; + code?: string; + headers?: string; +} + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "response", + initialState, + reducers: { + setResponse: (state, action: PayloadAction) => { + state.value = action.payload; + }, + setCode: (state, action: PayloadAction) => { + state.code = action.payload; + }, + setHeaders: (state, action: PayloadAction) => { + state.headers = action.payload; + }, + clearResponse: (state) => { + state.value = undefined; + }, + clearCode: (state) => { + state.code = undefined; + }, + clearHeaders: (state) => { + state.headers = undefined; + }, + }, +}); + +export const { + setResponse, + clearResponse, + setCode, + clearCode, + setHeaders, + clearHeaders, +} = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/SecuritySchemes/index.tsx b/docs/src/theme/ApiExplorer/SecuritySchemes/index.tsx new file mode 100644 index 00000000..2e03a0c6 --- /dev/null +++ b/docs/src/theme/ApiExplorer/SecuritySchemes/index.tsx @@ -0,0 +1,280 @@ +import React from "react"; + +import Link from "@docusaurus/Link"; +import { useTypedSelector } from "@theme/ApiItem/hooks"; + +function SecuritySchemes(props: any) { + const options = useTypedSelector((state: any) => state.auth.options); + const selected = useTypedSelector((state: any) => state.auth.selected); + const infoAuthPath = `/${props.infoPath}#authentication`; + + if (selected === undefined) return null; + + if (options[selected]?.[0]?.type === undefined) { + return null; + } + + const selectedAuth = options[selected]; + return ( +
+ +

+ Authorization: {selectedAuth[0].name ?? selectedAuth[0].type} +

+
+ {selectedAuth.map((auth: any) => { + const isHttp = auth.type === "http"; + const isApiKey = auth.type === "apiKey"; + const isOauth2 = auth.type === "oauth2"; + const isOpenId = auth.type === "openIdConnect"; + + if (isHttp) { + if (auth.scheme === "bearer") { + const { name, key, type, scopes, ...rest } = auth; + return ( + +
+                  
+                    name:{" "}
+                    {name ?? key}
+                  
+                  
+                    type: 
+                    {type}
+                  
+                  {scopes && scopes.length > 0 && (
+                    
+                      scopes: 
+                      
+                        {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+                      
+                    
+                  )}
+                  {Object.keys(rest).map((k, i) => {
+                    return (
+                      
+                        {k}: 
+                        {typeof rest[k] === "object"
+                          ? JSON.stringify(rest[k], null, 2)
+                          : String(rest[k])}
+                      
+                    );
+                  })}
+                
+
+ ); + } + if (auth.scheme === "basic") { + const { name, key, type, scopes, ...rest } = auth; + return ( + +
+                  
+                    name:{" "}
+                    {name ?? key}
+                  
+                  
+                    type: 
+                    {type}
+                  
+                  {scopes && scopes.length > 0 && (
+                    
+                      scopes: 
+                      
+                        {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+                      
+                    
+                  )}
+                  {Object.keys(rest).map((k, i) => {
+                    return (
+                      
+                        {k}: 
+                        {typeof rest[k] === "object"
+                          ? JSON.stringify(rest[k], null, 2)
+                          : String(rest[k])}
+                      
+                    );
+                  })}
+                
+
+ ); + } + return ( + +
+                
+                  name:{" "}
+                  {auth.name ?? auth.key}
+                
+                
+                  type: 
+                  {auth.type}
+                
+                
+                  in: 
+                  {auth.in}
+                
+              
+
+ ); + } + + if (isApiKey) { + const { name, key, type, scopes, ...rest } = auth; + return ( + +
+                
+                  name:{" "}
+                  {name ?? key}
+                
+                
+                  type: 
+                  {type}
+                
+                {scopes && scopes.length > 0 && (
+                  
+                    scopes: 
+                    
+                      {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+                    
+                  
+                )}
+                {Object.keys(rest).map((k, i) => {
+                  return (
+                    
+                      {k}: 
+                      {typeof rest[k] === "object"
+                        ? JSON.stringify(rest[k], null, 2)
+                        : String(rest[k])}
+                    
+                  );
+                })}
+              
+
+ ); + } + + if (isOauth2) { + const { name, key, type, scopes, flows, ...rest } = auth; + return ( + +
+                
+                  name:{" "}
+                  {name ?? key}
+                
+                
+                  type: 
+                  {type}
+                
+                {scopes && scopes.length > 0 && (
+                  
+                    scopes: 
+                    
+                      {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+                    
+                  
+                )}
+                {Object.keys(rest).map((k, i) => {
+                  return (
+                    
+                      {k}: 
+                      {typeof rest[k] === "object"
+                        ? JSON.stringify(rest[k], null, 2)
+                        : String(rest[k])}
+                    
+                  );
+                })}
+                {flows && (
+                  
+                    
+                      flows: 
+                      {JSON.stringify(flows, null, 2)}
+                    
+                  
+                )}
+              
+
+ ); + } + + if (isOpenId) { + const { name, key, scopes, type, ...rest } = auth; + return ( + +
+                
+                  name:{" "}
+                  {name ?? key}
+                
+                
+                  type: 
+                  {type}
+                
+                {scopes && scopes.length > 0 && (
+                  
+                    scopes: 
+                    
+                      {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+                    
+                  
+                )}
+                {Object.keys(rest).map((k, i) => {
+                  return (
+                    
+                      {k}: 
+                      {typeof rest[k] === "object"
+                        ? JSON.stringify(rest[k], null, 2)
+                        : String(rest[k])}
+                    
+                  );
+                })}
+              
+
+ ); + } + + return undefined; + })} +
+ ); +} + +export default SecuritySchemes; diff --git a/docs/src/theme/ApiExplorer/Server/_Server.scss b/docs/src/theme/ApiExplorer/Server/_Server.scss new file mode 100644 index 00000000..c6bbb7d7 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Server/_Server.scss @@ -0,0 +1,26 @@ +.openapi-explorer__server-container { + background: var(--openapi-card-background-color); + border-radius: var(--openapi-card-border-radius); + color: var(--ifm-pre-color); + line-height: var(--ifm-pre-line-height); + margin-bottom: var(--ifm-spacing-vertical); + margin-top: 0; + overflow: auto; + + /* hack for view calculation when monaco is hidden */ + position: relative; + + &:empty { + display: none; + } +} + +.openapi-explorer__server-url { + font-size: var(--openapi-explorer-font-size-input); + font-family: var(--ifm-font-family-monospace); +} + +.openapi-explorer__server-description { + padding-left: 0.5rem; + font-weight: var(--ifm-font-weight-bold); +} diff --git a/docs/src/theme/ApiExplorer/Server/index.tsx b/docs/src/theme/ApiExplorer/Server/index.tsx new file mode 100644 index 00000000..bd0140e7 --- /dev/null +++ b/docs/src/theme/ApiExplorer/Server/index.tsx @@ -0,0 +1,123 @@ +import React, { useState } from "react"; + +import FloatingButton from "@theme/ApiExplorer/FloatingButton"; +import FormItem from "@theme/ApiExplorer/FormItem"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import FormTextInput from "@theme/ApiExplorer/FormTextInput"; +import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; + +import { setServer, setServerVariable } from "./slice"; + +function Server() { + const [isEditing, setIsEditing] = useState(false); + const value = useTypedSelector((state: any) => state.server.value); + const options = useTypedSelector((state: any) => state.server.options); + const dispatch = useTypedDispatch(); + + if (options.length <= 0) { + return null; + } + + if (options.length < 1 && value?.variables === undefined) { + return null; + } + + if (!value) { + const defaultOption = options[0]; + dispatch(setServer(JSON.stringify(defaultOption))); + } + + // Default to first option when existing server state is mismatched + if (value) { + const urlExists = options.find((s: any) => s.url === value.url); + if (!urlExists) { + const defaultOption = options[0]; + dispatch(setServer(JSON.stringify(defaultOption))); + } + } + + if (!isEditing) { + let url = ""; + if (value) { + url = value.url.replace(/\/$/, ""); + if (value.variables) { + Object.keys(value.variables).forEach((variable) => { + url = url.replace( + `{${variable}}`, + value.variables?.[variable].default ?? "", + ); + }); + } + } + return ( + setIsEditing(true)} label="Edit"> + + + {url} + + + + ); + } + return ( +
+ setIsEditing(false)} label="Hide"> + + s.url)} + onChange={(e: React.ChangeEvent) => { + dispatch( + setServer( + JSON.stringify( + options.filter((s: any) => s.url === e.target.value)[0], + ), + ), + ); + }} + value={value?.url} + /> + + {value?.description} + + + {value?.variables && + Object.keys(value.variables).map((key) => { + if (value.variables?.[key].enum !== undefined) { + return ( + + ) => { + dispatch( + setServerVariable( + JSON.stringify({ key, value: e.target.value }), + ), + ); + }} + value={value?.variables[key].default} + /> + + ); + } + return ( + + ) => { + dispatch( + setServerVariable( + JSON.stringify({ key, value: e.target.value }), + ), + ); + }} + value={value?.variables?.[key].default} + /> + + ); + })} + +
+ ); +} + +export default Server; diff --git a/docs/src/theme/ApiExplorer/Server/slice.ts b/docs/src/theme/ApiExplorer/Server/slice.ts new file mode 100644 index 00000000..ffb2b4fc --- /dev/null +++ b/docs/src/theme/ApiExplorer/Server/slice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { ServerObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; +// TODO: we might want to export this + +export interface State { + value?: ServerObject; + options: ServerObject[]; +} + +const initialState: State = {} as any; + +export const slice = createSlice({ + name: "server", + initialState, + reducers: { + setServer: (state, action: PayloadAction) => { + state.value = state.options.find( + (s) => s.url === JSON.parse(action.payload).url, + ); + }, + setServerVariable: (state, action: PayloadAction) => { + if (state.value?.variables) { + const parsedPayload = JSON.parse(action.payload); + state.value.variables[parsedPayload.key].default = parsedPayload.value; + } + }, + }, +}); + +export const { setServer, setServerVariable } = slice.actions; + +export default slice.reducer; diff --git a/docs/src/theme/ApiExplorer/buildPostmanRequest.ts b/docs/src/theme/ApiExplorer/buildPostmanRequest.ts new file mode 100644 index 00000000..f26ae7f2 --- /dev/null +++ b/docs/src/theme/ApiExplorer/buildPostmanRequest.ts @@ -0,0 +1,525 @@ +import { AuthState, Scheme } from "@theme/ApiExplorer/Authorization/slice"; +import { Body, Content } from "@theme/ApiExplorer/Body/slice"; +import { + ParameterObject, + ServerObject, +} from "docusaurus-plugin-openapi-docs/src/openapi/types"; +import cloneDeep from "lodash/cloneDeep"; +import sdk from "postman-collection"; + +type Param = { + value?: string | string[]; +} & ParameterObject; + +function setQueryParams(postman: sdk.Request, queryParams: Param[]) { + postman.url.query.clear(); + + const qp = queryParams + .map((param) => { + if (!param.value) { + return undefined; + } + + // Handle array values + if (Array.isArray(param.value)) { + if (param.style === "spaceDelimited") { + return new sdk.QueryParam({ + key: param.name, + value: param.value.join(" "), + }); + } else if (param.style === "pipeDelimited") { + return new sdk.QueryParam({ + key: param.name, + value: param.value.join("|"), + }); + } else if (param.explode) { + return param.value.map( + (val) => + new sdk.QueryParam({ + key: param.name, + value: val, + }), + ); + } else { + return new sdk.QueryParam({ + key: param.name, + value: param.value.join(","), + }); + } + } + + const decodedValue = decodeURI(param.value); + const tryJson = () => { + try { + return JSON.parse(decodedValue); + } catch (e) { + return false; + } + }; + + const jsonResult = tryJson(); + + // Handle object values + if (jsonResult && typeof jsonResult === "object") { + if (param.style === "deepObject") { + return Object.entries(jsonResult).map( + ([key, val]) => + new sdk.QueryParam({ + key: `${param.name}[${key}]`, + value: val, + }), + ); + } else if (param.explode) { + return Object.entries(jsonResult).map( + ([key, val]) => + new sdk.QueryParam({ + key: key, + value: val, + }), + ); + } else { + return new sdk.QueryParam({ + key: param.name, + value: Object.entries(jsonResult) + .map(([key, val]) => `${key},${val}`) + .join(","), + }); + } + } + + // Handle boolean values + if (typeof decodedValue === "boolean") { + return new sdk.QueryParam({ + key: param.name, + value: decodedValue ? "true" : "false", + }); + } + + // Parameter allows empty value: "/hello?extended" + if (param.allowEmptyValue) { + if (decodedValue === "true") { + return new sdk.QueryParam({ + key: param.name, + value: null, + }); + } + return undefined; + } + + return new sdk.QueryParam({ + key: param.name, + value: param.value, + }); + }) + .flat() // Flatten the array in case of nested arrays from map + .filter((item) => item !== undefined); + + if (qp.length > 0) { + postman.addQueryParams(qp); + } +} + +function setPathParams(postman: sdk.Request, pathParams: Param[]) { + // Map through the path parameters + const source = pathParams.map((param) => { + if (!param.value) { + return undefined; + } + + let serializedValue; + + // Handle different styles + if (Array.isArray(param.value)) { + if (param.style === "label") { + serializedValue = `.${param.value.join(".")}`; + } else if (param.style === "matrix") { + serializedValue = `;${param.name}=${param.value.join(";")}`; + } else { + serializedValue = param.value.join(","); + } + return new sdk.Variable({ + key: param.name, + value: serializedValue, + }); + } + + const decodedValue = decodeURI(param.value); + const tryJson = () => { + try { + return JSON.parse(decodedValue); + } catch (e) { + return false; + } + }; + + const jsonResult = tryJson(); + + if (typeof jsonResult === "object") { + if (param.style === "matrix") { + serializedValue = Object.entries(jsonResult) + .map(([key, val]) => `;${key}=${val}`) + .join(""); + } else { + serializedValue = Object.entries(jsonResult) + .map(([key, val]) => `${key}=${val}`) + .join(","); + } + } else { + serializedValue = decodedValue || `:${param.name}`; + } + + return new sdk.Variable({ + key: param.name, + value: serializedValue, + }); + }); + + postman.url.variables.assimilate(source, false); +} + +function buildCookie(cookieParams: Param[]) { + const cookies = cookieParams + .map((param) => { + if (param.value) { + const decodedValue = decodeURI(param.value as string); + const tryJson = () => { + try { + return JSON.parse(decodedValue); + } catch (e) { + return false; + } + }; + + const jsonResult = tryJson(); + if (typeof jsonResult === "object") { + if (param.style === "form") { + // Handle form style + if (param.explode) { + // Serialize each key-value pair as a separate cookie + return Object.entries(jsonResult).map( + ([key, val]) => + new sdk.Cookie({ + key: key, + value: val, + }), + ); + } else { + // Serialize the object as a single cookie with key-value pairs joined by commas + return new sdk.Cookie({ + key: param.name, + value: Object.entries(jsonResult) + .map(([key, val]) => `${key},${val}`) + .join(","), + }); + } + } + } else { + // Handle scalar values + return new sdk.Cookie({ + key: param.name, + value: param.value, + }); + } + } + return undefined; + }) + .flat() // Flatten the array in case of nested arrays from map + .filter((item) => item !== undefined); + + const list = new sdk.CookieList(null, cookies); + return list.toString(); +} + +function setHeaders( + postman: sdk.Request, + contentType: string, + accept: string, + cookie: string, + headerParams: Param[], + other: { key: string; value: string }[], +) { + postman.headers.clear(); + + if (contentType) { + postman.addHeader({ key: "Content-Type", value: contentType }); + } + + if (accept) { + postman.addHeader({ key: "Accept", value: accept }); + } + + headerParams.forEach((param) => { + if (param.value) { + const decodedValue = decodeURI(param.value as string); + const tryJson = () => { + try { + return JSON.parse(decodedValue); + } catch (e) { + return false; + } + }; + + const jsonResult = tryJson(); + if (Array.isArray(param.value)) { + if (param.style === "simple") { + if (param.explode) { + // Each item in the array is a separate header + jsonResult.forEach((val: any) => { + postman.addHeader({ key: param.name, value: val }); + }); + } else { + // Array values are joined by commas + postman.addHeader({ + key: param.name, + value: param.value.join(","), + }); + } + } + } else if (typeof jsonResult === "object") { + if (param.style === "simple") { + if (param.explode) { + // Each key-value pair in the object is a separate header + Object.entries(jsonResult).forEach(([key, val]) => { + postman.addHeader({ key: param.name, value: `${key}=${val}` }); + }); + } else { + // Object is serialized as a single header with key-value pairs joined by commas + postman.addHeader({ + key: param.name, + value: Object.entries(jsonResult) + .map(([key, val]) => `${key},${val}`) + .join(","), + }); + } + } + } else { + // Handle scalar values + postman.addHeader({ key: param.name, value: param.value }); + } + } + }); + + other.forEach((header) => { + postman.addHeader(header); + }); + + if (cookie) { + postman.addHeader({ key: "Cookie", value: cookie }); + } +} + +// TODO: this is all a bit hacky +function setBody(clonedPostman: sdk.Request, body: Body) { + if (clonedPostman.body === undefined) { + return; + } + + if (body.type === "empty") { + clonedPostman.body = undefined; + return; + } + + if (body.type === "raw" && body.content?.type === "file") { + // treat it like file. + clonedPostman.body.mode = "file"; + clonedPostman.body.file = { src: body.content.value.src }; + return; + } + + switch (clonedPostman.body.mode) { + case "raw": { + // check file even though it should already be set from above + if (body.type !== "raw" || body.content?.type === "file") { + clonedPostman.body = undefined; + return; + } + clonedPostman.body.raw = body.content?.value ?? ""; + return; + } + case "formdata": { + clonedPostman.body.formdata?.clear(); + if (body.type !== "form") { + // treat it like raw. + clonedPostman.body.mode = "raw"; + clonedPostman.body.raw = `${body.content?.value}`; + return; + } + const params = Object.entries(body.content) + .filter((entry): entry is [string, NonNullable] => !!entry[1]) + .map(([key, content]) => { + if (content.type === "file") { + return new sdk.FormParam({ key: key, ...content }); + } + return new sdk.FormParam({ key: key, value: content.value }); + }); + clonedPostman.body.formdata?.assimilate(params, false); + return; + } + case "urlencoded": { + clonedPostman.body.urlencoded?.clear(); + if (body.type !== "form") { + // treat it like raw. + clonedPostman.body.mode = "raw"; + clonedPostman.body.raw = `${body.content?.value}`; + return; + } + const params = Object.entries(body.content) + .filter((entry): entry is [string, NonNullable] => !!entry[1]) + .map(([key, content]) => { + if (content.type !== "file" && content.value) { + return new sdk.QueryParam({ key: key, value: content.value }); + } + return undefined; + }) + .filter((item): item is sdk.QueryParam => item !== undefined); + clonedPostman.body.urlencoded?.assimilate(params, false); + return; + } + default: + return; + } +} + +// TODO: finish these types +interface Options { + server?: ServerObject; + queryParams: Param[]; + pathParams: Param[]; + cookieParams: Param[]; + headerParams: Param[]; + contentType: string; + accept: string; + body: Body; + auth: AuthState; +} + +function buildPostmanRequest( + postman: sdk.Request, + { + queryParams, + pathParams, + cookieParams, + contentType, + accept, + headerParams, + body, + server, + auth, + }: Options, +) { + const clonedPostman = cloneDeep(postman); + + clonedPostman.url.protocol = undefined; + clonedPostman.url.host = [window.location.origin]; + + if (server) { + let url = server.url.replace(/\/$/, ""); + const variables = server.variables; + if (variables) { + Object.keys(variables).forEach((variable) => { + url = url.replace(`{${variable}}`, variables[variable].default); + }); + } + clonedPostman.url.host = [url]; + } + + setQueryParams(clonedPostman, queryParams); + setPathParams( + clonedPostman, + pathParams.map((path) => ({ + ...path, + value: path.value || path.schema.example, + })), + ); + + const cookie = buildCookie(cookieParams); + let otherHeaders = []; + + let selectedAuth: Scheme[] = []; + if (auth.selected !== undefined) { + selectedAuth = auth.options[auth.selected]; + } + + for (const a of selectedAuth) { + // Bearer Auth + if (a.type === "http" && a.scheme === "bearer") { + const { token } = auth.data[a.key]; + if (token === undefined) { + otherHeaders.push({ + key: "Authorization", + value: "Bearer ", + }); + continue; + } + otherHeaders.push({ + key: "Authorization", + value: `Bearer ${token}`, + }); + continue; + } + + if (a.type === "oauth2") { + let token; + if (auth.data[a.key]) { + token = auth.data[a.key].token; + } + if (token === undefined) { + otherHeaders.push({ + key: "Authorization", + value: "Bearer ", + }); + continue; + } + otherHeaders.push({ + key: "Authorization", + value: `Bearer ${token}`, + }); + continue; + } + + // Basic Auth + if (a.type === "http" && a.scheme === "basic") { + const { username, password } = auth.data[a.key]; + if (username === undefined || password === undefined) { + continue; + } + otherHeaders.push({ + key: "Authorization", + value: `Basic ${window.btoa(`${username}:${password}`)}`, + }); + continue; + } + + // API Key + if (a.type === "apiKey" && a.in === "header") { + const { apiKey } = auth.data[a.key]; + if (apiKey === undefined) { + otherHeaders.push({ + key: a.name, + value: `<${a.name ?? a.type}>`, + }); + continue; + } + otherHeaders.push({ + key: a.name, + value: apiKey, + }); + continue; + } + } + + setHeaders( + clonedPostman, + contentType, + accept, + cookie, + headerParams, + otherHeaders, + ); + + setBody(clonedPostman, body); + + return clonedPostman; +} + +export default buildPostmanRequest; diff --git a/docs/src/theme/ApiExplorer/index.tsx b/docs/src/theme/ApiExplorer/index.tsx new file mode 100644 index 00000000..0de525af --- /dev/null +++ b/docs/src/theme/ApiExplorer/index.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +import CodeSnippets from "@theme/ApiExplorer/CodeSnippets"; +import Request from "@theme/ApiExplorer/Request"; +import Response from "@theme/ApiExplorer/Response"; +import SecuritySchemes from "@theme/ApiExplorer/SecuritySchemes"; +import { ApiItem } from "docusaurus-plugin-openapi-docs/src/types"; +import sdk from "postman-collection"; + +function ApiExplorer({ + item, + infoPath, +}: { + item: NonNullable; + infoPath: string; +}) { + const postman = new sdk.Request(item.postman); + + return ( + <> + + + + {item.method !== "event" && ( + + )} + + ); +} + +export default ApiExplorer; diff --git a/docs/src/theme/ApiExplorer/persistanceMiddleware.ts b/docs/src/theme/ApiExplorer/persistanceMiddleware.ts new file mode 100644 index 00000000..3b1bde35 --- /dev/null +++ b/docs/src/theme/ApiExplorer/persistanceMiddleware.ts @@ -0,0 +1,64 @@ +import { Middleware } from "@reduxjs/toolkit"; +import { + setAuthData, + setSelectedAuth, +} from "@theme/ApiExplorer/Authorization/slice"; +import { AppDispatch, RootState } from "@theme/ApiItem/store"; +/* eslint-disable import/no-extraneous-dependencies*/ +import { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types"; + +import { createStorage, hashArray } from "./storage-utils"; + +export function createPersistanceMiddleware(options: ThemeConfig["api"]) { + const persistanceMiddleware: Middleware<{}, RootState, AppDispatch> = + (storeAPI) => (next) => (action) => { + const result = next(action); + + const state = storeAPI.getState(); + + const storage = createStorage("sessionStorage"); + + if (action.type === setAuthData.type) { + for (const [key, value] of Object.entries(state.auth.data)) { + if (Object.values(value as any).filter(Boolean).length > 0) { + storage.setItem(key, JSON.stringify(value)); + } else { + storage.removeItem(key); + } + } + } + + if (action.type === setSelectedAuth.type) { + if (state.auth.selected) { + storage.setItem( + hashArray(Object.keys(state.auth.options)), + state.auth.selected, + ); + } + } + + // TODO: determine way to rehydrate without flashing + if (action.type === "contentType/setContentType") { + storage.setItem("contentType", action.payload); + } + + if (action.type === "accept/setAccept") { + storage.setItem("accept", action.payload); + } + + if (action.type === "server/setServer") { + storage.setItem("server", action.payload); + } + + if (action.type === "server/setServerVariable") { + const server = storage.getItem("server") ?? "{}"; + const variables = JSON.parse(action.payload); + let serverObject = JSON.parse(server); + serverObject.variables[variables.key].default = variables.value; + storage.setItem("server", JSON.stringify(serverObject)); + } + + return result; + }; + return persistanceMiddleware; +} diff --git a/docs/src/theme/ApiExplorer/postman-collection.d.ts b/docs/src/theme/ApiExplorer/postman-collection.d.ts new file mode 100644 index 00000000..9a49b232 --- /dev/null +++ b/docs/src/theme/ApiExplorer/postman-collection.d.ts @@ -0,0 +1,3 @@ +declare module "postman-collection" { + export = Request.sdk; +} diff --git a/docs/src/theme/ApiExplorer/react-modal.d.ts b/docs/src/theme/ApiExplorer/react-modal.d.ts new file mode 100644 index 00000000..2ba6abf7 --- /dev/null +++ b/docs/src/theme/ApiExplorer/react-modal.d.ts @@ -0,0 +1 @@ +declare module "react-modal"; diff --git a/docs/src/theme/ApiExplorer/storage-utils.ts b/docs/src/theme/ApiExplorer/storage-utils.ts new file mode 100644 index 00000000..298a5938 --- /dev/null +++ b/docs/src/theme/ApiExplorer/storage-utils.ts @@ -0,0 +1,32 @@ +import crypto from "crypto-js"; + +export function hashArray(arr: string[]) { + function hash(message: string) { + return crypto.SHA1(message).toString(); + } + const hashed = arr.map((item) => hash(item)); + hashed.sort(); + const res = hashed.join(); + return hash(res); +} + +type Persistance = false | "localStorage" | "sessionStorage" | undefined; + +export function createStorage(persistance: Persistance): Storage { + if (persistance === false) { + return { + getItem: () => null, + setItem: () => {}, + clear: () => {}, + key: () => null, + removeItem: () => {}, + length: 0, + }; + } + + if (persistance === "sessionStorage") { + return sessionStorage; + } + + return localStorage; +}