diff --git a/src/app/package.json b/src/app/package.json index fa047bdc6..7bbfdeefd 100644 --- a/src/app/package.json +++ b/src/app/package.json @@ -1,5 +1,5 @@ { - "name": "redpanda", + "name": "open-apparel-registry", "version": "0.1.0", "license": "MIT", "private": true, @@ -10,11 +10,9 @@ "axios": "0.18.0", "core-js": "2.6.4", "file-saver": "2.0.0", - "json2csv": "4.1.6", "immutability-helper": "2.9.0", "lodash": "4.17.11", "mapbox-gl": "0.52.0", - "papaparse": "4.6.3", "prop-types": "15.6.2", "react": "16.7.0", "react-copy-to-clipboard": "5.0.1", @@ -40,8 +38,7 @@ "build": "react-scripts build", "test": "CI=1 react-scripts test", "lint": "eslint src/ --ext .js --ext .jsx", - "eject": "react-scripts eject", - "deploy": "npm run build && firebase deploy" + "eject": "react-scripts eject" }, "devDependencies": { "@babel/cli": "7.0.0-beta.49", diff --git a/src/app/src/__tests__/utils.tests.js b/src/app/src/__tests__/utils.tests.js index 61e8b4fca..107b5eed5 100644 --- a/src/app/src/__tests__/utils.tests.js +++ b/src/app/src/__tests__/utils.tests.js @@ -1,4 +1,5 @@ /* eslint-env jest */ +/* eslint-disable no-useless-escape */ const mapValues = require('lodash/mapValues'); const isEqual = require('lodash/isEqual'); @@ -44,6 +45,9 @@ const { makeResetPasswordEmailURL, getTokenFromQueryString, makeResetPasswordConfirmURL, + joinDataIntoCSVString, + caseInsensitiveIncludes, + sortFacilitiesAlphabeticallyByName, } = require('../util/util'); const { @@ -305,7 +309,7 @@ it('gets the value from an event on a DOM input', () => { expect(getValueFromEvent(mockEvent)).toEqual(value); }); -it('gets the checked state from an even on a DOM checkbox input', () => { +it('gets the checked state from an event on a DOM checkbox input', () => { const checked = true; const mockEvent = { target: { @@ -716,3 +720,135 @@ it('gets a `token` from a querystring', () => { expect(getTokenFromQueryString(missingQueryString)).toBe(expectedMissingQueryStringMatch); }); + +it('joins a 2-d array into a correctly escaped CSV string', () => { + const numericArray = [ + [ + 1, + 2, + ], + [ + 3, + 4, + ], + ]; + const expectedNumericArrayMatch = '1,2\n3,4\n'; + expect(joinDataIntoCSVString(numericArray)).toBe(expectedNumericArrayMatch); + + const stringArray = [ + [ + 'hello', + 'world', + ], + [ + 'foo', + 'bar', + ], + ]; + const expectedStringArrayMatch = '"hello","world"\n"foo","bar"\n'; + expect(joinDataIntoCSVString(stringArray)).toBe(expectedStringArrayMatch); + + const mixedArray = [ + [ + 1, + 'hello', + ], + [ + 2, + 'world', + ], + ]; + const expectedMixedArrayMatch = '1,"hello"\n2,"world"\n'; + expect(joinDataIntoCSVString(mixedArray)).toBe(expectedMixedArrayMatch); + + const escapedArray = [ + [ + 'foo, bar, baz', + 'hello "world"', + ], + [ + 'foo, "bar", baz', + 'hello, world', + ], + ]; + const expectedEscapedArrayMatch = + '"foo, bar, baz","hello \"world\""\n"foo, \"bar\", baz","hello, world"\n'; + expect(joinDataIntoCSVString(escapedArray)).toBe(expectedEscapedArrayMatch); +}); + +it('checks whether one string includes another regardless of char case', () => { + const uppercaseTarget = 'HELLOWORLD'; + const lowercaseTest = 'world'; + const lowercaseTarget = 'helloworld'; + const uppercaseTest = 'WORLD'; + const uppercaseNonMatchTest = 'FOO'; + const lowercaseNonMatchTest = 'foo'; + + expect(caseInsensitiveIncludes(uppercaseTarget, lowercaseTest)).toBe(true); + expect(caseInsensitiveIncludes(lowercaseTarget, uppercaseTest)).toBe(true); + expect(caseInsensitiveIncludes(lowercaseTarget, lowercaseTest)).toBe(true); + expect(caseInsensitiveIncludes(uppercaseTarget, uppercaseTest)).toBe(true); + + expect(caseInsensitiveIncludes(uppercaseTarget, lowercaseNonMatchTest)).toBe(false); + expect(caseInsensitiveIncludes(lowercaseTarget, uppercaseNonMatchTest)).toBe(false); + expect(caseInsensitiveIncludes(lowercaseTarget, lowercaseNonMatchTest)).toBe(false); + expect(caseInsensitiveIncludes(uppercaseTarget, uppercaseNonMatchTest)).toBe(false); +}); + +it('sorts an array of facilities alphabetically by name without mutating the input', () => { + const inputData = [ + { + properties: { + name: 'hello World', + }, + }, + { + properties: { + name: 'FOO', + }, + }, + { + properties: { + name: 'Bar', + }, + }, + { + properties: { + name: 'baz', + }, + }, + ]; + + const expectedSortedData = [ + { + properties: { + name: 'Bar', + }, + }, + { + properties: { + name: 'baz', + }, + }, + { + properties: { + name: 'FOO', + }, + }, + { + properties: { + name: 'hello World', + }, + }, + ]; + + expect(isEqual( + sortFacilitiesAlphabeticallyByName(inputData), + expectedSortedData, + )).toBe(true); + + expect(isEqual( + inputData, + expectedSortedData, + )).toBe(false); +}); diff --git a/src/app/src/util/util.js b/src/app/src/util/util.js index 970c7e7e5..06a09fe89 100644 --- a/src/app/src/util/util.js +++ b/src/app/src/util/util.js @@ -9,12 +9,17 @@ import size from 'lodash/size'; import negate from 'lodash/negate'; import omitBy from 'lodash/omitBy'; import isEmpty from 'lodash/isEmpty'; +import isNumber from 'lodash/isNumber'; import values from 'lodash/values'; import flow from 'lodash/flow'; import noop from 'lodash/noop'; import compact from 'lodash/compact'; import startsWith from 'lodash/startsWith'; import head from 'lodash/head'; +import replace from 'lodash/replace'; +import trimEnd from 'lodash/trimEnd'; +import includes from 'lodash/includes'; +import lowerCase from 'lodash/lowerCase'; import { featureCollection, bbox } from '@turf/turf'; import { saveAs } from 'file-saver'; @@ -32,6 +37,8 @@ import { import { createListItemCSV } from './util.listItemCSV'; +import { createFacilitiesCSV } from './util.facilitiesCSV'; + export function DownloadCSV(data, fileName) { saveAs( new Blob([data], { type: 'text/csv;charset=utf-8;' }), @@ -50,6 +57,9 @@ export const downloadListItemCSV = list => `${list.id}_${list.name}_${(new Date()).toLocaleDateString()}.csv`, ); +export const downloadFacilitiesCSV = facilities => + DownloadCSV(createFacilitiesCSV(facilities), 'facilities.csv'); + export const makeUserLoginURL = () => '/user-login/'; export const makeUserLogoutURL = () => '/user-logout/'; export const makeUserSignupURL = () => '/user-signup/'; @@ -339,3 +349,50 @@ export const makeResetPasswordEmailURL = () => export const makeResetPasswordConfirmURL = () => '/rest-auth/password/reset/confirm/'; + +export const joinDataIntoCSVString = data => data + .reduce((csvAccumulator, nextRow) => { + const joinedColumns = nextRow + .reduce((rowAccumulator, nextColumn) => { + if (isNumber(nextColumn)) { + return rowAccumulator.concat(nextColumn, ','); + } + + return rowAccumulator.concat( + '' + '"' + replace(nextColumn, '"', '\"') + '"', // eslint-disable-line + ',', + ); + }, ''); + + return csvAccumulator.concat( + trimEnd(joinedColumns, ','), + '\n', + ); + }, ''); + +export const caseInsensitiveIncludes = (target, test) => + includes(lowerCase(target), lowerCase(test)); + +export const sortFacilitiesAlphabeticallyByName = data => data + .slice() + .sort(( + { + properties: { + name: firstFacilityName, + }, + }, + { + properties: { + name: secondFacilityName, + }, + }, + ) => { + const a = lowerCase(firstFacilityName); + const b = lowerCase(secondFacilityName); + + if (a === b) { + return 0; + } + + return (a < b) ? -1 : 1; + }); diff --git a/src/app/src/util/util.listItemCSV.js b/src/app/src/util/util.listItemCSV.js index 3d4f4ec0c..90f8dacc1 100644 --- a/src/app/src/util/util.listItemCSV.js +++ b/src/app/src/util/util.listItemCSV.js @@ -3,7 +3,8 @@ import get from 'lodash/get'; import fill from 'lodash/fill'; import isEmpty from 'lodash/isEmpty'; import flow from 'lodash/flow'; -import { unparse } from 'papaparse'; + +import { joinDataIntoCSVString } from './util'; import { facilityMatchStatusChoicesEnum } from './constants'; @@ -96,4 +97,4 @@ export const listItemReducer = (acc, next) => { export const formatDataForCSV = listItems => listItems.reduce(listItemReducer, [csvHeaders]); -export const createListItemCSV = flow(formatDataForCSV, unparse); +export const createListItemCSV = flow(formatDataForCSV, data => joinDataIntoCSVString(data)); diff --git a/src/app/yarn.lock b/src/app/yarn.lock index 59381ba52..5200d7713 100644 --- a/src/app/yarn.lock +++ b/src/app/yarn.lock @@ -3913,7 +3913,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@2, commander@^2.11.0, commander@^2.15.1, commander@^2.8.1: +commander@2, commander@^2.11.0, commander@^2.8.1: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -7299,17 +7299,6 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json2csv@4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-4.1.6.tgz#d56b15d72a050b6902ee960232ffd66c18f3051a" - integrity sha512-pmitnvvuX9OC6lL6T6F0sl6NsoXG94xLHKnKwdRSEpASFd3xdKHvsksPpNFZtDULP0AwOA0MGKrm1I0c7Enaxg== - dependencies: - commander "^2.15.1" - jsonparse "^1.3.1" - lodash.clonedeep "^4.5.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - json3@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" @@ -7347,11 +7336,6 @@ jsonlint-lines-primitives@~1.6.0: JSV ">= 4.0.x" nomnom ">= 1.5.x" -jsonparse@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -7620,11 +7604,6 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" @@ -7640,21 +7619,11 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.template@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" @@ -8613,11 +8582,6 @@ pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.7.tgz#2473439021b57f1516c82f58be7275ad8ef1bb27" integrity sha512-3HNK5tW4x8o5mO8RuHZp3Ydw9icZXx0RANAOMzlMzx7LVXhMJ4mo3MOBpzyd7r/+RUu8BmndP47LXT+vzjtWcQ== -papaparse@4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.6.3.tgz#742e5eaaa97fa6c7e1358d2934d8f18f44aee781" - integrity sha512-LRq7BrHC2kHPBYSD50aKuw/B/dGcg29omyJbKWY3KsYUZU69RKwaBHu13jGmCYBtOc4odsLCrFyk6imfyNubJQ== - param-case@2.1.x: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"