updates on the build regarding the create order dropdown and fix on the customer and createorder page
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -39,6 +39,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mui-daterange-picker": "^1.0.5",
|
||||
"notistack": "^3.0.1",
|
||||
"open-location-code": "^1.0.3",
|
||||
"papaparse": "^5.5.3",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -14285,6 +14286,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/open-location-code": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz",
|
||||
"integrity": "sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
@@ -30691,6 +30698,11 @@
|
||||
"is-wsl": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"open-location-code": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz",
|
||||
"integrity": "sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw=="
|
||||
},
|
||||
"optionator": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mui-daterange-picker": "^1.0.5",
|
||||
"notistack": "^3.0.1",
|
||||
"open-location-code": "^1.0.3",
|
||||
"papaparse": "^5.5.3",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.8.1",
|
||||
|
||||
@@ -2,12 +2,14 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Autocomplete, TextField, CircularProgress } from '@mui/material';
|
||||
import { debounce } from '@mui/material/utils';
|
||||
import { OpenLocationCode } from 'open-location-code';
|
||||
|
||||
// Free OSM Nominatim geocoder — replaces Google Places Autocomplete /
|
||||
// react-geocode across the console. No API key required. Nominatim's usage
|
||||
// policy caps the public endpoint at ~1 req/sec, so every caller here is
|
||||
// debounced (500ms) and cancels stale in-flight predictions.
|
||||
const NOMINATIM_SEARCH_URL = 'https://nominatim.openstreetmap.org/search';
|
||||
const NOMINATIM_REVERSE_URL = 'https://nominatim.openstreetmap.org/reverse';
|
||||
|
||||
// Builds a Google-Places-shaped `address_components` array (each entry
|
||||
// `{ long_name, short_name, types: [] }`) from Nominatim's flat `address`
|
||||
@@ -44,11 +46,99 @@ export const toPlace = (result) => ({
|
||||
address_components: buildAddressComponents(result.address)
|
||||
});
|
||||
|
||||
// Nominatim requests have no built-in timeout — a stalled connection (proxy
|
||||
// silently dropping packets, an overloaded public endpoint) would otherwise
|
||||
// leave `fetch` pending forever, so callers see the spinner spin with nothing
|
||||
// ever happening. This bounds every request to REQUEST_TIMEOUT_MS.
|
||||
const REQUEST_TIMEOUT_MS = 8000;
|
||||
const fetchWithTimeout = (url, options) => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
|
||||
};
|
||||
|
||||
// Nominatim (and Google Places, previously) both do full-text search against
|
||||
// a database of named places — neither understands Plus Codes (e.g.
|
||||
// "5CM7+8Q7"), because a Plus Code isn't a name to look up, it's lat/lng
|
||||
// encoded directly into the string. Decoding it is a pure local computation
|
||||
// via the Open Location Code algorithm, so this resolves reliably even where
|
||||
// OSM has zero data for the area (small towns, unlisted businesses).
|
||||
const olc = new OpenLocationCode();
|
||||
const PLUS_CODE_CHARS = '23456789CFGHJMPQRVWX';
|
||||
const PLUS_CODE_RE = new RegExp(`[${PLUS_CODE_CHARS}]{2,8}\\+[${PLUS_CODE_CHARS}]{2,3}`, 'i');
|
||||
|
||||
// A decoded Plus Code is just coordinates — on its own it has no street/
|
||||
// locality name to show the operator, only the code itself. Reverse-geocoding
|
||||
// those coordinates fills in the human-readable side (road, suburb, city...)
|
||||
// the same way a normal search result would, so the option in the dropdown
|
||||
// reads as an actual address instead of a bare code. Best-effort: if Nominatim
|
||||
// has no data for that exact point either, callers fall back to the code text.
|
||||
async function reverseGeocode(lat, lon) {
|
||||
try {
|
||||
const params = new URLSearchParams({ lat: String(lat), lon: String(lon), format: 'json', addressdetails: '1' });
|
||||
const res = await fetchWithTimeout(`${NOMINATIM_REVERSE_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return null;
|
||||
const result = await res.json();
|
||||
if (!result || result.error) return null;
|
||||
return result;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Short codes (the common case — "5CM7+8Q7" rather than the full
|
||||
// "6JWV5CM7+8Q7") only encode an offset; recovering the actual location
|
||||
// needs a nearby reference point, which is what `bias` (the operator's
|
||||
// tenant/app location) provides. Returns a Nominatim-result-shaped object
|
||||
// (same shape `toPlace()` expects) so it can flow through the exact same
|
||||
// path as a real search hit, or null if no code is present / it can't be
|
||||
// resolved without a reference point.
|
||||
const plusCodeToResult = async (text, bias) => {
|
||||
const match = text?.match(PLUS_CODE_RE);
|
||||
if (!match) return null;
|
||||
const code = match[0].toUpperCase();
|
||||
if (!olc.isValid(code)) return null;
|
||||
try {
|
||||
let fullCode = code;
|
||||
if (olc.isShort(code)) {
|
||||
const refLat = Number(bias?.lat);
|
||||
const refLng = Number(bias?.lng);
|
||||
// (0,0) isn't a plausible tenant location — it's what `Number('')` and
|
||||
// `Number(undefined)` half-produce (empty string coerces to 0), so treat
|
||||
// it as "no reference point yet" rather than decoding against Null Island.
|
||||
if (!Number.isFinite(refLat) || !Number.isFinite(refLng) || (refLat === 0 && refLng === 0)) return null;
|
||||
fullCode = olc.recoverNearest(code, refLat, refLng);
|
||||
}
|
||||
if (!olc.isFull(fullCode)) return null;
|
||||
const { latitudeCenter, longitudeCenter } = olc.decode(fullCode);
|
||||
// The decoded centroid is exact — keep it as the authoritative coordinate
|
||||
// even though `reverse` below carries its own (possibly snapped-to-nearest-
|
||||
// feature) lat/lon; only its display_name/address are what we want.
|
||||
const reverse = await reverseGeocode(latitudeCenter, longitudeCenter);
|
||||
return {
|
||||
...reverse,
|
||||
place_id: `pluscode-${fullCode}`,
|
||||
lat: String(latitudeCenter),
|
||||
lon: String(longitudeCenter),
|
||||
display_name: reverse?.display_name ? `${reverse.display_name} (near ${code})` : `${code} (Plus Code location)`,
|
||||
address: reverse?.address || {}
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildParams = (query, bias) => {
|
||||
const params = new URLSearchParams({ q: query, format: 'json', addressdetails: '1', limit: '5' });
|
||||
if (bias?.lat && bias?.lng) {
|
||||
// bias.lat/lng arrive as strings from the tenant-location API. `-` coerces to
|
||||
// number automatically, but `+` on a string concatenates instead of adding
|
||||
// (e.g. "8.17445" + 0.5 === "8.174450.5"), producing a malformed, non-numeric
|
||||
// viewbox that Nominatim rejects with 400 Bad Request on every single search.
|
||||
const lat = Number(bias?.lat);
|
||||
const lng = Number(bias?.lng);
|
||||
if (Number.isFinite(lat) && Number.isFinite(lng)) {
|
||||
const d = 0.5; // ~55km box around the operator's zone — soft bias, not a hard filter
|
||||
params.set('viewbox', `${bias.lng - d},${bias.lat + d},${bias.lng + d},${bias.lat - d}`);
|
||||
params.set('viewbox', `${lng - d},${lat + d},${lng + d},${lat - d}`);
|
||||
}
|
||||
return params;
|
||||
};
|
||||
@@ -59,10 +149,12 @@ const buildParams = (query, bias) => {
|
||||
// or null if nothing matched.
|
||||
export async function geocodeAddress(address, { bias } = {}) {
|
||||
if (!address) return null;
|
||||
const plusCodeResult = await plusCodeToResult(address, bias);
|
||||
if (plusCodeResult) return toPlace(plusCodeResult);
|
||||
try {
|
||||
const params = buildParams(address, bias);
|
||||
params.set('limit', '1');
|
||||
const res = await fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
|
||||
const res = await fetchWithTimeout(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return null;
|
||||
const results = await res.json();
|
||||
if (!results?.length) return null;
|
||||
@@ -115,10 +207,13 @@ const AddressAutocomplete = ({
|
||||
return;
|
||||
}
|
||||
const params = buildParams(query, bias1);
|
||||
fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } })
|
||||
const searchPromise = fetchWithTimeout(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } })
|
||||
.then((res) => (res.ok ? res.json() : []))
|
||||
.then((results) => callback(results || []))
|
||||
.catch(() => callback([]));
|
||||
.catch(() => []);
|
||||
const plusCodePromise = plusCodeToResult(query, bias1).catch(() => null);
|
||||
Promise.all([searchPromise, plusCodePromise]).then(([results, plusCodeResult]) => {
|
||||
callback(plusCodeResult ? [plusCodeResult, ...(results || [])] : results || []);
|
||||
});
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
@@ -153,6 +248,13 @@ const AddressAutocomplete = ({
|
||||
filterOptions={(x) => x}
|
||||
getOptionLabel={(option) => (typeof option === 'string' ? option : option.display_name || '')}
|
||||
isOptionEqualToValue={(option, val) => option.place_id === val?.place_id}
|
||||
noOptionsText={
|
||||
loading
|
||||
? 'Searching…'
|
||||
: inputValue.trim().length >= 3
|
||||
? 'No match found — drop the business name/Plus Code, or try just City + Postcode'
|
||||
: 'Type an address to search'
|
||||
}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
|
||||
@@ -25,7 +25,7 @@ import MainCard from 'components/MainCard';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
@@ -140,6 +140,30 @@ const CreateCustomer = () => {
|
||||
longitude: place.geometry.location.lng()
|
||||
});
|
||||
};
|
||||
// ============================================= || Fallback geocode for manually typed address || =============================================
|
||||
// AddressAutocomplete only resolves lat/lng when a suggestion is actually clicked
|
||||
// from its dropdown (handlePickPlaceSelected). The common real-world case is an
|
||||
// operator typing/pasting the full address into that single Address box and never
|
||||
// clicking a suggestion — that left startPoint at (0,0) forever, and because the
|
||||
// submit validation below used to check pickCust.latitude for an empty string
|
||||
// that's never actually set, customers were saved with latitude/longitude "0"/"0"
|
||||
// with no warning at all. This covers both the raw Address-box text and the
|
||||
// manually filled Location/City/Postcode fields as a fallback, reusing the same
|
||||
// handler a real dropdown selection would call so the rest of the fields fill in too.
|
||||
useEffect(() => {
|
||||
if (startPoint.latitude !== 0 || startPoint.longitude !== 0) return undefined;
|
||||
const manualFields = [doorno, pickCust.suburb, pickCust.city, pickCust.state, pickCust.postcode].filter(Boolean).join(', ');
|
||||
const manualAddress =
|
||||
(inputValue2 && inputValue2.trim().length >= 6 && inputValue2) || (pickCust.suburb && pickCust.city && manualFields);
|
||||
if (!manualAddress) return undefined;
|
||||
const timer = setTimeout(async () => {
|
||||
const place = await geocodeAddress(manualAddress, { bias: { lat: appLocaLat, lng: appLocaLng } });
|
||||
if (place) handlePickPlaceSelected(place);
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValue2, doorno, pickCust.suburb, pickCust.city, pickCust.state, pickCust.postcode, appLocaLat, appLocaLng]);
|
||||
|
||||
// ==================================================== || getapplocations || ====================================================
|
||||
const getapplocations = async () => {
|
||||
setLoading(true);
|
||||
@@ -554,10 +578,8 @@ const CreateCustomer = () => {
|
||||
opentoast('Enter Post Code ');
|
||||
} else if (landmark === '') {
|
||||
opentoast('Enter Land Mark ');
|
||||
} else if (pickCust.latitude === '') {
|
||||
opentoast('Invalid latitude ');
|
||||
} else if (pickCust.longitude === '') {
|
||||
opentoast('Invaiid Longitude ');
|
||||
} else if (!startPoint.latitude || !startPoint.longitude) {
|
||||
opentoast('Could not locate address on map, please recheck the address');
|
||||
} else {
|
||||
createprofile();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import { TbMapPinCode } from 'react-icons/tb';
|
||||
import { FaLocationDot } from 'react-icons/fa6';
|
||||
import axios from 'axios';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
|
||||
import Loader from 'components/Loader';
|
||||
import * as geolib from 'geolib';
|
||||
import MainCard from 'components/MainCard';
|
||||
@@ -578,10 +578,10 @@ const Createorder1 = () => {
|
||||
deliverycustomer: dropCust.firstname || '',
|
||||
deliveryid: isNumChange2 == 0 ? +dropCust.customerid || 0 : 0,
|
||||
deliverylandmark: dropCust.landmark || '',
|
||||
deliverylat: dropCust.latitude.toString(),
|
||||
deliverylat: (dropCust.latitude || 0).toString(),
|
||||
deliverylocation: dropCust.suburb || '',
|
||||
deliverylocationid: dropCust.deliverylocationid || 0,
|
||||
deliverylong: dropCust.longitude.toString(),
|
||||
deliverylong: (dropCust.longitude || 0).toString(),
|
||||
deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`,
|
||||
deliverytype: pickCust.customerid !== 0 || dropCust.customerid !== 0 ? 'B' : 'C',
|
||||
delivered: '',
|
||||
@@ -606,10 +606,10 @@ const Createorder1 = () => {
|
||||
pickupcontactno: pickCust.contactno || '',
|
||||
pickupcustomer: pickCust.firstname || '',
|
||||
pickuplandmark: pickCust.landmark || '',
|
||||
pickuplat: pickCust.latitude.toString(),
|
||||
pickuplat: (pickCust.latitude || 0).toString(),
|
||||
pickuplocation: pickCust.suburb || '',
|
||||
pickuplocationid: pickCust.deliverylocationid || 0,
|
||||
pickuplong: pickCust.longitude.toString(),
|
||||
pickuplong: (pickCust.longitude || 0).toString(),
|
||||
processing: '',
|
||||
ready: '',
|
||||
remarks: '',
|
||||
@@ -637,8 +637,8 @@ const Createorder1 = () => {
|
||||
email: pickCust.email || '',
|
||||
firstname: pickCust.firstname || '',
|
||||
landmark: pickCust.landmark || '',
|
||||
latitude: pickCust.latitude.toString() || '',
|
||||
longitude: pickCust.longitude.toString() || '',
|
||||
latitude: (pickCust.latitude || 0).toString(),
|
||||
longitude: (pickCust.longitude || 0).toString(),
|
||||
locationid: pickCust.deliverylocationid || 0,
|
||||
postcode: pickCust.postcode || '',
|
||||
primaryaddress: 1,
|
||||
@@ -663,8 +663,8 @@ const Createorder1 = () => {
|
||||
email: dropCust.email || '',
|
||||
firstname: dropCust.firstname || '',
|
||||
landmark: dropCust.landmark || '',
|
||||
latitude: dropCust.latitude.toString(),
|
||||
longitude: dropCust.longitude.toString(),
|
||||
latitude: (dropCust.latitude || 0).toString(),
|
||||
longitude: (dropCust.longitude || 0).toString(),
|
||||
locationid: dropCust.deliverylocationid || 0,
|
||||
postcode: dropCust.postcode || '',
|
||||
primaryaddress: 1,
|
||||
@@ -689,6 +689,8 @@ const Createorder1 = () => {
|
||||
opentoast('Enter Pickup Postcode ', 'warning', 2000);
|
||||
} else if (!pickCust.landmark) {
|
||||
opentoast('Enter Pickup Landmark ', 'warning', 2000);
|
||||
} else if (!pickCust.latitude || !pickCust.longitude) {
|
||||
opentoast('Could not locate Pickup address on map, please recheck the address', 'error', 3000);
|
||||
} else if (!dropCust.firstname) {
|
||||
opentoast('Enter Drop Contact Name ', 'warning', 2000);
|
||||
} else if (!dropCust.contactno) {
|
||||
@@ -703,6 +705,8 @@ const Createorder1 = () => {
|
||||
opentoast('Enter Drop postcode ', 'warning', 2000);
|
||||
} else if (!dropCust.landmark) {
|
||||
opentoast('Enter Drop Landmark ', 'warning', 2000);
|
||||
} else if (!dropCust.latitude || !dropCust.longitude) {
|
||||
opentoast('Could not locate Drop address on map, please recheck the address', 'error', 3000);
|
||||
} else if (!selectedtime) {
|
||||
opentoast('Choose deliverytime ', 'warning', 2000);
|
||||
} else if (!setSubCatId) {
|
||||
@@ -991,6 +995,45 @@ const Createorder1 = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================= || Fallback geocode for manually typed addresses || =============================================
|
||||
// AddressAutocomplete only resolves lat/lng when a suggestion is actually clicked
|
||||
// from its dropdown (handlePickPlaceSelected/handleDropPlaceSelected). The common
|
||||
// real-world case is an operator typing/pasting the full address into that single
|
||||
// Address box and never clicking a suggestion (none showed up, or they just hit
|
||||
// tab/moved on) — that left startPoint/endPoint at (0,0) forever, distance never
|
||||
// calculated, and "Select Pickup and Drop" firing no matter what was typed.
|
||||
// These effects cover that: once typing pauses, whatever address text exists —
|
||||
// the raw Address-box text, or the manually filled Door No/Location/City/Postcode
|
||||
// fields as a fallback — gets geocoded, reusing the same handler that a real
|
||||
// dropdown selection would call so suburb/city/postcode get filled in too.
|
||||
useEffect(() => {
|
||||
if (startPoint.latitude !== 0 || startPoint.longitude !== 0) return undefined;
|
||||
const manualAddress =
|
||||
(inputValue2 && inputValue2.trim().length >= 6 && inputValue2) ||
|
||||
(pickCust.suburb && pickCust.city && [pickCust.doorno, pickCust.suburb, pickCust.city, pickCust.postcode].filter(Boolean).join(', '));
|
||||
if (!manualAddress) return undefined;
|
||||
const timer = setTimeout(async () => {
|
||||
const place = await geocodeAddress(manualAddress, { bias: { lat: appLocaLat, lng: appLocaLng } });
|
||||
if (place) handlePickPlaceSelected(place);
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValue2, pickCust.doorno, pickCust.suburb, pickCust.city, pickCust.postcode, appLocaLat, appLocaLng]);
|
||||
|
||||
useEffect(() => {
|
||||
if (endPoint.latitude !== 0 || endPoint.longitude !== 0) return undefined;
|
||||
const manualAddress =
|
||||
(inputValue3 && inputValue3.trim().length >= 6 && inputValue3) ||
|
||||
(dropCust.suburb && dropCust.city && [dropCust.doorno, dropCust.suburb, dropCust.city, dropCust.postcode].filter(Boolean).join(', '));
|
||||
if (!manualAddress) return undefined;
|
||||
const timer = setTimeout(async () => {
|
||||
const place = await geocodeAddress(manualAddress, { bias: { lat: appLocaLat, lng: appLocaLng } });
|
||||
if (place) handleDropPlaceSelected(place);
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValue3, dropCust.doorno, dropCust.suburb, dropCust.city, dropCust.postcode, appLocaLat, appLocaLng]);
|
||||
|
||||
// ============================================= || gettenantlocations (branches) || =============================================
|
||||
const gettenantlocations = async () => {
|
||||
try {
|
||||
@@ -1749,7 +1792,7 @@ const Createorder1 = () => {
|
||||
placeholder="Select"
|
||||
value={dropCust.address}
|
||||
onChange={(e) => {
|
||||
setPickCust({ ...dropCust, address: e.target.value });
|
||||
setDropCust({ ...dropCust, address: e.target.value });
|
||||
if (e.target.value == '') {
|
||||
setAddId2(0);
|
||||
setShowDistance(false);
|
||||
|
||||
@@ -7798,6 +7798,11 @@ onetime@^5.1.2:
|
||||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
open-location-code@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz"
|
||||
integrity sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==
|
||||
|
||||
open@^8.0.9, open@^8.4.0:
|
||||
version "8.4.0"
|
||||
resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user