diff --git a/app/client/.eslintrc.js b/app/client/.eslintrc.js index 45bc21c710..0da3449150 100644 --- a/app/client/.eslintrc.js +++ b/app/client/.eslintrc.js @@ -35,6 +35,12 @@ const eslintConfig = { // Allow type imports as they don’t lead to bundling the dependency allowTypeImports: true, }, + { + name: "sql-formatter", + importNames: ["format"], + message: + "Reason: Instead of `import { format }` (which bundles all formatting dialects), please import only dialects you need (e.g. `import { formatDialect, postgresql }`. See https://github.com/sql-formatter-org/sql-formatter/issues/452", + }, ], patterns: [ ...(baseNoRestrictedImports.patterns ?? []), @@ -45,10 +51,10 @@ const eslintConfig = { ], }, ], - // Annoyingly, the `no-restricted-imports` rule doesn’t allow to restrict imports of - // `editorComponents/CodeEditor` but not `editorComponents/CodeEditor/*`: https://stackoverflow.com/q/64995811/1192426 - // So we’re using `no-restricted-syntax` instead. "no-restricted-syntax": [ + // Annoyingly, the `no-restricted-imports` rule doesn’t allow to restrict imports of + // `editorComponents/CodeEditor` but not `editorComponents/CodeEditor/*`: https://stackoverflow.com/q/64995811/1192426 + // So we’re using `no-restricted-syntax` instead. "error", { // Match all @@ -61,6 +67,20 @@ const eslintConfig = { message: "Please don’t import CodeEditor directly – this will cause it to be bundled in the main chunk. Instead, use the LazyCodeEditor component.", }, + // Annoyingly, no-restricted-imports follows the gitignore exclude syntax, + // so there’s no way to exclude all @uppy/* but not @uppy/*/*.css imports: + // https://github.com/eslint/eslint/issues/16927 + { + // Match all + // - `import` statements + // - that are not `import type` statements – we allow type imports as they don’t lead to bundling the dependency + // - that import `@uppy/*` unless the `*` part ends with `.css` + // Note: using `\\u002F` instead of `/` due to https://eslint.org/docs/latest/extend/selectors#known-issues + selector: + "ImportDeclaration[importKind!='type'][source.value=/^@uppy\\u002F(?!.*.css$)/]", + message: + "Please don’t import Uppy directly. End users rarely use Uppy (e.g. only when they need to upload a file) – but Uppy bundles ~200 kB of JS. Please import it lazily instead.", + }, ], }, }; diff --git a/app/client/package.json b/app/client/package.json index 345471962b..124b50804e 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -94,7 +94,7 @@ "dayjs": "^1.10.6", "deep-diff": "^1.0.2", "design-system": "npm:@appsmithorg/design-system@2.1.15", - "design-system-old": "npm:@appsmithorg/design-system-old@1.1.10", + "design-system-old": "npm:@appsmithorg/design-system-old@1.1.11", "downloadjs": "^1.4.7", "fast-deep-equal": "^3.1.3", "fast-xml-parser": "^3.17.5", diff --git a/app/client/src/WidgetQueryGenerators/MSSQL/index.ts b/app/client/src/WidgetQueryGenerators/MSSQL/index.ts index b93aec4356..010829915c 100644 --- a/app/client/src/WidgetQueryGenerators/MSSQL/index.ts +++ b/app/client/src/WidgetQueryGenerators/MSSQL/index.ts @@ -1,5 +1,5 @@ import { BaseQueryGenerator } from "../BaseQueryGenerator"; -import { format } from "sql-formatter"; +import { formatDialect, sql } from "sql-formatter"; import { QUERY_TYPE } from "../types"; import type { WidgetQueryGenerationConfig, @@ -74,9 +74,9 @@ export default abstract class MSSQL extends BaseQueryGenerator { ); //formats sql string - const res = format(template, { + const res = formatDialect(template, { params, - language: "sql", + dialect: sql, }); return { diff --git a/app/client/src/WidgetQueryGenerators/MySQL/index.ts b/app/client/src/WidgetQueryGenerators/MySQL/index.ts index 7af5151bad..8b320a58ce 100644 --- a/app/client/src/WidgetQueryGenerators/MySQL/index.ts +++ b/app/client/src/WidgetQueryGenerators/MySQL/index.ts @@ -1,5 +1,5 @@ import { BaseQueryGenerator } from "../BaseQueryGenerator"; -import { format } from "sql-formatter"; +import { formatDialect, mysql } from "sql-formatter"; import { QUERY_TYPE } from "../types"; import type { WidgetQueryGenerationConfig, @@ -74,9 +74,9 @@ export default abstract class MySQL extends BaseQueryGenerator { ); //formats sql string - const res = format(template, { + const res = formatDialect(template, { params, - language: "mysql", + dialect: mysql, }); return { diff --git a/app/client/src/WidgetQueryGenerators/PostgreSQL/index.ts b/app/client/src/WidgetQueryGenerators/PostgreSQL/index.ts index fd8bb82558..7375ed9656 100644 --- a/app/client/src/WidgetQueryGenerators/PostgreSQL/index.ts +++ b/app/client/src/WidgetQueryGenerators/PostgreSQL/index.ts @@ -1,5 +1,5 @@ import { BaseQueryGenerator } from "../BaseQueryGenerator"; -import { format } from "sql-formatter"; +import { formatDialect, postgresql } from "sql-formatter"; import { QUERY_TYPE } from "../types"; import type { WidgetQueryGenerationConfig, @@ -82,9 +82,9 @@ export default abstract class PostgreSQL extends BaseQueryGenerator { { template: "", params: {} }, ); //formats sql string - const res = format(template, { + const res = formatDialect(template, { params, - language: "postgresql", + dialect: postgresql, }); return { diff --git a/app/client/src/WidgetQueryGenerators/Snowflake/index.ts b/app/client/src/WidgetQueryGenerators/Snowflake/index.ts index 483420020c..2243981b4c 100644 --- a/app/client/src/WidgetQueryGenerators/Snowflake/index.ts +++ b/app/client/src/WidgetQueryGenerators/Snowflake/index.ts @@ -1,5 +1,5 @@ import { BaseQueryGenerator } from "../BaseQueryGenerator"; -import { format } from "sql-formatter"; +import { formatDialect, snowflake } from "sql-formatter"; import { QUERY_TYPE } from "../types"; import type { WidgetQueryGenerationConfig, @@ -75,9 +75,9 @@ export default abstract class Snowflake extends BaseQueryGenerator { ); //formats sql string - const res = format(template, { + const res = formatDialect(template, { params, - language: "snowflake", + dialect: snowflake, paramTypes: { positional: true, }, diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 121a7d0509..833c46ba0a 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -62,6 +62,7 @@ import type { Action } from "entities/Action"; import { SegmentedControlContainer } from "../../pages/Editor/QueryEditor/EditorJSONtoForm"; import ActionExecutionInProgressView from "./ActionExecutionInProgressView"; import { CloseDebugger } from "./Debugger/DebuggerTabs"; +import { EMPTY_RESPONSE } from "./emptyResponse"; type TextStyleProps = { accent: "primary" | "secondary" | "error"; @@ -202,22 +203,6 @@ type Props = ReduxStateProps & responseDisplayFormat: { title: string; value: string }; }; -export const EMPTY_RESPONSE: ActionResponse = { - statusCode: "", - duration: "", - body: "", - headers: {}, - request: { - headers: {}, - body: {}, - httpMethod: "", - url: "", - }, - size: "", - responseDisplayFormat: "", - dataTypes: [], -}; - const StatusCodeText = styled(BaseText)>` color: ${(props) => props.code.startsWith("2") diff --git a/app/client/src/components/editorComponents/emptyResponse.tsx b/app/client/src/components/editorComponents/emptyResponse.tsx new file mode 100644 index 0000000000..1a2cbb2c2b --- /dev/null +++ b/app/client/src/components/editorComponents/emptyResponse.tsx @@ -0,0 +1,17 @@ +import type { ActionResponse } from "api/ActionAPI"; + +export const EMPTY_RESPONSE: ActionResponse = { + statusCode: "", + duration: "", + body: "", + headers: {}, + request: { + headers: {}, + body: {}, + httpMethod: "", + url: "", + }, + size: "", + responseDisplayFormat: "", + dataTypes: [], +}; diff --git a/app/client/src/components/propertyControls/DatePickerControl.tsx b/app/client/src/components/propertyControls/DatePickerControl.tsx index 1cfa18c25f..9f735a7072 100644 --- a/app/client/src/components/propertyControls/DatePickerControl.tsx +++ b/app/client/src/components/propertyControls/DatePickerControl.tsx @@ -1,7 +1,7 @@ import React from "react"; import type { ControlData, ControlProps } from "./BaseControl"; import BaseControl from "./BaseControl"; -import moment from "moment-timezone"; +import moment from "moment"; import { TimePrecision } from "@blueprintjs/datetime"; import type { WidgetProps } from "widgets/BaseWidget"; import { ISO_DATE_FORMAT } from "constants/WidgetValidation"; diff --git a/app/client/src/pages/Editor/APIEditor/GraphQL/GraphQLEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/GraphQL/GraphQLEditorForm.tsx index b4119c1839..59f2de3e0f 100644 --- a/app/client/src/pages/Editor/APIEditor/GraphQL/GraphQLEditorForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/GraphQL/GraphQLEditorForm.tsx @@ -6,7 +6,7 @@ import classNames from "classnames"; import styled from "styled-components"; import { API_EDITOR_FORM_NAME } from "@appsmith/constants/forms"; import type { Action } from "entities/Action"; -import { EMPTY_RESPONSE } from "components/editorComponents/ApiResponseView"; +import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse"; import type { AppState } from "@appsmith/reducers"; import { getApiName } from "selectors/formSelectors"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; diff --git a/app/client/src/pages/Editor/APIEditor/RestAPIForm.tsx b/app/client/src/pages/Editor/APIEditor/RestAPIForm.tsx index ec839d0a50..72b11ecd2e 100644 --- a/app/client/src/pages/Editor/APIEditor/RestAPIForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/RestAPIForm.tsx @@ -6,7 +6,7 @@ import styled from "styled-components"; import { API_EDITOR_FORM_NAME } from "@appsmith/constants/forms"; import type { Action } from "entities/Action"; import PostBodyData from "./PostBodyData"; -import { EMPTY_RESPONSE } from "components/editorComponents/ApiResponseView"; +import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse"; import type { AppState } from "@appsmith/reducers"; import { getApiName } from "selectors/formSelectors"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; diff --git a/app/client/src/plugins/Linting/Linter.ts b/app/client/src/plugins/Linting/Linter.ts index e4b5936d9a..ed343339a3 100644 --- a/app/client/src/plugins/Linting/Linter.ts +++ b/app/client/src/plugins/Linting/Linter.ts @@ -1,11 +1,11 @@ import type { ILinter } from "./linters"; -import { BaseLinter, WorkerLinter } from "./linters"; +import { WorkerLinter } from "./linters"; import type { LintTreeRequestPayload, updateJSLibraryProps } from "./types"; export class Linter { linter: ILinter; - constructor(options: { useWorker: boolean }) { - this.linter = options.useWorker ? new WorkerLinter() : new BaseLinter(); + constructor() { + this.linter = new WorkerLinter(); this.lintTree = this.lintTree.bind(this); this.updateJSLibraryGlobals = this.updateJSLibraryGlobals.bind(this); this.start = this.start.bind(this); diff --git a/app/client/src/plugins/Linting/linters/index.ts b/app/client/src/plugins/Linting/linters/index.ts index a16b47d21c..06f32b1dd1 100644 --- a/app/client/src/plugins/Linting/linters/index.ts +++ b/app/client/src/plugins/Linting/linters/index.ts @@ -4,7 +4,6 @@ import type { updateJSLibraryProps, } from "plugins/Linting/types"; import { LINT_WORKER_ACTIONS as LINT_ACTIONS } from "plugins/Linting/types"; -import { handlerMap } from "plugins/Linting/handlers"; export interface ILinter { lintTree(args: LintTreeRequestPayload): any; @@ -13,21 +12,6 @@ export interface ILinter { shutdown(): void; } -export class BaseLinter implements ILinter { - lintTree(args: LintTreeRequestPayload) { - return handlerMap[LINT_ACTIONS.LINT_TREE](args); - } - updateJSLibraryGlobals(args: updateJSLibraryProps) { - return handlerMap[LINT_ACTIONS.UPDATE_LINT_GLOBALS](args); - } - start() { - return; - } - shutdown() { - return; - } -} - export class WorkerLinter implements ILinter { server: GracefulWorkerService; constructor() { diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index 3a6010e015..04b9ab24bf 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -97,7 +97,7 @@ import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import * as log from "loglevel"; -import { EMPTY_RESPONSE } from "components/editorComponents/ApiResponseView"; +import { EMPTY_RESPONSE } from "components/editorComponents/emptyResponse"; import type { AppState } from "@appsmith/reducers"; import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "@appsmith/constants/ApiConstants"; import { evaluateActionBindings } from "sagas/EvaluationsSaga"; diff --git a/app/client/src/sagas/LintingSagas.ts b/app/client/src/sagas/LintingSagas.ts index b69066248d..0583bec230 100644 --- a/app/client/src/sagas/LintingSagas.ts +++ b/app/client/src/sagas/LintingSagas.ts @@ -25,7 +25,7 @@ import { getFixedTimeDifference } from "workers/common/DataTreeEvaluator/utils"; const APPSMITH_CONFIGS = getAppsmithConfigs(); -export const lintWorker = new Linter({ useWorker: true }); +export const lintWorker = new Linter(); function* updateLintGlobals( action: ReduxAction<{ add?: boolean; libs: TJSLibrary[] }>, diff --git a/app/client/src/sagas/OnboardingSagas.ts b/app/client/src/sagas/OnboardingSagas.ts index 6b8acefa21..e5490e8b1c 100644 --- a/app/client/src/sagas/OnboardingSagas.ts +++ b/app/client/src/sagas/OnboardingSagas.ts @@ -24,7 +24,6 @@ import { import { getCurrentUser } from "selectors/usersSelectors"; import history from "utils/history"; -import TourApp from "pages/Editor/GuidedTour/app.json"; import { getHadReachedStep, @@ -119,6 +118,9 @@ function* createApplication() { } if (workspace) { + const TourAppPromise = import("pages/Editor/GuidedTour/app.json"); + const TourApp: Awaited = yield TourAppPromise; + const appFileObject = new File([JSON.stringify(TourApp)], "app.json", { type: "application/json", }); diff --git a/app/client/src/utils/editor/EditorUtils.ts b/app/client/src/utils/editor/EditorUtils.ts index 8e74008b35..c6ba51112a 100644 --- a/app/client/src/utils/editor/EditorUtils.ts +++ b/app/client/src/utils/editor/EditorUtils.ts @@ -4,7 +4,4 @@ import PropertyControlRegistry from "../PropertyControlRegistry"; export const editorInitializer = async () => { registerWidgets(); PropertyControlRegistry.registerPropertyControlBuilders(); - - const { default: moment } = await import("moment-timezone"); - moment.tz.setDefault(moment.tz.guess()); }; diff --git a/app/client/src/utils/importUppy.ts b/app/client/src/utils/importUppy.ts new file mode 100644 index 0000000000..ffcfa991c0 --- /dev/null +++ b/app/client/src/utils/importUppy.ts @@ -0,0 +1,36 @@ +// We’re setting this flag to true when we know for sure the Uppy module was loaded and initialized. +// When it’s `true`, the other modules will know that the importUppy function will resolve immediately +// (in the next tick). They can use it to e.g. decide whether to show the loading spinner +export let isUppyLoaded = false; + +export async function importUppy() { + const [Uppy, Dashboard, GoogleDrive, OneDrive, Url, Webcam] = + await Promise.all([ + import(/* webpackChunkName: "uppy" */ "@uppy/core").then( + (m) => m.default, + ), + import(/* webpackChunkName: "uppy" */ "@uppy/dashboard").then( + (m) => m.default, + ), + import(/* webpackChunkName: "uppy" */ "@uppy/google-drive").then( + (m) => m.default, + ), + import(/* webpackChunkName: "uppy" */ "@uppy/onedrive").then( + (m) => m.default, + ), + import(/* webpackChunkName: "uppy" */ "@uppy/url").then((m) => m.default), + import(/* webpackChunkName: "uppy" */ "@uppy/webcam").then( + (m) => m.default, + ), + ]); + isUppyLoaded = true; + + return { + Uppy, + Dashboard, + GoogleDrive, + OneDrive, + Url, + Webcam, + }; +} diff --git a/app/client/src/widgets/CurrencyInputWidget/widget/derived.js b/app/client/src/widgets/CurrencyInputWidget/widget/derived.js index b01d9d4ada..154ae98f72 100644 --- a/app/client/src/widgets/CurrencyInputWidget/widget/derived.js +++ b/app/client/src/widgets/CurrencyInputWidget/widget/derived.js @@ -77,7 +77,7 @@ export default { .replace(new RegExp(`[${getLocaleDecimalSeperator()}]`), "."), ); - if (_.isNaN(parsed)) { + if (Number.isNaN(parsed)) { parsed = undefined; } diff --git a/app/client/src/widgets/DatePickerWidget/component/index.tsx b/app/client/src/widgets/DatePickerWidget/component/index.tsx index f819072745..85b9e0af6e 100644 --- a/app/client/src/widgets/DatePickerWidget/component/index.tsx +++ b/app/client/src/widgets/DatePickerWidget/component/index.tsx @@ -8,7 +8,7 @@ import { import { ControlGroup, Classes, Label } from "@blueprintjs/core"; import type { ComponentProps } from "widgets/BaseComponent"; import { DateInput } from "@blueprintjs/datetime"; -import moment from "moment-timezone"; +import moment from "moment"; import "@blueprintjs/datetime/lib/css/blueprint-datetime.css"; import type { DatePickerType } from "../constants"; import { WIDGET_PADDING } from "constants/WidgetConstants"; diff --git a/app/client/src/widgets/DatePickerWidget2/component/index.tsx b/app/client/src/widgets/DatePickerWidget2/component/index.tsx index 308c38f7cf..7a52c81eb0 100644 --- a/app/client/src/widgets/DatePickerWidget2/component/index.tsx +++ b/app/client/src/widgets/DatePickerWidget2/component/index.tsx @@ -5,7 +5,7 @@ import type { IRef, Alignment } from "@blueprintjs/core"; import { ControlGroup, Classes } from "@blueprintjs/core"; import type { ComponentProps } from "widgets/BaseComponent"; import { DateInput } from "@blueprintjs/datetime"; -import moment from "moment-timezone"; +import moment from "moment"; import "@blueprintjs/datetime/lib/css/blueprint-datetime.css"; import type { DatePickerType } from "../constants"; import { TimePrecision } from "../constants"; diff --git a/app/client/src/widgets/FilePickerWidgetV2/component/index.tsx b/app/client/src/widgets/FilePickerWidgetV2/component/index.tsx index 2f74048716..f8d37b231a 100644 --- a/app/client/src/widgets/FilePickerWidgetV2/component/index.tsx +++ b/app/client/src/widgets/FilePickerWidgetV2/component/index.tsx @@ -1,8 +1,5 @@ import React from "react"; import type { ComponentProps } from "widgets/BaseComponent"; -import "@uppy/core/dist/style.css"; -import "@uppy/dashboard/dist/style.css"; -import "@uppy/webcam/dist/style.css"; import { BaseButton } from "widgets/ButtonWidget/component"; import { Colors } from "constants/Colors"; @@ -13,13 +10,6 @@ function FilePickerComponent(props: FilePickerComponentProps) { computedLabel = `${props.files.length} files selected`; } - /** - * opens modal - */ - const openModal = () => { - props.uppy.getPlugin("Dashboard").openModal(); - }; - return ( @@ -38,7 +28,7 @@ function FilePickerComponent(props: FilePickerComponentProps) { } export interface FilePickerComponentProps extends ComponentProps { label: string; - uppy: any; + openModal: () => void; isLoading: boolean; files?: any[]; buttonColor: string; diff --git a/app/client/src/widgets/FilePickerWidgetV2/widget/index.styled.tsx b/app/client/src/widgets/FilePickerWidgetV2/widget/index.styled.tsx new file mode 100644 index 0000000000..8d1ddadf41 --- /dev/null +++ b/app/client/src/widgets/FilePickerWidgetV2/widget/index.styled.tsx @@ -0,0 +1,209 @@ +import CloseIcon from "assets/icons/ads/cross.svg"; +import UpIcon from "assets/icons/ads/up-arrow.svg"; +import { Colors } from "constants/Colors"; +import { createGlobalStyle } from "styled-components"; +import "@uppy/core/dist/style.css"; +import "@uppy/dashboard/dist/style.css"; +import "@uppy/webcam/dist/style.css"; + +// Using the `&&` trick (which duplicates the selector) to increase specificity of custom styles. Otherwise, +// the custom styles may be overridden by @uppy/*.css imports – they have the same specificity, +// and which styles exactly will be applied depends on the order of imports. +const INCREASE_SPECIFICITY_SELECTOR = "&&"; + +export const FilePickerGlobalStyles = createGlobalStyle<{ + borderRadius?: string; +}>` + + /* Sets the font-family to theming font-family of the upload modal */ + .uppy-Root { + ${INCREASE_SPECIFICITY_SELECTOR} { + font-family: var(--wds-font-family); + } + } + + /*********************************************************/ + /* Set the new dropHint upload icon */ + .uppy-Dashboard-dropFilesHereHint { + ${INCREASE_SPECIFICITY_SELECTOR} { + background-image: none; + border-radius: ${({ borderRadius }) => borderRadius}; + } + } + + .uppy-Dashboard-dropFilesHereHint { + ${INCREASE_SPECIFICITY_SELECTOR} { + &::before { + border: 2.5px solid var(--wds-accent-color); + width: 60px; + height: 60px; + border-radius: ${({ borderRadius }) => borderRadius}; + display: inline-block; + content: ' '; + position: absolute; + top: 43%; + } + + &::after { + display: inline-block; + content: ' '; + position: absolute; + top: 46%; + width: 30px; + height: 30px; + + -webkit-mask-image: url(${UpIcon}); + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: 30px; + background: var(--wds-accent-color); + } + } + } + /*********************************************************/ + + /*********************************************************/ + /* Set the styles for the upload button */ + .uppy-StatusBar-actionBtn--upload { + ${INCREASE_SPECIFICITY_SELECTOR} { + background-color: var(--wds-accent-color) !important; + border-radius: ${({ borderRadius }) => borderRadius}; + } + } + + .uppy-Dashboard-Item-action--remove { + ${INCREASE_SPECIFICITY_SELECTOR} { + /* Sets the border radius of the button when it is focused */ + &:focus { + border-radius: ${({ borderRadius }) => + borderRadius === "0.375rem" ? "0.25rem" : borderRadius} !important; + } + + .uppy-c-icon { + & path:first-child { + /* Sets the black background of remove file button hidden */ + visibility: hidden; + } + + & path:last-child { + /* Sets the cross mark color of remove file button */ + fill: #858282; + } + + background-color: #FFFFFF; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.06), 0px 1px 3px rgba(0, 0, 0, 0.1); + + & { + /* Sets the black background of remove file button hidden*/ + border-radius: ${({ borderRadius }) => + borderRadius === "0.375rem" ? "0.25rem" : borderRadius}; + } + } + } + } + /*********************************************************/ + + /*********************************************************/ + /* Sets the back cancel button color to match theming primary color */ + .uppy-DashboardContent-back { + ${INCREASE_SPECIFICITY_SELECTOR} { + color: var(--wds-accent-color); + + &:hover { + color: var(--wds-accent-color); + background-color: ${Colors.ATHENS_GRAY}; + } + } + } + /*********************************************************/ + + /*********************************************************/ + /* Sets the style according to reskinning for x button at the top right corner of the modal */ + .uppy-Dashboard-close { + ${INCREASE_SPECIFICITY_SELECTOR} { + background-color: white; + width: 32px; + height: 32px; + text-align: center; + top: -33px; + border-radius: ${({ borderRadius }) => borderRadius}; + + & span { + font-size: 0; + } + + & span::after { + content: ' '; + -webkit-mask-image: url(${CloseIcon}); + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: 20px; + background: #858282; + position: absolute; + top: 32%; + left: 32%; + width: 12px; + height: 12px; + } + } + } + } + /*********************************************************/ + + + /*********************************************************/ + /* Sets the border radius of the upload modal */ + .uppy-Dashboard-inner { + ${INCREASE_SPECIFICITY_SELECTOR} { + border-radius: ${({ borderRadius }) => borderRadius} !important; + } + } + + .uppy-Dashboard-innerWrap { + ${INCREASE_SPECIFICITY_SELECTOR} { + border-radius: ${({ borderRadius }) => borderRadius} !important; + } + } + + .uppy-Dashboard-AddFiles { + ${INCREASE_SPECIFICITY_SELECTOR} { + border-radius: ${({ borderRadius }) => borderRadius} !important; + } + } + /*********************************************************/ + + /*********************************************************/ + /* Sets the error message style according to reskinning*/ + .uppy-Informer { + ${INCREASE_SPECIFICITY_SELECTOR} { + bottom: 82px; + & p[role="alert"] { + border-radius: ${({ borderRadius }) => borderRadius}; + background-color: transparent; + color: #D91921; + border: 1px solid #D91921; + } + } + } + /*********************************************************/ + + /*********************************************************/ + /* Style the + add more files button on top right corner of the upload modal */ + .uppy-DashboardContent-addMore { + ${INCREASE_SPECIFICITY_SELECTOR} { + color: var(--wds-accent-color); + font-weight: 400; + &:hover { + background-color: ${Colors.ATHENS_GRAY}; + color: var(--wds-accent-color); + } + + & svg { + fill: var(--wds-accent-color) !important; + } + } + } + /*********************************************************/ + +} +`; diff --git a/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx b/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx index 3e23d80778..07f9d9afb2 100644 --- a/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx @@ -1,14 +1,7 @@ -import Uppy from "@uppy/core"; -import Dashboard from "@uppy/dashboard"; -import GoogleDrive from "@uppy/google-drive"; -import OneDrive from "@uppy/onedrive"; -import Url from "@uppy/url"; +import type Dashboard from "@uppy/dashboard"; +import type { Uppy } from "@uppy/core"; import type { UppyFile } from "@uppy/utils"; -import Webcam from "@uppy/webcam"; -import CloseIcon from "assets/icons/ads/cross.svg"; -import UpIcon from "assets/icons/ads/up-arrow.svg"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -import { Colors } from "constants/Colors"; import type { WidgetType } from "constants/WidgetConstants"; import { FILE_SIZE_LIMIT_FOR_BLOBS } from "constants/WidgetConstants"; import { ValidationTypes } from "constants/WidgetValidation"; @@ -20,9 +13,9 @@ import log from "loglevel"; import React from "react"; import shallowequal from "shallowequal"; -import { createGlobalStyle } from "styled-components"; import { createBlobUrl, isBlobUrl } from "utils/AppsmithUtils"; import type { DerivedPropertiesMap } from "utils/WidgetFactory"; +import { importUppy, isUppyLoaded } from "utils/importUppy"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; import FilePickerComponent from "../component"; @@ -30,178 +23,12 @@ import FileDataTypes from "../constants"; import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; import type { AutocompletionDefinitions } from "widgets/constants"; import parseFileData from "./FileParser"; +import { FilePickerGlobalStyles } from "./index.styled"; const CSV_ARRAY_LABEL = "Array of Objects (CSV, XLS(X), JSON, TSV)"; const ARRAY_CSV_HELPER_TEXT = `All non CSV, XLS(X), JSON or TSV filetypes will have an empty value. \n Large files used in widgets directly might slow down the app.`; -const FilePickerGlobalStyles = createGlobalStyle<{ - borderRadius?: string; -}>` - - /* Sets the font-family to theming font-family of the upload modal */ - .uppy-Root { - font-family: var(--wds-font-family); - } - - /*********************************************************/ - /* Set the new dropHint upload icon */ - .uppy-Dashboard-dropFilesHereHint { - background-image: none; - border-radius: ${({ borderRadius }) => borderRadius}; - } - - .uppy-Dashboard-dropFilesHereHint::before { - border: 2.5px solid var(--wds-accent-color); - width: 60px; - height: 60px; - border-radius: ${({ borderRadius }) => borderRadius}; - display: inline-block; - content: ' '; - position: absolute; - top: 43%; - } - - .uppy-Dashboard-dropFilesHereHint::after { - display: inline-block; - content: ' '; - position: absolute; - top: 46%; - width: 30px; - height: 30px; - - -webkit-mask-image: url(${UpIcon}); - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - -webkit-mask-size: 30px; - background: var(--wds-accent-color); - } - /*********************************************************/ - - /*********************************************************/ - /* Set the styles for the upload button */ - .uppy-StatusBar-actionBtn--upload { - background-color: var(--wds-accent-color) !important; - border-radius: ${({ borderRadius }) => borderRadius}; - } - - .uppy-Dashboard-Item-action--remove { - - /* Sets the border radius of the button when it is focused */ - &:focus { - border-radius: ${({ borderRadius }) => - borderRadius === "0.375rem" ? "0.25rem" : borderRadius} !important; - } - - .uppy-c-icon { - & path:first-child { - /* Sets the black background of remove file button hidden */ - visibility: hidden; - } - - & path:last-child { - /* Sets the cross mark color of remove file button */ - fill: #858282; - } - - background-color: #FFFFFF; - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.06), 0px 1px 3px rgba(0, 0, 0, 0.1); - - & { - /* Sets the black background of remove file button hidden*/ - border-radius: ${({ borderRadius }) => - borderRadius === "0.375rem" ? "0.25rem" : borderRadius}; - } - } - } - /*********************************************************/ - - /*********************************************************/ - /* Sets the back cancel button color to match theming primary color */ - .uppy-DashboardContent-back { - color: var(--wds-accent-color); - - &:hover { - color: var(--wds-accent-color); - background-color: ${Colors.ATHENS_GRAY}; - } - } - /*********************************************************/ - - /*********************************************************/ - /* Sets the style according to reskinning for x button at the top right corner of the modal */ - .uppy-Dashboard-close { - background-color: white; - width: 32px; - height: 32px; - text-align: center; - top: -33px; - border-radius: ${({ borderRadius }) => borderRadius}; - - & span { - font-size: 0; - } - - & span::after { - content: ' '; - -webkit-mask-image: url(${CloseIcon}); - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; - -webkit-mask-size: 20px; - background: #858282; - position: absolute; - top: 32%; - left: 32%; - width: 12px; - height: 12px; - } - } - } - /*********************************************************/ - - - /*********************************************************/ - /* Sets the border radius of the upload modal */ - .uppy-Dashboard-inner, .uppy-Dashboard-innerWrap { - border-radius: ${({ borderRadius }) => borderRadius} !important; - } - - .uppy-Dashboard-AddFiles { - border-radius: ${({ borderRadius }) => borderRadius} !important; - } - /*********************************************************/ - - /*********************************************************/ - /* Sets the error message style according to reskinning*/ - .uppy-Informer { - bottom: 82px; - & p[role="alert"] { - border-radius: ${({ borderRadius }) => borderRadius}; - background-color: transparent; - color: #D91921; - border: 1px solid #D91921; - } - } - /*********************************************************/ - - /*********************************************************/ - /* Style the + add more files button on top right corner of the upload modal */ - .uppy-DashboardContent-addMore { - color: var(--wds-accent-color); - font-weight: 400; - &:hover { - background-color: ${Colors.ATHENS_GRAY}; - color: var(--wds-accent-color); - } - - & svg { - fill: var(--wds-accent-color) !important; - } - } - /*********************************************************/ - -} -`; class FilePickerWidget extends BaseWidget< FilePickerWidgetProps, FilePickerWidgetState @@ -212,8 +39,9 @@ class FilePickerWidget extends BaseWidget< super(props); this.isWidgetUnmounting = false; this.state = { - isLoading: false, - uppy: this.initializeUppy(), + areFilesLoading: false, + isWaitingForUppyToLoad: false, + isUppyModalOpen: false, }; } @@ -532,10 +360,12 @@ class FilePickerWidget extends BaseWidget< } /** - * if uppy is not initialized before, initialize it - * else setState of uppy instance + * Import and initialize the Uppy instance. We use memoize() to ensure that + * once we initialize the instance, we keep returning the initialized one. */ - initializeUppy = () => { + loadAndInitUppyOnce = _.memoize(async () => { + const { Uppy } = await importUppy(); + const uppyState = { id: this.props.widgetId, autoProceed: false, @@ -556,13 +386,18 @@ class FilePickerWidget extends BaseWidget< }, }; - return Uppy(uppyState); - }; + const uppy = Uppy(uppyState); + + await this.initializeUppyEventListeners(uppy); + await this.initializeSelectedFiles(uppy); + + return uppy; + }); /** * set states on the uppy instance with new values */ - reinitializeUppy = (props: FilePickerWidgetProps) => { + reinitializeUppy = async (props: FilePickerWidgetProps) => { const uppyState = { id: props.widgetId, autoProceed: false, @@ -581,14 +416,18 @@ class FilePickerWidget extends BaseWidget< }, }; - this.state.uppy.setOptions(uppyState); + const uppy = await this.loadAndInitUppyOnce(); + uppy.setOptions(uppyState); }; /** * add all uppy events listeners needed */ - initializeUppyEventListeners = () => { - this.state.uppy + initializeUppyEventListeners = async (uppy: Uppy) => { + const { Dashboard, GoogleDrive, OneDrive, Url, Webcam } = + await importUppy(); + + uppy .use(Dashboard, { target: "body", metaFields: [], @@ -609,11 +448,15 @@ class FilePickerWidget extends BaseWidget< disablePageScrollWhenModalOpen: true, proudlyDisplayPoweredByUppy: false, onRequestCloseModal: () => { - const plugin = this.state.uppy.getPlugin("Dashboard"); + const plugin = uppy.getPlugin("Dashboard") as Dashboard; if (plugin) { plugin.closeModal(); } + + this.setState({ + isUppyModalOpen: false, + }); }, locale: { strings: { @@ -628,7 +471,7 @@ class FilePickerWidget extends BaseWidget< }); if (location.protocol === "https:") { - this.state.uppy.use(Webcam, { + uppy.use(Webcam, { onBeforeSnapshot: () => Promise.resolve(), countdown: false, mirror: true, @@ -637,7 +480,7 @@ class FilePickerWidget extends BaseWidget< }); } - this.state.uppy.on("file-removed", (file: UppyFile, reason: any) => { + uppy.on("file-removed", (file: UppyFile, reason: any) => { /** * The below line will not update the selectedFiles meta prop when cancel-all event is triggered. * cancel-all event occurs when close or reset function of uppy is executed. @@ -651,7 +494,7 @@ class FilePickerWidget extends BaseWidget< * Once the file is removed we update the selectedFiles * with the current files present in the uppy's internal state */ - const updatedFiles = this.state.uppy + const updatedFiles = uppy .getFiles() .map((currentFile: UppyFile, index: number) => ({ type: currentFile.type, @@ -674,7 +517,7 @@ class FilePickerWidget extends BaseWidget< } }); - this.state.uppy.on("files-added", (files: UppyFile[]) => { + uppy.on("files-added", (files: UppyFile[]) => { // Deep cloning the selectedFiles const selectedFiles = this.props.selectedFiles ? klona(this.props.selectedFiles) @@ -730,7 +573,7 @@ class FilePickerWidget extends BaseWidget< }); }); - this.state.uppy.on("upload", () => { + uppy.on("upload", () => { this.onFilesSelected(); }); }; @@ -751,29 +594,31 @@ class FilePickerWidget extends BaseWidget< }, }); - this.setState({ isLoading: true }); + this.setState({ areFilesLoading: true }); } }; handleActionComplete = () => { - this.setState({ isLoading: false }); + this.setState({ areFilesLoading: false }); }; - componentDidUpdate(prevProps: FilePickerWidgetProps) { + async componentDidUpdate(prevProps: FilePickerWidgetProps) { super.componentDidUpdate(prevProps); + const { selectedFiles: previousSelectedFiles = [] } = prevProps; const { selectedFiles = [] } = this.props; if (previousSelectedFiles.length && selectedFiles.length === 0) { - this.state.uppy.reset(); + (await this.loadAndInitUppyOnce()).reset(); } else if ( !shallowequal(prevProps.allowedFileTypes, this.props.allowedFileTypes) || prevProps.maxNumFiles !== this.props.maxNumFiles || prevProps.maxFileSize !== this.props.maxFileSize ) { - this.reinitializeUppy(this.props); + await this.reinitializeUppy(this.props); } this.clearFilesFromMemory(prevProps.selectedFiles); } + // Reclaim the memory used by blobs. clearFilesFromMemory(previousFiles: any[] = []) { const { selectedFiles: newFiles = [] } = this.props; @@ -788,13 +633,13 @@ class FilePickerWidget extends BaseWidget< }); } - initializeSelectedFiles() { + async initializeSelectedFiles(uppy: Uppy) { /** * Since on unMount the uppy instance closes and it's internal state is lost along with the files present in it. * Below we add the files again to the uppy instance so that the files are retained. */ this.props.selectedFiles?.forEach((fileItem: any) => { - this.state.uppy.addFile({ + uppy.addFile({ name: fileItem.name, type: fileItem.type, data: new Blob([fileItem.data]), @@ -806,12 +651,11 @@ class FilePickerWidget extends BaseWidget< }); } - componentDidMount() { + async componentDidMount() { super.componentDidMount(); try { - this.initializeUppyEventListeners(); - this.initializeSelectedFiles(); + await this.loadAndInitUppyOnce(); } catch (e) { log.debug("Error in initializing uppy"); } @@ -819,7 +663,9 @@ class FilePickerWidget extends BaseWidget< componentWillUnmount() { this.isWidgetUnmounting = true; - this.state.uppy.close(); + this.loadAndInitUppyOnce().then((uppy) => { + uppy.close(); + }); } static getSetterConfig(): SetterConfig { @@ -846,17 +692,39 @@ class FilePickerWidget extends BaseWidget< buttonColor={this.props.buttonColor} files={this.props.selectedFiles || []} isDisabled={this.props.isDisabled} - isLoading={this.props.isLoading || this.state.isLoading} + isLoading={ + this.props.isLoading || + this.state.areFilesLoading || + this.state.isWaitingForUppyToLoad + } key={this.props.widgetId} label={this.props.label} maxWidth={this.props.maxWidth} minHeight={this.props.minHeight} minWidth={this.props.minWidth} + openModal={async () => { + // If Uppy is still loading, show a spinner to indicate that handling the click + // will take some time. + // + // Copying the `isUppyLoaded` value because `isUppyLoaded` *will* always be true + // by the time `await this.initUppyInstanceOnce()` resolves. + const isUppyLoadedByThisPoint = isUppyLoaded; + + if (!isUppyLoadedByThisPoint) + this.setState({ isWaitingForUppyToLoad: true }); + const uppy = await this.loadAndInitUppyOnce(); + if (!isUppyLoadedByThisPoint) + this.setState({ isWaitingForUppyToLoad: false }); + + const dashboardPlugin = uppy.getPlugin("Dashboard") as Dashboard; + dashboardPlugin.openModal(); + this.setState({ isUppyModalOpen: true }); + }} shouldFitContent={this.isAutoLayoutMode} - uppy={this.state.uppy} widgetId={this.props.widgetId} /> - {this.state.uppy && this.state.uppy.getID() === this.props.widgetId && ( + + {this.state.isUppyModalOpen && ( )} @@ -869,8 +737,9 @@ class FilePickerWidget extends BaseWidget< } interface FilePickerWidgetState extends WidgetState { - isLoading: boolean; - uppy: any; + areFilesLoading: boolean; + isWaitingForUppyToLoad: boolean; + isUppyModalOpen: boolean; } interface FilePickerWidgetProps extends WidgetProps { diff --git a/app/client/src/widgets/FilepickerWidget/component/index.tsx b/app/client/src/widgets/FilepickerWidget/component/index.tsx index 2671de9736..708c2944b0 100644 --- a/app/client/src/widgets/FilepickerWidget/component/index.tsx +++ b/app/client/src/widgets/FilepickerWidget/component/index.tsx @@ -19,7 +19,7 @@ class FilePickerComponent extends React.Component< openModal = () => { if (!this.props.isDisabled) { - this.props.uppy.getPlugin("Dashboard").openModal(); + this.props.openModal(); } }; @@ -40,7 +40,7 @@ class FilePickerComponent extends React.Component< } public closeModal() { - this.props.uppy.getPlugin("Dashboard").closeModal(); + this.props.closeModal(); } } @@ -50,7 +50,8 @@ export interface FilePickerComponentState { export interface FilePickerComponentProps extends ComponentProps { label: string; - uppy: any; + openModal: () => void; + closeModal: () => void; isLoading: boolean; files?: any[]; } diff --git a/app/client/src/widgets/FilepickerWidget/widget/index.tsx b/app/client/src/widgets/FilepickerWidget/widget/index.tsx index 562eba1166..91b61354d5 100644 --- a/app/client/src/widgets/FilepickerWidget/widget/index.tsx +++ b/app/client/src/widgets/FilepickerWidget/widget/index.tsx @@ -1,17 +1,13 @@ import React from "react"; +import type { Uppy } from "@uppy/core"; import type { WidgetProps, WidgetState } from "../../BaseWidget"; import BaseWidget from "../../BaseWidget"; import type { WidgetType } from "constants/WidgetConstants"; import FilePickerComponent from "../component"; -import Uppy from "@uppy/core"; -import GoogleDrive from "@uppy/google-drive"; -import Webcam from "@uppy/webcam"; -import Url from "@uppy/url"; -import OneDrive from "@uppy/onedrive"; import { ValidationTypes } from "constants/WidgetValidation"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { DerivedPropertiesMap } from "utils/WidgetFactory"; -import Dashboard from "@uppy/dashboard"; +import type Dashboard from "@uppy/dashboard"; import shallowequal from "shallowequal"; import _ from "lodash"; import FileDataTypes from "./FileDataTypes"; @@ -19,6 +15,7 @@ import log from "loglevel"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; import type { AutocompletionDefinitions } from "widgets/constants"; +import { importUppy, isUppyLoaded } from "utils/importUppy"; import type { SetterConfig } from "entities/AppTheming"; class FilePickerWidget extends BaseWidget< @@ -28,8 +25,8 @@ class FilePickerWidget extends BaseWidget< constructor(props: FilePickerWidgetProps) { super(props); this.state = { - isLoading: false, - uppy: this.initializeUppy(), + areFilesLoading: false, + isWaitingForUppyToLoad: false, }; } @@ -253,10 +250,12 @@ class FilePickerWidget extends BaseWidget< } /** - * if uppy is not initialized before, initialize it - * else setState of uppy instance + * Import and initialize the Uppy instance. We use memoize() to ensure that + * once we initialize the instance, we keep returning it. */ - initializeUppy = () => { + loadAndInitUppyOnce = _.memoize(async () => { + const { Uppy } = await importUppy(); + const uppyState = { id: this.props.widgetId, autoProceed: false, @@ -277,13 +276,17 @@ class FilePickerWidget extends BaseWidget< }, }; - return Uppy(uppyState); - }; + const uppy = Uppy(uppyState); + + await this.initializeUppyEventListeners(uppy); + + return uppy; + }); /** * set states on the uppy instance with new values */ - reinitializeUppy = (props: FilePickerWidgetProps) => { + reinitializeUppy = async (props: FilePickerWidgetProps) => { const uppyState = { id: props.widgetId, autoProceed: false, @@ -302,14 +305,18 @@ class FilePickerWidget extends BaseWidget< }, }; - this.state.uppy.setOptions(uppyState); + const uppy = await this.loadAndInitUppyOnce(); + uppy.setOptions(uppyState); }; /** * add all uppy events listeners needed */ - initializeUppyEventListeners = () => { - this.state.uppy + initializeUppyEventListeners = async (uppy: Uppy) => { + const { Dashboard, GoogleDrive, OneDrive, Url, Webcam } = + await importUppy(); + + uppy .use(Dashboard, { target: "body", metaFields: [], @@ -330,7 +337,7 @@ class FilePickerWidget extends BaseWidget< disablePageScrollWhenModalOpen: true, proudlyDisplayPoweredByUppy: false, onRequestCloseModal: () => { - const plugin = this.state.uppy.getPlugin("Dashboard"); + const plugin = uppy.getPlugin("Dashboard") as Dashboard; if (plugin) { plugin.closeModal(); @@ -349,7 +356,7 @@ class FilePickerWidget extends BaseWidget< }); if (location.protocol === "https:") { - this.state.uppy.use(Webcam, { + uppy.use(Webcam, { onBeforeSnapshot: () => Promise.resolve(), countdown: false, mirror: true, @@ -358,7 +365,7 @@ class FilePickerWidget extends BaseWidget< }); } - this.state.uppy.on("file-removed", (file: any) => { + uppy.on("file-removed", (file: any) => { const updatedFiles = this.props.selectedFiles ? this.props.selectedFiles.filter((dslFile) => { return file.id !== dslFile.id; @@ -367,7 +374,7 @@ class FilePickerWidget extends BaseWidget< this.props.updateWidgetMetaProperty("selectedFiles", updatedFiles); }); - this.state.uppy.on("files-added", (files: any[]) => { + uppy.on("files-added", (files: any[]) => { const dslFiles = this.props.selectedFiles ? [...this.props.selectedFiles] : []; @@ -415,7 +422,7 @@ class FilePickerWidget extends BaseWidget< }); }); - this.state.uppy.on("upload", () => { + uppy.on("upload", () => { this.onFilesSelected(); }); }; @@ -436,40 +443,42 @@ class FilePickerWidget extends BaseWidget< }, }); - this.setState({ isLoading: true }); + this.setState({ areFilesLoading: true }); } }; handleActionComplete = () => { - this.setState({ isLoading: false }); + this.setState({ areFilesLoading: false }); }; - componentDidUpdate(prevProps: FilePickerWidgetProps) { + async componentDidUpdate(prevProps: FilePickerWidgetProps) { if ( prevProps.selectedFiles && prevProps.selectedFiles.length > 0 && this.props.selectedFiles === undefined ) { - this.state.uppy.reset(); + (await this.loadAndInitUppyOnce()).reset(); } else if ( !shallowequal(prevProps.allowedFileTypes, this.props.allowedFileTypes) || prevProps.maxNumFiles !== this.props.maxNumFiles || prevProps.maxFileSize !== this.props.maxFileSize ) { - this.reinitializeUppy(this.props); + await this.reinitializeUppy(this.props); } } - componentDidMount() { + async componentDidMount() { try { - this.initializeUppyEventListeners(); + await this.loadAndInitUppyOnce(); } catch (e) { log.debug("Error in initializing uppy"); } } componentWillUnmount() { - this.state.uppy.close(); + this.loadAndInitUppyOnce().then((uppy) => { + uppy.close(); + }); } static getSetterConfig(): SetterConfig { @@ -490,12 +499,37 @@ class FilePickerWidget extends BaseWidget< getPageView() { return ( { + const uppy = await this.loadAndInitUppyOnce(); + + const dashboardPlugin = uppy.getPlugin("Dashboard") as Dashboard; + dashboardPlugin.closeModal(); + }} files={this.props.selectedFiles || []} isDisabled={this.props.isDisabled} - isLoading={this.props.isLoading || this.state.isLoading} + isLoading={ + this.props.isLoading || + this.state.areFilesLoading || + this.state.isWaitingForUppyToLoad + } key={this.props.widgetId} label={this.props.label} - uppy={this.state.uppy} + openModal={async () => { + // If Uppy is still loading, show a spinner to indicate that handling the click + // will take some time. + // + // Copying the `isUppyLoaded` value because `isUppyLoaded` *will* always be true + // by the time `await this.initUppyInstanceOnce()` resolves. + const isUppyLoadedByThisPoint = isUppyLoaded; + if (!isUppyLoadedByThisPoint) + this.setState({ isWaitingForUppyToLoad: true }); + const uppy = await this.loadAndInitUppyOnce(); + if (!isUppyLoadedByThisPoint) + this.setState({ isWaitingForUppyToLoad: false }); + + const dashboardPlugin = uppy.getPlugin("Dashboard") as Dashboard; + dashboardPlugin.openModal(); + }} widgetId={this.props.widgetId} /> ); @@ -507,8 +541,8 @@ class FilePickerWidget extends BaseWidget< } export interface FilePickerWidgetState extends WidgetState { - isLoading: boolean; - uppy: any; + areFilesLoading: boolean; + isWaitingForUppyToLoad: boolean; } export interface FilePickerWidgetProps extends WidgetProps { diff --git a/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx b/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx index 6507339f31..82eba113a4 100644 --- a/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx +++ b/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx @@ -1,6 +1,4 @@ import * as Sentry from "@sentry/react"; -import _ from "workers/common/JSLibrary/lodash-wrapper"; -import moment from "moment"; import React, { useCallback, useContext, useMemo, useState } from "react"; import type { BaseInputComponentProps } from "./BaseInputField"; @@ -135,7 +133,7 @@ function CurrencyInputField({ Sentry.captureException(e); } - const value = derived.value({ text }, moment, _); + const value = derived.value({ text }); return { text, diff --git a/app/client/src/workers/common/JSLibrary/lodash-wrapper.js b/app/client/src/workers/common/JSLibrary/lodash-wrapper.js index 28947d3ea3..a137ed970a 100644 --- a/app/client/src/workers/common/JSLibrary/lodash-wrapper.js +++ b/app/client/src/workers/common/JSLibrary/lodash-wrapper.js @@ -1,9 +1,59 @@ +// 🚧 NOTE: this file exists only for the worker thread, as the worker thread needs to pass +// the full Lodash library around. *Do not* import it in the main thread code, as that will +// result in bundling the full Lodash. If you’re trying to pass a Lodash reference into some +// function in the main thread, consider if you can instead: +// +// - import and call Lodash directly: +// +// Before: +// // a.js +// export function mapArray(_) { return _.map(someArray, someFunction); } +// // b.js +// import { mapArray } from './a'; +// import _ from 'lodash'; +// mapArray(_); +// +// After: +// // a.js +// import _ from 'lodash'; +// export function mapArray() { return _.map(someArray, someFunction); } +// // b.js +// import { mapArray } from './a'; +// mapArray(); +// +// - pass only the function you need about: +// +// Before: +// // a.js +// export function mapArray(_) { return _.map(someArray, someFunction); } +// // b.js +// import { mapArray } from './a'; +// import _ from 'lodash'; +// mapArray(_); +// +// After: +// // a.js +// export function mapArray(_) { return _.map(someArray, someFunction); } +// // b.js +// import { mapArray } from './a'; +// import _ from 'lodash'; +// mapArray({ map: _.map }); +if ( + typeof window !== "undefined" && + // Jest mocks the `window` object when running worker tests + process.env.NODE_ENV !== "test" +) { + throw new Error("lodash-wrapper.js must only be used in a worker thread"); +} + +///////////////////////////////////////////////////////////////////////// +// // We use babel-plugin-lodash to only import the lodash functions we use. // Unfortunately, the plugin doesn’t work with the following pattern: // import _ from 'lodash'; // const something = _; // When it encounters code like above, it will replace _ with `undefined`, -// which will break the app. +// which will break the app (https://github.com/lodash/babel-plugin-lodash/issues/235). // // Given that we *need* to use the full lodash in ./resetJSLibraries.js, // we use this workaround where we’re importing Lodash using CommonJS require(). diff --git a/app/client/yarn.lock b/app/client/yarn.lock index ab303314a1..01583ffde1 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -9660,7 +9660,7 @@ __metadata: dayjs: ^1.10.6 deep-diff: ^1.0.2 design-system: "npm:@appsmithorg/design-system@2.1.15" - design-system-old: "npm:@appsmithorg/design-system-old@1.1.10" + design-system-old: "npm:@appsmithorg/design-system-old@1.1.11" diff: ^5.0.0 dotenv: ^8.1.0 downloadjs: ^1.4.7 @@ -13674,9 +13674,9 @@ __metadata: languageName: node linkType: hard -"design-system-old@npm:@appsmithorg/design-system-old@1.1.10": - version: 1.1.10 - resolution: "@appsmithorg/design-system-old@npm:1.1.10" +"design-system-old@npm:@appsmithorg/design-system-old@1.1.11": + version: 1.1.11 + resolution: "@appsmithorg/design-system-old@npm:1.1.11" dependencies: emoji-mart: 3.0.1 peerDependencies: @@ -13696,7 +13696,7 @@ __metadata: remixicon-react: ^1.0.0 styled-components: 5.3.6 tinycolor2: ^1.4.2 - checksum: 9eb74a7d59f3db7f6b19f31fecf0381247a094a8655cf4a5e64b7318e2659e28c643382f429d2aed601acf1209c062de7fb6071fa03ea0e5eb3830f840384fa7 + checksum: 968fc1be2ded862c2cac3bc8ae8eab6642d16ffda7b586aeb87ed69d1bad08ee5e3d1c05098adfb9e80f9c1a6fa6d4bc05d0cd464b700aa39005a5bb6c63b7ca languageName: node linkType: hard