Skip to content

Commit ed64168

Browse files
devin-ai-integration[bot]sriramveeraghantasriram@plane.so
authored
chore(utils): copy helper functions from web/helpers (#6264)
* chore(utils): copy helper functions from web/helpers Co-Authored-By: [email protected] <[email protected]> * chore(utils): bump version to 0.24.2 Co-Authored-By: [email protected] <[email protected]> * chore: bump root package version to 0.24.2 Co-Authored-By: [email protected] <[email protected]> * fix: remove duplicate function and simplify auth utils Co-Authored-By: [email protected] <[email protected]> * fix: improve HTML entity escaping in sanitizeHTML Co-Authored-By: [email protected] <[email protected]> * fix: version changes --------- Co-authored-by: sriram veeraghanta <[email protected]> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent f54f3a6 commit ed64168

File tree

8 files changed

+807
-57
lines changed

8 files changed

+807
-57
lines changed

packages/constants/src/auth.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export enum E_PASSWORD_STRENGTH {
77

88
export const PASSWORD_MIN_LENGTH = 8;
99

10-
export const PASSWORD_CRITERIA = [
10+
export const SPACE_PASSWORD_CRITERIA = [
1111
{
1212
key: "min_8_char",
1313
label: "Min 8 characters",

packages/utils/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"clsx": "^2.1.1",
1919
"date-fns": "^4.1.0",
2020
"isomorphic-dompurify": "^2.16.0",
21+
"lodash": "^4.17.21",
2122
"react": "^18.3.1",
2223
"tailwind-merge": "^2.5.5",
2324
"zxcvbn": "^4.4.2"

packages/utils/src/array.ts

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import isEmpty from "lodash/isEmpty";
2+
import { IIssueLabel, IIssueLabelTree } from "@plane/types";
3+
4+
/**
5+
* @description Groups an array of objects by a specified key
6+
* @param {any[]} array Array to group
7+
* @param {string} key Key to group by (supports dot notation for nested objects)
8+
* @returns {Object} Grouped object with keys being the grouped values
9+
* @example
10+
* const array = [{type: 'A', value: 1}, {type: 'B', value: 2}, {type: 'A', value: 3}];
11+
* groupBy(array, 'type') // returns { A: [{type: 'A', value: 1}, {type: 'A', value: 3}], B: [{type: 'B', value: 2}] }
12+
*/
13+
export const groupBy = (array: any[], key: string) => {
14+
const innerKey = key.split("."); // split the key by dot
15+
return array.reduce((result, currentValue) => {
16+
const key = innerKey.reduce((obj, i) => obj?.[i], currentValue) ?? "None"; // get the value of the inner key
17+
(result[key] = result[key] || []).push(currentValue);
18+
return result;
19+
}, {});
20+
};
21+
22+
/**
23+
* @description Orders an array by a specified key in ascending or descending order
24+
* @param {any[]} orgArray Original array to order
25+
* @param {string} key Key to order by (supports dot notation for nested objects)
26+
* @param {"ascending" | "descending"} ordering Sort order
27+
* @returns {any[]} Ordered array
28+
* @example
29+
* const array = [{value: 2}, {value: 1}, {value: 3}];
30+
* orderArrayBy(array, 'value', 'ascending') // returns [{value: 1}, {value: 2}, {value: 3}]
31+
*/
32+
export const orderArrayBy = (orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending") => {
33+
if (!orgArray || !Array.isArray(orgArray) || orgArray.length === 0) return [];
34+
35+
const array = [...orgArray];
36+
37+
if (key[0] === "-") {
38+
ordering = "descending";
39+
key = key.slice(1);
40+
}
41+
42+
const innerKey = key.split("."); // split the key by dot
43+
44+
return array.sort((a, b) => {
45+
const keyA = innerKey.reduce((obj, i) => obj[i], a); // get the value of the inner key
46+
const keyB = innerKey.reduce((obj, i) => obj[i], b); // get the value of the inner key
47+
if (keyA < keyB) {
48+
return ordering === "ascending" ? -1 : 1;
49+
}
50+
if (keyA > keyB) {
51+
return ordering === "ascending" ? 1 : -1;
52+
}
53+
return 0;
54+
});
55+
};
56+
57+
/**
58+
* @description Checks if an array contains duplicate values
59+
* @param {any[]} array Array to check for duplicates
60+
* @returns {boolean} True if duplicates exist, false otherwise
61+
* @example
62+
* checkDuplicates([1, 2, 2, 3]) // returns true
63+
* checkDuplicates([1, 2, 3]) // returns false
64+
*/
65+
export const checkDuplicates = (array: any[]) => new Set(array).size !== array.length;
66+
67+
/**
68+
* @description Finds the string with the most characters in an array of strings
69+
* @param {string[]} strings Array of strings to check
70+
* @returns {string} String with the most characters
71+
* @example
72+
* findStringWithMostCharacters(['a', 'bb', 'ccc']) // returns 'ccc'
73+
*/
74+
export const findStringWithMostCharacters = (strings: string[]): string => {
75+
if (!strings || strings.length === 0) return "";
76+
77+
return strings.reduce((longestString, currentString) =>
78+
currentString.length > longestString.length ? currentString : longestString
79+
);
80+
};
81+
82+
/**
83+
* @description Checks if two arrays have the same elements regardless of order
84+
* @param {any[] | null} arr1 First array
85+
* @param {any[] | null} arr2 Second array
86+
* @returns {boolean} True if arrays have same elements, false otherwise
87+
* @example
88+
* checkIfArraysHaveSameElements([1, 2], [2, 1]) // returns true
89+
* checkIfArraysHaveSameElements([1, 2], [1, 3]) // returns false
90+
*/
91+
export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => {
92+
if (!arr1 || !arr2) return false;
93+
if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false;
94+
if (arr1.length === 0 && arr2.length === 0) return true;
95+
96+
return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e));
97+
};
98+
99+
100+
type GroupedItems<T> = { [key: string]: T[] };
101+
102+
/**
103+
* @description Groups an array of objects by a specified field
104+
* @param {T[]} array Array to group
105+
* @param {keyof T} field Field to group by
106+
* @returns {GroupedItems<T>} Grouped object
107+
* @example
108+
* const array = [{type: 'A', value: 1}, {type: 'B', value: 2}];
109+
* groupByField(array, 'type') // returns { A: [{type: 'A', value: 1}], B: [{type: 'B', value: 2}] }
110+
*/
111+
export const groupByField = <T>(array: T[], field: keyof T): GroupedItems<T> =>
112+
array.reduce((grouped: GroupedItems<T>, item: T) => {
113+
const key = String(item[field]);
114+
grouped[key] = (grouped[key] || []).concat(item);
115+
return grouped;
116+
}, {});
117+
118+
/**
119+
* @description Sorts an array of objects by a specified field
120+
* @param {any[]} array Array to sort
121+
* @param {string} field Field to sort by
122+
* @returns {any[]} Sorted array
123+
* @example
124+
* const array = [{value: 2}, {value: 1}];
125+
* sortByField(array, 'value') // returns [{value: 1}, {value: 2}]
126+
*/
127+
export const sortByField = (array: any[], field: string): any[] =>
128+
array.sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0));
129+
130+
/**
131+
* @description Orders grouped data by a specified field
132+
* @param {GroupedItems<T>} groupedData Grouped data object
133+
* @param {keyof T} orderBy Field to order by
134+
* @returns {GroupedItems<T>} Ordered grouped data
135+
*/
136+
export const orderGroupedDataByField = <T>(groupedData: GroupedItems<T>, orderBy: keyof T): GroupedItems<T> => {
137+
for (const key in groupedData) {
138+
if (groupedData.hasOwnProperty(key)) {
139+
groupedData[key] = groupedData[key].sort((a, b) => {
140+
if (a[orderBy] < b[orderBy]) return -1;
141+
if (a[orderBy] > b[orderBy]) return 1;
142+
return 0;
143+
});
144+
}
145+
}
146+
return groupedData;
147+
};
148+
149+
/**
150+
* @description Builds a tree structure from an array of labels
151+
* @param {IIssueLabel[]} array Array of labels
152+
* @param {any} parent Parent ID
153+
* @returns {IIssueLabelTree[]} Tree structure
154+
*/
155+
export const buildTree = (array: IIssueLabel[], parent = null) => {
156+
const tree: IIssueLabelTree[] = [];
157+
158+
array.forEach((item: any) => {
159+
if (item.parent === parent) {
160+
const children = buildTree(array, item.id);
161+
item.children = children;
162+
tree.push(item);
163+
}
164+
});
165+
166+
return tree;
167+
};
168+
169+
/**
170+
* @description Returns valid keys from object whose value is not falsy
171+
* @param {any} obj Object to check
172+
* @returns {string[]} Array of valid keys
173+
* @example
174+
* getValidKeysFromObject({a: 1, b: 0, c: null}) // returns ['a']
175+
*/
176+
export const getValidKeysFromObject = (obj: any) => {
177+
if (!obj || isEmpty(obj) || typeof obj !== "object" || Array.isArray(obj)) return [];
178+
179+
return Object.keys(obj).filter((key) => !!obj[key]);
180+
};
181+
182+
/**
183+
* @description Converts an array of strings into an object with boolean true values
184+
* @param {string[]} arrayStrings Array of strings
185+
* @returns {Object} Object with string keys and boolean values
186+
* @example
187+
* convertStringArrayToBooleanObject(['a', 'b']) // returns {a: true, b: true}
188+
*/
189+
export const convertStringArrayToBooleanObject = (arrayStrings: string[]) => {
190+
const obj: { [key: string]: boolean } = {};
191+
192+
for (const arrayString of arrayStrings) {
193+
obj[arrayString] = true;
194+
}
195+
196+
return obj;
197+
};

packages/utils/src/auth.ts

+69-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,71 @@
11
import { ReactNode } from "react";
22
import zxcvbn from "zxcvbn";
3-
import { E_PASSWORD_STRENGTH, PASSWORD_CRITERIA, PASSWORD_MIN_LENGTH } from "@plane/constants";
3+
import {
4+
E_PASSWORD_STRENGTH,
5+
SPACE_PASSWORD_CRITERIA,
6+
PASSWORD_MIN_LENGTH,
7+
EErrorAlertType,
8+
EAuthErrorCodes,
9+
} from "@plane/constants";
410

5-
import { EPageTypes, EErrorAlertType, EAuthErrorCodes } from "@plane/constants";
11+
/**
12+
* @description Password strength levels
13+
*/
14+
export enum PasswordStrength {
15+
EMPTY = "empty",
16+
WEAK = "weak",
17+
FAIR = "fair",
18+
GOOD = "good",
19+
STRONG = "strong",
20+
}
21+
22+
/**
23+
* @description Password strength criteria type
24+
*/
25+
export type PasswordCriterion = {
26+
regex: RegExp;
27+
description: string;
28+
};
29+
30+
/**
31+
* @description Password strength criteria
32+
*/
33+
export const PASSWORD_CRITERIA: PasswordCriterion[] = [
34+
{ regex: /[a-z]/, description: "lowercase" },
35+
{ regex: /[A-Z]/, description: "uppercase" },
36+
{ regex: /[0-9]/, description: "number" },
37+
{ regex: /[^a-zA-Z0-9]/, description: "special character" },
38+
];
39+
40+
/**
41+
* @description Checks if password meets all criteria
42+
* @param {string} password - Password to check
43+
* @returns {boolean} Whether password meets all criteria
44+
*/
45+
export const checkPasswordCriteria = (password: string): boolean =>
46+
PASSWORD_CRITERIA.every((criterion) => criterion.regex.test(password));
47+
48+
/**
49+
* @description Checks password strength against criteria
50+
* @param {string} password - Password to check
51+
* @returns {PasswordStrength} Password strength level
52+
* @example
53+
* checkPasswordStrength("abc") // returns PasswordStrength.WEAK
54+
* checkPasswordStrength("Abc123!@#") // returns PasswordStrength.STRONG
55+
*/
56+
export const checkPasswordStrength = (password: string): PasswordStrength => {
57+
if (!password || password.length === 0) return PasswordStrength.EMPTY;
58+
if (password.length < PASSWORD_MIN_LENGTH) return PasswordStrength.WEAK;
59+
60+
const criteriaCount = PASSWORD_CRITERIA.filter((criterion) => criterion.regex.test(password)).length;
61+
62+
const zxcvbnScore = zxcvbn(password).score;
63+
64+
if (criteriaCount <= 1 || zxcvbnScore <= 1) return PasswordStrength.WEAK;
65+
if (criteriaCount === 2 || zxcvbnScore === 2) return PasswordStrength.FAIR;
66+
if (criteriaCount === 3 || zxcvbnScore === 3) return PasswordStrength.GOOD;
67+
return PasswordStrength.STRONG;
68+
};
669

770
export type TAuthErrorInfo = {
871
type: EErrorAlertType;
@@ -26,9 +89,9 @@ export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => {
2689
return passwordStrength;
2790
}
2891

29-
const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every(
30-
(criterion) => criterion
31-
);
92+
const passwordCriteriaValidation = SPACE_PASSWORD_CRITERIA.map((criteria) =>
93+
criteria.isCriteriaValid(password)
94+
).every((criterion) => criterion);
3295
const passwordStrengthScore = zxcvbn(password).score;
3396

3497
if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) {
@@ -76,7 +139,7 @@ const errorCodeMessages: {
76139
// sign up
77140
[EAuthErrorCodes.USER_ALREADY_EXIST]: {
78141
title: `User already exists`,
79-
message: (email = undefined) => `Your account is already registered. Sign in now.`,
142+
message: () => `Your account is already registered. Sign in now.`,
80143
},
81144
[EAuthErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: {
82145
title: `Email and password required`,

packages/utils/src/color.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
export type RGB = { r: number; g: number; b: number };
99

1010
/**
11-
* Validates and clamps color values to RGB range (0-255)
11+
* @description Validates and clamps color values to RGB range (0-255)
1212
* @param {number} value - The color value to validate
1313
* @returns {number} Clamped and floored value between 0-255
14+
* @example
15+
* validateColor(-10) // returns 0
16+
* validateColor(300) // returns 255
17+
* validateColor(128) // returns 128
1418
*/
1519
export const validateColor = (value: number) => {
1620
if (value < 0) return 0;

0 commit comments

Comments
 (0)