Skip to content

Commit fc90b96

Browse files
committed
Simple Card Addition
1 parent 8569eff commit fc90b96

File tree

11 files changed

+345
-0
lines changed

11 files changed

+345
-0
lines changed

simple_version/RentalCars/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
src/app/.vite
2+
src/app/app.functions/.env
3+
*node_modules
4+
*package-lock.json
5+
src/app/dist
6+
*bun.lockb
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "RentalCarsSimple",
3+
"platformVersion": "2023.2",
4+
"srcDir": "src"
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Using NPM dependencies
2+
3+
To add your own dependencies, you can list the package in dependencies within the package.json file. When the app is built, dependencies will be bundled with your function code. All dependencies must be published to NPM and be public.
4+
For example, if you wanted to add the `lodash` library in a serverless function, you could add lodash in the `package.json`'s dependencies and then in the serverless function `require` the package.
5+
6+
In this example we actually add overrides for two [preloaded packages](https://developers.hubspot.com/docs/cms/data/serverless-functions/reference#preloaded-packages) to demonstrate the ability to override versions of the preloaded packages.
7+
8+
```
9+
{
10+
"name": "demo.functions",
11+
"version": "1.0.0",
12+
"description": "",
13+
"dependencies": {
14+
"@hubspot/api-client": "^7.0.1",
15+
"axios": "^0.27.2",
16+
"lodash": "^4.17.21",
17+
}
18+
}
19+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const axios = require("axios");
2+
3+
function searchLocations({ zipCode }) {
4+
const data = JSON.stringify({
5+
sorts: [],
6+
query: zipCode,
7+
properties: ["address_1", "city", "state", "postal_code", "lat", "lng", "number_of_available_vehicles", "hs_object_id"],
8+
limit: 100,
9+
after: 0
10+
});
11+
12+
const config = {
13+
method: 'post',
14+
url: 'https://api.hubapi.com/crm/v3/objects/locations/search',
15+
headers: {
16+
'Content-Type': 'application/json',
17+
'Authorization': 'Bearer ' + process.env['PRIVATE_APP_ACCESS_TOKEN'],
18+
},
19+
data: data
20+
};
21+
22+
return axios.request(config);
23+
}
24+
25+
exports.main = async (context) => {
26+
try {
27+
const { zipCode } = context.parameters;
28+
29+
console.log(context.parameters);
30+
31+
const response = await searchLocations({ zipCode });
32+
console.log(JSON.stringify(response.data));
33+
34+
return { results: response.data.results, total: response.data.total };
35+
} catch (error) {
36+
console.error(error);
37+
return { error: error.message };
38+
}
39+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const axios = require('axios');
2+
3+
4+
exports.main = async (context, sendResponse) => {
5+
6+
let vehicleIds = context.parameters.vehicles;
7+
8+
let data = JSON.stringify({
9+
"inputs": vehicleIds.map((id) => {
10+
return {
11+
"id": id
12+
}
13+
}),
14+
"properties": [
15+
"vin",
16+
"make",
17+
"model",
18+
"year",
19+
"daily_price"
20+
]
21+
});
22+
23+
let config = {
24+
method: 'post',
25+
maxBodyLength: Infinity,
26+
url: 'https://api.hubapi.com/crm/v3/objects/vehicles/batch/read',
27+
headers: {
28+
'Authorization': 'Bearer ' + process.env['PRIVATE_APP_ACCESS_TOKEN'],
29+
'Content-Type': 'application/json'
30+
},
31+
data: data
32+
};
33+
34+
return axios.request(config)
35+
.then((response) => {
36+
console.log(JSON.stringify(response.data));
37+
return response.data
38+
})
39+
.catch((error) => {
40+
console.log(error);
41+
throw error;
42+
});
43+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "hubspot-example-function",
3+
"version": "0.1.0",
4+
"author": "HubSpot",
5+
"license": "MIT",
6+
"dependencies": {
7+
"@hubspot/api-client": "^7.0.1",
8+
"axios": "^0.27.2",
9+
"us-zips": "^2022.9.1"
10+
}
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"appFunctions": {
3+
"getLocations": {
4+
"file": "getLocations.js"
5+
}
6+
}
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "Cars Rental Simple Version",
3+
"description": "This is an example private app that shows a custom card on the Contact record tab built with React-based frontend. This card demonstrates a simple handshake with HubSpot's serverless backend.",
4+
"uid": "car_rental_simple",
5+
"scopes": [
6+
"crm.objects.contacts.read",
7+
"crm.objects.contacts.write",
8+
"crm.objects.custom.read",
9+
"crm.objects.custom.write",
10+
"collector.graphql_query.execute"
11+
],
12+
"public": false,
13+
"extensions": {
14+
"crm": {
15+
"cards": [
16+
{
17+
"file": "extensions/simplecreaterental.json"
18+
}
19+
]
20+
}
21+
}
22+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import {
2+
Button,
3+
DateInput,
4+
DescriptionList,
5+
DescriptionListItem,
6+
Divider,
7+
Flex,
8+
Input,
9+
Link,
10+
LoadingSpinner,
11+
MultiSelect,
12+
NumberInput,
13+
Select,
14+
StepIndicator,
15+
Table,
16+
TableBody,
17+
TableCell,
18+
TableHead,
19+
TableHeader,
20+
TableRow,
21+
Text,
22+
ToggleGroup,
23+
hubspot
24+
} from "@hubspot/ui-extensions";
25+
import _ from 'lodash';
26+
import moment from 'moment';
27+
import React, { useEffect, useState } from "react";
28+
29+
import {
30+
CrmActionButton,
31+
CrmActionLink,
32+
CrmCardActions
33+
} from '@hubspot/ui-extensions/crm';
34+
35+
36+
const ITEMS_PER_PAGE = 10;
37+
38+
// Define the extension to be run within the Hubspot CRM
39+
hubspot.extend(({ context, runServerlessFunction, actions }) => (
40+
<Extension
41+
context={context}
42+
runServerless={runServerlessFunction}
43+
sendAlert={actions.addAlert}
44+
fetchProperties={actions.fetchCrmObjectProperties}
45+
/>
46+
));
47+
48+
const Extension = ({ context, runServerless, sendAlert, fetchProperties }) => {
49+
50+
const [locations, setLocations] = useState([]);
51+
const [locationsOnPage, setLocationsOnPage] = useState([]);
52+
const [vehicles, setVehicles] = useState([]);
53+
const [vehiclesOnPage, setVehiclesOnPage] = useState([]);
54+
55+
const [locationCount, setLocationCount] = useState(0);
56+
const [vehicleCount, setVehicleCount] = useState(0);
57+
const [locationFetching, setLocationFetching] = useState(false);
58+
59+
const [locationPage, setLocationPage] = useState(1);
60+
61+
const [zipCode, setZipCode] = useState("");
62+
63+
64+
const [currentPage, setCurrentPage] = useState(1); // For controlling current page
65+
const [numPages, setNumPages] = useState(0); // For storing the total number of pages
66+
67+
// Function to change the current page
68+
const changePage = (newPage) => {
69+
if (newPage >= 1 && newPage <= numPages) {
70+
setCurrentPage(newPage);
71+
}
72+
};
73+
74+
// Whenever the locationCount or locations change, reset the paging
75+
useEffect(() => {
76+
setNumPages(Math.ceil(locationCount / ITEMS_PER_PAGE));
77+
setCurrentPage(1);
78+
}, [locationCount, locations]);
79+
80+
// Calculate the slice of locations for the current page
81+
const locationsOnCurrentPage = locations.slice(
82+
(currentPage - 1) * ITEMS_PER_PAGE,
83+
currentPage * ITEMS_PER_PAGE
84+
);
85+
86+
87+
88+
function fetchLocations() {
89+
sendAlert({ message: "Fetching locations...", type: "info" });
90+
setLocationFetching(true);
91+
runServerless({ name: "getLocations", parameters: { "zipCode": zipCode } }).then((resp) => {
92+
setLocations(resp.response.results);
93+
setLocationCount(resp.response.total);
94+
setLocationFetching(false);
95+
//reset the table
96+
setLocationPage(1);
97+
})
98+
99+
}
100+
101+
const debouncedFetchLocations = _.debounce(fetchLocations, 500);
102+
103+
104+
return (
105+
<>
106+
<Flex direction="column" gap="sm">
107+
<Flex direction="row" justify="start" gap="sm" align="end">
108+
<Input
109+
name="zipCode"
110+
label="Zip Code"
111+
value={zipCode}
112+
onChange={(e) => setZipCode(e)}
113+
/>
114+
<Button
115+
onClick={() => {
116+
fetchLocations();
117+
}}
118+
variant="primary"
119+
size="md"
120+
type="button"
121+
>
122+
Search!
123+
Button>
124+
Flex>
125+
<Divider />
126+
<Text>
127+
{locationFetching && <LoadingSpinner />}
128+
Text>
129+
Flex>
130+
<Table
131+
bordered={true}
132+
paginated={true}
133+
pageCount={numPages}
134+
onPageChange={(newPage) => changePage(newPage)}
135+
>
136+
<TableHead>
137+
<TableRow>
138+
<TableHeader>ZipTableHeader>
139+
<TableHeader>AddressTableHeader>
140+
<TableHeader>Available VehiclesTableHeader>
141+
TableRow>
142+
TableHead>
143+
<TableBody>
144+
{locationsOnCurrentPage.map((location, index) => {
145+
return (
146+
<TableRow>
147+
<TableCell>
148+
<CrmActionLink
149+
actionType="PREVIEW_OBJECT"
150+
actionContext={{
151+
objectTypeId: "2-19860301",
152+
objectId: location.id
153+
}}
154+
variant="secondary"
155+
>
156+
{location.properties.postal_code}
157+
CrmActionLink>
158+
TableCell>
159+
<TableCell>{location.properties.address_1 + " " + location.properties.city + ", " + location.properties.state}TableCell>
160+
<TableCell>{location.properties.number_of_available_vehicles}TableCell>
161+
TableRow>
162+
);
163+
})}
164+
TableBody>
165+
Table>
166+
>
167+
);
168+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "hubspot-example-extension",
3+
"version": "0.1.0",
4+
"author": "HubSpot",
5+
"license": "MIT",
6+
"dependencies": {
7+
"@hubspot/ui-extensions": "latest",
8+
"react": "^18.2.0",
9+
"lodash": "^4.17.21",
10+
"moment": "^2.29.1"
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "crm-card",
3+
"data": {
4+
"title": "Create Rental",
5+
"uid": "create_rental",
6+
"location": "crm.record.tab",
7+
"module": {
8+
"file": "CreateRental.jsx"
9+
},
10+
"objectTypes": [{ "name": "contacts" }]
11+
}
12+
}
13+

0 commit comments

Comments
 (0)