├── extension.zip ├── assets └── csv-export.png ├── src ├── index.js ├── helpers.js └── App.js ├── extension.json ├── .gitignore ├── package.json ├── README.md ├── LICENSE └── public └── index.html /extension.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/export-to-csv/master/extension.zip -------------------------------------------------------------------------------- /assets/csv-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/export-to-csv/master/assets/csv-export.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'isomorphic-fetch'; 4 | 5 | import App from './App'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Export to CSV", 3 | "font_awesome_class": "fa-file-excel-o", 4 | "image_url": "https://s3-us-west-2.amazonaws.com/cosmicjs/e3dfe870-732f-11e9-a54a-7f96bf56c48e-5b056dc79c.png", 5 | "repo_url": "https://github.com/cosmicjs/export-to-csv" 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | build.zip 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmicjs-to-csv-extension", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "evergreen-ui": "^4.15.0", 7 | "isomorphic-fetch": "^2.2.1", 8 | "qs": "^6.7.0", 9 | "react": "^16.8.6", 10 | "react-dom": "^16.8.6", 11 | "react-scripts": "3.0.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "export": "npm run build && cp extension.json build/extension.json && zip -r extension.zip build", 17 | "test": "react-scripts test" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "bestzip": "^2.1.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmic JS To CSV Extension 2 | 3 | ![Cosmic JS](https://cosmic-s3.imgix.net/1f436f20-7bde-11e9-93c7-d1a8b031d015-Screen-Shot-2019-05-21-at-10.23.36-AM.png?w=1600) 4 | 5 | The CosmicJS to CSV extension is a JAM-stack application that allows you to export your Buckets in a CSV format. 6 | The plugin also will automatically parse metadata fields as specified by the Object Type settings. 7 | 8 | ## Installing 9 | 10 | ### Cosmic App Store 11 | [Install Extension](https://cosmicjs.com/extensions/export-to-csv) 12 | 13 | ### Manually build 14 | 15 | After cloning this repository, dependencies can be installed with `npm install` or `yarn install`. 16 | After the required dependencies are installed, the extension can be built with `npm run build`. 17 | 18 | This will create a `build.zip` file in the root project directory, which can be dragged into the Cosmic 19 | JS extensions area to be automatically installed. 20 | 21 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Flynn Buckingham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Export to CSV 17 | 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | export async function fetchBucketData(bucketSlug, readKey) { 3 | const { object_types: objectTypes, ...res } = await fetch( 4 | `https://api.cosmicjs.com/v1/${bucketSlug}/object-types?read_key=${readKey}` 5 | ).then(res => res.json()); 6 | 7 | if (res.error) { 8 | throw new Error(res.error); 9 | } 10 | 11 | return { objectTypes }; 12 | } 13 | 14 | // serialize a JS array to a valid CSV string 15 | const arrayToCSVRowString = array => array 16 | .map(item => '"' + (`${item}` 17 | // escape quotes 18 | .replace(/"/g, '""') 19 | // escape commas 20 | .replace(/,/g, '\,')) + '"') 21 | .join(','); 22 | 23 | // generate CSV heading row 24 | const mapCosmicObjectToCSVHeading = (expectedKeys, expectedMetafields) => 25 | arrayToCSVRowString([ 26 | ...expectedKeys, 27 | // remap metafield slugs to dot.path equivalent 28 | ...expectedMetafields.map(key => `metadata.${key}`), 29 | ]); 30 | 31 | // mutative function for sanitizing and generating a basic CSV row 32 | function mapCosmicObjectToCSVRow(cosmicObject, expectedKeys, expectedMetafields) { 33 | // get root level Cosmic node values as array (in same order as expectedKeys) 34 | const rowValues = expectedKeys.map(key => cosmicObject[key]); 35 | 36 | // attempt to get metadata values (if present) and append to values 37 | const metadata = cosmicObject.metadata || {}; 38 | expectedMetafields.forEach(key => rowValues.push(metadata[key] || null)); 39 | 40 | // return CSV safe row 41 | return arrayToCSVRowString(rowValues); 42 | } 43 | 44 | export async function generateCSVByObjectType(bucketSlug, readKey, objectType, limitStatus) { 45 | const limit = 128; 46 | 47 | const filterByStatus = o => !limitStatus || o.state === limitStatus; 48 | 49 | const generateQuery = (page = 0) => 50 | `https://api.cosmicjs.com/v1/${bucketSlug}/objects?type=${ 51 | objectType.slug}&limit=${limit}&skip=${limit * page}${ 52 | limitStatus ? `&status=${limitStatus}` : ''}&read_key=${readKey}`; 53 | 54 | // derive expected metafields from the objectType provided 55 | const expectedMetafields = objectType.metafields ? 56 | objectType.metafields.map(({ key }) => key) : []; 57 | 58 | // fetch the initial dataset for the node. Use as iteratee to determine 59 | // pagination state 60 | let responseData = await fetch(generateQuery()).then(res => res.json()); 61 | 62 | if (!responseData.objects || !responseData.objects.length) { 63 | // no objects, no parse. 64 | throw new Error(responseData.error || responseData.message || 'No objects returned'); 65 | } 66 | 67 | // determine the expected keys from the first object returned 68 | const expectedKeys = Object.keys(responseData.objects[0]) 69 | // purge any unwanted keys from heading (metadata should always be omitted!) 70 | .filter(key => !['_id', 'metafields', 'metadata'].includes(key)); 71 | 72 | // row mapping mutates the source object so building heading after rows 73 | const heading = mapCosmicObjectToCSVHeading(expectedKeys, expectedMetafields) 74 | 75 | // generate initial rows 76 | const rows = responseData.objects 77 | .filter(filterByStatus) 78 | .map(o => mapCosmicObjectToCSVRow(o, expectedKeys, expectedMetafields)); 79 | 80 | let pageIndex = 1; 81 | 82 | // perform fetches until page object count is less than limit 83 | while (responseData.objects && rows.length !== responseData.total) { 84 | responseData = await fetch(generateQuery(pageIndex)) 85 | .then(res => res.json()); 86 | 87 | if (!responseData.objects) { 88 | continue; 89 | } 90 | 91 | // append CSV rows 92 | rows.push(...responseData.objects 93 | .filter(filterByStatus) 94 | .map(o => mapCosmicObjectToCSVRow(o, expectedKeys, expectedMetafields)) 95 | ); 96 | 97 | pageIndex += 1; 98 | } 99 | 100 | if (!rows.length) { 101 | throw new Error(`Not enough ${objectType.title} of specified status to build CSV`); 102 | } 103 | 104 | return { csvData: heading + '\n' + rows.join('\n'), total: rows.length }; 105 | } 106 | 107 | // adapted from https://stackoverflow.com/a/33542499/10532549 108 | export function generateDOMDownload(filename, data) { 109 | const blob = new Blob([data], { type: 'text/csv' }); 110 | if (window.navigator.msSaveOrOpenBlob) { 111 | window.navigator.msSaveBlob(blob, filename); 112 | } else{ 113 | const elem = window.document.createElement('a'); 114 | elem.href = window.URL.createObjectURL(blob); 115 | elem.download = filename; 116 | document.body.appendChild(elem); 117 | elem.click(); 118 | document.body.removeChild(elem); 119 | } 120 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import qs from 'qs'; 4 | 5 | import { 6 | toaster, 7 | Heading, 8 | Paragraph, 9 | Pane, 10 | Button, 11 | Spinner, 12 | SegmentedControl, 13 | } from 'evergreen-ui'; 14 | 15 | import { 16 | fetchBucketData, 17 | generateCSVByObjectType, 18 | generateDOMDownload, 19 | } from './helpers'; 20 | 21 | const { 22 | bucket_slug: bucketSlug, 23 | read_key: readKey, 24 | } = qs.parse(window.location.href.split('?')[1] || ''); 25 | 26 | function App() { 27 | const [state, setState] = useState({ 28 | didError: false, 29 | objectTypes: null, 30 | activeBuilds: {}, 31 | limitStatus: false, 32 | }); 33 | 34 | useEffect(() => { 35 | fetchBucketData(bucketSlug, readKey) 36 | .then(data => setState({ ...state, ...data })) 37 | .catch(error => setState({ didError: error })); 38 | }, []); 39 | 40 | if (state.didError) { 41 | return ( 42 | 48 | 49 | Could not load CSV extension. 50 | {state.didError.message} 51 | 52 | 53 | ) 54 | } 55 | 56 | if (!state.objectTypes) { 57 | return ( 58 | 64 | 65 | 66 | ) 67 | } 68 | 69 | const handleCSVGenerateClick = async (objectType) => { 70 | const updateBuildState = status => setState({ 71 | ...state, 72 | activeBuilds: { 73 | ...state.activeBuilds, 74 | [objectType.slug]: status, 75 | }, 76 | }); 77 | 78 | updateBuildState(true); 79 | try { 80 | const { csvData, total } = await generateCSVByObjectType(bucketSlug, readKey, objectType, state.limitStatus); 81 | toaster.success(`Generated ${objectType.slug} CSV, generated ${total} rows`); 82 | generateDOMDownload(`${bucketSlug}-${objectType.slug}-${new Date().toISOString()}.csv`, csvData); 83 | } catch (e) { 84 | console.error(e); 85 | toaster.danger(`Failed to generate ${objectType.slug} CSV. ${e.message}`); 86 | } 87 | updateBuildState(false); 88 | } 89 | 90 | return ( 91 | 94 | 98 | Export Objects to CSV 99 | 100 | 103 | Export your Cosmic JS Objects to CSV format based on Object Type. 104 | Metafields are based on the expected values found in the Bucket Object Settings. 105 | You can filter which Objects are in the report by status by selecting Object status below. 106 | 107 | 112 | 117 | 120 | Object status 121 | 122 | 123 | 124 | setState({ 133 | ...state, 134 | limitStatus: value, 135 | })} 136 | /> 137 | 138 | 139 | {state.objectTypes.map((objectType) => { 140 | const isBuildingCSV = state.activeBuilds && state.activeBuilds[objectType.slug]; 141 | 142 | return ( 143 | 149 | 154 | 157 | Export your {objectType.title} 158 | 159 | 160 | 161 | 174 | 175 | 176 | ); 177 | })} 178 | 179 | ); 180 | } 181 | 182 | export default App; 183 | --------------------------------------------------------------------------------