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

Core js refacto #282

Merged
merged 24 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion _dev/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ module.exports = {
},
extends: ['airbnb-base'],
rules: {
'max-len': ['error', {code: 140}],
'max-len': ['error', {
code: 140,
ignoreComments: true,
}],
'no-underscore-dangle': 'off',
'no-restricted-syntax': 'off',
'no-param-reassign': 'off'
Expand Down
8 changes: 4 additions & 4 deletions _dev/js/theme/components/display/useToggleDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const useToggleDisplay = () => {

/**
* Show element
* @param element <HTMLElement> Element to show
* @param element {HTMLElement} Element to show
* @returns {void}
*/
const show = (element) => {
Expand All @@ -13,7 +13,7 @@ const useToggleDisplay = () => {

/**
* Hide element
* @param element <HTMLElement> Element to hide
* @param element {HTMLElement} Element to hide
* @returns {void}
*/
const hide = (element) => {
Expand All @@ -23,8 +23,8 @@ const useToggleDisplay = () => {

/**
* Toggle element
* @param element <HTMLElement> Element to toggle
* @param display <bool> Display or hide
* @param element {HTMLElement} Element to toggle
* @param display {boolean} Display or hide
* @returns {void}
*/
const toggle = (element, display) => {
Expand Down
32 changes: 32 additions & 0 deletions _dev/js/theme/components/http/useDefaultHttpRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import prestashop from 'prestashop';
import useHttpRequest from './useHttpRequest';

/**
* Default http request accepting payload as object and returning promise with response
* @param {string} url - url to send request
* @param {object} payload - payload to send
* @returns {Promise<unknown>}
*/
const useDefaultHttpRequest = (url, payload) => {
const { request } = useHttpRequest(url);

return new Promise((resolve, reject) => {
request
.query(payload)
.post()
.json((resp) => {
if (resp.errors) {
const errors = Array.isArray(resp.errors) ? resp.errors : [resp.errors];

reject(Error(errors.join('\n')));
} else {
resolve(resp);
}
})
.catch(() => {
reject(Error(prestashop.t.alert.genericHttpError));
});
});
};

export default useDefaultHttpRequest;
30 changes: 29 additions & 1 deletion _dev/js/theme/components/http/useHttpController.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import getUniqueId from '@js/theme/utils/getUniqueId';
import getUniqueId from '../../utils/getUniqueId';

/**
* @module useHttpController
* This function is used to control HTTP requests.
*/
const useHttpController = () => {
let requestsStack = {};

/**
* @method
* Adds request to request stack
* @param {number} id - unique id of request
* @param {Promise} request - request promise
* @param {AbortController} controller - AbortController object
*/
const addRequestToRequestStack = (id, request, controller) => {
const newRequestStack = { ...requestsStack };
newRequestStack[id] = { request, controller };

requestsStack = newRequestStack;
};

/**
* @method
* Removes request from request stack
* @param {number} id - unique id of request
*/
const removeRequestFromRequestStack = (id) => {
const { [id]: erasedId, ...newRequestStack } = requestsStack;

requestsStack = newRequestStack;
};

/**
* @method
* Dispatches request and adds it to request stack
* @param {object} request - request object
* @param {AbortController} controller - AbortController object
* @returns {(function(*): Promise<void>)|*}
*/
const dispatch = (request, controller) => {
const id = getUniqueId();
addRequestToRequestStack(id, request, controller);
Expand All @@ -26,6 +49,11 @@ const useHttpController = () => {
};
};

/**
* @method
* Aborts all requests in request stack
* @returns {void}
*/
const abortAll = () => {
for (const id in requestsStack) {
if (Object.hasOwn(requestsStack, id)) {
Expand Down
218 changes: 218 additions & 0 deletions _dev/js/theme/components/http/useHttpPayloadDefinition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* useHttpPayloadDefinition - validate payload against definition
* @module useHttpPayloadDefinition
* @param {object} payload - payload to validate
* @param {object} definition - definition to validate payload against
*/
const useHttpPayloadDefinition = (payload, definition) => {
const ERROR_MESSAGES = {
REQUIRED: 'field is required',
TYPE: 'field must be of type',
MIN_LENGTH: 'field must be at least',
MAX_LENGTH: 'field must be at most',
MIN_VALUE: 'field must be at least',
MAX_VALUE: 'field must be at most',
REGEX: 'field must match the following regex',
};

const defaultDefinitionForField = {
required: false,
minLength: null,
maxLength: null,
minValue: null,
maxValue: null,
regex: null,
};

const requiredDefinitionFields = [
'type',
];

if (!definition) {
throw new Error('Payload definition is required');
}

if (!payload) {
throw new Error('Payload is required');
}

/**
* @method
* Sets default definition for field
* @param customDefinition - custom definition for field
* @returns {object} - definition for field
*/
const setDefaultDefinitionForField = (customDefinition) => ({
...defaultDefinitionForField,
...customDefinition,
});

const payloadDefinition = {};

Object.keys(definition).forEach((fieldName) => {
const definitionForField = definition[fieldName];

payloadDefinition[fieldName] = setDefaultDefinitionForField(definitionForField);
});

/**
* @method
* Gets value type
* @param value
* @returns {"undefined"|"object"|"boolean"|"number"|"string"|"function"|"symbol"|"bigint"}
*/
const getValueType = (value) => typeof value;

/**
* @method
* Validates field value against field definition and returns errors
* @param {string} fieldName - name of field
* @param {any} value - value of field
* @param {object} fieldDefinition - definition for field
* @returns {string[]}
*/
const validate = (fieldName, value, fieldDefinition) => {
const validateErrors = [];
const {
type,
required,
minLength,
maxLength,
minValue,
maxValue,
regex,
} = fieldDefinition;

if (required && !value) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.REQUIRED}`);
}

switch (type) {
case 'string':
if (typeof value !== 'string') {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} string, ${getValueType(value)} received`);
}
break;
case 'float':
if (typeof value !== 'number') {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} float, ${getValueType(value)} received`);
}
break;
case 'int':
if (typeof value !== 'number') {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} int, ${getValueType(value)} received`);
}
break;
case 'boolean':
if (typeof value !== 'boolean') {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} boolean, ${getValueType(value)} received`);
}
break;
case 'object':
if (typeof value !== 'object') {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} object, ${getValueType(value)} received`);
}
break;
case 'array':
if (!Array.isArray(value)) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.TYPE} array, ${getValueType(value)} received`);
}
break;
default:
break;
}

if (minLength && value.length < minLength) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MIN_LENGTH} ${minLength}`);
}

if (maxLength && value.length > maxLength) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MAX_LENGTH} ${maxLength}`);
}

if (minValue && value < minValue) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MIN_VALUE} ${minValue}`);
}

if (maxValue && value > maxValue) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.MAX_VALUE} ${maxValue}`);
}

if (regex && !regex.test(value)) {
validateErrors.push(`'${fieldName}' ${ERROR_MESSAGES.REGEX} ${regex}`);
}

return validateErrors;
};

/**
* @method
* Validates definition for field and returns errors
* @param fieldName
* @param fieldsDefinition
* @returns {string[]}
*/
const validateDefinitionForField = (fieldName, fieldsDefinition) => {
const definitionErrors = [];
const definitionKeys = Object.keys(fieldsDefinition);

requiredDefinitionFields.forEach((requiredDefinitionField) => {
if (!definitionKeys.includes(requiredDefinitionField)) {
definitionErrors.push(`'${fieldName}' definition is missing ${requiredDefinitionField} field`);
}
});

return definitionErrors;
};

/**
* @method
* Validates definition and returns errors
* @param fieldsDefinition
* @returns {string[]}
*/
const validateDefinition = (fieldsDefinition) => {
const definitionErrors = [];
const definitionKeys = Object.keys(fieldsDefinition);

definitionKeys.forEach((defName) => {
const fieldName = defName;
const fieldDef = fieldsDefinition[defName];
const definitionForFieldErrors = validateDefinitionForField(fieldName, fieldDef);

definitionErrors.push(...definitionForFieldErrors);
});

return definitionErrors;
};

/**
* @method
* Validates payload against definition and returns errors
* @returns {string[]}
*/
const validatePayload = () => {
const payloadErrors = [];
const definitionErrors = validateDefinition(payloadDefinition);

if (definitionErrors.length) {
payloadErrors.push(...definitionErrors);
}

Object.keys(payloadDefinition).forEach((fieldName) => {
const definitionForField = payloadDefinition[fieldName];

const definitionForFieldErrors = validate(fieldName, payload[fieldName], definitionForField);

payloadErrors.push(...definitionForFieldErrors);
});

return payloadErrors;
};

return {
validatePayload,
};
};

export default useHttpPayloadDefinition;
7 changes: 7 additions & 0 deletions _dev/js/theme/components/http/useHttpRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import wretch from 'wretch';
import QueryStringAddon from 'wretch/addons/queryString';
import AbortAddon from 'wretch/addons/abort';

/**
* useHttpRequest
* @param url {string} - request url
* @param options {object} - request options
* @param addons {array} - request addons, wretch/addons, default used: [AbortAddon, QueryStringAddon]
* @returns {{request: wretch, controller: AbortController}}
*/
const useHttpRequest = (url, options = {}, addons = []) => {
if (!options?.headers) {
options.headers = {};
Expand Down
4 changes: 2 additions & 2 deletions _dev/js/theme/components/password/usePasswordPolicy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import prestashop from 'prestashop';
/**
* Verify password score.
* Estimate guesses needed to crack the password.
* @param {String} password
* @returns {Promise}
* @param {string} password
* @returns {promise}
*/
window.prestashop.checkPasswordScore = async (password) => {
const zxcvbn = (await import('zxcvbn')).default;
Expand Down
Loading