PromucFlow_constructor/app/client/src/utils/helpers.tsx
Valera Melnikov 8be4936ca0
chore: add color validation + native color picker (#25355)
## Description
1. Reduced the number of default colors. Because the amount of suggested
options was too much: very little difference between shades and
sometimes with hues too. By removing half of them, we allow builders
make better choices faster. The transparent color has also been removed.
2. Added validation of color values because HTML colors are remarkably
easy to get wrong, because they allow so many different values and now
we support and validate all these guys
    - `hex` - `#bada55`
    - `name` - `LightGoldenrodYellow`
    - `special name` - `currentColor`
    - `rgb` - `rgb(0 0 0)`
    - `rgba` - `rgba(0, 0, 0, .45)`
    - `hsl` - `hsl(4.71239rad, 60%, 70%)`
    - `hsla` - `hsla(180deg 100% 50% / .8)`
    - `hwb` - `hwb(180deg 0% 0% / 100%)`
    - `lab` - `lab(2000.1337% -8.6911 -159.131231 / .987189732)`
    - `lch` - `lch(54.292% 106.839 40.853)`
<img width="283" alt="Снимок экрана 2023-08-02 в 17 58 07"
src="https://github.com/appsmithorg/appsmith/assets/11555074/a8fef365-506d-432e-85ad-cdb550de1f60">
    
3. Added support for a Full color picker. Now we can easily switch
between modes and builders can easily choose any colors.
<img width="259" alt="Снимок экрана 2023-08-02 в 17 43 34"
src="https://github.com/appsmithorg/appsmith/assets/11555074/be09cd92-7c69-43eb-812a-0b1fe3ac9ef6">


#### PR fixes following issue(s)
Fixes  #22996

#### Media

https://www.loom.com/share/098e0116e49744e7b10689d4a18ab664?sid=15405577-160e-4b48-bfef-bc8dcfa97efe

#### Type of change
- New feature (non-breaking change which adds functionality)

## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [x] Jest
- [x] Cypress

## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: Valera Melnikov <melnikov.vv@greendatasoft.ru>
2023-08-07 11:55:10 +03:00

1158 lines
31 KiB
TypeScript

import React from "react";
import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import lazyLottie from "./lazyLottie";
import welcomeConfettiAnimationURL from "assets/lottie/welcome-confetti.json.txt";
import {
DATA_TREE_KEYWORDS,
DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS,
JAVASCRIPT_KEYWORDS,
} from "constants/WidgetValidation";
import { get, set, isNil, has, uniq } from "lodash";
import type { Workspace } from "@appsmith/constants/workspaceConstants";
import { hasCreateNewAppPermission } from "@appsmith/utils/permissionHelpers";
import moment from "moment";
import { isDynamicValue } from "./DynamicBindingUtils";
import type { ApiResponse } from "api/ApiResponses";
import type { DSLWidget } from "widgets/constants";
import * as Sentry from "@sentry/react";
import { matchPath } from "react-router";
import {
BUILDER_CUSTOM_PATH,
BUILDER_PATH,
BUILDER_PATH_DEPRECATED,
VIEWER_CUSTOM_PATH,
VIEWER_PATH,
VIEWER_PATH_DEPRECATED,
} from "constants/routes";
import history from "./history";
import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants";
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { checkContainerScrollable } from "widgets/WidgetUtils";
import type { ContainerWidgetProps } from "widgets/ContainerWidget/widget";
import type { WidgetProps } from "widgets/BaseWidget";
import { getContainerIdForCanvas } from "sagas/WidgetOperationUtils";
import scrollIntoView from "scroll-into-view-if-needed";
import validateColor from "validate-color";
export const snapToGrid = (
columnWidth: number,
rowHeight: number,
x: number,
y: number,
) => {
const snappedX = Math.round(x / columnWidth);
const snappedY = Math.round(y / rowHeight);
return [snappedX, snappedY];
};
export const formatBytes = (bytes: string | number) => {
if (!bytes) return;
const value = typeof bytes === "string" ? parseInt(bytes) : bytes;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
if (value === 0) return "0 bytes";
const i = parseInt(String(Math.floor(Math.log(value) / Math.log(1024))));
if (i === 0) return bytes + " " + sizes[i];
return (value / Math.pow(1024, i)).toFixed(1) + " " + sizes[i];
};
export const getAbsolutePixels = (size?: string | null) => {
if (!size) return 0;
const _dex = size.indexOf("px");
if (_dex === -1) return 0;
return parseInt(size.slice(0, _dex), 10);
};
export const Directions: { [id: string]: string } = {
UP: "up",
DOWN: "down",
LEFT: "left",
RIGHT: "right",
RIGHT_BOTTOM: "RIGHT_BOTTOM",
};
export type Direction = (typeof Directions)[keyof typeof Directions];
const SCROLL_THRESHOLD = 20;
export const getScrollByPixels = function (
elem: {
top: number;
height: number;
},
scrollParent: Element,
child: Element,
): {
scrollAmount: number;
speed: number;
} {
const scrollParentBounds = scrollParent.getBoundingClientRect();
const scrollChildBounds = child.getBoundingClientRect();
const scrollAmount =
2 *
GridDefaults.CANVAS_EXTENSION_OFFSET *
GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
const topBuff =
elem.top + scrollChildBounds.top > 0
? elem.top +
scrollChildBounds.top -
SCROLL_THRESHOLD -
scrollParentBounds.top
: 0;
const bottomBuff =
scrollParentBounds.bottom -
(elem.top + elem.height + scrollChildBounds.top + SCROLL_THRESHOLD);
if (topBuff < SCROLL_THRESHOLD) {
const speed = Math.max(
(SCROLL_THRESHOLD - topBuff) / (2 * SCROLL_THRESHOLD),
0.1,
);
return {
scrollAmount: 0 - scrollAmount,
speed,
};
}
if (bottomBuff < SCROLL_THRESHOLD) {
const speed = Math.max(
(SCROLL_THRESHOLD - bottomBuff) / (2 * SCROLL_THRESHOLD),
0.1,
);
return {
scrollAmount,
speed,
};
}
return {
scrollAmount: 0,
speed: 0,
};
};
export const scrollElementIntoParentCanvasView = (
el: {
top: number;
height: number;
} | null,
parent: Element | null,
child: Element | null,
) => {
if (el) {
const scrollParent = parent;
if (scrollParent && child) {
const { scrollAmount: scrollBy } = getScrollByPixels(
el,
scrollParent,
child,
);
if (scrollBy < 0 && scrollParent.scrollTop > 0) {
scrollParent.scrollBy({ top: scrollBy, behavior: "smooth" });
}
if (scrollBy > 0) {
scrollParent.scrollBy({ top: scrollBy, behavior: "smooth" });
}
}
}
};
export function hasClass(ele: HTMLElement, cls: string) {
return ele.classList.contains(cls);
}
function addClass(ele: HTMLElement, cls: string) {
if (!hasClass(ele, cls)) ele.classList.add(cls);
}
function removeClass(ele: HTMLElement, cls: string) {
if (hasClass(ele, cls)) {
ele.classList.remove(cls);
}
}
export const removeSpecialChars = (value: string, limit?: number) => {
const separatorRegex = /\W+/;
return value
.split(separatorRegex)
.join("_")
.slice(0, limit || 30);
};
export const flashElement = (
el: HTMLElement,
flashTimeout = 1000,
flashClass = "flash",
) => {
if (!el) return;
addClass(el, flashClass);
setTimeout(() => {
removeClass(el, flashClass);
}, flashTimeout);
};
/**
* flash elements with a background color
*
* @param id
* @param timeout
* @param flashTimeout
* @param flashColor
*/
export const flashElementsById = (
id: string | string[],
timeout = 0,
flashTimeout?: number,
flashClass?: string,
) => {
let ids: string[] = [];
if (Array.isArray(id)) {
ids = ids.concat(id);
} else {
ids = ids.concat([id]);
}
ids.forEach((id) => {
setTimeout(() => {
const el = document.getElementById(id);
if (el) flashElement(el, flashTimeout, flashClass);
}, timeout);
});
};
/**
* Scrolls to the widget of WidgetId without any animantion.
* @param widgetId
* @param canvasWidgets
*/
export const quickScrollToWidget = (
widgetId: string,
canvasWidgets: CanvasWidgetsReduxState,
) => {
if (!widgetId || widgetId === "") return;
window.requestIdleCallback(() => {
const el = document.getElementById(widgetId);
const canvas = document.getElementById("canvas-viewport");
if (el && canvas && !isElementVisibleInContainer(el, canvas, 5)) {
const scrollElement = getWidgetElementToScroll(widgetId, canvasWidgets);
if (scrollElement) {
scrollIntoView(scrollElement, {
block: "center",
inline: "nearest",
behavior: "smooth",
});
}
}
});
};
/** Checks if a percentage of element is visible inside a container or not
The function first retrieves the bounding rectangles of both the
container and the element using the getBoundingClientRect() method.
It then calculates the visible area of the element inside the container
by determining the intersection between the two bounding rectangles.
The function then calculates the percentage of the element that is
visible by dividing the visible area by the total area of the element
and multiplying by 100. Finally, it returns true if the visible percentage
is greater than or equal to the desired percentage, and false otherwise.
Note that this function assumes that the element and the container
are both positioned using the CSS position property, and that the
container is positioned relative to its containing block. If the
element or the container have a different positioning, the
function may need to be adjusted accordingly.
**/
function isElementVisibleInContainer(
element: HTMLElement,
container: HTMLElement,
percentage = 100,
) {
const elementBounds = element.getBoundingClientRect();
const containerBounds = container.getBoundingClientRect();
// Calculate the visible area of the element inside the container
const visibleWidth =
Math.min(elementBounds.right, containerBounds.right) -
Math.max(elementBounds.left, containerBounds.left);
const visibleHeight =
Math.min(elementBounds.bottom, containerBounds.bottom) -
Math.max(elementBounds.top, containerBounds.top);
const visibleArea = visibleWidth * visibleHeight;
// Calculate the percentage of the element that is visible
const elementArea = element.clientWidth * element.clientHeight;
const visiblePercentage = (visibleArea / elementArea) * 100;
// Return whether the visible percentage is greater than or equal to the desired percentage
return visiblePercentage >= percentage;
}
function getWidgetElementToScroll(
widgetId: string,
canvasWidgets: CanvasWidgetsReduxState,
) {
const widget = canvasWidgets[widgetId];
const parentId = widget.parentId || "";
const containerId = getContainerIdForCanvas(parentId);
if (containerId === MAIN_CONTAINER_WIDGET_ID) {
if (widget.type !== "MODAL_WIDGET") {
return document.getElementById(widgetId);
}
}
const containerWidget = canvasWidgets[
containerId
] as ContainerWidgetProps<WidgetProps>;
if (checkContainerScrollable(containerWidget)) {
return document.getElementById(widgetId);
} else {
return document.getElementById(containerId);
}
}
export const resolveAsSpaceChar = (value: string, limit?: number) => {
// ensures that all special characters are disallowed
// while allowing all utf-8 characters
const removeSpecialCharsRegex =
/`|\~|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\+|\=|\[|\{|\]|\}|\||\\|\'|\<|\,|\.|\>|\?|\/|\""|\;|\:|\s/;
const duplicateSpaceRegex = /\s+/;
return value
.split(removeSpecialCharsRegex)
.join(" ")
.split(duplicateSpaceRegex)
.join(" ")
.slice(0, limit || 30);
};
export const PLATFORM_OS = {
MAC: "MAC",
IOS: "IOS",
LINUX: "LINUX",
ANDROID: "ANDROID",
WINDOWS: "WINDOWS",
};
const platformOSRegex = {
[PLATFORM_OS.MAC]: /mac.*/i,
[PLATFORM_OS.IOS]: /(?:iphone|ipod|ipad|Pike v.*)/i,
[PLATFORM_OS.LINUX]: /(?:linux.*)/i,
[PLATFORM_OS.ANDROID]: /android.*|aarch64|arm.*/i,
[PLATFORM_OS.WINDOWS]: /win.*/i,
};
export const getPlatformOS = () => {
const browserPlatform =
typeof navigator !== "undefined" ? navigator.platform : null;
if (browserPlatform) {
const platformOSList = Object.entries(platformOSRegex);
const platform = platformOSList.find(([, regex]) =>
regex.test(browserPlatform),
);
return platform ? platform[0] : null;
}
return null;
};
export const isMacOrIOS = () => {
const platformOS = getPlatformOS();
return platformOS === PLATFORM_OS.MAC || platformOS === PLATFORM_OS.IOS;
};
export const getBrowserInfo = () => {
const userAgent =
typeof navigator !== "undefined" ? navigator.userAgent : null;
if (userAgent) {
let specificMatch;
let match =
userAgent.match(
/(opera|chrome|safari|firefox|msie|CriOS|trident(?=\/))\/?\s*(\d+)/i,
) || [];
// browser
if (/CriOS/i.test(match[1])) match[1] = "Chrome";
if (match[1] === "Chrome") {
specificMatch = userAgent.match(/\b(OPR|Edge)\/(\d+)/);
if (specificMatch) {
const opera = specificMatch.slice(1);
return {
browser: opera[0].replace("OPR", "Opera"),
version: opera[1],
};
}
specificMatch = userAgent.match(/\b(Edg)\/(\d+)/);
if (specificMatch) {
const edge = specificMatch.slice(1);
return {
browser: edge[0].replace("Edg", "Edge (Chromium)"),
version: edge[1],
};
}
}
// version
match = match[2]
? [match[1], match[2]]
: [navigator.appName, navigator.appVersion, "-?"];
const version = userAgent.match(/version\/(\d+)/i);
version && match.splice(1, 1, version[1]);
return { browser: match[0], version: match[1] };
}
return null;
};
/**
* Removes the trailing slashes from the path
* @param path
* @example
* ```js
* let trimmedUrl = trimTrailingSlash('/url/')
* console.log(trimmedUrl) //will output /url
* ```
* @example
* ```js
* let trimmedUrl = trimTrailingSlash('/yet-another-url//')
* console.log(trimmedUrl) // will output /yet-another-url
* ```
*/
export const trimTrailingSlash = (path: string) => {
const trailingUrlRegex = /\/+$/;
return path.replace(trailingUrlRegex, "");
};
/**
* checks if ellipsis is active
* this function is meant for checking the existence of ellipsis by CSS.
* Since ellipsis by CSS are not part of DOM, we are checking with scroll width\height and offsetidth\height.
* ScrollWidth\ScrollHeight is always greater than the offsetWidth\OffsetHeight when ellipsis made by CSS is active.
* Using clientWidth to fix this https://stackoverflow.com/a/21064102/8692954
* @param element
*/
export const isEllipsisActive = (element: HTMLElement | null) => {
return element && element.clientWidth < element.scrollWidth;
};
export const isVerticalEllipsisActive = (element: HTMLElement | null) => {
return element && element.clientHeight < element.scrollHeight;
};
/**
* converts array to sentences
* for e.g - ['Pawan', 'Abhinav', 'Hetu'] --> 'Pawan, Abhinav and Hetu'
*
* @param arr string[]
*/
export const convertArrayToSentence = (arr: string[]) => {
return arr.join(", ").replace(/,\s([^,]+)$/, " and $1");
};
/**
* checks if the name is conflicting with
* 1. API names,
* 2. Queries name
* 3. Javascript reserved names
* 4. Few internal function names that are in the evaluation tree
*
* return if false name conflicts with anything from the above list
*
* @param name
* @param invalidNames
*/
export const isNameValid = (
name: string,
invalidNames: Record<string, any>,
) => {
return !(
has(JAVASCRIPT_KEYWORDS, name) ||
has(DATA_TREE_KEYWORDS, name) ||
has(DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS, name) ||
has(APPSMITH_GLOBAL_FUNCTIONS, name) ||
has(invalidNames, name)
);
};
/*
* Filter out empty items from an array
* for e.g - ['Pawan', undefined, 'Hetu'] --> ['Pawan', 'Hetu']
*
* @param array any[]
*/
export const removeFalsyEntries = (arr: any[]): any[] => {
return arr.filter(Boolean);
};
/**
* checks if variable passed is of type string or not
*
* for e.g -> 'Pawan' -> true
* ['Pawan', 'Goku'] -> false
* { name: "Pawan"} -> false
*/
export const isString = (str: any) => {
return typeof str === "string" || str instanceof String;
};
/**
* Returns substring between two set of strings
* eg ->
* getSubstringBetweenTwoWords("abcdefgh", "abc", "fgh") -> de
*/
export const getSubstringBetweenTwoWords = (
str: string,
startWord: string,
endWord: string,
) => {
const endIndexOfStartWord = str.indexOf(startWord) + startWord.length;
const startIndexOfEndWord = str.lastIndexOf(endWord);
if (startIndexOfEndWord < endIndexOfStartWord) return "";
return str.substring(startIndexOfEndWord, endIndexOfStartWord);
};
export const playWelcomeAnimation = (container: string) => {
playLottieAnimation(container, welcomeConfettiAnimationURL);
};
const playLottieAnimation = (
selector: string,
animationURL: string,
styles?: any,
) => {
const container: Element = document.querySelector(selector) as Element;
if (!container) return;
const el = document.createElement("div");
Object.assign(el.style, {
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
"z-index": 99,
width: "100%",
height: "100%",
...styles,
});
container.appendChild(el);
const animObj = lazyLottie.loadAnimation({
container: el,
path: animationURL,
loop: false,
});
animObj.play();
animObj.addEventListener("complete", () => {
container.removeChild(el);
});
};
export const getSelectedText = () => {
if (typeof window.getSelection === "function") {
const selectionObj = window.getSelection();
return selectionObj && selectionObj.toString();
}
};
/**
* calculates and returns the scrollwidth
*
* @returns
*/
export const scrollbarWidth = () => {
const scrollDiv = document.createElement("div");
scrollDiv.setAttribute(
"style",
"width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;",
);
document.body.appendChild(scrollDiv);
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
return scrollbarWidth;
};
// Flatten object
// From { isValid: false, settings: { color: false}}
// To { isValid: false, settings.color: false}
export const flattenObject = (data: Record<string, any>) => {
const result: Record<string, any> = {};
function recurse(cur: any, prop: any) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
for (let i = 0, l = cur.length; i < l; i++)
recurse(cur[i], prop + "[" + i + "]");
if (cur.length == 0) result[prop] = [];
} else {
let isEmpty = true;
for (const p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop + "." + p : p);
}
if (isEmpty && prop) result[prop] = {};
}
}
recurse(data, "");
return result;
};
/**
* renames key in object
*
* @param object
* @param key
* @param newKey
* @returns
*/
export const renameKeyInObject = (object: any, key: string, newKey: string) => {
if (object[key]) {
set(object, newKey, object[key]);
}
return object;
};
// Can be used to check if the user has developer role access to workspace
export const getCanCreateApplications = (currentWorkspace: Workspace) => {
const userWorkspacePermissions = currentWorkspace.userPermissions || [];
const canManage = hasCreateNewAppPermission(userWorkspacePermissions ?? []);
return canManage;
};
export const getIsSafeRedirectURL = (redirectURL: string) => {
try {
return (
new URL(redirectURL, window.location.origin).origin ===
window.location.origin
);
} catch (e) {
return false;
}
};
export const stopClickEventPropagation = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
e.stopPropagation();
};
/**
*
* Get text for how much time before an action happened
* Eg: 1 Month, 12 Seconds
*
* @param date 2021-09-08T14:14:12Z
*
*/
export const howMuchTimeBeforeText = (
date: string,
options: { lessThanAMinute: boolean } = { lessThanAMinute: false },
) => {
if (!date || !moment.isMoment(moment(date))) {
return "";
}
const { lessThanAMinute } = options;
const now = moment();
const checkDate = moment(date);
const years = now.diff(checkDate, "years");
const months = now.diff(checkDate, "months");
const days = now.diff(checkDate, "days");
const hours = now.diff(checkDate, "hours");
const minutes = now.diff(checkDate, "minutes");
const seconds = now.diff(checkDate, "seconds");
if (years > 0) return `${years} yr${years > 1 ? "s" : ""}`;
else if (months > 0) return `${months} mth${months > 1 ? "s" : ""}`;
else if (days > 0) return `${days} day${days > 1 ? "s" : ""}`;
else if (hours > 0) return `${hours} hr${hours > 1 ? "s" : ""}`;
else if (minutes > 0) return `${minutes} min${minutes > 1 ? "s" : ""}`;
else
return lessThanAMinute
? "less than a minute"
: `${seconds} sec${seconds > 1 ? "s" : ""}`;
};
/**
*
* Truncate string and append given string in the end
* eg: Flint Lockwood Diatonic Super Mutating Dynamic Food Replicator
* -> Flint...
*
*/
export const truncateString = (
str: string,
limit: number,
appendStr = "...",
) => {
if (str.length <= limit) return str;
let _subString = str.substring(0, limit);
_subString = _subString.trim() + appendStr;
return _subString;
};
/**
* returns the modText ( ctrl or command ) based on the user machine
*
* @returns
*/
export const modText = () => (isMacOrIOS() ? "\u2318" : "Ctrl +");
export const altText = () => (isMacOrIOS() ? "\u2325" : "Alt +");
export const shiftText = () => (isMacOrIOS() ? "\u21EA" : "Shift +");
export const undoShortCut = () => <span>{modText()} Z</span>;
export const redoShortCut = () =>
isMacOrIOS() ? (
<span>
{modText()} {shiftText()} Z
</span>
) : (
<span>{modText()} Y</span>
);
/**
* @returns the original string after trimming the string past `?`
*/
export const trimQueryString = (value = "") => {
const index = value.indexOf("?");
if (index === -1) return value;
return value.slice(0, index);
};
/**
* returns the value in the query string for a key
*/
export const getSearchQuery = (search = "", key: string) => {
const params = new URLSearchParams(search);
return decodeURIComponent(params.get(key) || "");
};
/*
* unfocus all window selection
*
* @param document
* @param window
*/
export function unFocus(document: Document, window: Window) {
if (document.getSelection()) {
document.getSelection()?.empty();
} else {
try {
window.getSelection()?.removeAllRanges();
// eslint-disable-next-line no-empty
} catch (e) {}
}
}
export function getLogToSentryFromResponse(response?: ApiResponse) {
return response && response?.responseMeta?.status >= 500;
}
/**
* extract colors from string
*
* @returns
* @param widgets
*/
export function extractColorsFromString(widgets: CanvasWidgetsReduxState) {
const colors = new Set();
Object.values(widgets).forEach((widget) => {
Object.values(widget).forEach((widgetProp) => {
if (isString(widgetProp) && validateColor(widgetProp)) {
colors.add(widgetProp);
}
});
});
return Array.from(colors) as Array<string>;
}
/**
* validate color string
*
* @returns {boolean} true if string is valid color or includes url
* @param color
*/
export function isValidColor(color: string) {
return color?.includes("url") || validateColor(color);
}
/*
* Function to merge property pane config of a widget
*
*/
export const mergeWidgetConfig = (target: any, source: any) => {
const sectionMap: Record<string, any> = {};
target.forEach((section: { sectionName: string }) => {
sectionMap[section.sectionName] = section;
});
source.forEach((section: { sectionName: string; children: any[] }) => {
const targetSection = sectionMap[section.sectionName];
if (targetSection) {
Array.prototype.push.apply(targetSection.children, section.children);
} else {
target.push(section);
}
});
return target;
};
export const getLocale = () => {
return navigator.languages?.[0] || "en-US";
};
/**
* Function to check if the DynamicBindingPathList is valid
* @param currentDSL
* @returns
*/
export const captureInvalidDynamicBindingPath = (
currentDSL: Readonly<DSLWidget>,
) => {
//Get the dynamicBindingPathList of the current DSL
const dynamicBindingPathList = get(currentDSL, "dynamicBindingPathList");
dynamicBindingPathList?.forEach((dBindingPath) => {
const pathValue = get(currentDSL, dBindingPath.key); //Gets the value for the given dynamic binding path
/**
* Checks if dynamicBindingPathList contains a property path that doesn't have a binding
*/
if (!isDynamicValue(pathValue)) {
Sentry.captureException(
new Error(
`INVALID_DynamicPathBinding_CLIENT_ERROR: Invalid dynamic path binding list: ${currentDSL.widgetName}.${dBindingPath.key}`,
),
);
return;
}
});
if (currentDSL.children) {
currentDSL.children.map(captureInvalidDynamicBindingPath);
}
return currentDSL;
};
/**
* Function to handle undefined returned in case of using [].find()
* @param result
* @param errorMessage
* @returns the result if not undefined or throws an Error
*/
export function shouldBeDefined<T>(
result: T | undefined | null,
errorMessage: string,
): T {
if (result === undefined || result === null) {
throw new TypeError(errorMessage);
}
return result;
}
/*
* Check if a value is null / undefined / empty string
*
* @param value: any
*/
export const isEmptyOrNill = (value: any) => {
return isNil(value) || (isString(value) && value === "");
};
export const isURLDeprecated = (url: string) => {
return !!matchPath(url, {
path: [
trimQueryString(BUILDER_PATH_DEPRECATED),
trimQueryString(VIEWER_PATH_DEPRECATED),
],
strict: false,
exact: false,
});
};
export const matchPath_BuilderSlug = (path: string) =>
matchPath<{ applicationSlug: string; pageSlug: string; pageId: string }>(
path,
{
path: trimQueryString(BUILDER_PATH),
strict: false,
exact: false,
},
);
export const matchPath_ViewerSlug = (path: string) =>
matchPath<{ applicationSlug: string; pageSlug: string; pageId: string }>(
path,
{
path: trimQueryString(VIEWER_PATH),
strict: false,
exact: false,
},
);
export const matchPath_BuilderCustomSlug = (path: string) =>
matchPath<{ customSlug: string }>(path, {
path: trimQueryString(BUILDER_CUSTOM_PATH),
});
export const matchPath_ViewerCustomSlug = (path: string) =>
matchPath<{ customSlug: string }>(path, {
path: trimQueryString(VIEWER_CUSTOM_PATH),
});
export const getUpdatedRoute = (
path: string,
params: Record<string, string>,
) => {
const updatedPath = path;
const matchBuilderSlugPath = matchPath_BuilderSlug(path);
const matchBuilderCustomPath = matchPath_BuilderCustomSlug(path);
const matchViewerSlugPath = matchPath_ViewerSlug(path);
const matchViewerCustomPath = matchPath_ViewerCustomSlug(path);
/*
* Note: When making changes to the order of these conditions
* Be sure to check if it is sync with the order of paths AppRouter.ts
* Context: https://github.com/appsmithorg/appsmith/pull/19833
*/
if (matchBuilderSlugPath?.params) {
return getUpdateRouteForSlugPath(
path,
matchBuilderSlugPath.params.applicationSlug,
matchBuilderSlugPath.params.pageSlug,
params,
);
} else if (matchBuilderCustomPath?.params) {
return getUpdatedRouteForCustomSlugPath(
path,
matchBuilderCustomPath.params.customSlug,
params,
);
} else if (matchViewerSlugPath) {
return getUpdateRouteForSlugPath(
path,
matchViewerSlugPath.params.applicationSlug,
matchViewerSlugPath.params.pageSlug,
params,
);
} else if (matchViewerCustomPath) {
return getUpdatedRouteForCustomSlugPath(
path,
matchViewerCustomPath.params.customSlug,
params,
);
}
return updatedPath;
};
const getUpdatedRouteForCustomSlugPath = (
path: string,
customSlug: string,
params: Record<string, string>,
) => {
let updatedPath = path;
if (params.customSlug) {
updatedPath = updatedPath.replace(`${customSlug}`, `${params.customSlug}-`);
} else if (params.applicationSlug && params.pageSlug) {
updatedPath = updatedPath.replace(
`${customSlug}`,
`${params.applicationSlug}/${params.pageSlug}-`,
);
}
return updatedPath;
};
const getUpdateRouteForSlugPath = (
path: string,
applicationSlug: string,
pageSlug: string,
params: Record<string, string>,
) => {
let updatedPath = path;
if (params.customSlug) {
updatedPath = updatedPath.replace(
`${applicationSlug}/${pageSlug}`,
`${params.customSlug}-`,
);
return updatedPath;
}
if (params.applicationSlug)
updatedPath = updatedPath.replace(applicationSlug, params.applicationSlug);
if (params.pageSlug)
updatedPath = updatedPath.replace(pageSlug, `${params.pageSlug}-`);
return updatedPath;
};
// to split relative url into array, so specific parts can be bolded on UI preview
export const splitPathPreview = (
url: string,
customSlug?: string,
): string | string[] => {
const slugMatch = matchPath<{ pageId: string; pageSlug: string }>(
url,
VIEWER_PATH,
);
const customSlugMatch = matchPath<{ pageId: string; customSlug: string }>(
url,
VIEWER_CUSTOM_PATH,
);
if (!customSlug && slugMatch?.isExact) {
const { pageSlug } = slugMatch.params;
const splitUrl = url.split(pageSlug);
splitUrl.splice(
1,
0,
pageSlug.slice(0, pageSlug.length - 1), // to split -
pageSlug.slice(pageSlug.length - 1),
);
return splitUrl;
} else if (customSlug && customSlugMatch?.isExact) {
const { customSlug } = customSlugMatch.params;
const splitUrl = url.split(customSlug);
splitUrl.splice(
1,
0,
customSlug.slice(0, customSlug.length - 1), // to split -
customSlug.slice(customSlug.length - 1),
);
return splitUrl;
}
return url;
};
export const updateSlugNamesInURL = (params: Record<string, string>) => {
const { pathname, search } = window.location;
// Do not update old URLs
if (isURLDeprecated(pathname)) return;
const newURL = getUpdatedRoute(pathname, params);
history.replace(newURL + search);
};
/**
* Function to get valid supported mimeType for different browsers
* @param media "video" | "audio"
* @returns mimeType string
*/
export const getSupportedMimeTypes = (media: "video" | "audio") => {
const videoTypes = ["webm", "ogg", "mp4", "x-matroska"];
const audioTypes = ["webm", "ogg", "mp3", "x-matroska"];
const codecs = [
"should-not-be-supported",
"vp9",
"vp9.0",
"vp8",
"vp8.0",
"avc1",
"av1",
"h265",
"h.265",
"h264",
"h.264",
"opus",
"pcm",
"aac",
"mpeg",
"mp4a",
];
const supported: Array<string> = [];
const isSupported = MediaRecorder.isTypeSupported;
const types = media === "video" ? videoTypes : audioTypes;
types.forEach((type: string) => {
const mimeType = `${media}/${type}`;
// without codecs
isSupported(mimeType) && supported.push(mimeType);
// with codecs
codecs.forEach((codec) =>
[
`${mimeType};codecs=${codec}`,
`${mimeType};codecs=${codec.toUpperCase()}`,
].forEach(
(variation) => isSupported(variation) && supported.push(variation),
),
);
});
return supported[0];
};
export function AutoBind(target: any, _: string, descriptor: any) {
if (typeof descriptor.value === "function")
descriptor.value = descriptor.value.bind(target);
return descriptor;
}
/**
* Add item to an array which could be undefined
* @param arr1 Base Array (could be undefined)
* @param item Item to add to array
* @param makeUnique Should make sure array has unique entries
* @returns array which includes items from arr1 and item
*/
export function pushToArray(
item: unknown,
arr1?: unknown[],
makeUnique = false,
) {
if (Array.isArray(arr1)) arr1.push(item);
else return [item];
if (makeUnique) return uniq(arr1);
return arr1;
}
/**
* Add items to array which could be undefined
* @param arr1 Base Array (could be undefined)
* @param items Items to add to arr1
* @param makeUnique Should make sure array has unique entries
* @returns array which contains items from arr1 and items
*/
export function concatWithArray(
items: unknown[],
arr1?: unknown[],
makeUnique = false,
) {
let finalArr: unknown[] = [];
if (Array.isArray(arr1)) finalArr = arr1.concat(items);
else finalArr = finalArr.concat(items);
if (makeUnique) return uniq(finalArr);
return finalArr;
}
export const capitalizeFirstLetter = (str: string) => {
// Find the index of the first letter of the first sentence
const firstLetterIndex = str.search(/[a-z]/i);
// If there are no letters in the string, return the original string
if (firstLetterIndex === -1) {
return str;
}
// Capitalize the first letter of the first sentence and return the modified string
return (
str.slice(0, firstLetterIndex) +
str.charAt(firstLetterIndex).toUpperCase() +
str.slice(firstLetterIndex + 1).toLocaleLowerCase()
);
};