PromucFlow_constructor/app/client/src/utils/AppsmithUtils.tsx
Paul Li 54579a4e6c
feat: Switch Group widget (#7590)
* Feat: Switch Group Widget

-- The first MVP of the widget

* feat: Switch Group Widget

-- Follow the same config and implementation as the other group widgets

* feat: Switch Group Widget

-- Elaborate the help text for defaultSelectedValues

* feat: Switch Group Widget

-- Add a widget icon

* feat: Switch Group Widget

-- Remove unnecessary property control at ItemsControl.tsx
-- Refactor some code snippets for higher code quality
-- Add basic cypress test cases

* fix: icon for the widget optimised & replaced

* feat: Switch Group Widget

-- Add a unit test for defaultSelectedValuesValidation

* feat: Switch Group Widget

-- Make the validation type for defaultSelectedValues property to ValidationTypes.ARRAY
-- Remove original validation function and its unit test

* feat: Switch Group Widget

-- Fix on typo
-- Fix on formatting issue
-- Change the help text for isRequired property

* feat: Switch Group Widget

-- Revert help text for isRequired property to the original one

* feat: Switch Group Widget

-- Set strict property to true on defaultSelectedValues

* feat: Switch group widget

-- Refactor utility function, getCamelCaseString
-- Add the corresponding test case

* feat: Switch group widget

-- Implement options property as a plain JS field
-- Reimplement update logic for selectedValues when options changes
-- Add a new utility function for checking equality of object arrays
-- Add a unit test for the above function
-- Rewrite the corresponding Cypress test cases

* feat: Switch Group Widget

-- Remove isArrayEqual utility function and directly use functions from lodash

* feat: Swtich Group Widget

-- Make selectedValues as a derived property

* feat: Switch Group Widget

-- Replace the widget icon

* feat: Switch Group Widget

-- Rewrite a test case for onSelectionChange property

* feat: Switch Group Widget

-- Remove redundant calls for openPropertyPane

* feat: Switch Group Widget

-- Remove closePropertyPane call from afterEach hook

* feat: Switch Group Widget

-- Change the selector for every switch element in onSelectionChange test case

* feat: Switch Group Widget

-- Fix on failed Cypress test case, adding closePropertyPane command to onSelectionChange

* feat: Switch Group Widget

-- Remove template literal from a selector

* feat: Switch Group Widget

-- Make click on onSelectionChange test case forced

* feat: Switch Group Widget

-- Fix on crash issue when editing on Options property

* feat: Switch Group Widget

-- Add the widget icon to show in entity explorer

* feat: Switch Group Widget

-- Fix on blue color on mouse down
-- Add a new property for alignment

Co-authored-by: somangshu <somangshu.goswami1508@gmail.com>
2021-12-09 20:02:47 +08:00

421 lines
11 KiB
TypeScript

import {
CurrentApplicationData,
Page,
ReduxAction,
} from "constants/ReduxActionConstants";
import { getAppsmithConfigs } from "configs";
import * as Sentry from "@sentry/react";
import AnalyticsUtil from "./AnalyticsUtil";
import FormControlRegistry from "./FormControlRegistry";
import { Property } from "api/ActionAPI";
import _ from "lodash";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import * as log from "loglevel";
import { LogLevelDesc } from "loglevel";
import produce from "immer";
import { AppIconCollection, AppIconName } from "components/ads/AppIcon";
import { ERROR_CODES } from "constants/ApiConstants";
import { createMessage, ERROR_500 } from "../constants/messages";
import localStorage from "utils/localStorage";
import { APP_MODE } from "entities/App";
import { trimQueryString } from "./helpers";
import {
getApplicationEditorPageURL,
getApplicationViewerPageURL,
} from "constants/routes";
export const createReducer = (
initialState: any,
handlers: { [type: string]: (state: any, action: any) => any },
) => {
return function reducer(state = initialState, action: ReduxAction<any>) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
};
};
export const createImmerReducer = (
initialState: any,
handlers: { [type: string]: any },
) => {
return function reducer(state = initialState, action: ReduxAction<any>) {
if (handlers.hasOwnProperty(action.type)) {
return produce(handlers[action.type])(state, action);
} else {
return state;
}
};
};
export const appInitializer = () => {
FormControlRegistry.registerFormControlBuilders();
const appsmithConfigs = getAppsmithConfigs();
log.setLevel(getEnvLogLevel(appsmithConfigs.logLevel));
};
export const initializeAnalyticsAndTrackers = () => {
const appsmithConfigs = getAppsmithConfigs();
try {
if (appsmithConfigs.sentry.enabled && !window.Sentry) {
window.Sentry = Sentry;
Sentry.init({
...appsmithConfigs.sentry,
beforeBreadcrumb(breadcrumb) {
if (
breadcrumb.category === "console" &&
breadcrumb.level !== "error"
) {
return null;
}
if (breadcrumb.category === "sentry.transaction") {
return null;
}
if (breadcrumb.category === "redux.action") {
if (
breadcrumb.data &&
breadcrumb.data.type === "SET_EVALUATED_TREE"
) {
breadcrumb.data = undefined;
}
}
return breadcrumb;
},
});
}
} catch (e) {
log.error(e);
}
try {
if (appsmithConfigs.smartLook.enabled && !(window as any).smartlook) {
const { id } = appsmithConfigs.smartLook;
AnalyticsUtil.initializeSmartLook(id);
}
if (appsmithConfigs.segment.enabled && !(window as any).analytics) {
if (appsmithConfigs.segment.apiKey) {
// This value is only enabled for Appsmith's cloud hosted version. It is not set in self-hosted environments
AnalyticsUtil.initializeSegment(appsmithConfigs.segment.apiKey);
} else if (appsmithConfigs.segment.ceKey) {
// This value is set in self-hosted environments. But if the analytics are disabled, it's never used.
AnalyticsUtil.initializeSegment(appsmithConfigs.segment.ceKey);
}
}
} catch (e) {
Sentry.captureException(e);
log.error(e);
}
};
export const mapToPropList = (map: Record<string, string>): Property[] => {
return _.map(map, (value, key) => {
return { key: key, value: value };
});
};
export const getNextEntityName = (
prefix: string,
existingNames: string[],
startWithoutIndex?: boolean,
) => {
const regex = new RegExp(`^${prefix}(\\d+)$`);
const usedIndices: number[] = existingNames.map((name) => {
if (name && regex.test(name)) {
const matches = name.match(regex);
const ind =
matches && Array.isArray(matches) ? parseInt(matches[1], 10) : 0;
return Number.isNaN(ind) ? 0 : ind;
}
return 0;
}) as number[];
const lastIndex = Math.max(...usedIndices, ...[0]);
if (startWithoutIndex && lastIndex === 0) {
const exactMatchFound = existingNames.some(
(name) => prefix && name.trim() === prefix.trim(),
);
if (!exactMatchFound) {
return prefix.trim();
}
}
return prefix + (lastIndex + 1);
};
export const getDuplicateName = (prefix: string, existingNames: string[]) => {
const trimmedPrefix = prefix.replace(/ /g, "");
const regex = new RegExp(`^${trimmedPrefix}(\\d+)$`);
const usedIndices: number[] = existingNames.map((name) => {
if (name && regex.test(name)) {
const matches = name.match(regex);
const ind =
matches && Array.isArray(matches) ? parseInt(matches[1], 10) : 0;
return Number.isNaN(ind) ? 0 : ind;
}
return 0;
}) as number[];
const lastIndex = Math.max(...usedIndices, ...[0]);
return trimmedPrefix + `_${lastIndex + 1}`;
};
export const createNewApiName = (actions: ActionDataState, pageId: string) => {
const pageApiNames = actions
.filter((a) => a.config.pageId === pageId)
.map((a) => a.config.name);
return getNextEntityName("Api", pageApiNames);
};
export const createNewJSFunctionName = (
jsActions: ActionDataState,
pageId: string,
) => {
const pageJsFunctionNames = jsActions
.filter((a) => a.config.pageId === pageId)
.map((a) => a.config.name);
return getNextEntityName("JSObject", pageJsFunctionNames);
};
export const noop = () => {
log.debug("noop");
};
export const stopEventPropagation = (e: any) => {
e.stopPropagation();
};
export const createNewQueryName = (
queries: ActionDataState,
pageId: string,
) => {
const pageApiNames = queries
.filter((a) => a.config.pageId === pageId)
.map((a) => a.config.name);
const newName = getNextEntityName("Query", pageApiNames);
return newName;
};
export const convertToString = (value: any): string => {
if (_.isUndefined(value)) {
return "";
}
if (_.isObject(value)) {
return JSON.stringify(value, null, 2);
}
if (_.isString(value)) return value;
return value.toString();
};
const getEnvLogLevel = (configLevel: LogLevelDesc): LogLevelDesc => {
let logLevel = configLevel;
if (localStorage && localStorage.getItem) {
const localStorageLevel = localStorage.getItem(
"logLevelOverride",
) as LogLevelDesc;
if (localStorageLevel) logLevel = localStorageLevel;
}
return logLevel;
};
export const getInitialsAndColorCode = (
fullName: any,
colorPalette: string[],
): string[] => {
let inits = "";
// if name contains space. eg: "Full Name"
if (fullName && fullName.includes(" ")) {
const namesArr = fullName.split(" ");
let initials = namesArr.map((name: string) => name.charAt(0));
initials = initials.join("").toUpperCase();
inits = initials.slice(0, 2);
} else {
// handle for camelCase
const str = fullName ? fullName.replace(/([a-z])([A-Z])/g, "$1 $2") : "";
const namesArr = str.split(" ");
let initials = namesArr.map((name: string) => name.charAt(0));
initials = initials.join("").toUpperCase();
inits = initials.slice(0, 2);
}
const colorCode = getColorCode(inits, colorPalette);
return [inits, colorCode];
};
export const getColorCode = (
initials: string,
colorPalette: string[],
): string => {
let asciiSum = 0;
for (let i = 0; i < initials.length; i++) {
asciiSum += initials[i].charCodeAt(0);
}
return colorPalette[asciiSum % colorPalette.length];
};
export const getApplicationIcon = (initials: string): AppIconName => {
let asciiSum = 0;
for (let i = 0; i < initials.length; i++) {
asciiSum += initials[i].charCodeAt(0);
}
return AppIconCollection[asciiSum % AppIconCollection.length];
};
export function hexToRgb(
hex: string,
): {
r: number;
g: number;
b: number;
} {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: {
r: -1,
g: -1,
b: -1,
};
}
export function getQueryParams() {
const urlParams = new URLSearchParams(window.location.search);
const keys = urlParams.keys();
let key = keys.next().value;
const queryParams: Record<string, string> = {};
while (key) {
queryParams[key] = urlParams.get(key) as string;
key = keys.next().value;
}
return queryParams;
}
export function convertObjectToQueryParams(object: any): string {
if (!_.isNil(object)) {
const paramArray: string[] = _.map(_.keys(object), (key) => {
return encodeURIComponent(key) + "=" + encodeURIComponent(object[key]);
});
return "?" + _.join(paramArray, "&");
} else {
return "";
}
}
export const retryPromise = (
fn: () => Promise<any>,
retriesLeft = 5,
interval = 1000,
): Promise<any> => {
return new Promise((resolve, reject) => {
fn()
.then(resolve)
.catch(() => {
setTimeout(() => {
if (retriesLeft === 1) {
return Promise.reject({
code: ERROR_CODES.SERVER_ERROR,
message: createMessage(ERROR_500),
show: false,
});
}
// Passing on "reject" is the important part
retryPromise(fn, retriesLeft - 1, interval).then(resolve, reject);
}, interval);
});
});
};
export const getRandomPaletteColor = (colorPalette: string[]) => {
return colorPalette[Math.floor(Math.random() * colorPalette.length)];
};
export const isBlobUrl = (url: string) => {
return typeof url === "string" && url.startsWith("blob:");
};
/**
*
* @param data string file data
* @param type string file type
* @returns string containing blob id and type
*/
export const createBlobUrl = (data: Blob | string, type: string) => {
let url = URL.createObjectURL(data);
url = url.replace(
`${window.location.protocol}//${window.location.hostname}/`,
"",
);
return `${url}?type=${type}`;
};
/**
*
* @param blobId string blob id along with type.
* @returns [string,string] [blobUrl, type]
*/
export const parseBlobUrl = (blobId: string) => {
const url = `blob:${window.location.protocol}//${
window.location.hostname
}/${blobId.substring(5)}`;
return url.split("?type=");
};
/**
* Convert a string into camelCase
* @param sourceString input string
* @returns camelCase string
*/
export const getCamelCaseString = (sourceString: string) => {
let out = "";
// Split the input string to separate words using RegEx
const regEx = /[A-Z\xC0-\xD6\xD8-\xDE]?[a-z\xDF-\xF6\xF8-\xFF]+|[A-Z\xC0-\xD6\xD8-\xDE]+(?![a-z\xDF-\xF6\xF8-\xFF])|\d+/g;
const words = sourceString.match(regEx);
if (words) {
words.forEach(function(el, idx) {
const add = el.toLowerCase();
out += idx === 0 ? add : add[0].toUpperCase() + add.slice(1);
});
}
return out;
};
/*
* gets the page url
*
* Note: for edit mode, the page will have different url ( contains '/edit' at the end )
*
* @param page
* @returns
*/
export const getPageURL = (
page: Page,
appMode: APP_MODE | undefined,
currentApplicationDetails: CurrentApplicationData | undefined,
) => {
if (appMode === APP_MODE.PUBLISHED) {
return trimQueryString(
getApplicationViewerPageURL({
applicationId: currentApplicationDetails?.id,
pageId: page.pageId,
}),
);
}
return getApplicationEditorPageURL(
currentApplicationDetails?.id,
page.pageId,
);
};