Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch product listing (photos) basic flow #16

Merged
merged 42 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
384a1e9
Upgrade React to v18.2.0
marcusvx Aug 16, 2024
3335f29
Add craco to allow custom webpack configurations
marcusvx Aug 17, 2024
8b4a19f
Create Batch listing edit component
marcusvx Sep 14, 2024
b953f05
Cleanup of unucessary tabs and feature from the new batch product lis…
marcusvx Sep 14, 2024
c66aab3
Reorganize batch product listing components
marcusvx Sep 14, 2024
f6503ee
Basic functionality of the Product Details table integrated with tabs
marcusvx Sep 21, 2024
792498f
Implement read of images metadata with exifr
marcusvx Sep 28, 2024
2aaea72
Fix failing tests and remove outdated tests
marcusvx Sep 28, 2024
7f670eb
Finish implmementing tag input component for batch product listing
marcusvx Sep 29, 2024
9663a10
Remove dead code
marcusvx Oct 5, 2024
198d1af
Start implementation of redux connection for BatchEditListingPage
marcusvx Oct 9, 2024
ff57002
Refactor most of the logic of BatchEditListingPage to redux
marcusvx Oct 14, 2024
522506d
Change uppy store to redux store. Initial upload process implemented
marcusvx Oct 17, 2024
11713a1
Add modal warning about listing validation and ai terms when saving
marcusvx Oct 19, 2024
c659e8f
Finished upload process
marcusvx Oct 23, 2024
8a23035
General layout improvements: make headers and table header sticky
marcusvx Oct 26, 2024
a28c9a4
Upgrade React to v18.2.0
marcusvx Aug 16, 2024
23b4e7a
Add craco to allow custom webpack configurations
marcusvx Aug 17, 2024
affff7b
Create Batch listing edit component
marcusvx Sep 14, 2024
b7961cf
Cleanup of unucessary tabs and feature from the new batch product lis…
marcusvx Sep 14, 2024
69864b3
Reorganize batch product listing components
marcusvx Sep 14, 2024
0e61758
Basic functionality of the Product Details table integrated with tabs
marcusvx Sep 21, 2024
74f7768
Implement read of images metadata with exifr
marcusvx Sep 28, 2024
066ed76
Fix failing tests and remove outdated tests
marcusvx Sep 28, 2024
e61a5a6
Finish implmementing tag input component for batch product listing
marcusvx Sep 29, 2024
397e547
Remove dead code
marcusvx Oct 5, 2024
2a3545e
Start implementation of redux connection for BatchEditListingPage
marcusvx Oct 9, 2024
3df8030
Refactor most of the logic of BatchEditListingPage to redux
marcusvx Oct 14, 2024
37f3f87
Change uppy store to redux store. Initial upload process implemented
marcusvx Oct 17, 2024
3556148
Add modal warning about listing validation and ai terms when saving
marcusvx Oct 19, 2024
9449328
Finished upload process
marcusvx Oct 23, 2024
cc2551c
General layout improvements: make headers and table header sticky
marcusvx Oct 26, 2024
e531b98
General UI improvments for the editable listing table
marcusvx Oct 27, 2024
14b8da9
Merge branch 'feature/uppy-upload-products' of github.com:theluupe/ma…
marcusvx Oct 27, 2024
500d135
Fix row saving on batch edit listing. Select all rows by default
marcusvx Oct 27, 2024
822a452
Update batch listing creation to use server events to handle original…
marcusvx Oct 30, 2024
eebbe0f
Localization of hard coded strings/ Ant theme with markeplace styles
marcusvx Nov 1, 2024
d7f05cb
Batch edit listing result improvements.
marcusvx Nov 2, 2024
ae80117
Create navigation flows between BatchEditListingPage and ManageListin…
marcusvx Nov 5, 2024
f2756e6
Merge branch 'dev' into feature/uppy-upload-products
marcusvx Nov 5, 2024
588623d
Remove unused packages/components
marcusvx Nov 5, 2024
464a00a
Several fixes and improvements
marcusvx Nov 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ build
.env.*.local
npm-debug.log
yarn-error.log
.idea
25 changes: 25 additions & 0 deletions craco.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { InjectManifest } = require('workbox-webpack-plugin');

module.exports = {
reactScriptsVersion: 'sharetribe-scripts',
webpack: {
configure: {
module: {
rules: [
{
test: /\.m?js$/,
marcusvx marked this conversation as resolved.
Show resolved Hide resolved
resolve: {
fullySpecified: false,
},
},
],
},
},
plugins: [
new InjectManifest({
marcusvx marked this conversation as resolved.
Show resolved Hide resolved
swSrc: './src/sw.js',
swDest: 'sw.js',
}),
],
},
};
41 changes: 34 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"license": "Apache-2.0",
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@babel/runtime": "^7.17.9",
"@google-cloud/secret-manager": "^5.6.0",
"@loadable/component": "^5.16.4",
Expand All @@ -12,6 +13,23 @@
"@sentry/browser": "^8.26.0",
"@sentry/node": "^8.26.0",
"@slack/web-api": "^7.2.0",
"@tanstack/react-table": "^8.20.5",
"@uppy/box": "^3.0.0",
"@uppy/core": "^4.1.0",
"@uppy/dashboard": "^4.0.2",
"@uppy/drag-drop": "^4.0.1",
"@uppy/dropbox": "^4.0.0",
"@uppy/file-input": "^4.0.0",
"@uppy/golden-retriever": "^4.0.0",
"@uppy/google-drive": "^4.0.0",
"@uppy/onedrive": "^4.0.0",
"@uppy/progress-bar": "^4.0.0",
"@uppy/react": "^4.0.1",
"@uppy/remote-sources": "^2.1.0",
"@uppy/store-redux": "^4.0.0",
"@uppy/thumbnail-generator": "^4.0.0",
"@uppy/transloadit": "^4.0.1",
"@uppy/url": "^4.0.0",
"antd": "^5.21.6",
"auth0": "^4.8.0",
"autosize": "^5.0.1",
Expand All @@ -26,6 +44,7 @@
"decimal.js": "^10.4.3",
"dotenv": "^10.0.0",
"dotenv-expand": "^5.1.0",
"exifr": "^7.1.3",
"express": "^4.19.2",
"express-enforces-ssl": "^1.1.0",
"express-openid-connect": "^2.17.1",
Expand All @@ -44,8 +63,9 @@
"prop-types": "^15.8.1",
"query-string": "^7.1.1",
"raf": "^3.4.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "18.2.0",
"react-dates": "^21.8.0",
marcusvx marked this conversation as resolved.
Show resolved Hide resolved
"react-dom": "18.2.0",
"react-fast-marquee": "^1.6.5",
"react-final-form": "6.5.9",
"react-final-form-arrays": "3.1.3",
Expand All @@ -55,6 +75,7 @@
"react-moment-proptypes": "^1.8.1",
"react-redux": "^7.2.8",
"react-router-dom": "^5.3.2",
"react-select": "^5.8.1",
"react-with-direction": "^1.4.0",
"redux": "^4.2.0",
"redux-thunk": "^2.4.1",
Expand All @@ -69,10 +90,12 @@
"sitemap": "^7.1.1",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.21",
"transloadit": "^3.0.2",
"unified": "^9.2.2",
"url": "^0.11.0"
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
Expand All @@ -82,7 +105,9 @@
"cross-env": "^7.0.3",
"inquirer": "^8.2.4",
"nodemon": "^3.1.4",
"prettier": "^1.18.2"
"prettier": "^1.18.2",
"workbox-precaching": "^7.1.0",
"workbox-webpack-plugin": "^7.1.0"
},
"resolutions": {
"moment": "^2.30.1"
Expand All @@ -102,17 +127,19 @@
"clean": "rm -rf build/*",
"config": "node scripts/config.js",
"config-check": "node scripts/config.js --check",
"dev-frontend": "sharetribe-scripts start",
"dev-frontend": "craco start",
"dev-backend": "nodemon server/apiServer.js",
"dev-backend:debug": "nodemon --inspect server/apiServer.js",
"dev": "yarn run config-check&&cross-env NODE_ENV=development REACT_APP_DEV_API_SERVER_PORT=3500 concurrently --kill-others \"yarn run dev-frontend\" \"yarn run dev-backend\"",
"dev:debug": "yarn run config-check&&cross-env NODE_ENV=development REACT_APP_DEV_API_SERVER_PORT=3500 concurrently --kill-others \"yarn run dev-frontend\" \"yarn run dev-backend:debug\"",
"build": "yarn build-web&&yarn build-server",
"build-web": "sharetribe-scripts build",
"build-web": "craco build",
"build-server": "sharetribe-scripts build-server",
"format": "prettier --write '**/*.{js,css}'",
"format-ci": "prettier --list-different '**/*.{js,css}'",
"format-docs": "prettier --write '**/*.md'",
"test": "NODE_ICU_DATA=node_modules/full-icu sharetribe-scripts test",
"test-ci": "yarn run test-server --runInBand && sharetribe-scripts test --runInBand",
"test": "NODE_ICU_DATA=node_modules/full-icu craco test",
"test-ci": "yarn run test-server --runInBand && craco test --runInBand",
"eject": "sharetribe-scripts eject",
"start": "node --icu-data-dir=node_modules/full-icu server/index.js",
"dev-server": "cross-env-shell NODE_ENV=development PORT=4000 REACT_APP_MARKETPLACE_ROOT_URL=http://localhost:4000 \"yarn run build&&nodemon --watch server server/index.js\"",
Expand Down
18 changes: 18 additions & 0 deletions server/api-util/httpHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const https = require('node:https');

const httpFileUrlToStream = url => {
return new Promise((resolve, reject) => {
https
.get(url, res => {
if (res.statusCode !== 200) {
reject(new Error(`Failed to get image. Status code: ${res.statusCode}`));
}
resolve(res); // `res` is the readable stream here
})
.on('error', reject);
});
};

module.exports = {
httpFileUrlToStream,
};
46 changes: 46 additions & 0 deletions server/api-util/httpHelpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { httpFileUrlToStream } = require('./httpHelpers');
const https = require('node:https');

jest.mock('node:https');

describe('httpFileUrlToStream', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should resolve with a stream if the request is successful (status code 200)', async () => {
const mockStream = { on: jest.fn((event, handler) => handler()), statusCode: 200 }; // Mock readable stream
https.get.mockImplementation((url, callback) => {
callback({ statusCode: 200, ...mockStream });
return { on: jest.fn() }; // Mock returned 'req' object from https.get
});

const result = await httpFileUrlToStream('https://example.com/image.jpg');
expect(result).toEqual(mockStream);
});

it('should reject with an error if the status code is not 200', async () => {
const mockStream = { on: jest.fn((event, handler) => handler()) };
https.get.mockImplementation((url, callback) => {
callback({ statusCode: 404, ...mockStream });
return { on: jest.fn() };
});

await expect(httpFileUrlToStream('https://example.com/image.jpg')).rejects.toThrow(
'Failed to get image. Status code: 404'
);
});

it('should reject if there is a network error', async () => {
const error = new Error('Network error');
https.get.mockImplementation(() => ({
on: jest.fn((event, handler) => {
if (event === 'error') handler(error);
}),
}));

await expect(httpFileUrlToStream('https://example.com/image.jpg')).rejects.toThrow(
'Network error'
);
});
});
35 changes: 35 additions & 0 deletions server/api-util/storageManagerHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const axios = require('axios');

const ASSET_UPLOAD_URL = '/assets/marketplace/original';

class StorageManagerClient {
client = axios.create({
baseURL: `${process.env.STORAGE_MANAGER_URL}/api`,
});

constructor() {
this.client.interceptors.request.use(config => {
config.headers['x-api-key'] = process.env.STORAGE_MANAGER_API_KEY;
return config;
}, Promise.reject);
}

async uploadOriginalAsset(userId, listingId, imageUrl, metadata = {}) {
try {
const response = await this.client.post(ASSET_UPLOAD_URL, {
userId,
listingId,
tempSslUrl: imageUrl,
metadata,
});
return response.data;
} catch (error) {
console.error('Failed to upload original asset:', error);
throw error;
}
}
}

module.exports = {
StorageManagerClient,
};
34 changes: 34 additions & 0 deletions server/api/transloadit-params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const Transloadit = require('transloadit');
const moment = require('moment');

module.exports = (req, res) => {
const { userId } = req.body;

const authKey = process.env.TRANSLOADIT_AUTH_KEY;
const transloadit = new Transloadit({
authKey,
authSecret: process.env.TRANSLOADIT_AUTH_SECRET,
});

const expires = moment
.utc()
.add(1, 'hour')
.format('YYYY/MM/DD HH:mm:ss Z');

const params = {
auth: {
key: authKey,
expires,
},
template_id: process.env.TRANSLOADIT_UPLOAD_LISTING_ASSETS_TEMPLATE_ID,
marcusvx marked this conversation as resolved.
Show resolved Hide resolved
fields: {
userId,
},
};

const signature = transloadit.calcSignature(params);
res
.status(200)
.send(signature)
.end();
};
2 changes: 2 additions & 0 deletions server/apiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { verifySlackRequestMiddleware, slackInteractivity } = require('./api/slac
const transitionPrivileged = require('./api/transition-privileged');
const createUserWithIdp = require('./api/auth/createUserWithIdp');
const { authenticateAuth0, authenticateAuth0Callback } = require('./api/auth/auth0');
const transloaditParams = require('./api/transloadit-params');

const router = express.Router();

Expand Down Expand Up @@ -58,6 +59,7 @@ router.get('/login-as', loginAs);
router.post('/transaction-line-items', transactionLineItems);
router.post('/initiate-privileged', initiatePrivileged);
router.post('/transition-privileged', transitionPrivileged);
router.post('/transloadit-params', transloaditParams);

// Create user with identity provider (e.g. Facebook or Google)
// This endpoint is called to create a new user after user has confirmed
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions server/scripts/events/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const notifyProfileListingUpdated = require('./notifyProfileListingUpdated');
const notifyUserCreated = require('./notifyUserCreated');
const notifyUserUpdated = require('./notifyUserUpdated');
const notifyProductListingCreated = require('./notifyProductListingCreated');

async function loadEventScripts() {
console.warn("\nLoading event's scripts..");
notifyProfileListingUpdated();
notifyUserCreated();
notifyUserUpdated();
notifyProductListingCreated();
console.warn("Loading event's scripts DONE\n");
}

Expand Down
52 changes: 52 additions & 0 deletions server/scripts/events/notifyProductListingCreated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const { generateScript, integrationSdkInit } = require('../../api-util/scriptManager');
const { StorageManagerClient } = require('../../api-util/storageManagerHelper');
const { httpFileUrlToStream } = require('../../api-util/httpHelpers');

const SCRIPT_NAME = 'notifyProductListingCreated';
const EVENT_TYPES = 'listing/created';
const RESOURCE_TYPE = 'listing';

const processEvent = async (integrationSdk, event, storageManagerClient) => {
const { resourceType, eventType, resourceId, resource } = event.attributes;
if (resourceType !== RESOURCE_TYPE || eventType !== EVENT_TYPES) return;

const { attributes: listing, relationships } = resource;
const userId = relationships?.author?.data?.id?.uuid;
const listingId = resourceId?.uuid;
const imageUrl = listing?.privateData?.transloaditSslUrl;

if (!imageUrl || !userId || !listingId) return;

try {
const originalAssetData = await storageManagerClient.uploadOriginalAsset(
userId,
listingId,
imageUrl
);
const imageStream = await httpFileUrlToStream(imageUrl);
const { data: sdkImage } = await integrationSdk.images.upload({ image: imageStream });

await integrationSdk.listings.update(
marcusvx marked this conversation as resolved.
Show resolved Hide resolved
{
id: listingId,
privateData: { originalAssetUrl: originalAssetData.source, transloaditSslUrl: null },
images: [sdkImage.data.id],
},
{ expand: true, include: ['images'] }
);
} catch (error) {
console.error('Error processing event:', error);
}
};

function script() {
const integrationSdk = integrationSdkInit();
const storageManagerClient = new StorageManagerClient();

const queryEvents = args => integrationSdk.events.query({ ...args, eventTypes: EVENT_TYPES });
const analyzeEvent = event => processEvent(integrationSdk, event, storageManagerClient);
marcusvx marked this conversation as resolved.
Show resolved Hide resolved

generateScript(SCRIPT_NAME, queryEvents, analyzeEvent);
}

module.exports = script;
Loading