499 lines
12 KiB
TypeScript
499 lines
12 KiB
TypeScript
import React from "react";
|
|
import { GridDefaults } from "constants/WidgetConstants";
|
|
import lottie from "lottie-web";
|
|
import confetti from "assets/lottie/binding.json";
|
|
import welcomeConfetti from "assets/lottie/welcome-confetti.json";
|
|
import successAnimation from "assets/lottie/success-animation.json";
|
|
import {
|
|
DATA_TREE_KEYWORDS,
|
|
JAVASCRIPT_KEYWORDS,
|
|
WINDOW_OBJECT_METHODS,
|
|
WINDOW_OBJECT_PROPERTIES,
|
|
} from "constants/WidgetValidation";
|
|
import { GLOBAL_FUNCTIONS } from "./autocomplete/EntityDefinitions";
|
|
import { set } from "lodash";
|
|
import { Org } from "constants/orgConstants";
|
|
import {
|
|
isPermitted,
|
|
PERMISSION_TYPE,
|
|
} from "pages/Applications/permissionHelpers";
|
|
import { User } from "constants/userConstants";
|
|
import { getAppsmithConfigs } from "configs";
|
|
import { sha256 } from "js-sha256";
|
|
|
|
const { intercomAppID, isAppsmithCloud } = getAppsmithConfigs();
|
|
|
|
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 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,
|
|
flashColor = "#FFCB33",
|
|
) => {
|
|
el.style.backgroundColor = flashColor;
|
|
|
|
setTimeout(() => {
|
|
el.style.backgroundColor = "transparent";
|
|
}, 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,
|
|
flashColor?: 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);
|
|
|
|
el?.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "center",
|
|
inline: "center",
|
|
});
|
|
|
|
if (el) flashElement(el, flashTimeout, flashColor);
|
|
}, timeout);
|
|
});
|
|
};
|
|
|
|
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 isMac = () => {
|
|
const platform =
|
|
typeof navigator !== "undefined" ? navigator.platform : undefined;
|
|
return !platform ? false : /Mac|iPod|iPhone|iPad/.test(platform);
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param element
|
|
*/
|
|
export const isEllipsisActive = (element: HTMLElement | null) => {
|
|
return (
|
|
element &&
|
|
(element.offsetWidth < element.scrollWidth ||
|
|
element.offsetHeight < 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 !(
|
|
name in JAVASCRIPT_KEYWORDS ||
|
|
name in DATA_TREE_KEYWORDS ||
|
|
name in GLOBAL_FUNCTIONS ||
|
|
name in WINDOW_OBJECT_PROPERTIES ||
|
|
name in WINDOW_OBJECT_METHODS ||
|
|
name in invalidNames
|
|
);
|
|
};
|
|
|
|
/*
|
|
* 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 playOnboardingAnimation = () => {
|
|
playLottieAnimation("#root", confetti);
|
|
};
|
|
|
|
export const playWelcomeAnimation = (container: string) => {
|
|
playLottieAnimation(container, welcomeConfetti);
|
|
};
|
|
|
|
export const playOnboardingStepCompletionAnimation = () => {
|
|
playLottieAnimation(".onboarding-step-indicator", successAnimation, {
|
|
"background-color": "white",
|
|
padding: "60px",
|
|
});
|
|
};
|
|
|
|
const playLottieAnimation = (
|
|
selector: string,
|
|
animation: any,
|
|
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 = lottie.loadAnimation({
|
|
container: el,
|
|
animationData: animation,
|
|
loop: false,
|
|
});
|
|
|
|
const duration = (animObj.totalFrames / animObj.frameRate) * 1000;
|
|
|
|
animObj.play();
|
|
setTimeout(() => {
|
|
container.removeChild(el);
|
|
}, duration);
|
|
};
|
|
|
|
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 org
|
|
export const getCanCreateApplications = (currentOrg: Org) => {
|
|
const userOrgPermissions = currentOrg.userPermissions || [];
|
|
const canManage = isPermitted(
|
|
userOrgPermissions,
|
|
PERMISSION_TYPE.CREATE_APPLICATION,
|
|
);
|
|
return canManage;
|
|
};
|
|
|
|
export const getIsSafeRedirectURL = (redirectURL: string) => {
|
|
try {
|
|
return new URL(redirectURL).origin === window.location.origin;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export function bootIntercom(user?: User) {
|
|
if (intercomAppID && window.Intercom) {
|
|
let { email, username } = user || {};
|
|
let name;
|
|
if (!isAppsmithCloud) {
|
|
username = sha256(username || "");
|
|
email = sha256(email || "");
|
|
} else {
|
|
name = user?.name;
|
|
}
|
|
|
|
window.Intercom("boot", {
|
|
app_id: intercomAppID,
|
|
user_id: username,
|
|
email,
|
|
// keep name undefined instead of an empty string so that intercom auto assigns a name
|
|
name,
|
|
});
|
|
}
|
|
}
|
|
|
|
export const stopClickEventPropagation = (
|
|
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
|
) => {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
/**
|
|
* returns the modText ( ctrl or command ) based on the user machine
|
|
*
|
|
* @returns
|
|
*/
|
|
export const modText = () => (isMac() ? <span>⌘</span> : "CTRL");
|