diff --git a/.version b/.version index c819688d6b..7b5655c811 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.35.3 +2.35.4 diff --git a/cypress-tests/cypress/constants/texts/exportImport.js b/cypress-tests/cypress/constants/texts/exportImport.js index b193a1a529..1a405efa77 100644 --- a/cypress-tests/cypress/constants/texts/exportImport.js +++ b/cypress-tests/cypress/constants/texts/exportImport.js @@ -18,7 +18,7 @@ export const exportAppModalText = { }; export const importText = { - importOption: "Import", + importOption: "Import from device", couldNotImportAppToastMessage: `Could not import: SyntaxError: Unexpected token`, appImportedToastMessage: "App imported successfully.", }; diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js index 548e1cea40..5852d8d680 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js @@ -62,7 +62,7 @@ describe("dashboard", () => { cy.get(commonSelectors.editRectangleIcon).should("be.visible"); cy.get(commonSelectors.appCreateButton).verifyVisibleElement( "have.text", - "Create new app" + "Create an app" ); cy.get(dashboardSelector.folderLabel).should("be.visible"); cy.get(dashboardSelector.folderLabel).should(($el) => { diff --git a/frontend/.version b/frontend/.version index c819688d6b..7b5655c811 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.35.3 +2.35.4 diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index 2614646d85..d02f5fa139 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -405,8 +405,8 @@ "wishToDeleteFolder": "Are you sure you want to delete the folder {{folderName}}? Apps within the folder will not be deleted." }, "header": { - "createNewApplication": "Create new app", - "import": "Import", + "createNewApplication": "Create an app", + "import": "Import from device", "chooseFromTemplate": "Choose from template" }, "pagination": { diff --git a/frontend/src/Editor/Inspector/Components/Table/Table.jsx b/frontend/src/Editor/Inspector/Components/Table/Table.jsx index 7b53a82203..c2cb70bdb1 100644 --- a/frontend/src/Editor/Inspector/Components/Table/Table.jsx +++ b/frontend/src/Editor/Inspector/Components/Table/Table.jsx @@ -1139,9 +1139,7 @@ class TableComponent extends React.Component {
{actions.value.map((action, index) => this.renderActionButton(action, index))}
- {actions.value.length === 0 && ( - - )} + {actions.value.length === 0 && } New action button diff --git a/frontend/src/HomePage/AppList.jsx b/frontend/src/HomePage/AppList.jsx index 80efa14b0f..5863e85d2e 100644 --- a/frontend/src/HomePage/AppList.jsx +++ b/frontend/src/HomePage/AppList.jsx @@ -10,7 +10,7 @@ const AppList = (props) => { {props.isLoading && ( <> {Array.from(Array(2)).map((_, rowIndex) => ( -
+
{Array.from(Array(3)).map((_, index) => (
diff --git a/frontend/src/HomePage/AppMenu.jsx b/frontend/src/HomePage/AppMenu.jsx index 468493d104..4e88978be3 100644 --- a/frontend/src/HomePage/AppMenu.jsx +++ b/frontend/src/HomePage/AppMenu.jsx @@ -71,6 +71,10 @@ export const AppMenu = function AppMenu({ onClick={() => openAppActionModal('remove-app-from-folder')} /> )} + + )} + {canUpdateApp && canCreateApp && ( + <> openAppActionModal('clone-app')} diff --git a/frontend/src/HomePage/BlankPage.jsx b/frontend/src/HomePage/BlankPage.jsx index 2d7b048d61..f07463c647 100644 --- a/frontend/src/HomePage/BlankPage.jsx +++ b/frontend/src/HomePage/BlankPage.jsx @@ -65,18 +65,20 @@ export const BlankPage = function BlankPage({ )}

- - Create new application - -
+
+ + Create new application + +
+
{isImportingApp && } - {this.props.t('homePage.header.createNewApplication', 'Create new app')} + {this.props.t('homePage.header.createNewApplication', 'Create an app')} @@ -789,7 +790,7 @@ class HomePageComponent extends React.Component { data-cy="import-option-label" onChange={this.readAndImport} > - {this.props.t('homePage.header.import', 'Import')} + {this.props.t('homePage.header.import', 'Import from device')}
+ {isLoading && ( + + )} {(meta?.total_count > 0 || appSearchKey) && ( <> @@ -873,23 +884,22 @@ class HomePageComponent extends React.Component {
)} - {isLoading || - (meta.total_count > 0 && ( - - ))} + {meta.total_count > 0 && ( + + )}
{this.pageCount() > MAX_APPS_PER_PAGE && ( diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 076ce114da..ccf24462e2 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -883,7 +883,7 @@ button { } .app-icon-skeleton { - background-color: #91a4f6; + background-color: #ECEEF0 !important; border-radius: 4px; margin-bottom: 20px; height: 40px; @@ -8126,7 +8126,7 @@ tbody { .logo-nav-card { transform: translate(5px, 50px) !important; - z-index: 100; + z-index: 101; } .theme-dark { @@ -11366,6 +11366,22 @@ tbody { .app-list { overflow-y: auto; height: calc(100vh - 23rem); + + .skeleton-container { + display: flex; + flex-direction: column; + + .col { + display: flex; + justify-content: center; + margin-bottom: 1rem; + } + + .card-skeleton-container { + width: 304px; + + } + } } .menu-ico { @@ -12503,10 +12519,12 @@ tbody { } .group-chip { - padding: 5px 8px; + padding: 2px 8px; + margin: 0; border-radius: 6px; background-color: var(--slate3); color: var(--slate11); + min-height: 24px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -12525,7 +12543,7 @@ tbody { border: 1px solid var(--slate1); box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08); padding: 9px 10px; - gap: 15px; + gap: 10px; cursor: default; max-height: 240px; overflow: auto; diff --git a/server/.version b/server/.version index c819688d6b..7b5655c811 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -2.35.3 +2.35.4 diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 2d788708ea..0b28ef675c 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -48,24 +48,6 @@ import { ImportExportResourcesModule } from './modules/import_export_resources/i import { MailerModule } from '@nestjs-modules/mailer'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; -const port = +process.env.SMTP_PORT || 587; -const transport = - process.env.NODE_ENV === 'development' - ? { - host: 'localhost', - ignoreTLS: true, - secure: false, - } - : { - host: process.env.SMTP_DOMAIN, - port: port, - secure: port == 465, - auth: { - user: process.env.SMTP_USERNAME, - pass: process.env.SMTP_PASSWORD, - }, - }; - const imports = [ ScheduleModule.forRoot(), ConfigModule.forRoot({ @@ -99,7 +81,22 @@ const imports = [ }, }), MailerModule.forRoot({ - transport: transport, + transport: + process.env.NODE_ENV === 'development' + ? { + host: 'localhost', + ignoreTLS: true, + secure: false, + } + : { + host: process.env.SMTP_DOMAIN, + port: +process.env.SMTP_PORT || 587, + secure: process.env.SMTP_SSL === 'true', + auth: { + user: process.env.SMTP_USERNAME, + pass: process.env.SMTP_PASSWORD, + }, + }, preview: process.env.NODE_ENV === 'development', template: { dir: join(__dirname, 'mails'), diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index 4a4179eba6..84b275e3ee 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -240,6 +240,7 @@ export class AppsController { return response; } + // Deprecated - moved to import - export - controller @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) @Post(':id/clone') diff --git a/server/src/controllers/import_export_resources.controller.ts b/server/src/controllers/import_export_resources.controller.ts index a96c3504aa..a5b14b32da 100644 --- a/server/src/controllers/import_export_resources.controller.ts +++ b/server/src/controllers/import_export_resources.controller.ts @@ -40,7 +40,7 @@ export class ImportExportResourcesController { async import(@User() user, @Body() importResourcesDto: ImportResourcesDto) { const ability = await this.appsAbilityFactory.appsActions(user); - if (!ability.can('cloneApp', App)) { + if (!ability.can('importApp', App)) { throw new ForbiddenException('You do not have permissions to perform this action'); } const isNotCompatibleVersion = !checkVersionCompatibility(importResourcesDto.tooljet_version); @@ -54,7 +54,7 @@ export class ImportExportResourcesController { @UseGuards(JwtAuthGuard) @Post('/clone') async clone(@User() user, @Body() cloneResourcesDto: CloneResourcesDto) { - const ability = await this.appsAbilityFactory.appsActions(user); + const ability = await this.appsAbilityFactory.appsActions(user, cloneResourcesDto?.app?.[0]?.id); if (!ability.can('cloneApp', App)) { throw new ForbiddenException('You do not have permissions to perform this action'); diff --git a/server/src/modules/casl/abilities/apps-ability.factory.ts b/server/src/modules/casl/abilities/apps-ability.factory.ts index 19cb7b8ce1..73e70c2760 100644 --- a/server/src/modules/casl/abilities/apps-ability.factory.ts +++ b/server/src/modules/casl/abilities/apps-ability.factory.ts @@ -8,6 +8,7 @@ import { UsersService } from 'src/services/users.service'; type Actions = | 'authorizeOauthForSource' | 'cloneApp' + | 'importApp' | 'createApp' | 'createDataSource' | 'createQuery' @@ -41,18 +42,22 @@ export class AppsAbilityFactory { async appsActions(user: User, id?: string) { const { can, build } = new AbilityBuilder>(Ability as AbilityClass); + const canUpdateApp = await this.usersService.userCan(user, 'update', 'App', id); if (await this.usersService.userCan(user, 'create', 'User')) { can('createUsers', App, { organizationId: user.organizationId }); } - if (await this.usersService.userCan(user, 'update', 'App', id)) { + if (canUpdateApp) { can('editApp', App, { organizationId: user.organizationId }); } if (await this.usersService.userCan(user, 'create', 'App')) { can('createApp', App); - can('cloneApp', App, { organizationId: user.organizationId }); + can('importApp', App); + if (canUpdateApp) { + can('cloneApp', App, { organizationId: user.organizationId }); + } } if (await this.usersService.userCan(user, 'read', 'App', id)) { @@ -72,7 +77,7 @@ export class AppsAbilityFactory { }); } - if (await this.usersService.userCan(user, 'update', 'App', id)) { + if (canUpdateApp) { can('updateParams', App, { organizationId: user.organizationId }); can('createVersions', App, { organizationId: user.organizationId }); can('deleteVersions', App, { organizationId: user.organizationId });