diff --git a/lib/customers.js b/lib/customers.js index 2df001c5..7586267d 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -138,6 +138,154 @@ async function updateCustomer (id, data, userToken) { return getCustomerById(id) } +/** + * Update all customer record + * + * @name save + * @function + * + * @param {string} id Customer's id + * @param {object} data Fields to update + * + * @returns {Promise} Newly updated Customer + */ + +function edit (id, data, userToken) { + const defaults = [ + 'front_camera', + 'id_card_data', + 'id_card_photo', + 'us_ssn', + 'subscriber_info', + 'name' + ] + const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, _.omitBy(_.isNil, data))) + if (_.isEmpty(filteredData)) return getCustomerById(id) + const formattedData = enhanceEditedPhotos(enhanceEditedFields(filteredData, userToken)) + const defaultDbData = { + customer_id: id, + created: new Date(), + ...formattedData + } + + const cs = new Pgp.helpers.ColumnSet(_.keys(defaultDbData), + { table: 'edited_customer_data' }) + const onConflict = ' ON CONFLICT (customer_id) DO UPDATE SET ' + + cs.assignColumns({ from: 'EXCLUDED', skip: ['customer_id', 'created'] }) + const upsert = Pgp.helpers.insert(defaultDbData, cs) + onConflict + return db.none(upsert) + .then(getCustomerById(id)) +} + +/** + * Add *edited_by and *edited_at fields with acting user's token + * and date of override respectively before saving to db. + * + * @name enhanceEditedFields + * @function + * + * @param {object} fields Fields to be enhanced + * @param {string} userToken Acting user's token + * @returns {object} fields enhanced with *_by and *_at fields + */ + +function enhanceEditedFields (fields, userToken) { + if (!userToken) return fields + _.mapKeys((field) => { + fields[field + '_by'] = userToken + fields[field + '_at'] = 'now()^' + }, fields) + return fields +} + +/** + * Add *_path to edited photos fields + * + * @name enhanceEditedFields + * @function + * + * @param {object} fields Fields to be enhanced + * @returns {object} fields enhanced with *_path + */ + +function enhanceEditedPhotos (fields) { + return _.mapKeys((field) => { + if (_.includes(field, ['front_camera', 'id_card_photo'])) { + return field + '_path' + } + return field + }, fields) +} + +/** + * Remove the edited data from the db record + * + * @name enhanceOverrideFields + * @function + * + * @param {string} id Customer's id + * @param {object} data Fields to be deleted + * + * @returns {Promise} Newly updated Customer + * + */ + +function deleteEditedData (id, data) { + // TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION + const defaults = [ + 'front_camera', + 'id_card_data', + 'id_card_photo', + 'us_ssn', + 'subcriber_info', + 'name' + ] + const filteredData = _.pick(defaults, _.mapKeys(_.snakeCase, data)) + if (_.isEmpty(filteredData)) return getCustomerById(id) + + const cs = new Pgp.helpers.ColumnSet(_.keys(filteredData), + { table: 'edited_customer_data' }) + const update = Pgp.helpers.update(filteredData, cs) + db.none(update) + return getCustomerById(id) +} + +/** + * Replace customer's compliance photos + * + * @name save + * @function + * + * @param {string} id Customer's id + * @param {File} photo New photo data + * @param {string} photoType Photo's compliance type + * + * @returns {object} path New photo path + * + */ +async function updateEditedPhoto (id, photo, photoType) { + const newPatch = {} + const baseDir = photoType === 'frontCamera' ? frontCameraBaseDir : idPhotoCardBasedir + const { createReadStream, filename } = photo + const stream = createReadStream() + + const randomString = uuid.v4().toString() + '/' + + // i.e. ..62ed29c5-f37e-4fb7-95bb-c52d4a3738f7/filename.jpg + const rpath = path.join(randomString, filename) + + // create the directory tree if needed + _.attempt(() => makeDir.sync(path.join(baseDir, randomString))) + + // i.e. ..//idphotocard/62ed29c5-f37e-4fb7-95bb-c52d4a3738f7/filename.jpg + const pathName = path.join(baseDir, rpath) + + await stream.pipe(fs.createWriteStream(pathName)) + newPatch[photoType] = rpath + + return newPatch +} + const invalidateCustomerNotifications = (id, data) => { if (data.authorized_override !== 'verified') return Promise.resolve() @@ -489,9 +637,8 @@ function getCustomersList (phone = null, name = null, address = null, id = null) c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions, c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created, row_number() OVER (partition by c.id order by t.created desc) AS rn, - coalesce(sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id), 0) as total_spent, sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs, - ccf.custom_fields + coalesce(sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (partition by c.id), 0) AS total_spent, ccf.custom_fields FROM customers c LEFT OUTER JOIN ( SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code FROM cash_in_txs WHERE send_confirmed = true UNION @@ -510,7 +657,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null) AND ($6 IS NULL OR id_card_data::json->>'address' = $6) AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7) limit $3` - return db.any(sql, [passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ]) + return db.any(sql, [ passableErrorCodes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ]) .then(customers => Promise.all(_.map(customer => { return populateOverrideUsernames(customer) .then(camelize) @@ -518,17 +665,17 @@ function getCustomersList (phone = null, name = null, address = null, id = null) } /** - * Query all customers, ordered by last activity + * Query a specific customer, ordered by last activity * and with aggregate columns based on their * transactions * - * @returns {array} Array of customers with it's transactions aggregations + * @returns {array} A single customer instance with non edited */ function getCustomerById (id) { const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') - const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override, - phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration, - id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at, + const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_at, front_camera_path, front_camera_at, front_camera_override, + phone, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration, + id_card_photo_at, id_card_photo_path, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at, sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, tx_class as last_tx_class, subscriber_info, custom_fields from ( @@ -536,8 +683,8 @@ function getCustomerById (id) { greatest(0, date_part('day', c.suspended_until - now())) as days_suspended, c.suspended_until > now() as is_suspended, c.front_camera_path, c.front_camera_at, c.front_camera_override, - c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, - c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions, + c.phone, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration, + c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions, c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, row_number() over (partition by c.id order by t.created desc) as rn, sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs, @@ -556,10 +703,52 @@ function getCustomerById (id) { where c.id = $2 ) as cl where rn = 1` return db.oneOrNone(sql, [passableErrorCodes, id]) + .then(customerData => { + return getEditedData(id) + .then(customerEditedData => selectLatestData(customerData, customerEditedData)) + }) .then(populateOverrideUsernames) .then(camelize) } +/** + * Query the specific customer manually edited data + * + * @param {String} id customer id + * + * @returns {array} A single customer instance with the most recent edited data + */ +function getEditedData (id) { + const sql = `SELECT * FROM edited_customer_data WHERE customer_id = $1` + return db.oneOrNone(sql, [id]) + .then(_.omitBy(_.isNil)) +} + +function selectLatestData (customerData, customerEditedData) { + const defaults = [ + 'front_camera', + 'id_card_data', + 'id_card_photo', + 'us_ssn', + 'subscriber_info', + 'name' + ] + _.map(field => { + let fieldName = field + if (_.includes(field, ['front_camera', 'id_card_photo'])) fieldName = fieldName + '_path' + const atField = field + '_at' + const byField = field + '_by' + if (!_.has(fieldName, customerData) || !_.has(fieldName, customerEditedData)) return + if (customerData[atField] < customerEditedData[atField]) { + customerData[fieldName] = customerEditedData[fieldName] + customerData[atField] = customerEditedData[atField] + customerData[byField] = customerEditedData[byField] + } + } + , defaults) + return customerData +} + /** * @param {String} id customer id * @param {Object} patch customer update record @@ -768,35 +957,35 @@ function updateFrontCamera (id, patch) { }) } -function addCustomField(customerId, label, value) { +function addCustomField (customerId, label, value) { const sql = `SELECT * FROM custom_field_definitions WHERE label=$1 LIMIT 1` return db.oneOrNone(sql, [label]) .then(res => db.tx(t => { - if (_.isNil(res)) { - const fieldId = uuid.v4() - const q1 = t.none(`INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label]) - const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, fieldId, value]) - return t.batch([q1, q2]) - } + if (_.isNil(res)) { + const fieldId = uuid.v4() + const q1 = t.none(`INSERT INTO custom_field_definitions (id, label) VALUES ($1, $2)`, [fieldId, label]) + const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, fieldId, value]) + return t.batch([q1, q2]) + } - if (!_.isNil(res) && !res.active) { - const q1 = t.none(`UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id]) - const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value]) - return t.batch([q1, q2]) - } else if (!_.isNil(res) && res.active) { - const q1 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value]) - return t.batch([q1]) - } - }) + if (!_.isNil(res) && !res.active) { + const q1 = t.none(`UPDATE custom_field_definitions SET active = true WHERE id=$1`, [res.id]) + const q2 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value]) + return t.batch([q1, q2]) + } else if (!_.isNil(res) && res.active) { + const q1 = t.none(`INSERT INTO customer_custom_field_pairs (customer_id, custom_field_id, value) VALUES ($1, $2, $3)`, [customerId, res.id, value]) + return t.batch([q1]) + } + }) ) } -function saveCustomField(customerId, fieldId, newValue) { +function saveCustomField (customerId, fieldId, newValue) { const sql = `UPDATE customer_custom_field_pairs SET value=$1 WHERE customer_id=$2 AND custom_field_id=$3` return db.none(sql, [newValue, customerId, fieldId]) } -function removeCustomField(customerId, fieldId) { +function removeCustomField (customerId, fieldId) { const sql = `SELECT * FROM customer_custom_field_pairs WHERE custom_field_id=$1` return db.any(sql, [fieldId]) .then(res => db.tx(t => { @@ -827,5 +1016,8 @@ module.exports = { addCustomField, saveCustomField, removeCustomField, + edit, + deleteEditedData, + updateEditedPhoto, updateTxCustomerPhoto } diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index b1ccda5d..53f86ccc 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -9,6 +9,7 @@ const helmet = require('helmet') const nocache = require('nocache') const cookieParser = require('cookie-parser') const { ApolloServer, AuthenticationError } = require('apollo-server-express') +const { graphqlUploadExpress } = require('graphql-upload') const _ = require('lodash/fp') const { asyncLocalStorage, defaultStore } = require('../async-storage') @@ -47,10 +48,12 @@ app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL)) app.use(computeSchema) app.use(findOperatorId) app.use(session) +app.use(graphqlUploadExpress()) const apolloServer = new ApolloServer({ typeDefs, resolvers, + uploads: false, schemaDirectives: { auth: AuthDirective }, diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js index a26a86fd..36a24b0f 100644 --- a/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -3,6 +3,7 @@ const customers = require('../../../customers') const filters = require('../../filters') const resolvers = { + Customer: { isAnonymous: parent => (parent.customerId === anonymous.uuid) }, @@ -13,13 +14,32 @@ const resolvers = { }, Mutation: { setCustomer: (root, { customerId, customerInput }, context, info) => { + // TODO: To be replaced by function that fetchs the token const token = !!context.req.cookies.lamassu_sid && context.req.session.user.id if (customerId === anonymous.uuid) return customers.getCustomerById(customerId) return customers.updateCustomer(customerId, customerInput, token) }, addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value), saveCustomField: (...[, { customerId, fieldId, newValue }]) => customers.saveCustomField(customerId, fieldId, newValue), - removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId) + removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId), + editCustomer: async (root, { customerId, customerEdit }, context) => { + // TODO: To be replaced by function that fetchs the token + const token = !!context.req.cookies.lid && context.req.session.user.id + const editedData = await customerEdit + return customers.edit(customerId, editedData, token) + }, + replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => { + // TODO: To be replaced by function that fetchs the token + const token = !!context.req.cookies.lid && context.req.session.user.id + const photo = await newPhoto + if (!photo) return customers.getCustomerById(customerId) + return customers.updateEditedPhoto(customerId, photo, photoType) + .then(newPatch => customers.edit(customerId, newPatch, token)) + }, + deleteEditedData: (root, { customerId, customerEdit }) => { + // TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION + return customers.getCustomerById(customerId) + } } } diff --git a/lib/new-admin/graphql/resolvers/scalar.resolver.js b/lib/new-admin/graphql/resolvers/scalar.resolver.js index 57d556ae..54782105 100644 --- a/lib/new-admin/graphql/resolvers/scalar.resolver.js +++ b/lib/new-admin/graphql/resolvers/scalar.resolver.js @@ -1,12 +1,13 @@ const { GraphQLDateTime } = require('graphql-iso-date') const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json') - +const { GraphQLUpload } = require('graphql-upload') GraphQLDateTime.name = 'Date' const resolvers = { JSON: GraphQLJSON, JSONObject: GraphQLJSONObject, - Date: GraphQLDateTime + Date: GraphQLDateTime, + UploadGQL: GraphQLUpload } module.exports = resolvers diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index 134734a7..bbb90aa0 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -12,6 +12,8 @@ const typeDef = gql` authorizedOverride: String daysSuspended: Int isSuspended: Boolean + newPhoto: UploadGQL + photoType: String frontCameraPath: String frontCameraAt: Date frontCameraOverride: String @@ -21,6 +23,7 @@ const typeDef = gql` idCardData: JSONObject idCardDataOverride: String idCardDataExpiration: Date + idCardPhoto: UploadGQL idCardPhotoPath: String idCardPhotoOverride: String usSsn: String @@ -65,6 +68,12 @@ const typeDef = gql` subscriberInfo: Boolean } + input CustomerEdit { + idCardData: JSONObject + idCardPhoto: UploadGQL + usSsn: String + } + type Query { customers(phone: String, name: String, address: String, id: String): [Customer] @auth customer(customerId: ID!): Customer @auth @@ -76,6 +85,9 @@ const typeDef = gql` addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth + editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth + deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth + replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth } ` diff --git a/lib/new-admin/graphql/types/scalar.type.js b/lib/new-admin/graphql/types/scalar.type.js index 693adccb..c872c1d1 100644 --- a/lib/new-admin/graphql/types/scalar.type.js +++ b/lib/new-admin/graphql/types/scalar.type.js @@ -4,6 +4,7 @@ const typeDef = gql` scalar JSON scalar JSONObject scalar Date + scalar UploadGQL ` module.exports = typeDef diff --git a/migrations/1635159374499-editable-customer-data.js b/migrations/1635159374499-editable-customer-data.js new file mode 100644 index 00000000..c77ba5e8 --- /dev/null +++ b/migrations/1635159374499-editable-customer-data.js @@ -0,0 +1,33 @@ +const db = require('./db') + +exports.up = function (next) { + var sql = [ + `CREATE TABLE edited_customer_data ( + customer_id uuid PRIMARY KEY REFERENCES customers(id), + id_card_data JSON, + id_card_data_at TIMESTAMPTZ, + id_card_data_by UUID REFERENCES users(id), + front_camera_path TEXT, + front_camera_at TIMESTAMPTZ, + front_camera_by UUID REFERENCES users(id), + id_card_photo_path TEXT, + id_card_photo_at TIMESTAMPTZ, + id_card_photo_by UUID REFERENCES users(id), + subscriber_info JSON, + subscriber_info_at TIMESTAMPTZ, + subscriber_info_by UUID REFERENCES users(id), + name TEXT, + name_at TIMESTAMPTZ, + name_by UUID REFERENCES users(id), + us_ssn TEXT, + us_ssn_at TIMESTAMPTZ, + us_ssn_by UUID REFERENCES users(id), + created TIMESTAMPTZ NOT NULL DEFAULT now() )` + ] + + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/package-lock.json b/new-lamassu-admin/package-lock.json index 140b59b1..3c21f1c3 100644 --- a/new-lamassu-admin/package-lock.json +++ b/new-lamassu-admin/package-lock.json @@ -6888,6 +6888,14 @@ "tslib": "^1.9.3" } }, + "apollo-upload-client": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz", + "integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==", + "requires": { + "extract-files": "^11.0.0" + } + }, "apollo-utilities": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", @@ -12608,6 +12616,11 @@ } } }, + "extract-files": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", + "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==" + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", diff --git a/new-lamassu-admin/package.json b/new-lamassu-admin/package.json index ff436eb3..5c2d5569 100644 --- a/new-lamassu-admin/package.json +++ b/new-lamassu-admin/package.json @@ -14,6 +14,7 @@ "apollo-link": "^1.2.14", "apollo-link-error": "^1.1.13", "apollo-link-http": "^1.5.17", + "apollo-upload-client": "^16.0.0", "axios": "0.21.1", "base-64": "^1.0.0", "bignumber.js": "9.0.0", diff --git a/new-lamassu-admin/src/components/buttons/ActionButton.js b/new-lamassu-admin/src/components/buttons/ActionButton.js index 264adac8..02d7d107 100644 --- a/new-lamassu-admin/src/components/buttons/ActionButton.js +++ b/new-lamassu-admin/src/components/buttons/ActionButton.js @@ -12,7 +12,9 @@ const ActionButton = memo( const classNames = { [classes.actionButton]: true, [classes.primary]: color === 'primary', - [classes.secondary]: color === 'secondary' + [classes.secondary]: color === 'secondary', + [classes.spring]: color === 'spring', + [classes.tomato]: color === 'tomato' } return ( diff --git a/new-lamassu-admin/src/components/buttons/ActionButton.styles.js b/new-lamassu-admin/src/components/buttons/ActionButton.styles.js index a775f811..f924347a 100644 --- a/new-lamassu-admin/src/components/buttons/ActionButton.styles.js +++ b/new-lamassu-admin/src/components/buttons/ActionButton.styles.js @@ -1,11 +1,17 @@ import typographyStyles from 'src/components/typography/styles' import { white, - fontColor, subheaderColor, subheaderDarkColor, offColor, - offDarkColor + offDarkColor, + offDarkerColor, + secondaryColor, + secondaryColorDark, + secondaryColorDarker, + errorColor, + errorColorDark, + errorColorDarker } from 'src/styling/variables' const { p } = typographyStyles @@ -50,10 +56,45 @@ export default { } }, secondary: { - extend: colors(offColor, offDarkColor, white), + extend: colors(offColor, offDarkColor, offDarkerColor), + color: white, + '&:active': { + '& $actionButtonIcon': { + display: 'flex' + }, + '& $actionButtonIconActive': { + display: 'none' + } + }, + '& $actionButtonIcon': { + display: 'none' + }, + '& $actionButtonIconActive': { + display: 'flex' + } + }, + spring: { + extend: colors(secondaryColorDark, secondaryColor, secondaryColorDarker), + color: white, + '&:active': { + '& $actionButtonIcon': { + display: 'flex' + }, + '& $actionButtonIconActive': { + display: 'none' + } + }, + '& $actionButtonIcon': { + display: 'none' + }, + '& $actionButtonIconActive': { + display: 'flex' + } + }, + tomato: { + extend: colors(errorColorDark, errorColor, errorColorDarker), color: white, '&:active': { - color: fontColor, '& $actionButtonIcon': { display: 'flex' }, diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index bb034bbd..b9eb837a 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -31,7 +31,7 @@ import { getName } from './helper.js' const useStyles = makeStyles(styles) const IMAGE_WIDTH = 165 -const IMAGE_HEIGHT = 45 +const IMAGE_HEIGHT = 32 const POPUP_IMAGE_WIDTH = 360 const POPUP_IMAGE_HEIGHT = 240 @@ -57,7 +57,12 @@ const Photo = ({ show, src }) => { ) } -const CustomerData = ({ customer, updateCustomer }) => { +const CustomerData = ({ + customer, + updateCustomer, + editCustomer, + deleteEditedData +}) => { const classes = useStyles() const [listView, setListView] = useState(false) @@ -75,11 +80,14 @@ const CustomerData = ({ customer, updateCustomer }) => { : 'Failed' const customEntries = null // get customer custom entries + const customRequirements = null // get customer custom requirements const isEven = elem => elem % 2 === 0 const getVisibleCards = _.filter( - elem => !_.isEmpty(elem.data) || !_.isNil(elem.children) + elem => + !_.isEmpty(elem.fields) || + (!_.isNil(elem.children) && !_.isNil(elem.state)) ) const getAvailableFields = _.filter(({ value }) => value !== '') @@ -96,6 +104,12 @@ const CustomerData = ({ customer, updateCustomer }) => { }), usSsn: Yup.object().shape({ usSsn: Yup.string() + }), + idCardPhoto: Yup.object().shape({ + idCardPhoto: Yup.mixed() + }), + frontCamera: Yup.object().shape({ + frontCamera: Yup.mixed() }) } @@ -103,96 +117,98 @@ const CustomerData = ({ customer, updateCustomer }) => { { name: 'name', label: 'Name', - value: `${getName(customer)}`, component: TextInput }, { name: 'idNumber', label: 'ID number', - value: R.path(['documentNumber'])(idData) ?? '', component: TextInput }, { name: 'birthDate', label: 'Birth Date', - value: - (rawDob && - format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ?? - '', component: TextInput }, { name: 'age', label: 'Age', - value: - (rawDob && - differenceInYears( - parse(new Date(), 'yyyyMMdd', rawDob), - new Date() - )) ?? - '', component: TextInput }, { name: 'gender', label: 'Gender', - value: R.path(['gender'])(idData) ?? '', component: TextInput }, { name: 'state', label: country === 'Canada' ? 'Province' : 'State', - value: R.path(['state'])(idData) ?? '', component: TextInput }, { name: 'expirationDate', label: 'Expiration Date', - value: - (rawExpirationDate && - format('yyyy-MM-dd')( - parse(new Date(), 'yyyyMMdd', rawExpirationDate) - )) ?? - '', component: TextInput } ] const usSsnElements = [ { - name: 'us ssn', + name: 'usSsn', label: 'US SSN', - value: `${customer.usSsn ?? ''}`, component: TextInput, size: 190 } ] + const idCardPhotoElements = [{ name: 'idCardPhoto' }] + const frontCameraElements = [{ name: 'frontCamera' }] + const initialValues = { idScan: { - name: '', - idNumber: '', - birthDate: '', - age: '', - gender: '', - state: '', - expirationDate: '' + name: getName(customer) ?? '', + idNumber: R.path(['documentNumber'])(idData) ?? '', + birthDate: + (rawDob && + format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ?? + '', + age: + (rawDob && + differenceInYears( + parse(new Date(), 'yyyyMMdd', rawDob), + new Date() + )) ?? + '', + gender: R.path(['gender'])(idData) ?? '', + state: R.path(['state'])(idData) ?? '', + expirationDate: + (rawExpirationDate && + format('yyyy-MM-dd')( + parse(new Date(), 'yyyyMMdd', rawExpirationDate) + )) ?? + '' }, usSsn: { - usSsn: '' + usSsn: customer.usSsn ?? '' + }, + frontCamera: { + frontCamera: null + }, + idCardPhoto: { + idCardPhoto: null } } const cards = [ { - data: getAvailableFields(idScanElements), + fields: getAvailableFields(idScanElements), title: 'ID Scan', titleIcon: , state: R.path(['idCardDataOverride'])(customer), authorize: () => updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED }), reject: () => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED }), - save: values => console.log(values), + deleteEditedData: () => deleteEditedData({ idCardData: null }), + save: values => editCustomer({ idCardData: values }), validationSchema: schemas.idScan, initialValues: initialValues.idScan }, @@ -217,17 +233,18 @@ const CustomerData = ({ customer, updateCustomer }) => { authorize: () => updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }), reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }), - save: () => {}, children: {sanctionsDisplay} }, { + fields: getAvailableFields(frontCameraElements), title: 'Front facing camera', titleIcon: , state: R.path(['frontCameraOverride'])(customer), authorize: () => updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }), reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }), - save: () => {}, + save: values => editCustomer({ frontCamera: values.frontCamera }), + deleteEditedData: () => deleteEditedData({ frontCamera: null }), children: customer.frontCameraPath ? ( { customer )}`} /> - ) : null + ) : null, + hasImage: true, + validationSchema: schemas.frontCamera, + initialValues: initialValues.frontCamera }, { + fields: getAvailableFields(idCardPhotoElements), title: 'ID card image', titleIcon: , state: R.path(['idCardPhotoOverride'])(customer), authorize: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }), reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }), - save: () => {}, + save: values => editCustomer({ idCardPhoto: values.idCardPhoto }), + deleteEditedData: () => deleteEditedData({ idCardPhoto: null }), children: customer.idCardPhotoPath ? ( - ) : null + ) : null, + hasImage: true, + validationSchema: schemas.idCardPhoto, + initialValues: initialValues.idCardPhoto }, { - data: getAvailableFields(usSsnElements), + fields: getAvailableFields(usSsnElements), title: 'US SSN', titleIcon: , state: R.path(['usSsnOverride'])(customer), authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }), reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }), - save: () => {}, + save: values => editCustomer({ usSsn: values.usSsn }), + deleteEditedData: () => deleteEditedData({ usSsn: null }), validationSchema: schemas.usSsn, initialValues: initialValues.usSsn } @@ -272,11 +298,13 @@ const CustomerData = ({ customer, updateCustomer }) => { reject, state, titleIcon, - data, + fields, save, + deleteEditedData, children, validationSchema, - initialValues + initialValues, + hasImage }, idx ) => { @@ -288,11 +316,13 @@ const CustomerData = ({ customer, updateCustomer }) => { reject={reject} state={state} titleIcon={titleIcon} - data={data} + hasImage={hasImage} + fields={fields} children={children} validationSchema={validationSchema} initialValues={initialValues} - save={save}> + save={save} + deleteEditedData={deleteEditedData}> ) } @@ -317,7 +347,7 @@ const CustomerData = ({ customer, updateCustomer }) => { onClick={() => setListView(true)}>
- {!listView && ( + {!listView && customer && ( {visibleCards.map((elem, idx) => { @@ -333,7 +363,12 @@ const CustomerData = ({ customer, updateCustomer }) => { )} {customEntries && (
-
{'Custom data entry'}
+ Custom data entry +
+ )} + {customRequirements && ( +
+ Custom requirements
)}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.styles.js b/new-lamassu-admin/src/pages/Customers/CustomerData.styles.js index 5977bbfe..373e2f0c 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.styles.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.styles.js @@ -20,22 +20,30 @@ export default { marginRight: 12 }, wrapper: { - display: 'flex' + display: 'block', + overflow: 'hidden', + whiteSpace: 'nowrap' }, separator: { - display: 'flex', - flexBasis: '100%', - justifyContent: 'center', color: offColor, - margin: [[8, 0, 8, 0]], - '&::before, &::after': { - content: '', - flexGrow: 1, + margin: [[8, 0, 8, 150]], + position: 'relative', + display: 'inline-block', + '&:before, &:after': { + content: '""', + position: 'absolute', background: offColor, - height: 1, - fontSize: 1, - lineHeight: 0, - margin: [[0, 8, 0, 8]] + top: '50%', + width: 1000, + height: 1 + }, + '&:before': { + right: '100%', + marginRight: 15 + }, + '&:after': { + left: '100%', + marginLeft: 15 } } } diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index e197416b..6b083eb5 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -116,6 +116,45 @@ const SET_CUSTOMER = gql` } } ` +const EDIT_CUSTOMER = gql` + mutation editCustomer($customerId: ID!, $customerEdit: CustomerEdit) { + editCustomer(customerId: $customerId, customerEdit: $customerEdit) { + id + idCardData + usSsn + } + } +` + +const REPLACE_CUSTOMER_PHOTO = gql` + mutation replacePhoto( + $customerId: ID! + $photoType: String + $newPhoto: Upload + ) { + replacePhoto( + customerId: $customerId + photoType: $photoType + newPhoto: $newPhoto + ) { + id + newPhoto + photoType + } + } +` + +const DELETE_EDITED_CUSTOMER = gql` + mutation deleteEditedData($customerId: ID!, $customerEdit: CustomerEdit) { + deleteEditedData(customerId: $customerId, customerEdit: $customerEdit) { + id + frontCameraPath + idCardData + idCardPhotoPath + usSsn + } + } +` const CustomerProfile = memo(() => { const history = useHistory() @@ -133,6 +172,18 @@ const CustomerProfile = memo(() => { } ) + const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, { + onCompleted: () => getCustomer() + }) + + const [editCustomerData] = useMutation(EDIT_CUSTOMER, { + onCompleted: () => getCustomer() + }) + + const [deleteCustomerEditedData] = useMutation(DELETE_EDITED_CUSTOMER, { + onCompleted: () => getCustomer() + }) + const [setCustomer] = useMutation(SET_CUSTOMER, { onCompleted: () => getCustomer() }) @@ -145,6 +196,31 @@ const CustomerProfile = memo(() => { } }) + const replacePhoto = it => + replaceCustomerPhoto({ + variables: { + customerId, + newPhoto: it.newPhoto, + photoType: it.photoType + } + }) + + const editCustomer = it => + editCustomerData({ + variables: { + customerId, + customerEdit: it + } + }) + + const deleteEditedData = it => + deleteCustomerEditedData({ + variables: { + customerId, + customerEdit: it + } + }) + const onClickSidebarItem = code => setClickedItem(code) const configData = R.path(['config'])(customerResponse) ?? [] @@ -248,6 +324,7 @@ const CustomerProfile = memo(() => { {
+ updateCustomer={updateCustomer} + replacePhoto={replacePhoto} + editCustomer={editCustomer} + deleteEditedData={deleteEditedData}>
)} diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js index 6326b300..59827865 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.styles.js @@ -33,6 +33,12 @@ export default { margin: [[8, 0, 4, 0]], padding: [[0, 40.5, 0]] }, + retrieveInformation: { + display: 'flex', + flexDirection: 'row', + margin: [[0, 0, 4, 0]], + padding: [[0, 32.5, 0]] + }, panels: { display: 'flex' }, diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js index 2d967f3d..87ef5087 100644 --- a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js +++ b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.styles.js @@ -30,10 +30,10 @@ export default { color: white, backgroundColor: offDarkColor, '&:first-child': { - borderRadius: [5, 5, 0, 0] + borderRadius: [[5, 5, 0, 0]] }, '&:last-child': { - borderRadius: [0, 0, 5, 5] + borderRadius: [[0, 0, 5, 5]] } }, icon: { diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js index a97023dc..0239db8a 100644 --- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js +++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js @@ -2,6 +2,7 @@ import { CardContent, Card, Grid } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import classnames from 'classnames' import { Form, Formik, Field as FormikField } from 'formik' +import * as R from 'ramda' import { useState, React } from 'react' import ErrorMessage from 'src/components/ErrorMessage' @@ -9,20 +10,21 @@ import PromptWhenDirty from 'src/components/PromptWhenDirty' import { MainStatus } from 'src/components/Status' import { Tooltip } from 'src/components/Tooltip' import { ActionButton } from 'src/components/buttons' -import { Label1, Info3, H3 } from 'src/components/typography' +import { Label1, P, H3 } from 'src/components/typography' import { OVERRIDE_AUTHORIZED, OVERRIDE_REJECTED, OVERRIDE_PENDING } from 'src/pages/Customers/components/propertyCard' +import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' +import { ReactComponent as DeleteReversedIcon } from 'src/styling/icons/action/delete/white.svg' import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' import { ReactComponent as EditReversedIcon } from 'src/styling/icons/action/edit/white.svg' -import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg' -import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg' +import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/white.svg' +import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/white.svg' import { ReactComponent as CancelReversedIcon } from 'src/styling/icons/button/cancel/white.svg' -import { ReactComponent as CancelIcon } from 'src/styling/icons/button/cancel/zodiac.svg' +import { ReactComponent as ReplaceReversedIcon } from 'src/styling/icons/button/replace/white.svg' import { ReactComponent as SaveReversedIcon } from 'src/styling/icons/circle buttons/save/white.svg' -import { ReactComponent as SaveIcon } from 'src/styling/icons/circle buttons/save/zodiac.svg' import { comet } from 'src/styling/variables' import styles from './EditableCard.styles.js' @@ -34,7 +36,8 @@ const fieldStyles = { position: 'relative', width: 280, height: 48, - padding: [[0, 4, 4, 0]] + padding: [[0, 4, 4, 0]], + marginTop: 2 }, label: { color: comet, @@ -60,7 +63,8 @@ const fieldStyles = { editing: { '& > div': { '& > input': { - padding: 0 + padding: 0, + fontSize: 14 } } } @@ -68,9 +72,8 @@ const fieldStyles = { const fieldUseStyles = makeStyles(fieldStyles) -const EditableField = ({ editing, field, size, ...props }) => { +const EditableField = ({ editing, field, value, size, ...props }) => { const classes = fieldUseStyles() - const classNames = { [classes.field]: true, [classes.notEditing]: !editing @@ -81,7 +84,7 @@ const EditableField = ({ editing, field, size, ...props }) => { {!editing && ( <> {field.label} - {field.value} +

{value}

)} {editing && ( @@ -90,8 +93,8 @@ const EditableField = ({ editing, field, size, ...props }) => { { } const EditableCard = ({ - data, + fields, save, authorize, + hasImage, reject, state, title, titleIcon, - children + children, + validationSchema, + initialValues, + deleteEditedData }) => { const classes = useStyles() const [editing, setEditing] = useState(false) + const [input, setInput] = useState(null) const [error, setError] = useState(null) + const triggerInput = () => input.click() + const label1ClassNames = { [classes.label1]: true, [classes.label1Pending]: state === OVERRIDE_PENDING, [classes.label1Rejected]: state === OVERRIDE_REJECTED, [classes.label1Accepted]: state === OVERRIDE_AUTHORIZED } - const authorized = state === OVERRIDE_PENDING ? { label: 'Pending', type: 'neutral' } @@ -131,111 +140,176 @@ const EditableCard = ({ ? { label: 'Rejected', type: 'error' } : { label: 'Accepted', type: 'success' } - const editableField = field => { - return - } - return (
-
- {titleIcon} -

{title}

- -
- +
+
+ {titleIcon} +

{title}

+
+ {state && ( +
+ +
+ )}
+ {children} save(values)} + validationSchema={validationSchema} + initialValues={initialValues} + onSubmit={values => { + save(values) + setEditing(false) + }} onReset={() => { setEditing(false) setError(false) }}> -
- -
- - - {data?.map((field, idx) => { - return idx >= 0 && idx < 4 ? editableField(field) : null - })} + {({ values, touched, errors, setFieldValue }) => ( + + +
+ + + {!hasImage && + fields?.map((field, idx) => { + return idx >= 0 && idx < 4 ? ( + + ) : null + })} + + + {!hasImage && + fields?.map((field, idx) => { + return idx >= 4 ? ( + + ) : null + })} + - - {data?.map((field, idx) => { - return idx >= 4 ? editableField(field) : null - })} - - -
- {children} -
- {!editing && ( -
- setEditing(true)}> - {`Edit`} - -
- )} - {editing && ( -
- {data && ( -
+
+
+ {!editing && ( +
+
- Save + color="primary" + type="button" + Icon={DeleteIcon} + InverseIcon={DeleteReversedIcon} + onClick={() => deleteEditedData()}> + {`Delete`}
- )} -
+ - Cancel + color="primary" + Icon={EditIcon} + InverseIcon={EditReversedIcon} + onClick={() => setEditing(true)}> + {`Edit`}
- {authorized.label !== 'Accepted' && ( -
- authorize()}> - {'Authorize'} - + )} + {editing && ( +
+
+ {hasImage && ( + triggerInput()}> + { +
+ setInput(fileInput)} + onChange={event => { + // need to store it locally if we want to display it even after saving to db + const file = R.head(event.target.files) + if (!file) return + setFieldValue(R.head(fields).name, file) + }} + /> + Replace +
+ } +
+ )}
- )} - {authorized.label !== 'Rejected' && ( - reject()}> - {'Reject'} - - )} - {error && ( - Failed to save changes - )} -
- )} -
- +
+ {fields && ( +
+ + Save + +
+ )} +
+ + Cancel + +
+ {authorized.label !== 'Accepted' && ( +
+ authorize()}> + Authorize + +
+ )} + {authorized.label !== 'Rejected' && ( + reject()}> + Reject + + )} + {error && ( + Failed to save changes + )} +
+
+ )} +
+ + )} diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.styles.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.styles.js index 1c2a8603..3ef803d2 100644 --- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.styles.js +++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.styles.js @@ -16,15 +16,35 @@ export default { color: spring4 }, editButton: { - marginTop: 30, + marginTop: 20, display: 'flex', justifyContent: 'right' }, - button: { + deleteButton: { marginRight: 8 }, + headerWrapper: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + height: 40 + }, + editingWrapper: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20 + }, + replace: { + marginRight: 5 + }, + input: { + display: 'none' + }, + button: { + marginRight: 5 + }, editingButtons: { - marginTop: 30, display: 'flex', justifyContent: 'right' }, diff --git a/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js b/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js index 9bbd35de..8b55208c 100644 --- a/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js +++ b/new-lamassu-admin/src/pages/Customers/components/TransactionsList.js @@ -75,7 +75,6 @@ const TransactionsList = ({ customer, data, loading, locale }) => { view: R.path(['machineName']) }, { - header: 'Direction', width: 125, view: it => ( <> diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js index 5c27fb7f..2cb84f7f 100644 --- a/new-lamassu-admin/src/styling/variables.js +++ b/new-lamassu-admin/src/styling/variables.js @@ -7,6 +7,7 @@ const spring = '#48f694' // Secondary const comet = '#5f668a' const comet2 = '#72799d' +const comet3 = '#525772' const spring2 = '#44e188' const spring3 = '#ecfbef' const spring4 = '#3fd07e' @@ -25,6 +26,8 @@ const white = '#ffffff' // Error const tomato = '#ff584a' +const tomato1 = '#E45043' +const tomato2 = '#CE463A' const mistyRose = '#ffeceb' const pumpkin = '#ff7311' const linen = '#fbf3ec' @@ -45,8 +48,11 @@ const disabledColor2 = concrete const fontColor = primaryColor const offColor = comet const offDarkColor = comet2 +const offDarkerColor = comet3 const placeholderColor = comet const errorColor = tomato +const errorColorDark = tomato1 +const errorColorDarker = tomato2 const offErrorColor = mistyRose const inputBorderColor = primaryColor @@ -142,12 +148,15 @@ export { placeholderColor, offColor, offDarkColor, + offDarkerColor, fontColor, disabledColor, disabledColor2, linkPrimaryColor, linkSecondaryColor, errorColor, + errorColorDarker, + errorColorDark, offErrorColor, inputBorderColor, // font sizes diff --git a/new-lamassu-admin/src/utils/apollo.js b/new-lamassu-admin/src/utils/apollo.js index b04b9b69..1921cdcf 100644 --- a/new-lamassu-admin/src/utils/apollo.js +++ b/new-lamassu-admin/src/utils/apollo.js @@ -3,7 +3,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory' import { ApolloClient } from 'apollo-client' import { ApolloLink } from 'apollo-link' import { onError } from 'apollo-link-error' -import { HttpLink } from 'apollo-link-http' +import { createUploadLink } from 'apollo-upload-client' import React, { useContext } from 'react' import { useHistory, useLocation } from 'react-router-dom' @@ -15,6 +15,16 @@ const URI = const ALT_URI = process.env.NODE_ENV === 'development' ? 'http://localhost:4001' : '' +const uploadLink = createUploadLink({ + credentials: 'include', + uri: `${URI}/graphql` +}) + +const uploadLinkALT = createUploadLink({ + credentials: 'include', + uri: `${ALT_URI}/graphql` +}) + const getClient = (history, location, getUserData, setUserData, setRole) => new ApolloClient({ link: ApolloLink.from([ @@ -48,14 +58,8 @@ const getClient = (history, location, getUserData, setUserData, setRole) => }), ApolloLink.split( operation => operation.getContext().clientName === 'pazuz', - new HttpLink({ - credentials: 'include', - uri: `${ALT_URI}/graphql` - }), - new HttpLink({ - credentials: 'include', - uri: `${URI}/graphql` - }) + uploadLinkALT, + uploadLink ) ]), cache: new InMemoryCache(), diff --git a/package-lock.json b/package-lock.json index 0e4cd28b..5092830b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11479,6 +11479,30 @@ "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==" }, + "graphql-upload": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-12.0.0.tgz", + "integrity": "sha512-ovZ3Q7sZ17Bmn8tYl22MfrpNR7nYM/DUszXWgkue7SFIlI9jtqszHAli8id8ZcnGBc9GF0gUTNSskYWW+5aNNQ==", + "requires": { + "busboy": "^0.3.1", + "fs-capacitor": "^6.2.0", + "http-errors": "^1.8.0", + "isobject": "^4.0.0", + "object-path": "^0.11.5" + }, + "dependencies": { + "fs-capacitor": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-6.2.0.tgz", + "integrity": "sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw==" + }, + "isobject": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" + } + } + }, "graphql-ws": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.1.0.tgz", diff --git a/package.json b/package.json index 3f94362e..030bf13b 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "graphql-iso-date": "^3.6.1", "graphql-tools": "^7.0.2", "graphql-type-json": "^0.3.1", + "graphql-upload": "12.0.0", "helmet": "^3.8.1", "inquirer": "^5.2.0", "json2csv": "^5.0.3",