Merge pull request #10114 from ToolJet/release/platformv18

Release Platform v18
This commit is contained in:
Midhun G S 2024-06-27 11:10:07 +05:30 committed by GitHub
commit 0ca40bc52e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 44651 additions and 7253 deletions

View file

@ -296,7 +296,6 @@ describe(
});
});
cy.get(usersSelector.acceptInvite).click();
cy.get('[data-cy="draggable-widget-table1"]').should("be.visible");
});
}

View file

@ -43,7 +43,7 @@ describe("Password reset functionality", () => {
);
cy.get(commonSelectors.forgotPasswordPageSubHeader).should(($el) => {
expect($el.contents().first().text().trim()).to.eq(
commonText.newToTooljetText
"New to"
);
});
cy.get(commonSelectors.createAnAccountLink).verifyVisibleElement(

View file

@ -51,7 +51,6 @@ describe("User signup", () => {
it("Verify invalid invitation link", () => {
cy.log(invitationLink)
cy.visit(invitationLink);
cy.pause()
verifyInvalidInvitationLink();
cy.get(commonSelectors.backtoSignUpButton).click();
cy.get(commonSelectors.SignUpSectionHeader).should("be.visible");

View file

@ -21,7 +21,6 @@ import { buttonText } from "Texts/button";
describe("App Export Functionality", () => {
var data = {};
data.appName1 = `${fake.companyName}-App`;
let currentVersion = "";
let otherVersions = [];
beforeEach(() => {
@ -29,6 +28,7 @@ describe("App Export Functionality", () => {
});
it("Verify the elements of export dialog box", () => {
data.appName1 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName1);
cy.openApp();
cy.dragAndDropWidget(buttonText.defaultWidgetText);
@ -56,6 +56,9 @@ describe("App Export Functionality", () => {
});
it("Verify 'Export app' functionality of an application", () => {
data.appName1 = `${fake.companyName}-App`;
cy.apiCreateApp(data.appName1);
cy.visit("/");
cy.get(commonSelectors.appHeaderLable).should("be.visible");
@ -96,7 +99,7 @@ describe("App Export Functionality", () => {
.click();
createNewVersion((otherVersions = ["v2"]), (currentVersion = "v1"));
cy.wait(500);
cy.dragAndDropWidget("Toggle Switch", 50, 50);
cy.dragAndDropWidget("Text Input", 50, 50);
cy.waitForAutoSave();
cy.get(appVersionSelectors.currentVersionField((otherVersions = "v2")))
.should("be.visible")

View file

@ -34,9 +34,9 @@ describe("Bulk user upload", () => {
data.firstName = fake.firstName;
data.workspaceName = data.firstName.toLowerCase();
cy.apiLogin()
cy.apiLogin();
cy.apiCreateWorkspace(data.firstName, data.workspaceName);
cy.visit(`${data.workspaceName}`)
cy.visit(`${data.workspaceName}`);
common.navigateToManageUsers();
@ -122,9 +122,9 @@ describe("Bulk user upload", () => {
data.firstName = fake.firstName;
data.workspaceName = data.firstName.toLowerCase();
cy.apiLogin()
cy.apiLogin();
cy.apiCreateWorkspace(data.firstName, data.workspaceName);
cy.visit(`${data.workspaceName}`)
cy.visit(`${data.workspaceName}`);
common.navigateToManageUsers();
cy.get(usersSelector.buttonAddUsers).click();
@ -142,7 +142,10 @@ describe("Bulk user upload", () => {
force: true,
});
cy.get(usersSelector.buttonUploadUsers).click();
cy.wait(10000);
cy.wait(30000);
cy.get(".go2072408551")
.should("be.visible")
.and("have.text", "250 users are being added");
common.searchUser("test12@gmail.com");
cy.contains("td", "test12@gmail.com")
.parent()

View file

@ -131,7 +131,8 @@ describe("user invite flow cases", () => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceName);
cy.visit(`${data.workspaceName}`);
enableSignUp();
cy.wait(3000)
setSignupStatus(true, data.workspaceName);
logout();
cy.get(commonSelectors.createAnAccountLink).click();
@ -142,11 +143,6 @@ describe("user invite flow cases", () => {
cy.defaultWorkspaceLogin();
visitWorkspaceInvitation(data.email, data.workspaceName);
cy.clearAndType(commonSelectors.workEmailInputField, data.email);
cy.clearAndType(commonSelectors.passwordInputField, "password");
cy.get(commonSelectors.signInButton).click();
cy.get(usersSelector.acceptInvite).click();
cy.verifyToastMessage(commonSelectors.toastMessage, usersText.inviteToast);
logout();
});

View file

@ -552,21 +552,25 @@ export const defaultSSO = (enable) => {
});
};
export const setSignupStatus = (enable) => {
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request(
{
export const setSignupStatus = (enable, workspaceName = 'My workspace') => {
cy.task("updateId", {
dbconfig: Cypress.env("app_db"),
sql: `SELECT id FROM organizations WHERE name = '${workspaceName}'`,
}).then((resp) => {
const workspaceId = resp.rows[0].id;
cy.getCookie("tj_auth_token").then((cookie) => {
cy.request({
method: "PATCH",
url: "http://localhost:3000/api/organizations",
headers: {
"Tj-Workspace-Id": Cypress.env("workspaceId"),
"Tj-Workspace-Id": workspaceId,
Cookie: `tj_auth_token=${cookie.value}`,
},
body: { enableSignUp: enable },
},
{ log: false }
).then((response) => {
expect(response.status).to.equal(200);
}).then((response) => {
expect(response.status).to.equal(200);
});
});
});
};

View file

@ -20,7 +20,7 @@
display: flex;
padding: 1.5rem;
flex: 1 1 auto;
align-items: start;
align-items: flex-start;
}
.card-info {

2
frontend/.gitignore vendored
View file

@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
storybook-static

View file

@ -0,0 +1,14 @@
// storybookDecorators.js
import React from 'react';
export function withColorScheme(story, context) {
const darkMode = context?.globals?.backgrounds?.value === '#333333'; // Access theme mode from globals
const className = darkMode ? 'dark-theme' : '';
return (
<div className={className} style={{ backgroundColor: 'transparent' }}>
{story()}
</div>
);
}

View file

@ -1,6 +1,5 @@
/** @type { import('@storybook/react-webpack5').StorybookConfig } */
import custom from '../webpack.config'
const path = require('path');
import customWebpackConfig from '../webpack.config';
import path from 'path';
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
@ -17,16 +16,19 @@ const config = {
docs: {
autodocs: "tag",
},
webpackFinal: async (config) => {
webpackFinal: async (storybookConfig) => {
return {
...config,
module: { ...config.module, rules: [...config.module.rules, ...custom.module.rules] },
resolve : {
alias : {
'@': path.resolve(__dirname, 'src/')
...storybookConfig,
module: { ...storybookConfig.module, rules: [...storybookConfig.module.rules, ...customWebpackConfig.module.rules] },
resolve: {
...storybookConfig.resolve,
alias: {
...storybookConfig.resolve.alias,
'@': path.resolve(__dirname, '../src/')
}
}
};
},
};
export default config;

View file

@ -1,7 +1,8 @@
/** @type { import('@storybook/react').Preview } */
// import 'bootstrap/dist/css/bootstrap.min.css';
import '../src/_styles/theme.scss';
import './preview.scss'
import './preview.scss';
import { withColorScheme } from './decorators'; // Import the decorator
const preview = {
parameters: {
@ -13,6 +14,7 @@ const preview = {
},
},
},
decorators: [withColorScheme], // Adding the decorator to the decorators array
};
export default preview;

View file

@ -1,2 +1,2 @@
@import '~bootstrap/scss/bootstrap';
@import '../src/_styles/componentdesign.scss'

View file

@ -0,0 +1,131 @@
import { decodeEntities } from '@/_helpers/utils';
export const defaultWhiteLabellingSettings = {
WHITE_LABEL_LOGO: 'assets/images/rocket.svg',
WHITE_LABEL_TEXT: 'ToolJet',
WHITE_LABEL_FAVICON: 'assets/images/logo.svg',
};
export const whiteLabellingOptions = {
WHITE_LABEL_LOGO: 'App Logo',
WHITE_LABEL_TEXT: 'Page Title',
WHITE_LABEL_FAVICON: 'Favicon',
};
export async function fetchWhiteLabelDetails() {}
export async function checkWhiteLabelsDefaultState() {
return true;
}
export async function resetToDefaultWhiteLabels() {}
export function retrieveWhiteLabelText() {
return window.public_config?.WHITE_LABEL_TEXT || defaultWhiteLabellingSettings.WHITE_LABEL_TEXT;
}
export function retrieveWhiteLabelLogo() {
return window.public_config?.WHITE_LABEL_LOGO || defaultWhiteLabellingSettings.WHITE_LABEL_LOGO;
}
export function retrieveWhiteLabelFavicon() {
return window.public_config?.WHITE_LABEL_FAVICON || defaultWhiteLabellingSettings.WHITE_LABEL_FAVICON;
}
export const pageTitles = {
INSTANCE_SETTINGS: 'Settings',
WORKSPACE_SETTINGS: 'Workspace settings',
INTEGRATIONS: 'Marketplace',
WORKFLOWS: 'Workflows',
DATABASE: 'Database',
DATA_SOURCES: 'Data sources',
AUDIT_LOGS: 'Audit logs',
ACCOUNT_SETTINGS: 'Profile settings',
SETTINGS: 'Profile settings',
EDITOR: 'Editor',
WORKFLOW_EDITOR: 'workflowEditor',
VIEWER: 'Viewer',
DASHBOARD: 'Dashboard',
WORKSPACE_CONSTANTS: 'Workspace constants',
};
// to set favicon and title from router for individual pages
export async function setFaviconAndTitle(whiteLabelFavicon, whiteLabelText, location) {
if (!whiteLabelFavicon || !whiteLabelText) {
whiteLabelFavicon = await retrieveWhiteLabelFavicon();
whiteLabelText = await retrieveWhiteLabelText();
}
// Set favicon
let links = document.querySelectorAll("link[rel='icon']");
if (links.length === 0) {
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/svg+xml';
document.getElementsByTagName('head')[0].appendChild(link);
links = [link];
}
links.forEach((link) => {
link.href = `${whiteLabelFavicon || defaultWhiteLabellingSettings.WHITE_LABEL_FAVICON}`;
});
// Set title
const isEditorOrViewerGoingToRender = ['/apps/', '/applications/'].some((path) => location?.pathname.includes(path));
if (isEditorOrViewerGoingToRender) {
return;
}
const pathToTitle = {
'instance-settings': pageTitles.INSTANCE_SETTINGS,
'workspace-settings': pageTitles.WORKSPACE_SETTINGS,
integrations: pageTitles.INTEGRATIONS,
workflows: pageTitles.WORKFLOWS,
'data-sources': pageTitles.DATA_SOURCES,
'audit-logs': pageTitles.AUDIT_LOGS,
'account-settings': pageTitles.ACCOUNT_SETTINGS,
settings: pageTitles.INSTANCE_SETTINGS,
login: '',
signUp: '',
error: '',
signup: '',
'organization-invitations': '',
invitation: '',
'forgot-password': '',
'reset-password': '',
'workspace-constants': pageTitles.WORKSPACE_CONSTANTS,
setup: '',
};
const pageTitleKey = Object.keys(pathToTitle).find((path) => location?.pathname.includes(path));
const pageTitle = pathToTitle[pageTitleKey];
//For undefined routes
if (pageTitle === undefined) {
return;
}
if (pageTitleKey && !isEditorOrViewerGoingToRender) {
document.title = pageTitle
? `${decodeEntities(pageTitle)} | ${whiteLabelText || defaultWhiteLabellingSettings.WHITE_LABEL_TEXT}`
: `${decodeEntities(whiteLabelText) || defaultWhiteLabellingSettings.WHITE_LABEL_TEXT}`;
}
}
export async function fetchAndSetWindowTitle(pageDetails) {
const whiteLabelText = retrieveWhiteLabelText();
let pageTitleKey = pageDetails?.page || '';
let pageTitle = '';
switch (pageTitleKey) {
case pageTitles.VIEWER: {
const titlePrefix = pageDetails?.preview ? 'Preview - ' : '';
pageTitle = `${titlePrefix}${pageDetails?.appName || 'My App'}`;
break;
}
case pageTitles.EDITOR:
case pageTitles.WORKFLOW_EDITOR: {
pageTitle = pageDetails?.appName || 'My App';
break;
}
default: {
pageTitle = pageTitleKey;
break;
}
}
document.title = !(pageDetails?.preview === false) ? `${pageTitle} | ${whiteLabelText}` : `${pageTitle}`;
}

17
frontend/components.json Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/theme.scss",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

9
frontend/netlify.toml Normal file
View file

@ -0,0 +1,9 @@
[build]
base = "frontend/"
publish = "storybook-static"
command = "npx storybook build"
[template.environment]
NODE_ENV = "production"
NODE_VERSION = "18.18.2"
NPM_VERSION = "9.8.1"

30517
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,7 @@
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toggle-group": "^1.0.4",
"@react-google-maps/api": "^2.18.1",
"@sentry/react": "^7.100.1",
@ -35,6 +36,7 @@
"acorn": "^8.11.3",
"axios": "^1.3.3",
"bootstrap": "^5.2.3",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"deep-object-diff": "^1.1.9",
"dompurify": "^3.0.0",
@ -110,6 +112,8 @@
"semver": "^7.3.8",
"string-hash": "^1.1.3",
"superstruct": "^1.0.3",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"tinycolor2": "^1.6.0",
"url-join": "^5.0.0",
"use-react-router-breadcrumbs": "^4.0.1",
@ -137,6 +141,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"autoprefixer": "^10.4.17",
"babel-loader": "^9.1.2",
"babel-plugin-console-source": "^2.0.5",
"babel-plugin-import": "^1.13.6",
@ -158,10 +163,13 @@
"jest": "^29.4.2",
"node-sass": "^8.0.0",
"path": "^0.12.7",
"postcss": "^8.4.35",
"postcss-loader": "^8.1.0",
"prettier": "^2.8.4",
"sass-loader": "^13.2.0",
"storybook": "^7.2.1",
"style-loader": "^3.3.1",
"tailwindcss": "^3.4.1",
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
@ -192,7 +200,7 @@
"format": "eslint . --fix '**/*.{js,jsx}'",
"test": "jest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "npx storybook build"
},
"eslintConfig": {
"extends": "react-app"

View file

@ -0,0 +1,8 @@
// postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
// Other PostCSS plugins if needed
],
};

View file

@ -39,6 +39,7 @@ import { ManageOrgUsers } from '@/ManageOrgUsers';
import { ManageGroupPermissions } from '@/ManageGroupPermissions';
import OrganizationLogin from '@/_components/OrganizationLogin/OrganizationLogin';
import { ManageOrgVars } from '@/ManageOrgVars';
import { setFaviconAndTitle } from '@white-label/whiteLabelling';
const AppWrapper = (props) => {
const { isAppDarkMode } = useAppDarkMode();
@ -78,6 +79,7 @@ class AppComponent extends React.Component {
componentDidMount() {
authorizeWorkspace();
this.fetchMetadata();
setFaviconAndTitle(null, null, this.props.location);
setInterval(this.fetchMetadata, 1000 * 60 * 60 * 1);
}
// check if its getting routed from editor

View file

@ -9,6 +9,12 @@ import Spinner from '@/_ui/Spinner';
import { withRouter } from '@/_hoc/withRouter';
import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
import { updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import {
retrieveWhiteLabelText,
setFaviconAndTitle,
retrieveWhiteLabelFavicon,
checkWhiteLabelsDefaultState,
} from '@white-label/whiteLabelling';
class OrganizationInvitationPageComponent extends React.Component {
constructor(props) {
super(props);
@ -21,10 +27,18 @@ class OrganizationInvitationPageComponent extends React.Component {
this.organizationId = new URLSearchParams(props?.location?.search).get('oid');
this.organizationToken = new URLSearchParams(props?.location?.search).get('organizationToken');
this.source = new URLSearchParams(props?.location?.search).get('source');
this.whiteLabelText = retrieveWhiteLabelText();
this.whiteLabelFavicon = retrieveWhiteLabelFavicon();
}
componentDidMount() {
authenticationService.deleteLoginOrganizationId();
setFaviconAndTitle(this.whiteLabelText, this.whiteLabelFavicon, this.props?.location);
checkWhiteLabelsDefaultState(this.organizationId).then((res) => {
this.setState({ defaultState: res });
this.whiteLabelText = retrieveWhiteLabelText();
this.whiteLabelFavicon = retrieveWhiteLabelFavicon();
});
document.addEventListener('keydown', this.handleEnterKey);
}
@ -74,14 +88,14 @@ class OrganizationInvitationPageComponent extends React.Component {
<form action="." method="get" autoComplete="off">
<div className="common-auth-container-wrapper">
<h2 className="common-auth-section-header org-invite-header" data-cy="invite-page-header">
Join {organizationName ? organizationName : 'ToolJet'}
Join {organizationName ? organizationName : this.whiteLabelText}
</h2>
<div className="invite-sub-header" data-cy="invite-page-sub-header">
{`You are invited to ${
organizationName
? `a workspace ${organizationName}. Accept the invite to join the workspace.`
: 'ToolJet.'
: this.whiteLabelText
}`}
</div>

View file

@ -78,7 +78,7 @@
max-width: 100% !important;
width: 100% !important;
display: flex !important;
align-items: start !important;
align-items: flex-start !important;
justify-content: space-between !important;
padding: 0.35rem !important;
font-size: 11px !important;
@ -533,9 +533,10 @@
}
}
.disabled-cursor{
.disabled-cursor {
cursor: not-allowed;
}
.disabled-pointerevents{
.disabled-pointerevents {
pointer-events: none;
}

View file

@ -818,6 +818,8 @@ export const Container = ({
? 'Connect to your data source or use our sample data source to start playing around!'
: 'Connect to a data source to be able to create a query';
const showEmptyContainer = !appLoading && !isDragging && mode !== 'view';
return (
<ContainerWrapper
showComments={showComments}
@ -930,7 +932,7 @@ export const Container = ({
/>
</div>
</div>
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
{Object.keys(boxes).length === 0 && showEmptyContainer && (
<div style={{ paddingTop: '10%' }}>
<div className="row empty-box-cont">
<div className="col-md-4 dotted-cont">

View file

@ -46,13 +46,8 @@ import { withTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import Skeleton from 'react-loading-skeleton';
import EditorHeader from './Header';
import {
getWorkspaceId,
isValidUUID,
setWindowTitle,
defaultWhiteLabellingSettings,
pageTitles,
} from '@/_helpers/utils';
import { getWorkspaceId, isValidUUID } from '@/_helpers/utils';
import { fetchAndSetWindowTitle, pageTitles, defaultWhiteLabellingSettings } from '@white-label/whiteLabelling';
import '@/_styles/editor/react-select-search.scss';
import { withRouter } from '@/_hoc/withRouter';
import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError';
@ -621,7 +616,7 @@ const EditorComponent = (props) => {
app.name = newName;
updateState({ appName: newName, app: app });
updateState({ appName: newName });
setWindowTitle({ page: pageTitles.EDITOR, appName: newName });
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName: newName });
};
const onZoomChanged = (zoom) => {
@ -760,7 +755,7 @@ const EditorComponent = (props) => {
} = appData;
const startingPageHandle = props.params.pageHandle;
setWindowTitle({ page: pageTitles.EDITOR, appName });
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName });
useAppVersionStore.getState().actions.updateEditingVersion(editing_version);
current_version_id && useAppVersionStore.getState().actions.updateReleasedVersionId(current_version_id);
await fetchOrgEnvironmentConstants();

View file

@ -14,11 +14,13 @@ import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
import { useAppDataStore } from '@/_stores/appDataStore';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
class ManageAppUsersComponent extends React.Component {
constructor(props) {
super(props);
this.isUserAdmin = authenticationService.currentSessionValue?.admin;
this.whiteLabelText = retrieveWhiteLabelText();
this.state = {
showModal: false,
@ -175,7 +177,7 @@ class ManageAppUsersComponent extends React.Component {
const appLink = `${getHostURL()}/applications/`;
const shareableLink = appLink + (this.props.slug || appId);
const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid';
const embeddableLink = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="Tooljet app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
const embeddableLink = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="${this.whiteLabelText} app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
const shouldWeDisableShareModal = !this.props.isVersionReleased;
return (

View file

@ -73,7 +73,7 @@ export default function EditorHeader({
: '';
setAppPreviewLink(appVersionPreviewLink);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slug, currentVersionId, editingVersion]);
}, [slug, currentVersionId, editingVersion, pageHandle]);
return (
<div className={cx('header', { 'dark-theme theme-dark': darkMode })} style={{ width: '100%' }}>

View file

@ -25,7 +25,7 @@ import {
import queryString from 'query-string';
import ViewerLogoIcon from './Icons/viewer-logo.svg';
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
import { resolveReferences, isQueryRunnable, setWindowTitle, pageTitles, isValidUUID } from '@/_helpers/utils';
import { resolveReferences, isQueryRunnable, isValidUUID } from '@/_helpers/utils';
import { withTranslation } from 'react-i18next';
import _ from 'lodash';
import { Navigate } from 'react-router-dom';
@ -50,6 +50,7 @@ import { findAllEntityReferences } from '@/_stores/utils';
import { dfs } from '@/_stores/handleReferenceTransactions';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
import TooljetBanner from './Viewer/TooljetBanner';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
class ViewerComponent extends React.Component {
constructor(props) {
@ -491,7 +492,7 @@ class ViewerComponent extends React.Component {
useCurrentStateStore.getState().actions.initializeCurrentStateOnVersionSwitch();
this.setStateForApp(data, true);
this.setStateForContainer(data);
setWindowTitle({
fetchAndSetWindowTitle({
page: pageTitles.VIEWER,
appName: data.name,
preview,
@ -516,7 +517,7 @@ class ViewerComponent extends React.Component {
await appService
.fetchAppByVersion(appId, versionId)
.then((data) => {
setWindowTitle({
fetchAndSetWindowTitle({
page: pageTitles.VIEWER,
appName: data.name,
preview: true,

View file

@ -9,6 +9,7 @@ import { ButtonSolid } from '@/_components/AppButton';
import { withTranslation } from 'react-i18next';
import EnterIcon from '../../assets/images/onboardingassets/Icons/Enter';
import Spinner from '@/_ui/Spinner';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
class ForgotPasswordComponent extends React.Component {
constructor(props) {
super(props);
@ -21,6 +22,7 @@ class ForgotPasswordComponent extends React.Component {
};
}
darkMode = localStorage.getItem('darkMode') === 'true';
whiteLabelText = retrieveWhiteLabelText();
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value, emailError: '' });
@ -68,9 +70,10 @@ class ForgotPasswordComponent extends React.Component {
Forgot Password
</h2>
<p className="common-auth-sub-header" data-cy="forgot-password-sub-header">
New to ToolJet? &nbsp;
New to {this.whiteLabelText}? &nbsp;
<Link
to={'/signup'}
state={{ from: '/forgot-password' }}
tabIndex="-1"
style={{ color: this.darkMode && '#3E63DD' }}
data-cy="create-an-account-link"

View file

@ -20,7 +20,7 @@ import { SearchBox } from '@/_components';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { BreadCrumbContext } from '@/App';
import { pageTitles, setWindowTitle } from '@/_helpers/utils';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasource }) => {
const containerRef = useRef(null);
@ -69,7 +69,7 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
}, []);
useEffect(() => {
setWindowTitle({ page: `${selectedDataSource?.name || pageTitles.DATA_SOURCES}` });
fetchAndSetWindowTitle({ page: `${selectedDataSource?.name || pageTitles.DATA_SOURCES}` });
if (selectedDataSource) {
setModalProps({ ...modalProps, backdrop: false });
}
@ -328,6 +328,7 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
{datasources.map((item) => (
<Card
key={item.key}
darkMode={darkMode}
title={item.title}
src={item?.src}
usePluginIcon={isEmpty(item?.iconFile?.data)}

View file

@ -100,7 +100,7 @@ export const List = ({ updateSelectedDatasource }) => {
<EmptyFoldersIllustration />
</div>
<div className="tj-text-md text-secondary" data-cy="empty-ds-page-text">
No datasources added
{filteredData?.length === 0 && dataSources?.length !== 0 ? 'No results found' : 'No datasources added'}
</div>
</div>
);

View file

@ -6,6 +6,7 @@ import { GlobalDataSourcesPage } from './GlobalDataSourcesPage';
import { toast } from 'react-hot-toast';
import { BreadCrumbContext } from '@/App/App';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
export const GlobalDataSourcesContext = createContext({
showDataSourceManagerModal: false,
@ -39,6 +40,7 @@ export const GlobalDatasources = (props) => {
selectedDataSource
? updateSidebarNAV(selectedDataSource.name)
: !activeDatasourceList && updateSidebarNAV('Commonly used');
fetchAndSetWindowTitle({ page: `${selectedDataSource?.name || pageTitles.DATA_SOURCES}` });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(dataSources), JSON.stringify(selectedDataSource)]);

View file

@ -4,6 +4,7 @@ import EmptyIllustration from '@assets/images/no-apps.svg';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useNavigate } from 'react-router-dom';
import EmptyFoldersIllustration from '@assets/images/icons/no-queries-added.svg';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
export const BlankPage = function BlankPage({
readAndImport,
@ -17,6 +18,7 @@ export const BlankPage = function BlankPage({
canCreateApp,
}) {
const { t } = useTranslation();
const whiteLabelText = retrieveWhiteLabelText();
const [deploying, setDeploying] = useState(false);
const navigate = useNavigate();
@ -56,12 +58,17 @@ export const BlankPage = function BlankPage({
<div className="row homepage-empty-container">
<div className="col-7">
<h3 className="empty-welcome-header" data-cy="empty-homepage-welcome-header">
{t('blankPage.welcomeToToolJet', 'Welcome to your new ToolJet workspace')}
{t('blankPage.welcomeToToolJet', `Welcome to your new ${whiteLabelText} workspace`, {
whiteLabelText,
})}
</h3>
<p className={`empty-title`} data-cy="empty-homepage-description">
{t(
'blankPage.getStartedCreateNewApp',
'You can get started by creating a new application or by creating an application using a template in ToolJet Library.'
`You can get started by creating a new application or by creating an application using a template in ${whiteLabelText} Library.`,
{
whiteLabelText,
}
)}
</p>
<div className="row mt-4">

View file

@ -12,6 +12,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const [versionId, setVersionId] = useState(undefined);
const [exportTjDb, setExportTjDb] = useState(true);
const [currentVersion, setCurrentVersion] = useState(undefined);
const [versionSelectLoading, setVersionSelectLoading] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
@ -19,8 +20,11 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
setLoading(true);
try {
const fetchVersions = await appsService.getVersions(app.id);
const fetchTables = await appsService.getTables(app.id); // this is used to get all tables
const { versions } = fetchVersions;
const { tables } = fetchTables;
setVersions(versions);
setAllTables(tables);
const currentEditingVersion = versions?.filter((version) => version?.isCurrentEditingVersion)[0];
if (currentEditingVersion) {
setCurrentVersion(currentEditingVersion);
@ -39,11 +43,9 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
useEffect(() => {
async function fetchAppTables() {
setLoading(true);
setVersionSelectLoading(true);
try {
if (!versionId) return;
const fetchTables = await appsService.getTables(app.id); // this is used to get all tables
const { tables } = fetchTables;
const tbl = await appsService.getAppByVersion(app.id, versionId); // this is used to get particular App by version
const { dataQueries } = tbl;
const extractedIdData = [];
@ -68,14 +70,13 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const uniqueSet = new Set(extractedIdData);
const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item }));
setTables(selectedVersiontable);
setAllTables(tables);
} catch (error) {
toast.error('Could not fetch the tables.', {
position: 'top-center',
});
closeModal();
}
setLoading(false);
setVersionSelectLoading(false);
}
fetchAppTables();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -209,7 +210,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
Export All
</ButtonSolid>
<ButtonSolid
className="import-export-footer-btns"
className={`import-export-footer-btns ${versionSelectLoading ? 'btn-loading' : ''}`}
data-cy="export-selected-version-button"
onClick={() => exportApp(app, versionId, exportTjDb, tables)}
>

View file

@ -353,7 +353,7 @@ export const Folders = function Folders({
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<div className="col d-flex modal-footer-btn justify-content-end">
<ButtonSolid variant="tertiary" onClick={closeModal} data-cy="cancel-button">
{t('globals.cancel', 'Cancel')}
</ButtonSolid>

View file

@ -20,12 +20,13 @@ import Footer from './Footer';
import { OrganizationList } from '@/_components/OrganizationManager/List';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import BulkIcon from '@/_ui/Icon/bulkIcons/index';
import { getWorkspaceId, pageTitles, setWindowTitle } from '@/_helpers/utils';
import { getWorkspaceId } from '@/_helpers/utils';
import { getQueryParams } from '@/_helpers/routes';
import { withRouter } from '@/_hoc/withRouter';
import FolderFilter from './FolderFilter';
import { APP_ERROR_TYPE } from '@/_helpers/error_constants';
import Skeleton from 'react-loading-skeleton';
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
const { iconList, defaultIcon } = configs;
@ -87,7 +88,7 @@ class HomePageComponent extends React.Component {
};
componentDidMount() {
setWindowTitle({ page: pageTitles.DASHBOARD });
fetchAndSetWindowTitle({ page: pageTitles.DASHBOARD });
this.fetchApps(1, this.state.currentFolder.id);
this.fetchFolders();
this.setQueryParameter();
@ -238,7 +239,13 @@ class HomePageComponent extends React.Component {
};
event.target.value = null;
} catch (error) {
toast.error(error.message);
let errorMessage = 'Some Error Occured';
if (error?.error) {
errorMessage = error.error;
} else if (error?.message) {
errorMessage = error.message;
}
toast.error(errorMessage);
}
};
@ -271,7 +278,7 @@ class HomePageComponent extends React.Component {
if (error.statusCode === 409) {
return false;
}
toast.error(error?.error || 'App import failed');
toast.error(error?.error || error?.message || 'App import failed');
}
};
@ -720,7 +727,7 @@ class HomePageComponent extends React.Component {
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<div className="col d-flex modal-footer-btn justify-content-end">
<ButtonSolid
variant="tertiary"
onClick={() => this.setState({ showAddToFolderModal: false, appOperations: {} })}
@ -750,7 +757,7 @@ class HomePageComponent extends React.Component {
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<div className="col d-flex modal-footer-btn justify-content-end">
<ButtonSolid
onClick={() => this.setState({ showChangeIconModal: false, appOperations: {} })}
data-cy="cancel-button"

View file

@ -17,6 +17,7 @@ import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
import { updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import cx from 'classnames';
import SSOLoginModule from './SSOLoginModule';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
class LoginPageComponent extends React.Component {
constructor(props) {
@ -31,6 +32,7 @@ class LoginPageComponent extends React.Component {
this.paramOrganizationSlug = props?.params?.organizationId;
}
darkMode = localStorage.getItem('darkMode') === 'true';
whiteLabelText = retrieveWhiteLabelText();
componentDidMount() {
/* remove login oranization's id and slug from the cookie */
@ -130,7 +132,9 @@ class LoginPageComponent extends React.Component {
const signUpCTA = workspaceSignUpEnabled ? 'Sign up' : 'Create an account';
const signupText = workspaceSignUpEnabled
? this.props.t('loginSignupPage.newToWorkspace', `New to this workspace?`)
: this.props.t('loginSignupPage.newToTooljet', `New to Tooljet?`);
: this.props.t('loginSignupPage.newToTooljet', ` New to ${this.whiteLabelText}?`, {
whiteLabelText: this.whiteLabelText,
});
const signUpUrl = `/signup${this.paramOrganizationSlug ? `/${this.paramOrganizationSlug}` : ''}${
redirectTo ? `?redirectTo=${redirectTo}` : ''
}`;

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import useRouter from '@/_hooks/use-router';
import { authenticationService } from '@/_services';
import { appService, authenticationService } from '@/_services';
import { Navigate } from 'react-router-dom';
import Configs from './Configs/Config.json';
import { RedirectLoader } from '../_components';
@ -85,7 +85,22 @@ export function Authorize({ navigate }) {
const inviteeEmail = details?.inviteeEmail;
if (inviteeEmail) setInviteeEmail(inviteeEmail);
const errMessage = details?.message || err?.error || 'something went wrong';
setError(`${configs.name} login failed - ${errMessage}`);
if (!inviteeEmail && inviteFlowIdentifier) {
/* Some unexpected error happened from the provider side. Need to retreive email to continue */
appService
.getInviteeDetails(inviteFlowIdentifier)
.then((response) => {
setInviteeEmail(response.email);
})
.catch(() => {
console.error('Error while fetching invitee details');
})
.finally(() => {
setError(`${configs.name} login failed - ${errMessage}`);
});
} else {
setError(`${configs.name} login failed - ${errMessage}`);
}
});
};

View file

@ -6,6 +6,7 @@ import OnBoardingRadioInput from './OnBoardingRadioInput';
import ContinueButton from './ContinueButton';
import OnBoardingBubbles from './OnBoardingBubbles';
import { getuserName } from '@/_helpers/utils';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
import { redirectToDashboard } from '@/_helpers/routes';
import { ON_BOARDING_SIZE, ON_BOARDING_ROLES } from '@/_helpers/constants';
import LogoLightMode from '@assets/images/Logomark.svg';
@ -26,6 +27,7 @@ function OnBoardingForm({ userDetails = {}, token = '', organizationToken = '',
phoneNumber: '',
});
const source = new URLSearchParams(location?.search).get('source');
const whiteLabelText = retrieveWhiteLabelText();
const pageProps = {
formData,
@ -75,7 +77,7 @@ function OnBoardingForm({ userDetails = {}, token = '', organizationToken = '',
'Enter your phone number',
'Enter your phone number', //dummy for styling
];
const FormSubTitles = ['This information will help us improve ToolJet.'];
const FormSubTitles = [`This information will help us improve ${whiteLabelText}.`];
return (
<div className="flex">

View file

@ -4,6 +4,12 @@ import { useSessionManagement } from '@/_hooks/useSessionManagement';
import { getRedirectURL, pathnameToArray } from '@/_helpers/routes';
import { authenticationService } from '@/_services';
import { useLocation, useParams } from 'react-router-dom';
import {
resetToDefaultWhiteLabels,
retrieveWhiteLabelFavicon,
retrieveWhiteLabelText,
setFaviconAndTitle,
} from '@white-label/whiteLabelling';
export const AuthRoute = ({ children, navigate }) => {
const { isLoading, session, isValidSession, isInvalidSession, setLoading } = useSessionManagement({
@ -29,8 +35,7 @@ export const AuthRoute = ({ children, navigate }) => {
useEffect(
() => {
authenticationService.deleteAllAuthCookies();
fetchOrganizationDetails();
initialize();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[location.pathname]
@ -43,11 +48,30 @@ export const AuthRoute = ({ children, navigate }) => {
);
useEffect(() => {
const isComingFromPasswordReset = location?.state?.from === '/reset-password';
const isComingFromPasswordReset =
location?.state?.from === '/reset-password' || location?.state?.from === '/forgot-password';
if ((isInvalidSession || isComingFromPasswordReset) && !isGettingConfigs) setLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInvalidSession, isGettingConfigs]);
const initialize = () => {
const pathname = location.pathname;
authenticationService.deleteAllAuthCookies();
fetchOrganizationDetails();
verifyWhiteLabeling(pathname);
};
const verifyWhiteLabeling = (pathname) => {
const signupRegex = /^\/signup\/[^/]+$/;
const loginRegex = /^\/login\/[^/]+$/;
if (!signupRegex.test(pathname) && !loginRegex.test(pathname)) {
resetToDefaultWhiteLabels();
}
const whiteLabelText = retrieveWhiteLabelText();
const whiteLabelFavicon = retrieveWhiteLabelFavicon();
setFaviconAndTitle(whiteLabelFavicon, whiteLabelText, location);
};
const fetchOrganizationDetails = () => {
authenticationService.getOrganizationConfigs(organizationSlug).then(
(configs) => {

View file

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import { RouteLoader } from './RouteLoader';
import { useLocation, useParams } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { appService, authenticationService } from '@/_services';
import { authorizeUserAndHandleErrors, updateCurrentSession } from '@/_helpers/authorizeWorkspace';
import { toast } from 'react-hot-toast';
import { LinkExpiredPage } from '@/ConfirmationPage/LinkExpiredPage';
import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite, navigate }) => {
/* Needed to pass invite token to signup page if the user doesn't exist */
@ -15,6 +16,8 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
const organizationToken = params.organizationToken || (isOrgazanizationOnlyInvite ? params.token : null);
const accountToken = !isOrgazanizationOnlyInvite ? params.token : null;
const [extraProps, setExtraProps] = useState({});
const searchParams = new URLSearchParams(location?.search);
const redirectTo = searchParams.get('redirectTo');
useEffect(() => {
getInvitedUserSession();
@ -33,6 +36,8 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
name,
organization_invite_url,
is_workspace_sign_up_invite,
source,
organization_user_source,
} = invitedUserSession;
/*
We should only run the authorization against the session if the user has active workspace
@ -42,6 +47,10 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
email,
name,
});
if (source === 'workspace_signup' || organization_user_source === 'signup') {
acceptInvite(accountToken, organizationToken, navigate, source, redirectTo);
return;
}
if (is_workspace_sign_up_invite) {
setLoading(false);
return;
@ -141,6 +150,41 @@ export const OrganizationInviteRoute = ({ children, isOrgazanizationOnlyInvite,
);
};
const acceptInvite = (token, organizationToken, navigate, source, redirectTo) => {
if (token && organizationToken) {
authenticationService
.onboarding({
token,
organizationToken,
source,
})
.then((user) => {
onLoginSuccess(user, navigate, redirectTo);
})
.catch((res) => {
toast.error(res.error || 'Something went wrong', {
id: 'toast-login-auth-error',
position: 'top-center',
});
});
} else {
appService
.acceptInvite({
token: organizationToken,
})
.then((data) => {
toast.success(`Added to the workspace successfully.`);
updateCurrentSession({
isUserLoggingIn: true,
});
onLoginSuccess(data, navigate);
})
.catch(() => {
toast.error('Error while setting up your account.', { position: 'top-center' });
});
}
};
if (invalidLink) return <LinkExpiredPage />;
const clonedElement = React.cloneElement(children || <></>, extraProps);

View file

@ -17,6 +17,7 @@ import { extractErrorObj, onInvitedUserSignUpSuccess } from '@/_helpers/platform
import { isEmpty } from 'lodash';
import { EmailComponent } from './EmailComponent';
import SSOLoginModule from '@/LoginPage/SSOLoginModule';
import { checkWhiteLabelsDefaultState } from '@white-label/whiteLabelling';
class SignupPageComponent extends React.Component {
constructor(props) {
super(props);
@ -34,6 +35,7 @@ class SignupPageComponent extends React.Component {
emailError: '',
disableOnEdit: false,
email: this.inviteeEmail || '',
defaultState: false,
};
}
@ -42,6 +44,9 @@ class SignupPageComponent extends React.Component {
if (errorMessage) {
toast.error(errorMessage);
}
checkWhiteLabelsDefaultState(this.inviteOrganizationId).then((res) => {
this.setState({ defaultState: res });
});
}
backtoSignup = (email, name) => {
@ -122,7 +127,7 @@ class SignupPageComponent extends React.Component {
render() {
const { configs } = this.props;
const { isLoading, signupSuccess } = this.state;
const { isLoading, signupSuccess, defaultState } = this.state;
const comingFromInviteFlow = !!this.organizationToken;
const isSignUpButtonDisabled =
isLoading ||
@ -320,20 +325,22 @@ class SignupPageComponent extends React.Component {
</>
)}
<p className="signup-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
{defaultState && (
<p className="signup-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
)}
</div>
</>
)
@ -347,6 +354,8 @@ class SignupPageComponent extends React.Component {
name={this.state.name}
backtoSignup={this.backtoSignup}
darkMode={this.darkMode}
organizationId={this.inviteOrganizationId}
redirectTo={this.redirectTo}
/>
</div>
)}

View file

@ -1,8 +1,10 @@
import React from 'react';
import { ButtonSolid } from '@/_components/AppButton';
import { useNavigate } from 'react-router-dom';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
export const PasswordResetinfoScreen = function PasswordResetinfoScreen({ darkMode }) {
const whiteLabelText = retrieveWhiteLabelText();
const navigate = useNavigate();
return (
<div className="info-screen-wrapper">
@ -22,7 +24,7 @@ export const PasswordResetinfoScreen = function PasswordResetinfoScreen({ darkMo
Password has been reset
</h1>
<p className="info-screen-description" data-cy="reset-password-page-description">
Your password has been reset successfully, log into ToolJet to continue your session
Your password has been reset successfully, log into {whiteLabelText} to continue your session
</p>
<ButtonSolid
variant="secondary"

View file

@ -4,7 +4,14 @@ import { authenticationService } from '@/_services';
import { toast } from 'react-hot-toast';
import Spinner from '@/_ui/Spinner';
export const SignupInfoScreen = function SignupInfoScreen({ email, backtoSignup, name, darkMode }) {
export const SignupInfoScreen = function SignupInfoScreen({
email,
backtoSignup,
name,
darkMode,
organizationId,
redirectTo,
}) {
const [resendBtn, setResetBtn] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [buttonText, setButtonText] = useState('Resend verification mail in 30s');
@ -30,7 +37,7 @@ export const SignupInfoScreen = function SignupInfoScreen({ email, backtoSignup,
e.preventDefault();
authenticationService
.resendInvite(email)
.resendInvite(email, organizationId, redirectTo)
.then(() => {
setIsLoading(false);
setResetBtn(true);

View file

@ -1,10 +1,8 @@
import React, { useState, useEffect } from 'react';
import EnterIcon from '../../assets/images/onboardingassets/Icons/Enter';
import GoogleSSOLoginButton from '@ee/components/LoginPage/GoogleSSOLoginButton';
import GitSSOLoginButton from '@ee/components/LoginPage/GitSSOLoginButton';
import OnBoardingForm from '../OnBoardingForm/OnBoardingForm';
import { authenticationService } from '@/_services';
import { useLocation, useParams } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { LinkExpiredInfoScreen } from '@/SuccessInfoScreen';
import { ShowLoading } from '@/_components';
import { toast } from 'react-hot-toast';
@ -15,7 +13,9 @@ import EyeShow from '../../assets/images/onboardingassets/Icons/EyeShow';
import Spinner from '@/_ui/Spinner';
import { useTranslation } from 'react-i18next';
import { buildURLWithQuery } from '@/_helpers/utils';
import { onLoginSuccess } from '@/_helpers/platform/utils/auth.utils';
import { redirectToDashboard } from '@/_helpers/routes';
import { retrieveWhiteLabelText, setFaviconAndTitle, checkWhiteLabelsDefaultState } from '@white-label/whiteLabelling';
export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScreen() {
const [showOnboarding, setShowOnboarding] = useState(false);
@ -29,6 +29,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
const [showPassword, setShowPassword] = useState(false);
const [fallBack, setFallBack] = useState(false);
const { t } = useTranslation();
const [defaultState, setDefaultState] = useState(false);
const location = useLocation();
const params = useParams();
@ -39,6 +40,8 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
const source = searchParams.get('source');
const darkMode = localStorage.getItem('darkMode') === 'true';
const redirectTo = searchParams.get('redirectTo');
const navigate = useNavigate();
const whiteLabelText = retrieveWhiteLabelText();
const getUserDetails = () => {
setIsLoading(true);
@ -81,6 +84,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
(configs) => {
setIsGettingConfigs(false);
setConfigs(configs);
setFaviconAndTitle(null, null, location);
},
() => {
setIsGettingConfigs(false);
@ -89,6 +93,9 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
} else {
setIsGettingConfigs(false);
}
checkWhiteLabelsDefaultState(organizationId).then((res) => {
setDefaultState(res);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -121,7 +128,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
.then((user) => {
authenticationService.deleteLoginOrganizationId();
setIsLoading(false);
redirectToDashboard(user, redirectTo);
onLoginSuccess(user, navigate, redirectTo);
})
.catch((res) => {
setIsLoading(false);
@ -180,14 +187,14 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
) : (
<div className="common-auth-container-wrapper">
<h2 className="common-auth-section-header org-invite-header" data-cy="invite-page-header">
Join {configs?.name ? configs?.name : 'ToolJet'}
Join {configs?.name ? configs?.name : whiteLabelText}
</h2>
<div className="invite-sub-header" data-cy="invite-page-sub-header">
{`You are invited to ${
configs?.name
? `a workspace ${configs?.name}. Accept the invite to join the workspace.`
: 'ToolJet.'
: `${whiteLabelText}.`
}`}
</div>
@ -288,20 +295,22 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
)}
</ButtonSolid>
</div>
<p className="verification-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
{defaultState && (
<p className="verification-terms" data-cy="signup-terms-helper">
By signing up you are agreeing to the
<br />
<span>
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
Terms of Service{' '}
</a>
&
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
{' '}
Privacy Policy
</a>
</span>
</p>
)}
</div>
)}
</form>
@ -331,7 +340,7 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
{t('verificationSuccessPage.successfullyVerifiedEmail', 'Successfully verified email')}
</h1>
<p className="info-screen-description" data-cy="onboarding-page-description">
Continue to set up your workspace to start using ToolJet.
Continue to set up your workspace to start using {whiteLabelText}.
</p>
<ButtonSolid
className="verification-success-info-btn "
@ -347,7 +356,10 @@ export const VerificationSuccessInfoScreen = function VerificationSuccessInfoScr
</div>
) : (
<>
{t('verificationSuccessPage.setupTooljet', 'Set up ToolJet')}
{t('verificationSuccessPage.setupTooljet', `Set up ${whiteLabelText}`, {
whiteLabelText,
})}
<EnterIcon fill={'#fff'}></EnterIcon>
</>
)}

View file

@ -1,47 +0,0 @@
import React from 'react';
import List from './List';
import ListGroup from 'react-bootstrap/ListGroup';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'List',
component: List,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
// tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
};
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const WithIcon = {
render: (args) => (
<List>
<List.Item>Client side</List.Item>
<List.Item active>Server side</List.Item>
<List.Item>Client side</List.Item>
<List.Item disabled>Server side</List.Item>
<List.Item>Client side</List.Item>
<List.Item>Server side</List.Item>
<List.Item>Client side</List.Item>
<List.Item>Server side</List.Item>
</List>
),
};
export const WithIconAndEdit = {
render: (args) => (
<ListGroup>
<ListGroup.Item>Cras justo odio</ListGroup.Item>
<ListGroup.Item>Dapibus ac facilisis in</ListGroup.Item>
<ListGroup.Item>Morbi leo risus</ListGroup.Item>
<ListGroup.Item>Porta ac consectetur ac</ListGroup.Item>
<ListGroup.Item>Vestibulum at eros</ListGroup.Item>
</ListGroup>
),
};

View file

@ -5,7 +5,7 @@ import ToggleGroupItem from './ToggleGroupItem';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'ToggleGroup',
title: 'Components/ToggleGroup',
component: ToggleGroup,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout

View file

@ -1,45 +0,0 @@
import { Tabs } from './Tabs';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: 'Tabs',
component: Tabs,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
};
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};

View file

@ -85,7 +85,7 @@
background-color: #FFF8F7;
padding: 8px;
display: flex;
align-items: start;
align-items: flex-start;
justify-content: center;
border-radius: 6px;
border: 1px solid #FDD8D3;
@ -366,7 +366,7 @@
.table-schema-row {
display: flex;
align-items: center;
justify-content: start;
justify-content: flex-start;
gap: 6px;
input.form-control {
@ -494,7 +494,7 @@
.key-changes-container {
display: flex;
align-items: start;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
font-weight: 500;
@ -554,15 +554,18 @@
background: none !important;
color: var(--slate9) !important;
cursor: auto !important;
svg{
path:first-child{
svg {
path:first-child {
fill: var(--slate7) !important;
}
path:nth-child(2){
path:nth-child(2) {
fill: var(--base) !important;
}
}
}
&:disabled:hover {
background: var(--slate3) !important;
}

View file

@ -4,8 +4,8 @@ import TooljetDatabasePage from './TooljetDatabasePage';
import { usePostgrestQueryBuilder } from './usePostgrestQueryBuilder';
import { authenticationService } from '../_services/authentication.service';
import { BreadCrumbContext } from '@/App/App';
import { pageTitles, setWindowTitle } from '@/_helpers/utils';
import { useNavigate } from 'react-router-dom';
import { pageTitles, fetchAndSetWindowTitle } from '@white-label/whiteLabelling';
export const TooljetDatabaseContext = createContext({
organizationId: null,
@ -156,7 +156,7 @@ export const TooljetDatabase = (props) => {
}, []);
useEffect(() => {
setWindowTitle({ page: `${selectedTable?.table_name || pageTitles.DATABASE}` });
fetchAndSetWindowTitle({ page: `${selectedTable?.table_name || pageTitles.DATABASE}` });
}, [selectedTable]);
return (

View file

@ -1,8 +1,9 @@
import React from 'react';
import Logo from '@assets/images/rocket.svg';
import { retrieveWhiteLabelLogo } from '@white-label/whiteLabelling';
export default function AppLogo({ isLoadingFromHeader, className }) {
const url = window.public_config?.WHITE_LABEL_LOGO;
const url = retrieveWhiteLabelLogo();
return (
<>

View file

@ -90,7 +90,13 @@ export function AppModal({
closeModal();
}
} catch (error) {
toast.error(e.error, {
let errorMessage = 'Some Error Occured';
if (error?.error) {
errorMessage = error.error;
} else if (error?.message) {
errorMessage = error.message;
}
toast.error(errorMessage, {
position: 'top-center',
});
}

View file

@ -2,12 +2,14 @@ import React, { useState } from 'react';
import { datasourceService } from '@/_services';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
import Radio from '@/_ui/Radio';
import Button from '@/_ui/Button';
const Googlesheets = ({ optionchanged, createDataSource, options, isSaving, selectedDataSource }) => {
const [authStatus, setAuthStatus] = useState(null);
const whiteLabelText = retrieveWhiteLabelText();
const { t } = useTranslation();
function authGoogle() {
@ -53,7 +55,8 @@ const Googlesheets = ({ optionchanged, createDataSource, options, isSaving, sele
<p data-cy="google-sheet-connection-form-description">
{t(
'googleSheets.enableReadAndWrite',
'If you want your ToolJet apps to modify your Google sheets, make sure to select read and write access'
'If you want your ${whiteLabelText} apps to modify your Google sheets, make sure to select read and write access',
{ whiteLabelText }
)}
</p>
<div>
@ -64,7 +67,8 @@ const Googlesheets = ({ optionchanged, createDataSource, options, isSaving, sele
text={t('googleSheets.readOnly', 'Read only')}
helpText={t(
'googleSheets.readDataFromSheets',
'Your ToolJet apps can only read data from Google sheets'
'Your ${whiteLabelText} apps can only read data from Google sheets',
{ whiteLabelText }
)}
/>
<Radio
@ -74,7 +78,8 @@ const Googlesheets = ({ optionchanged, createDataSource, options, isSaving, sele
text={t('googleSheets.readWrite', 'Read and write')}
helpText={t(
'googleSheets.readModifySheets',
'Your ToolJet apps can read data from sheets, modify sheets, and more.'
'Your ${whiteLabelText} apps can read data from sheets, modify sheets, and more.',
{ whiteLabelText }
)}
/>
</div>

View file

@ -3,9 +3,11 @@ import { datasourceService } from '@/_services';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-hot-toast';
import Button from '@/_ui/Button';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
const Slack = ({ optionchanged, createDataSource, options, isSaving, _selectedDataSource }) => {
const [authStatus, setAuthStatus] = useState(null);
const whiteLabelText = retrieveWhiteLabelText();
const { t } = useTranslation();
function authGoogle() {
@ -51,7 +53,8 @@ const Slack = ({ optionchanged, createDataSource, options, isSaving, _selectedDa
<p>
{t(
'slack.connectToolJetToSlack',
'ToolJet can connect to Slack and list users, send messages, etc. Please select appropriate permission scopes.'
'${whiteLabelText} can connect to Slack and list users, send messages, etc. Please select appropriate permission scopes.',
{ whiteLabelText }
)}
</p>
<div>
@ -68,7 +71,8 @@ const Slack = ({ optionchanged, createDataSource, options, isSaving, _selectedDa
<small className="text-muted">
{t(
'slack.listUsersAndSendMessage',
'Your ToolJet app will be able to list users and send messages to users & channels.'
'Your ${whiteLabelText} app will be able to list users and send messages to users & channels.',
{ whiteLabelText }
)}
</small>
</span>

View file

@ -4,6 +4,7 @@ import Input from '@/_ui/Input';
import Radio from '@/_ui/Radio';
import Button from '@/_ui/Button';
import EncryptedFieldWrapper from './EncyrptedFieldWrapper';
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
const Zendesk = ({
optionchanged,
@ -15,6 +16,7 @@ const Zendesk = ({
optionsChanged,
}) => {
const [authStatus, setAuthStatus] = useState(null);
const whiteLabelText = retrieveWhiteLabelText();
function authZendesk() {
const provider = 'zendesk';
@ -91,7 +93,7 @@ const Zendesk = ({
<div className="mb-3">
<div className="form-label">Scope(s)</div>
<p>
If you want your ToolJet apps to modify your Zendesk resources, make sure to select read and write access
{`If you want your ${whiteLabelText} apps to modify your Zendesk resources, make sure to select read and write access`}
</p>
<div>
<Radio
@ -99,14 +101,14 @@ const Zendesk = ({
disabled={authStatus === 'waiting_for_token'}
onClick={() => optionchanged('access_type', 'read')}
text="Read only"
helpText="Your ToolJet apps can only read data from resources"
helpText={`Your ${whiteLabelText} apps can only read data from resources`}
/>
<Radio
checked={options?.access_type?.value === 'write'}
disabled={authStatus === 'waiting_for_token'}
onClick={() => optionchanged('access_type', 'write')}
text="Read and write"
helpText="Your ToolJet apps can read data from resources, modify resources, and more."
helpText={`Your ${whiteLabelText} apps can read data from resources, modify resources, and more.`}
/>
</div>
</div>

View file

@ -1453,6 +1453,18 @@ export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, s
if (key === 'runjs') return <RunjsIcon style={{ height, width }} />;
if (key === 'tooljetdb') return <RunTooljetDbIcon style={{ height, width }} />;
if (key === 'runpy') return <RunPyIcon style={{ height, width }} />;
if (typeof localStorage !== 'undefined') {
const darkMode = localStorage.getItem('darkMode') === 'true';
//Add darkMode icons in allSvgs if needed ending with Dark
if (darkMode) {
const darkSrc = `${key}Dark`;
if (allSvgs[darkSrc]) {
key = darkSrc;
}
}
}
const Icon = allSvgs[key];
if (!Icon) return <></>;

View file

@ -26,6 +26,7 @@ export const onLoginSuccess = (userResponse, navigate, redirectTo = null) => {
...restResponse,
authentication_status: null,
noWorkspaceAttachedInTheSession,
isOrgSwitchingFailed: null,
});
const redirectPath = redirectTo || getCookie('redirectPath');
const path = getRedirectURL(redirectPath);

View file

@ -1235,71 +1235,6 @@ export const humanizeifDefaultGroupName = (groupName) => {
}
};
export const defaultWhiteLabellingSettings = {
WHITE_LABEL_LOGO: 'https://app.tooljet.com/logo.svg',
WHITE_LABEL_TEXT: 'ToolJet',
WHITE_LABEL_FAVICON: 'https://app.tooljet.com/favico.png',
};
export const pageTitles = {
INSTANCE_SETTINGS: 'Settings',
WORKSPACE_SETTINGS: 'Workspace settings',
INTEGRATIONS: 'Marketplace',
WORKFLOWS: 'Workflows',
DATABASE: 'Database',
DATA_SOURCES: 'Data sources',
AUDIT_LOGS: 'Audit logs',
ACCOUNT_SETTINGS: 'Profile settings',
SETTINGS: 'Profile settings',
EDITOR: 'Editor',
WORKFLOW_EDITOR: 'workflowEditor',
VIEWER: 'Viewer',
DASHBOARD: 'Dashboard',
WORKSPACE_CONSTANTS: 'Workspace constants',
};
export const setWindowTitle = async (pageDetails, location) => {
const isEditorOrViewerGoingToRender = ['/apps/', '/applications/'].some((path) => location?.pathname.includes(path));
const pathToTitle = {
'instance-settings': pageTitles.INSTANCE_SETTINGS,
'workspace-settings': pageTitles.WORKSPACE_SETTINGS,
integrations: pageTitles.INTEGRATIONS,
workflows: pageTitles.WORKFLOWS,
database: pageTitles.DATABASE,
'data-sources': pageTitles.DATA_SOURCES,
'audit-logs': pageTitles.AUDIT_LOGS,
'account-settings': pageTitles.ACCOUNT_SETTINGS,
settings: pageTitles.SETTINGS,
'workspace-constants': pageTitles.WORKSPACE_CONSTANTS,
};
const whiteLabelText = defaultWhiteLabellingSettings.WHITE_LABEL_TEXT;
let pageTitleKey = pageDetails?.page || '';
let pageTitle = '';
if (!pageTitleKey && !isEditorOrViewerGoingToRender) {
pageTitleKey = Object.keys(pathToTitle).find((path) => location?.pathname?.includes(path)) || '';
}
switch (pageTitleKey) {
case pageTitles.VIEWER: {
const titlePrefix = pageDetails?.preview ? 'Preview - ' : '';
pageTitle = `${titlePrefix}${pageDetails?.appName || 'My App'}`;
break;
}
case pageTitles.EDITOR:
case pageTitles.WORKFLOW_EDITOR: {
pageTitle = pageDetails?.appName || 'My App';
break;
}
default: {
pageTitle = pathToTitle[pageTitleKey] || pageTitleKey;
break;
}
}
if (pageTitle) {
document.title = !(pageDetails?.preview === false)
? `${decodeEntities(pageTitle)} | ${whiteLabelText}`
: `${pageTitle}`;
}
};
// This function is written only to handle diff colors W.R.T button types
export const computeColor = (styleDefinition, value, meta) => {
if (styleDefinition.type?.value == 'primary') return value;

View file

@ -1,15 +1,17 @@
import React, { useEffect } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { setWindowTitle } from '@/_helpers/utils';
import { retrieveWhiteLabelFavicon, retrieveWhiteLabelText, setFaviconAndTitle } from '@white-label/whiteLabelling';
export const withRouter = (WrappedComponent) => (props) => {
const params = useParams();
const location = useLocation();
const navigate = useNavigate();
const whiteLabelFavicon = retrieveWhiteLabelFavicon();
const whiteLabelText = retrieveWhiteLabelText();
useEffect(() => {
setWindowTitle(null, location);
}, [location]);
setFaviconAndTitle(whiteLabelFavicon, whiteLabelText, location);
}, [whiteLabelFavicon, whiteLabelText, location]);
return <WrappedComponent {...props} params={params} location={location} navigate={navigate} />;
};

View file

@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { getWorkspaceId, setWindowTitle } from '@/_helpers/utils';
import { getWorkspaceId } from '@/_helpers/utils';
import { retrieveWhiteLabelFavicon, retrieveWhiteLabelText, setFaviconAndTitle } from '@white-label/whiteLabelling';
import { appendWorkspaceId, excludeWorkspaceIdFromURL } from '@/_helpers/routes';
/*
@ -26,6 +27,8 @@ export const useSessionManagement = (initialState = defaultState) => {
const navigate = useNavigate();
const { pathname, search, state } = location;
const { disableValidSessionCallback, disableInValidSessionCallback } = initialState;
const whiteLabelFavicon = retrieveWhiteLabelFavicon();
const whiteLabelText = retrieveWhiteLabelText();
useEffect(() => {
/* replacing the state. otherwise the route will keep isSwitchingPage value `true` */
@ -41,7 +44,7 @@ export const useSessionManagement = (initialState = defaultState) => {
}, []);
useEffect(() => {
setWindowTitle(null, location);
setFaviconAndTitle(whiteLabelFavicon, whiteLabelText, location);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);

View file

@ -24,6 +24,7 @@ export const appService = {
createAppUser,
setPasswordFromToken,
acceptInvite,
getInviteeDetails,
};
function getConfig() {
@ -202,3 +203,8 @@ function acceptInvite({ token, password }) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/accept-invite`, requestOptions).then(handleResponseWithoutValidation);
}
function getInviteeDetails(token) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/invitee-details?token=${token}`, requestOptions).then(handleResponse);
}

View file

@ -185,11 +185,11 @@ function activateAccountWithToken(email, password, organizationToken) {
return fetch(`${config.apiUrl}/activate-account-with-token`, requestOptions).then(handleResponse);
}
function resendInvite(email) {
function resendInvite(email, organizationId, redirectTo) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
body: JSON.stringify({ email, organizationId, redirectTo }),
};
return fetch(`${config.apiUrl}/resend-invite`, requestOptions)

View file

@ -0,0 +1,216 @@
:root {
//page
--page-default: #F6F8FA;
--page-weak: #FFFFFF;
//background
--background-surface-layer-01: #FFFFFF;
--background-surface-layer-02: #F6F8FA;
--background-surface-layer-03: #E4E7EB;
--background-accent-strong: #4368E3;
--background-accent-weak: #ECF0FE;
--background-success-strong: #1E823B;
--background-success-weak: #E8F3EB;
--background-error-strong: #D72D39;
--background-error-weak: #FCEEEF;
--background-warning-strong: #BF4F03;
--background-warning-weak: #FAEFE7;
--background-inverse: #1B1F24;
//text
--text-default: #1B1F24;
--text-medium: #2D343B;
--text-placeholder: #6A727C;
--text-inverse: #F6F8FA;
--text-brand: #4368E3;
--text-selected: #4368E3;
--text-success: #1E823B;
--text-warning: #BF4F03;
--text-danger: #D72D39;
--text-on-solid: #FFFFFF;
--text-disabled: #ACB2B9;
--text-disabled-on-solid: #FFFFFF99;
//icon
--icon-strong: #6A727C;
--icon-default: #ACB2B9;
--icon-weak: #CCD1D5;
--icon-inverse: #FFFFFF;
--icon-on-solid: #FFFFFF;
--icon-accent: #4368E3;
--icon-brand: #4368E3;
--icon-success: #1E823B;
--icon-warning: #BF4F03;
--icon-danger: #D72D39;
--icon-disabled: #E4E7EB;
//border
--border-strong: #ACB2B9;
--border-default: #CCD1D5;
--border-weak: #E4E7EB;
--border-accent-strong: #4368E3;
--border-accent-weak: #97AEFC;
--border-success-strong: #1E823B;
--border-success-weak: #7FBF92;
--border-warning-strong: #BF4F03;
--border-warning-weak: #E7A274;
--border-danger-strong: #D72D39;
--border-danger-weak: #F4979E;
--border-disabled: #F6F8FA;
//interactive overlays
--interactive-weak: #FFFFFF00;
--interactive-default: #CCD1D54D;
--interactive-hover: #ACB2B959;
--interactive-selected: #ACB2B973;
--interactive-disabled: #FFFFFF00;
--interactive-focus-outline: #4368E3;
--interactive-focus-active: #4368E3;
--interactive-focus-inner-shadow: #FFFFFF;
//button
--button-primary: #4368E3;
--button-primary-hover: #2D50C5;
--button-primary-pressed: #1E3C9E;
--button-primary-disabled: #C2CFFD;
--button-secondary: #FFFFFF;
--button-secondary-hover: #ECF0FE;
--button-secondary-pressed: #DEE5FE;
--button-secondary-disabled: #FFFFFF;
--button-outline: #FFFFFF;
--button-outline-hover: #ACB2B94D;
--button-outline-pressed: #ACB2B966;
--button-outline-disabled: #FFFFFF;
--button-danger-primary: #D72D39;
--button-danger-primary-hover: #B5121D;
--button-danger-primary-pressed: #8E0811;
--button-danger-primary-disabled: #F9C2C6;
--button-danger-secondary: #FFFFFF;
--button-danger-secondary-hover: #FCEEEF;
--button-danger-secondary-pressed: #FBDFE1;
--button-danger-secondary-disabled: #FFFFFF;
//controls
--switch-tab: #ffffff;
--switch-tag: #CCD1D54D;
--switch-background-off: #FFFFFF;
--switch-background-on: #4368E3;
--slider-handle: #FFFFFF;
--slider-track: #E4E7EB;
--slider-fill: #4368E3;
}
.dark-theme {
//page
--page-default: #181B1F;
--page-weak: #1E2226;
//background
--background-surface-layer-01: #1E2226;
--background-surface-layer-02: #2B3036;
--background-surface-layer-03: #3C434B;
--background-accent-strong: #4A6DD9;
--background-accent-weak: #0D183C;
--background-success-strong: #318344;
--background-success-weak: #05200B;
--background-warning-strong: #BA5722;
--background-warning-weak: #301100;
--background-error-strong: #D03F43;
--background-error-weak: #390809;
--background-Inverse: #FAFCFF;
//text
--text-default: #FAFCFF;
--text-medium: #CFD3D8;
--text-placeholder: #858C94;
--text-inverse: #181B1F;
--text-accent: #4A6DD9;
--text-success: #318344;
--text-warning: #BA5722;
--text-danger: #D03F43;
--text-on-solid: #FFFFFF;
--text-disabled: #545B64;
--text-disabled-on-solid: #FAFCFF66;
//icon
--icon-strong: #CFD3D8E5;
--icon-default: #CFD3D8A6;
--icon-weak: #CFD3D859;
--icon-inverse: #181B1F;
--icon-on-solid: #FAFCFF;
--icon-accent: #4A6DD9;
--icon-brand: #4A6DD9;
--icon-success: #1E823B;
--icon-warning: #BA5722;
--icon-danger: #D03F43;
--icon-disabled: #CFD3D833;
//border
--border-strong: #545B64;
--border-default: #3C434B;
--border-weak: #2B3036;
--border-accent-strong: #4A6DD9;
--border-accent-weak: #4A6DD966;
--border-success-strong: #519B62;
--border-success-weak: #31834466;
--border-warning-strong: #BA5722;
--border-warning-weak: #BA572266;
--border-danger-strong: #E26367;
--border-danger-weak: #D03F4366;
--border-disabled: #2B3036B2;
//interactive overlays
--interactive-weak: #00000000;
--interactive-default: #A1A7AE1F;
--interactive-hover: #A1A7AE29;
--interactive-selected: #A1A7AE38;
--interactive-disabled: #00000000;
--interactive-focus-outline: #4A6DD9;
--interactive-focus-inner-shadow: #121518;
//button
//button
--button-primary: #4A6DD9;
--button-primary-hover: #6787E8;
--button-primary-pressed: #8AA3F2;
--button-primary-disabled: #162A69;
--button-secondary: #1E2226;
--button-secondary-hover: #162A69;
--button-secondary-pressed: #213C90;
--button-secondary-disabled: #121518;
--button-outline: #A1A7AE1A;
--button-outline-hover: #A1A7AE33;
--button-outline-pressed: #A1A7AE4D;
--button-outline-disabled: #121518;
--button-danger-primary: #D03F43;
--button-danger-primary-hover: #E26367;
--button-danger-primary-pressed: #EC8B8E;
--button-danger-primary-disabled: #620D0F;
--button-danger-secondary: #121518;
--button-danger-secondary-hover: #620D0F;
--button-danger-secondary-pressed: #841417;
--button-danger-secondary-disabled: #121518;
//controls
--switch-tab: #2B3036;
--switch-tag: #121518;
--switch-background-off: #121518;
--switch-background-on: #4A6DD9;
--slider-handle: #121518;
--slider-track: #2B3036;
--slider-fill: #4A6DD9;
}

View file

@ -56,6 +56,7 @@
width: 262px;
height: 32px;
border: none !important;
background-color: var(--page-default) !important;
&:hover {
background: var(--slate2) !important;

View file

@ -4,7 +4,7 @@
.global-datasources-sidebar {
height: calc(100vh - 64px);
max-width: 288px;
background: var(--base);
background: var(--page-default);
display: grid;
grid-template-rows: auto 1fr auto;
border-right: 1px solid var(--slate5);
@ -105,7 +105,7 @@
.datasource-modal-container {
position: relative;
// background: var(--slate2);
background: var(--page-default);
.modal-header {
background-color: var(--slate3) !important;

View file

@ -1,8 +1,7 @@
@import "./colors.scss";
@import "./designtheme.scss";
.left-sidebar {
background: var(--base) !important;
background: var(--page-default) !important;
display: flex;
gap: 16px;

File diff suppressed because one or more lines are too long

View file

@ -12,8 +12,12 @@
@import "./ui-operations.scss";
@import 'react-loading-skeleton/dist/skeleton.css';
@import './table-component.scss';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import "./componentdesign.scss";
@import './pages-sidebar.scss';
@import "./componentdesign.scss";
/* ibm-plex-sans-100 - latin */
@font-face {
font-display: swap;
@ -469,10 +473,10 @@ button {
left: 0;
overflow-x: hidden;
flex: 1 1 auto;
background-color: var(--base) !important;
background-clip: border-box;
margin-top: 48px;
padding-top: 8px;
background: var(--base) !important;
.accordion-item {
border: solid var(--slate5);
@ -4634,13 +4638,12 @@ input[type="text"] {
}
.modal-footer-btn {
justify-content: end;
button {
& > *:nth-child(2) {
margin-left: 16px;
}
}
}
}
.home-modal-component-editor.dark {
@ -5730,7 +5733,7 @@ div#driver-page-overlay {
.node-key {
font-weight: 400 !important;
margin-left: -0.25rem !important;
justify-content: start !important;
justify-content: flex-start !important;
min-width: fit-content !important;
}
@ -5843,7 +5846,7 @@ div#driver-page-overlay {
border-bottom: 1px solid #eee;
padding: 5px;
display: flex;
justify-content: end;
justify-content: flex-end
}
.autosave-indicator {
@ -6311,7 +6314,7 @@ a.step-item-disabled {
}
.spinner {
min-height: 220px;
min-height: 100px;
}
}
@ -7540,6 +7543,7 @@ tbody {
.workspace-content-wrapper,
.database-page-content-wrap {
background-color: var(--page-default);
height: calc(100vh - 64px) !important;
}
@ -7551,7 +7555,7 @@ tbody {
.organization-page-sidebar {
height: calc(100vh - 64px);
max-width: 288px;
background-color: var(--base);
background-color: var(--page-default);
border-right: 1px solid var(--slate5) !important;
display: grid !important;
grid-template-rows: auto 1fr auto !important;
@ -7560,7 +7564,7 @@ tbody {
.marketplace-page-sidebar {
height: calc(100vh - 64px);
max-width: 288px;
background-color: var(--base);
background-color: var(--page-default);
border-right: 1px solid var(--slate5) !important;
display: grid !important;
grid-template-rows: auto 1fr auto !important;
@ -7568,7 +7572,7 @@ tbody {
.home-page-sidebar {
max-width: 288px;
background-color: var(--base);
background-color: var(--page-default);
border-right: 1px solid var(--slate5);
display: grid;
grid-template-rows: auto 1fr auto;
@ -7593,7 +7597,7 @@ tbody {
.tooljet-database-sidebar {
max-width: 288px;
background: var(--base);
background: var(--page-default);
border-right: 1px solid var(--slate5);
height: calc(100vh - 64px) !important;
@ -7987,6 +7991,7 @@ tbody {
.tj-dashboard-section-header {
background-color: var(--page-default);
max-width: 288px;
max-height: 64px;
padding-top: 20px;
@ -8053,6 +8058,7 @@ tbody {
border-radius: 6px;
.react-select__control {
background-color: var(--page-default);
border: none !important;
}
}
@ -8114,7 +8120,7 @@ tbody {
.home-page-footer {
height: 52px;
background-color: var(--base) !important;
background-color: var(--page-default) !important;
border-top: 1px solid var(--slate5) !important;
width: calc(100% - 336px) !important;
@ -8148,6 +8154,61 @@ tbody {
text-align: center;
}
.form-control-pagination {
padding: 0 4px;
width: fit-content;
width: 30px !important;
height: 20px !important;
text-align: center;
margin-bottom: 0px;
gap: 16px !important;
background: var(--base) !important;
border: 1px solid var(--slate7) !important;
border-radius: 6px;
color: var(--slate12) !important;
transition: none;
padding-left: 0.4375rem;
padding-right: 0.4375rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
overflow-x: auto;
white-space: nowrap;
&:hover {
background: var(--slate1) !important;
border: 1px solid var(--slate8) !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
outline: none;
}
&:focus-visible {
background: var(--indigo2) !important;
border: 1px solid var(--indigo9) !important;
box-shadow: none !important;
}
&.input-error-border {
border-color: #DB4324 !important;
}
&:-webkit-autofill {
box-shadow: 0 0 0 1000px var(--base) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
&:hover {
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
}
&:focus-visible {
box-shadow: 0 0 0 1000px var(--indigo2) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
}
}
}
@media only screen and (max-width: 768px) {
.unstyled-button {
height: unset;
@ -8248,6 +8309,7 @@ tbody {
}
.home-page-content {
background-color: var(--page-default);
height: calc(100vh - 64px) !important;
overflow-y: auto;
position: relative;
@ -8306,19 +8368,19 @@ tbody {
.toojet-db-table-footer {
height: 52px;
background: var(--base) !important;
background: var(--page-default) !important;
width: calc(100vw - 336px);
}
.toojet-db-table-footer-collapse {
height: 52px;
background: var(--base) !important;
background: var(--page-default) !important;
width: calc(100vw - 48px);
}
.toojet-db-table-footer-collapse {
height: 52px;
background: var(--base) !important;
background: var(--page-default) !important;
width: calc(100vw - 48px);
}
@ -8783,6 +8845,7 @@ tbody {
}
.tj-dashboard-header-wrap {
background-color: var(--page-default);
padding-top: 22px;
padding-bottom: 22px;
padding-left: 40px;
@ -10042,7 +10105,8 @@ tbody {
border-top: 1px solid var(--slate5) !important;
display: flex;
gap: 8px;
justify-content: end;
justify-content: flex-end;
.invite-btn {
width: 140px;
@ -10348,7 +10412,7 @@ tbody {
display: flex;
align-items: center;
gap: 8px;
justify-content: end;
justify-content: flex-end
}
.add-users-button {
@ -11165,7 +11229,7 @@ tbody {
.input-with-icon {
justify-content: end;
justify-content: flex-end
}
.form-check-input {
@ -11372,7 +11436,7 @@ tbody {
}
.profile-page-content-wrap {
background-color: var(--slate2);
background-color: var(--page-default);
padding-top: 40px;
}
@ -11622,6 +11686,7 @@ tbody {
.marketplace-body {
height: calc(100vh - 64px) !important;
overflow-y: auto;
background-color: var(--page-default);
}
.plugins-card {
@ -12341,6 +12406,7 @@ tbody {
.header>.navbar {
background-color: var(--base) !important;
border-bottom: 1px solid var(--slate5);
z-index: 10;
}
}
}
@ -12831,6 +12897,7 @@ tbody {
}
.workspace-constants-wrapper {
background-color: var(--page-default);
height: calc(100vh - 64px);
display: flex;
align-items: center;
@ -13099,6 +13166,17 @@ tbody {
}
}
.component-spinner {
animation: l13 1s infinite linear;
position: absolute;
}
@keyframes l13 {
100% {
transform: rotate(1turn)
}
}
.widget-version-identifier {
position: absolute;
right: 0px;

View file

@ -12,11 +12,20 @@ const Card = ({
className,
titleClassName,
actionButton,
darkMode,
}) => {
const DisplayIcon = ({ src }) => {
if (typeof src !== 'string') return;
if (usePluginIcon) {
//Fetch darkMode svgs
if (darkMode) {
const darkSrc = `${src}Dark`;
if (allSvgs[darkSrc]) {
src = darkSrc;
}
}
const Icon = allSvgs[src];
return <Icon style={{ height, width }} className="card-icon" />;
}

View file

@ -12,6 +12,7 @@ import { getPrivateRoute } from '@/_helpers/routes';
import { ConfirmDialog } from '@/_components';
import useGlobalDatasourceUnsavedChanges from '@/_hooks/useGlobalDatasourceUnsavedChanges';
import Settings from '@/_components/Settings';
import { retrieveWhiteLabelLogo, fetchWhiteLabelDetails } from '@white-label/whiteLabelling';
function Layout({
children,
@ -25,6 +26,8 @@ function Layout({
const currentUserValue = authenticationService.currentSessionValue;
const admin = currentUserValue?.admin;
const marketplaceEnabled = admin && window.public_config?.ENABLE_MARKETPLACE_FEATURE == 'true';
fetchWhiteLabelDetails();
const whiteLabelLogo = retrieveWhiteLabelLogo();
const {
checkForUnsavedChanges,
@ -60,7 +63,7 @@ function Layout({
to={getPrivateRoute('dashboard')}
onClick={(event) => checkForUnsavedChanges(getPrivateRoute('dashboard'), event)}
>
<Logo />
{whiteLabelLogo ? <img src={whiteLabelLogo} /> : <Logo />}
</Link>
</div>
<div>

View file

@ -43,11 +43,11 @@ const Pagination = ({
<Button.Content iconSrc={'assets/images/icons/chevron-left.svg'} />
</Button.UnstyledButton>
<div className="d-flex align-items-center">
<div className="d-flex align-items-center mx-1">
<input
disabled={isDisabled || disableInput}
type="text"
className="form-control mx-1"
className="form-control-pagination"
data-cy={`current-page-number-${currentPageNumber}`}
value={currentPageNumber}
onKeyDown={(event) => {

View file

@ -0,0 +1,207 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { cn } from '@/lib/utils';
import Loader from '@/components/ui/utilComponents/loader';
import SolidIcon from '@/_ui/Icon/SolidIcons';
// eslint-disable-next-line import/no-unresolved
import { Slot } from '@radix-ui/react-slot';
// eslint-disable-next-line import/no-unresolved
import { cva } from 'class-variance-authority';
import './Button.scss';
import { getDefaultIconFillColor, defaultButtonFillColour, getIconSize } from './buttonUtils.js';
const buttonVariants = cva('tw-flex tw-justify-center tw-items-center tw-font-medium', {
variants: {
variant: {
primary: `
tw-text-text-on-solid tw-bg-button-primary hover:tw-bg-button-primary-hover
active:tw-bg-button-primary-pressed active:tw-border-border-accent-strong
disabled:tw-bg-button-primary-disabled tw-border-none
tw-interactice-focus tw-focus-visible:tw-outline-none`,
secondary: `
tw-text-text-default tw-border tw-border-solid tw-border-border-accent-weak
tw-bg-button-secondary hover:tw-border-border-accent-strong
hover:tw-bg-button-secondary-hover active:tw-bg-button-secondary-pressed
active:tw-border-border-accent-strong
disabled:tw-border-border-default
disabled:tw-bg-button-secondary-disabled disabled:tw-text-text-disabled
tw-focus-visible:tw-border-border-accent-weak
tw-interactive-focus-nonsolid tw-focus-visible:tw-outline-none`,
outline: `
tw-text-text-default tw-border tw-border-solid tw-border-border-default
tw-bg-button-secondary hover:tw-border-border-default
hover:tw-bg-button-outline-hover active:tw-bg-button-outline-pressed
active:tw-border-border-strong
disabled:tw-border-border-default
disabled:tw-bg-button-outline-disabled disabled:tw-text-text-disabled
tw-focus-visible:tw-border-border-default
tw-interactive-focus-nonsolid tw-focus-visible:tw-interactive-focus-outline`,
ghost: `
tw-border-none tw-text-text-default tw-bg-[#ffffff00] hover:tw-bg-button-outline-hover
active:tw-bg-button-outline-pressed tw-focus-visible:tw-bg-button-outline
tw-interactive-focus-nonsolid tw-disabled:tw-text-text-disabled tw-focus-visible:tw-interactive-focus-outline tw-border-none`,
ghostBrand: `
tw-border-none tw-text-text-accent tw-bg-[#ffffff00] hover:tw-bg-button-secondary-hover
active:tw-bg-button-secondary-pressed tw-focus-visible:tw-bg-button-outline
tw-disabled:tw-text-text-disabled tw-focus-visible:tw-interactive-focus-outline tw-border-none tw-interactive-focus-nonsolid`,
dangerPrimary: `
tw-text-text-on-solid tw-bg-button-danger-primary hover:tw-bg-button-danger-primary-hover
active:tw-bg-button-danger-primary-pressed disabled:tw-bg-button-danger-primary-disabled
tw-border-none tw-interactice-focus tw-focus-visible:tw-outline-none`,
dangerSecondary: `
tw-text-text-default tw-border tw-border-solid tw-border-border-danger-weak
tw-bg-button-secondary hover:tw-border-border-danger-strong
hover:tw-bg-button-danger-secondary-hover
active:tw-border-border-danger-strong active:tw-bg-button-danger-secondary-pressed
tw-disabled:tw-text-text-disabled tw-disabled:tw-border-border-default
tw-disabled:tw-bg-button-danger-secondary-disabled
tw-focus-visible:tw-border-border-danger-weak tw-focus-visible:tw-interactive-focus-outline tw-interactive-focus-nonsolid`,
dangerGhost: `
tw-border-none tw-bg-[#ffffff00] tw-text-text-default hover:tw-bg-button-danger-secondary-hover
active:tw-bg-button-danger-secondary-pressed tw-disabled:tw-border-border-default
tw-disabled:tw-bg-button-danger-secondary-disabled
tw-disabled:tw-text-text-disabled tw-focus-visible:tw-interactive-focus-outline tw-interactive-focus-nonsolid`,
},
size: {
large: `tw-h-[40px] tw-gap-[8px] tw-py-[10px] tw-rounded-[10px] tw-text-lg`,
default: `tw-h-[32px] tw-gap-[6px] tw-py-7px] tw-rounded-[8px] tw-text-base`,
medium: `tw-h-[28px] tw-gap-[6px] tw-py-[5px] tw-rounded-[6px] tw-text-base`,
small: `tw-h-[20px] tw-gap-[4px] tw-py-[2px] tw-rounded-[4px] tw-text-sm`,
},
},
//this part will be updated/improved
compoundVariants: [
{
iconOnly: true,
size: 'large',
className: 'tw-w-[40px] tw-px-[10px]',
},
{
iconOnly: true,
size: 'default',
className: 'tw-w-[32px] tw-px-[7px]',
},
{
iconOnly: true,
size: 'medium',
className: 'tw-w-[28px] tw-px-[5px]',
},
{
iconOnly: true,
size: 'small',
className: 'tw-w-[20px] tw-px-[2px]',
},
{
iconOnly: false,
size: 'large',
className: 'tw-px-[20px]',
},
{
iconOnly: false,
size: 'default',
className: 'tw-px-[12px]',
},
{
iconOnly: false,
size: 'medium',
className: 'tw-px-[10px]',
},
{
iconOnly: false,
size: 'small',
className: 'tw-px-[8px]',
},
],
defaultVariants: {
variant: 'primary',
size: 'default',
},
});
const Button = forwardRef(
(
{
className,
variant = 'primary',
size = 'default',
leadingIcon,
trailingIcon,
isLoading,
disabled,
asChild = false,
fill = '',
iconOnly = false, // as normal button and icon have diff styles make sure to pass it as truw when icon only button is used
...props
},
ref
) => {
const iconFillColor = !defaultButtonFillColour.includes(fill) && fill ? fill : getDefaultIconFillColor(variant);
const Comp = asChild ? Slot : 'button';
const leadingIconElement = leadingIcon && (
<SolidIcon name={leadingIcon} height={getIconSize(size)} width={getIconSize(size)} fill={iconFillColor} />
);
const trailingIconElement = trailingIcon && (
<SolidIcon name={trailingIcon} height={getIconSize(size)} width={getIconSize(size)} fill={iconFillColor} />
);
return (
<Comp
className={cn(buttonVariants({ variant, size, iconOnly, className }))}
ref={ref}
disabled={disabled}
{...props}
>
{isLoading ? (
<div className="tw-flex tw-justify-center tw-items-center">
<Loader color={iconFillColor} width={getIconSize(size)} />
<a className="tw-invisible">{props.children}</a>
</div>
) : (
<>
{leadingIconElement}
{props.children}
{trailingIconElement}
</>
)}
</Comp>
);
}
);
Button.displayName = 'Button'; //debugging purposes and helpful in React Developer Tools
Button.propTypes = {
className: PropTypes.string,
variant: PropTypes.oneOf([
'primary',
'secondary',
'outline',
'ghost',
'ghostBrand',
'dangerPrimary',
'dangerSecondary',
'dangerGhost',
]),
size: PropTypes.oneOf(['large', 'default', 'medium', 'small']),
isLoading: PropTypes.bool,
iconOnly: PropTypes.bool,
disabled: PropTypes.bool,
asChild: PropTypes.bool,
fill: PropTypes.string,
leadingIcon: PropTypes.string,
trailingIcon: PropTypes.string,
};
Button.defaultProps = {
className: '',
variant: 'primary',
size: 'default',
isLoading: false,
disabled: false,
asChild: false,
iconOnly: false,
fill: '',
leadingIcon: '',
trailingIcon: '',
};
export { Button, buttonVariants };

View file

@ -0,0 +1,16 @@
.interactice-focus {
&:focus-visible {
box-shadow: 0px 0px 0px 2px var(--interactive-focus-outline, #4368E3), 0px 0px 0px 2px var(--interactive-focus-inner-shadow, #FFF) inset;
}
}
.interactive-focus-nonsolid {
&:focus-visible {
box-shadow: 0px 0px 0px 2px var(--interactive-focus-outline, #4368E3);
outline-offset: 2px !important;
}
}
.button-content {
min-width: 100px;
}

View file

@ -0,0 +1,104 @@
import { Button } from './Button';
import * as React from 'react';
// Function to determine default icon fill color
const getDefaultIconFillColor = (variant, customFill = '') => {
if (customFill) {
return customFill;
}
switch (variant) {
case 'primary':
case 'dangerPrimary':
return 'var(--icon-on-solid)';
case 'secondary':
case 'ghostBrand':
return 'var(--icon-brand)';
case 'outline':
case 'ghost':
return 'var(--icon-strong)';
case 'dangerSecondary':
case 'dangerGhost':
return 'var(--icon-danger)';
default:
return '';
}
};
// Storybook configuration
export default {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
argTypes: {
onClick: { action: 'Clicked' },
variant: {
control: {
type: 'select',
options: [
'primary',
'secondary',
'outline',
'ghost',
'ghostBrand',
'dangerPrimary',
'dangerSecondary',
'dangerGhost',
],
},
},
fill: { control: 'color' },
},
};
// Button template
const Template = (args) => <Button {...args} />;
// Primary button story
export const RocketButton = Template.bind({});
RocketButton.args = {
variant: 'primary',
children: 'Button',
size: 'default',
iconOnly: false,
};
// Button with leading icon story
export const RocketButtonWithIcon = (args) => {
const variant = args.variant || 'primary';
const fill = ''; // If fill is provided by user, it will be used; otherwise, it falls back to defaults
const color = getDefaultIconFillColor(variant, fill);
return <Button {...args} fill={color} iconOnly={false} leadingIcon="smilerectangle" />;
};
RocketButtonWithIcon.args = {
...RocketButton.args,
};
// Button with trailing icon story
export const RocketButtonWithTrailingIcon = (args) => {
const variant = args.variant || 'primary';
const fill = '';
const color = getDefaultIconFillColor(variant, fill);
return <Button {...args} fill={color} iconOnly={false} trailingIcon="smilerectangle" />;
};
RocketButtonWithTrailingIcon.args = {
...RocketButton.args,
};
// Button with icon only story
export const Icon = (args) => {
const variant = args.variant || 'primary';
const fill = '';
const color = getDefaultIconFillColor(variant, fill);
return <Button {...args} fill={color} trailingIcon="smilerectangle" />;
};
Icon.args = {
...RocketButton.args,
children: null,
iconOnly: true,
};

View file

@ -0,0 +1,32 @@
export const getDefaultIconFillColor = (variant) => {
switch (variant) {
case 'primary':
case 'dangerPrimary':
return 'var(--icon-on-solid)';
case 'secondary':
case 'ghostBrand':
return 'var(--icon-brand)';
case 'outline':
case 'ghost':
return 'var(--icon-strong)';
case 'dangerSecondary':
case 'dangerGhost':
return 'var(--icon-danger)';
default:
return '';
}
};
export const defaultButtonFillColour = ['#FFFFFF', '#4368E3', '#ACB2B9', '#D72D39']; // all default fill colors
export const getIconSize = (size) => {
switch (size) {
case 'large':
return '20px';
case 'default':
return '16px';
case 'medium':
return '14px';
case 'small':
return '12px';
}
};

View file

@ -0,0 +1,19 @@
import React from 'react';
const Loader = ({ color, width }) => {
const loaderStyle = {
width,
height: width,
aspectRatio: '1',
borderRadius: '50%',
background: `radial-gradient(farthest-side, ${
color || '#FFFFFF'
} 90%, transparent 94%) top/4px 4px no-repeat, conic-gradient(#0000 30%, ${color || '#FFFFFF'})`,
WebkitMask: `radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0)`,
animation: 'l13 1s infinite linear',
};
return <div className="component-spinner" style={loaderStyle}></div>;
};
export default Loader;

View file

@ -0,0 +1,6 @@
import cx from 'classnames';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(cx(inputs));
}

View file

@ -1,50 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from './Button';
import './header.scss';
export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => (
<header>
<div className="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" fill="#FFF" />
<path d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" fill="#555AB9" />
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{user ? (
<>
<span className="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
</>
) : (
<>
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
</>
)}
</div>
</div>
</header>
);
Header.propTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
}),
onLogin: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onCreateAccount: PropTypes.func.isRequired,
};
Header.defaultProps = {
user: null,
};

View file

@ -1,22 +0,0 @@
import { Header } from './Header';
export default {
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: 'fullscreen',
},
};
export const LoggedIn = {
args: {
user: {
name: 'Jane Doe',
},
},
};
export const LoggedOut = {};

View file

@ -1,67 +0,0 @@
import React from 'react';
import { Header } from './Header';
import './page.scss';
export const Page = () => {
const [user, setUser] = React.useState();
return (
<article>
<Header
user={user}
onLogin={() => setUser({ name: 'Jane Doe' })}
onLogout={() => setUser(undefined)}
onCreateAccount={() => setUser({ name: 'Jane Doe' })}
/>
<section className="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a{' '}
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
<strong>component-driven</strong>
</a>{' '}
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without needing to navigate to
them in your app. Here are some handy patterns for managing page data in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the of child component
stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at{' '}
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the{' '}
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">
docs
</a>
.
</p>
<div className="tip-wrapper">
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '}
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>
);
};

View file

@ -1,25 +0,0 @@
import { within, userEvent } from '@storybook/testing-library';
import { Page } from './Page';
export default {
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: 'fullscreen',
},
};
export const LoggedOut = {};
// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing
export const LoggedIn = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = await canvas.getByRole('button', {
name: /Log in/i,
});
await userEvent.click(loginButton);
},
};

View file

@ -1,32 +0,0 @@
.storybook-header {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 15px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.storybook-header svg {
display: inline-block;
vertical-align: top;
}
.storybook-header h1 {
font-weight: 700;
font-size: 20px;
line-height: 1;
margin: 6px 0 6px 10px;
display: inline-block;
vertical-align: top;
}
.storybook-header button + button {
margin-left: 10px;
}
.storybook-header .welcome {
color: #333;
font-size: 14px;
margin-right: 10px;
}

View file

@ -1,69 +0,0 @@
.storybook-page {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 24px;
padding: 48px 20px;
margin: 0 auto;
max-width: 600px;
color: #333;
}
.storybook-page h2 {
font-weight: 700;
font-size: 32px;
line-height: 1;
margin: 0 0 4px;
display: inline-block;
vertical-align: top;
}
.storybook-page p {
margin: 1em 0;
}
.storybook-page a {
text-decoration: none;
color: #1ea7fd;
}
.storybook-page ul {
padding-left: 30px;
margin: 1em 0;
}
.storybook-page li {
margin-bottom: 8px;
}
.storybook-page .tip {
display: inline-block;
border-radius: 1em;
font-size: 11px;
line-height: 12px;
font-weight: 700;
background: #e7fdd8;
color: #66bf3c;
padding: 4px 12px;
margin-right: 10px;
vertical-align: top;
}
.storybook-page .tip-wrapper {
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
}
.storybook-page .tip-wrapper svg {
display: inline-block;
height: 12px;
width: 12px;
margin-right: 4px;
vertical-align: top;
margin-top: 3px;
}
.storybook-page .tip-wrapper svg path {
fill: #1ea7fd;
}

122
frontend/tailwind.config.js Normal file
View file

@ -0,0 +1,122 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
content: ['./pages/**/*.{js,jsx}', './components/**/*.{js,jsx}', './app/**/*.{js,jsx}', './src/**/*.{js,jsx}'],
prefix: 'tw-',
corePlugins: {
preflight: false,
},
theme: {
extend: {
colors: {
'page-default': 'var(--page-default)',
'page-weak': 'var(--page-weak)',
'background-surface-layer-01': 'var(--background-surface-layer-01)',
'background-surface-layer-02': 'var(--background-surface-layer-02)',
'background-surface-layer-03': 'var(--background-surface-layer-03)',
'background-accent-strong': 'var(--background-accent-strong)',
'background-accent-weak': 'var(--background-accent-weak)',
'background-success-strong': 'var(--background-success-strong)',
'background-success-weak': 'var(--background-success-weak)',
'background-error-strong': 'var(--background-error-strong)',
'background-error-weak': 'var(--background-error-weak)',
'background-warning-stong': 'var(--background-warning-stong)',
'background-warning-weak': 'var(--background-warning-weak)',
'background-inverse': 'var(--background-Inverse)',
'text-default': 'var(--text-default)',
'text-medium': 'var(--text-medium)',
'text-placeholder': 'var(--text-placeholder)',
'text-inverse': 'var(--text-inverse)',
'text-brand': 'var(--text-brand)',
'text-accent': 'var(--text-brand)',
'text-selected': 'var(--text-selected)',
'text-success': 'var(--text-success)',
'text-warning': 'var(--text-warning)',
'text-danger': 'var(--text-danger)',
'text-on-solid': 'var(--text-on-solid)',
'text-disabled': 'var(--text-disabled)',
'text-disabled-on-solid': 'var(--text-disabled-on-solid)',
'icon-strong': 'var(--icon-strong)',
'icon-default': 'var(--icon-default)',
'icon-weak': 'var(--icon-weak)',
'icon-inverse': 'var(--icon-inverse)',
'icon-on-solid': 'var(--icon-on-solid)',
'icon-accent': 'var(--icon-accent)',
'icon-brand': 'var(--icon-brand)',
'icon-success': 'var(--icon-success)',
'icon-warning': 'var(--icon-warning)',
'icon-danger': 'var(--icon-danger)',
'icon-disabled': 'var(--icon-disabled)',
'border-strong': 'var(--border-strong)',
'border-default': 'var(--border-default)',
'border-weak': 'var(--border-weak)',
'border-accent-strong': 'var(--border-accent-strong)',
'border-accent-weak': 'var(--border-accent-weak)',
'border-success-strong': 'var(--border-success-strong)',
'border-success-weak': 'var(--border-success-weak)',
'border-warning-strong': 'var(--border-warning-strong)',
'border-warning-weak': 'var(--border-warning-weak)',
'border-danger-strong': 'var(--border-danger-strong)',
'border-danger-weak': 'var(--border-danger-weak)',
'border-disabled': 'var(--border-disabled)',
'interactive-weak': 'var(--interactive-weak)',
'interactive-default': 'var(--interactive-default)',
'interactive-hover': 'var(--interactive-hover)',
'interactive-selected': 'var(--interactive-selected)',
'interactive-disabled': 'var(--interactive-disabled)',
'interactive-focus-outline': 'var(--interactive-focus-outline)',
'interactive-focus-inner-shadow': 'var(--interactive-focus-inner-shadow)',
'button-primary': 'var(--button-primary)',
'button-primary-hover': 'var(--button-primary-hover)',
'button-primary-pressed': 'var(--button-primary-pressed)',
'button-primary-disabled': 'var(--button-primary-disabled)',
'button-secondary': 'var(--button-secondary)',
'button-secondary-hover': 'var(--button-secondary-hover)',
'button-secondary-pressed': 'var(--button-secondary-pressed)',
'button-secondary-disabled': 'var(--button-secondary-disabled)',
'button-outline': 'var(--button-outline)',
'button-outline-hover': 'var(--button-outline-hover)',
'button-outline-pressed': 'var(--button-outline-pressed)',
'button-outline-disabled': 'var(--button-outline-disabled)',
'button-danger-primary': 'var(--button-danger-primary)',
'button-danger-primary-hover': 'var(--button-danger-primary-hover)',
'button-danger-primary-pressed': 'var(--button-danger-primary-pressed)',
'button-danger-primary-disabled': 'var(--button-danger-primary-disabled)',
'button-danger-secondary': 'var(--button-danger-secondary)',
'button-danger-secondary-hover': 'var(--button-danger-secondary-hover)',
'button-danger-secondary-pressed': 'var(--button-danger-secondary-pressed)',
'button-danger-secondary-disabled': 'var(--button-danger-secondary-disabled)',
'switch-tab': 'var(--switch-tab)',
'switch-tag': 'var(--switch-tag)',
'switch-background-off': 'var(--switch-background-off)',
'switch-background-on': 'var(--switch-background-on)',
'slider-handle': 'var(--slider-handle)',
'slider-track': 'var(--slider-track)',
'slider-fill': 'var(--slider-fill)',
},
boxShadow: {
'interactive-focus-outline': ' 0px 0px 0px 2px var(--interactive-focus-outline)',
'interactive-focus-outline-inset': 'inset 0px 0px 0px 2px #fff',
},
fontSize: {
sm: ['11px', '16px'],
base: ['12px', '18px'],
lg: ['14px', '20px'],
xl: ['16px', '24px'],
},
fontWeight: {
thin: '100',
hairline: '100',
extralight: '200',
light: '300',
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
extrabold: '800',
black: '900',
},
},
},
plugins: [require('tailwindcss-animate')],
};

View file

@ -87,6 +87,7 @@ module.exports = {
'@': path.resolve(__dirname, 'src/'),
'@ee': path.resolve(__dirname, 'ee/'),
'@assets': path.resolve(__dirname, 'assets/'),
'@white-label': path.resolve(__dirname, 'ce/white-label'),
},
},
devtool: environment === 'development' ? 'eval-cheap-source-map' : 'hidden-source-map',
@ -146,6 +147,9 @@ module.exports = {
{
loader: 'css-loader',
},
{
loader: 'postcss-loader',
},
{
loader: 'sass-loader',
},

View file

@ -53,4 +53,4 @@
"prepare": "husky install",
"update-version": "node update-version.js"
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,10 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_814_122599)">
<path d="M5.31492 14.1198L5.3578 14.1306L15.4345 17.2148C15.5189 17.239 15.5977 17.2796 15.6663 17.3343C15.735 17.389 15.7922 17.4566 15.8346 17.5335C15.8771 17.6103 15.904 17.6948 15.9137 17.782C15.9215 17.8518 15.9182 17.9222 15.9041 17.9908L15.8915 18.0418L14.2029 23.4472C14.1435 23.6118 14.0274 23.7502 13.8755 23.8372C13.7374 23.9163 13.5777 23.9485 13.4205 23.9295L13.3734 23.9223L3.29674 20.8085C3.12842 20.7572 2.98715 20.6416 2.90369 20.4867C2.82783 20.3458 2.80533 20.183 2.8393 20.0278L2.85118 19.9814L4.52835 14.5875C4.57823 14.4173 4.69346 14.2737 4.84886 14.1881C4.9913 14.1096 5.15691 14.0857 5.31492 14.1198ZM22.326 16.588L22.3031 16.6117L15.5738 22.8417C15.3262 23.0893 15.1983 23.0075 15.2773 22.6851L15.2859 22.6521L16.6889 18.1378C16.7519 17.9512 16.8609 17.7834 17.0058 17.65C17.1347 17.5315 17.2883 17.4435 17.4551 17.3922L17.5183 17.3747L22.1431 16.3238C22.4709 16.206 22.5476 16.3372 22.347 16.5649L22.326 16.588ZM0.309343 9.55988L0.330828 9.58426L3.55262 13.0614C3.67909 13.2141 3.76791 13.3943 3.81197 13.5875C3.85112 13.7593 3.85393 13.9371 3.82049 14.1096L3.80626 14.174L2.4033 18.6884C2.31715 19.0263 2.16055 19.0375 2.0924 18.7221L2.08568 18.6884L0.0429213 9.74419C-0.0565851 9.40592 0.0964194 9.32653 0.309343 9.55988ZM20.8537 3.80734C20.9908 3.88939 21.0929 4.01818 21.1417 4.16921L21.1537 4.21093L23.5141 14.4482C23.539 14.5318 23.5462 14.6197 23.5352 14.7062C23.5242 14.7928 23.4954 14.8761 23.4506 14.9509C23.4057 15.0257 23.3456 15.0903 23.2743 15.1407C23.2172 15.1809 23.1541 15.2113 23.0873 15.2307L23.0366 15.2432L17.4864 16.5158C17.3173 16.5579 17.1384 16.5314 16.9889 16.4419C16.8519 16.3598 16.7498 16.231 16.7009 16.08L16.6889 16.0383L14.3354 5.80101C14.2931 5.63287 14.3191 5.45485 14.4078 5.30586C14.489 5.16929 14.6168 5.06731 14.7669 5.01807L14.8084 5.00597L20.3563 3.73344C20.5253 3.69126 20.7043 3.71782 20.8537 3.80734ZM13.6192 6.20097L13.6294 6.23508L15.8618 15.8989C15.9525 16.2504 15.7478 16.4394 15.4156 16.3642L15.3819 16.3558L5.8788 13.4316C5.54125 13.3389 5.46997 13.0695 5.69581 12.8228L5.72114 12.7964L12.9919 6.04317C13.2371 5.79803 13.5143 5.87952 13.6192 6.20097ZM8.39128 0.000197145C8.47781 0.00232254 8.56303 0.0216088 8.64203 0.0569367C8.70523 0.0851989 8.76345 0.123277 8.81455 0.169666L8.8515 0.205992L12.7039 4.37767C12.8204 4.50336 12.8823 4.66997 12.8763 4.84117C12.8708 4.99809 12.8086 5.14721 12.7021 5.26127L12.672 5.2915L4.95564 12.4469C4.82829 12.5645 4.65964 12.6271 4.48636 12.6211C4.32752 12.6156 4.17654 12.5529 4.06087 12.4453L4.03022 12.4149L0.170925 8.24092C0.0556386 8.11468 -0.00557994 7.9483 0.000400318 7.77747C0.00588223 7.62087 0.0674297 7.47196 0.173031 7.35743L0.202915 7.32707L7.92151 0.183146C7.98381 0.12307 8.05739 0.0759615 8.13804 0.0445557C8.2187 0.0131497 8.30476 -0.00192826 8.39128 0.000197145ZM10.3436 0.174841L10.3755 0.183182L19.1772 2.87673C19.5174 2.96727 19.5287 3.11973 19.2216 3.21647L19.1886 3.22627L14.5639 4.27488C14.3701 4.31463 14.1696 4.30737 13.9791 4.2537C13.8099 4.20601 13.6529 4.12289 13.5187 4.01014L13.4694 3.96648L10.2476 0.500741C9.9709 0.224092 10.0223 0.0953797 10.3436 0.174841Z" fill="#84E1E2"/>
</g>
<defs>
<clipPath id="clip0_814_122599">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.8879 4.07409C23.8227 4.02037 23.7399 3.99268 23.6555 3.99636C23.4239 3.99636 23.1244 4.15346 22.9632 4.23774L22.8994 4.27047C22.629 4.40137 22.3347 4.47544 22.0345 4.48812C21.7269 4.49794 21.4618 4.51594 21.1165 4.55195C19.0708 4.76223 18.1585 6.32999 17.2797 7.8462C16.801 8.67099 16.3068 9.5277 15.6293 10.1823C15.4891 10.3182 15.3402 10.4448 15.1834 10.5611C14.4821 11.0824 13.6017 11.4547 12.9168 11.7181C12.2573 11.9702 11.5373 12.1968 10.8418 12.4161C10.2043 12.6166 9.60293 12.8064 9.0498 13.0118C8.80023 13.1042 8.5883 13.1754 8.40093 13.2376C7.89689 13.4013 7.53359 13.5265 7.00254 13.8922C6.79553 14.0338 6.58769 14.1868 6.44696 14.3013C6.02542 14.6377 5.65232 15.0306 5.33823 15.469C5.06814 15.8734 4.75601 16.248 4.40707 16.5867C4.29497 16.6963 4.09614 16.7504 3.7983 16.7504C3.44972 16.7504 3.02669 16.6783 2.57911 16.6022C2.11762 16.5204 1.64058 16.4427 1.23146 16.4427C0.899252 16.4427 0.644778 16.4967 0.454945 16.6063C0.454945 16.6063 0.13501 16.7929 0 17.0343L0.132556 17.094C0.338114 17.2055 0.528428 17.343 0.698782 17.5031C0.87652 17.6673 1.0745 17.8081 1.28792 17.9221C1.35605 17.9471 1.418 17.9864 1.46957 18.0375C1.41393 18.1193 1.3321 18.2248 1.24619 18.3377C0.77406 18.9555 0.498312 19.3458 0.656233 19.5586C0.731923 19.5979 0.816462 19.617 0.901707 19.6142C1.93106 19.6142 2.48419 19.3466 3.18379 19.0079C3.38672 18.9097 3.59292 18.8091 3.83839 18.7051C4.24751 18.5276 4.68773 18.2445 5.15495 17.945C5.76618 17.5465 6.40523 17.1374 7.02546 16.9402C7.53518 16.7847 8.06612 16.7102 8.59894 16.7193C9.25354 16.7193 9.9425 16.8068 10.6069 16.8919C11.102 16.9557 11.615 17.0212 12.1182 17.0515C12.3138 17.0629 12.4946 17.0686 12.6705 17.0686C12.9059 17.0693 13.1411 17.057 13.375 17.0318L13.4315 17.0122C13.7842 16.7954 13.9494 16.3298 14.1098 15.8797C14.2129 15.5901 14.2997 15.3299 14.4371 15.1646C14.4452 15.1565 14.454 15.1491 14.4633 15.1425C14.4698 15.1389 14.4772 15.1376 14.4845 15.1388C14.4918 15.14 14.4984 15.1436 14.5034 15.149C14.5034 15.149 14.5034 15.1531 14.5034 15.1621C14.4216 16.9222 13.713 18.0399 12.9962 19.0333L12.5175 19.5463C12.5175 19.5463 13.1877 19.5463 13.569 19.399C14.96 18.9833 16.0098 18.0669 16.774 16.6055C16.9625 16.2304 17.131 15.8455 17.2789 15.4526C17.292 15.4199 17.4123 15.3593 17.4008 15.5287C17.3967 15.5786 17.3934 15.6343 17.3894 15.6924C17.3894 15.7267 17.3894 15.7619 17.3828 15.7971C17.3632 16.0426 17.3051 16.5613 17.3051 16.5613L17.7347 16.3314C18.7706 15.6768 19.5692 14.3562 20.1747 12.3015C20.4267 11.4457 20.6116 10.5955 20.7744 9.84681C20.9692 8.94674 21.1369 8.17514 21.3292 7.87566C21.6311 7.40599 22.0918 7.08851 22.5378 6.78003C22.5983 6.7383 22.6597 6.6982 22.7194 6.65402C23.2799 6.26044 23.8371 5.80632 23.9599 4.95943V4.94061C24.0491 4.30893 23.9738 4.14773 23.8879 4.07409Z" fill="#B6795F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,11 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_817_123574)">
<path d="M22.0922 18.2073C20.7873 18.1748 19.776 18.3054 18.9279 18.6641C18.6832 18.7619 18.2917 18.762 18.2591 19.0719C18.3896 19.2025 18.4059 19.4145 18.5201 19.5938C18.7159 19.92 19.0583 20.3605 19.3683 20.5889C19.7108 20.8498 20.0533 21.1107 20.4121 21.3391C21.0483 21.7306 21.766 21.9589 22.3857 22.3504C22.7447 22.5786 23.1033 22.8723 23.4622 23.117C23.6417 23.2474 23.7558 23.4595 23.9842 23.541V23.4921C23.87 23.3453 23.8373 23.1333 23.7232 22.9702C23.5602 22.8071 23.397 22.6602 23.2339 22.4971C22.7609 21.8611 22.1737 21.3065 21.5376 20.8498C21.0157 20.4909 19.8739 20.0016 19.6618 19.3981C19.6618 19.3981 19.6455 19.3818 19.6292 19.3656C19.9881 19.3329 20.4121 19.2025 20.7547 19.1045C21.3093 18.9577 21.8149 18.9903 22.3857 18.8436C22.6467 18.7783 22.9077 18.6968 23.1686 18.6152V18.4685C22.8752 18.1748 22.663 17.7834 22.3531 17.5061C21.5213 16.7883 20.6078 16.0871 19.6618 15.4999C19.1561 15.1737 18.5037 14.9616 17.9655 14.6844C17.7699 14.5863 17.4435 14.5375 17.3294 14.3744C17.0358 14.0156 16.8727 13.5426 16.6606 13.1185C16.1878 12.2213 15.7309 11.2265 15.3232 10.2805C15.0297 9.64434 14.8502 9.00818 14.4914 8.42105C12.8114 5.64824 10.9846 3.96826 8.17911 2.32082C7.5756 1.97834 6.85799 1.83151 6.09137 1.65213C5.68371 1.63576 5.27584 1.60322 4.86807 1.58685C4.60714 1.47265 4.34611 1.16281 4.11772 1.01597C3.1882 0.428728 0.790405 -0.843382 0.105339 0.836597C-0.335069 1.89679 0.757766 2.94061 1.13289 3.47894C1.41029 3.85406 1.76904 4.2781 1.96479 4.70224C2.07899 4.97934 2.11152 5.27311 2.22572 5.56669C2.48675 6.28419 2.73131 7.08355 3.07389 7.75236C3.25337 8.09484 3.44902 8.4537 3.67741 8.76353C3.80787 8.94291 4.03616 9.02456 4.08517 9.31813C3.85689 9.64435 3.84052 10.1337 3.70995 10.5414C3.12281 12.3845 3.35119 14.668 4.183 16.0218C4.44393 16.4294 5.06381 17.3267 5.89562 16.9841C6.6296 16.6906 6.46649 15.7608 6.67851 14.9453C6.72752 14.7495 6.69488 14.6191 6.7927 14.4886C6.79279 14.5049 6.7927 14.5213 6.7927 14.5213C7.02109 14.9779 7.24938 15.4184 7.4614 15.875C7.96709 16.6742 8.84781 17.5061 9.58178 18.0607C9.97338 18.3542 10.2832 18.8599 10.7725 19.0393V18.9903H10.7399C10.642 18.8435 10.4952 18.7783 10.3648 18.6641C10.0712 18.3705 9.74489 18.0117 9.5166 17.6855C8.83164 16.772 8.22802 15.7608 7.68979 14.7169C7.42886 14.2113 7.20047 13.6567 6.98845 13.1511C6.89053 12.9553 6.89053 12.6618 6.72742 12.564C6.48276 12.9227 6.12401 13.2327 5.94453 13.6731C5.63479 14.3744 5.60205 15.2389 5.48785 16.136C5.42267 16.1523 5.45521 16.136 5.42257 16.1686C4.90071 16.038 4.72123 15.4999 4.52548 15.0431C4.03616 13.8851 3.95461 12.0257 4.37874 10.6883C4.49294 10.3457 4.98226 9.26922 4.78651 8.94301C4.68869 8.63307 4.36237 8.45369 4.183 8.20903C3.97098 7.89909 3.74259 7.50769 3.59585 7.16511C3.20436 6.25175 3.00861 5.24048 2.58458 4.32701C2.38883 3.90287 2.04634 3.46257 1.76904 3.07118C1.4592 2.63077 1.11662 2.32082 0.871953 1.79886C0.790503 1.61949 0.676205 1.32591 0.806669 1.13017C0.839312 0.999702 0.904492 0.95079 1.03506 0.918148C1.24708 0.738668 1.85059 0.967058 2.06261 1.06488C2.66612 1.30944 3.17171 1.53793 3.6774 1.88041C3.90569 2.04352 4.15035 2.35346 4.44392 2.43502H4.7865C5.30847 2.54911 5.89561 2.46766 6.38493 2.61439C7.24947 2.89159 8.03226 3.29946 8.73371 3.73987C10.8705 5.09363 12.6319 7.01837 13.8227 9.31814C14.0184 9.69316 14.0999 10.0358 14.2793 10.4272C14.6219 11.2265 15.046 12.042 15.3884 12.8249C15.7309 13.5914 16.0571 14.3744 16.5464 15.0106C16.7912 15.353 17.7697 15.5325 18.2101 15.7119C18.5364 15.8587 19.0421 15.9892 19.3356 16.1686C19.8901 16.511 20.4447 16.9026 20.9667 17.2777C21.2277 17.4734 22.0432 17.8812 22.0922 18.2073L22.0922 18.2073Z" fill="#E59639"/>
<path d="M5.45483 4.03333C5.17753 4.03333 4.98188 4.06607 4.78613 4.11498C4.78613 4.1149 4.78613 4.13136 4.78613 4.14763H4.81878C4.94934 4.40855 5.17753 4.58803 5.34063 4.81632C5.4712 5.07725 5.58529 5.33828 5.71586 5.59931C5.73213 5.58294 5.7484 5.56667 5.7484 5.56667C5.97689 5.40346 6.09098 5.14253 6.09098 4.75114C5.99306 4.63694 5.97679 4.52275 5.89524 4.40855C5.79741 4.24534 5.58529 4.16389 5.45483 4.03333Z" fill="#E59639"/>
</g>
<defs>
<clipPath id="clip0_817_123574">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0857 7.70885V21.0928H0L11.0857 7.70885ZM11.0857 2.90601C11.0857 4.37606 10.5017 5.7859 9.46222 6.82539C8.42274 7.86487 7.01289 8.44885 5.54284 8.44885C4.07279 8.44885 2.66295 7.86487 1.62346 6.82539C0.583977 5.7859 0 4.37606 0 2.90601L11.0857 2.90601ZM12.9119 21.094C12.9119 19.6239 13.4959 18.2141 14.5354 17.1746C15.5749 16.1351 16.9847 15.5511 18.4548 15.5511C19.9248 15.5511 21.3347 16.1351 22.3742 17.1746C23.4137 18.2141 23.9976 19.6239 23.9976 21.094H12.9119ZM12.9119 16.2911V2.90601H24L12.9119 16.2899V16.2911Z" fill="#6DA78D"/>
</svg>

After

Width:  |  Height:  |  Size: 657 B

View file

@ -16,6 +16,7 @@ import {
lifecycleEvents,
URL_SSO_SOURCE,
WORKSPACE_USER_STATUS,
WORKSPACE_USER_SOURCE,
} from 'src/helpers/user_lifecycle';
import { dbTransactionWrap, generateInviteURL, generateNextNameAndSlug, isValidDomain } from 'src/helpers/utils.helper';
import { DeepPartial, EntityManager } from 'typeorm';
@ -74,7 +75,13 @@ export class OauthService {
manager
);
// Setting up invited organization, organization user status should be invited if user status is invited
await this.organizationUsersService.create(user, organization, !!user.invitationToken, manager);
await this.organizationUsersService.create(
user,
organization,
!!user.invitationToken,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
if (defaultOrganization) {
// Setting up default organization

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddSourceToOrganizationUsers1716975639914 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumns('organization_users', [
new TableColumn({
name: 'source',
type: 'enum',
enumName: 'source',
enum: ['signup', 'invite'],
default: `'invite'`,
isNullable: false,
}),
]);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumns('organization_users', ['source']);
}
}

View file

@ -38,6 +38,7 @@ import { InvitedUser } from 'src/decorators/invited-user.decorator';
import { InvitedUserSessionDto } from '@dto/invited-user-session.dto';
import { ActivateAccountWithTokenDto } from '@dto/activate-account-with-token.dto';
import { OrganizationInviteAuthGuard } from 'src/modules/auth/organization-invite-auth.guard';
import { ResendInviteDto } from '@dto/resend-invite.dto';
@Controller()
export class AppController {
@ -162,8 +163,8 @@ export class AppController {
@UseGuards(SignupDisableGuard)
@UseGuards(FirstUserSignupDisableGuard)
@Post('resend-invite')
async resendInvite(@Body('email') email: string) {
return this.authService.resendEmail(email);
async resendInvite(@Body() body: ResendInviteDto) {
return this.authService.resendEmail(body);
}
@UseGuards(FirstUserSignupDisableGuard)
@ -172,6 +173,11 @@ export class AppController {
return await this.authService.verifyInviteToken(token, organizationToken);
}
@Get('invitee-details')
async getInviteeDetails(@Query('token') token) {
return await this.authService.getInviteeDetails(token);
}
@UseGuards(FirstUserSignupDisableGuard)
@Get('verify-organization-token')
async verifyOrganizationToken(@Query('token') token) {

View file

@ -0,0 +1,18 @@
import { Transform } from 'class-transformer';
import { IsString, IsOptional, IsEmail, IsNotEmpty } from 'class-validator';
import { lowercaseString } from 'src/helpers/utils.helper';
export class ResendInviteDto {
@IsEmail()
@Transform(({ value }) => lowercaseString(value))
@IsNotEmpty()
email: string;
@IsOptional()
@IsString()
organizationId?: string;
@IsString()
@IsOptional()
redirectTo?: string;
}

View file

@ -24,6 +24,9 @@ export class OrganizationUser extends BaseEntity {
@Column({ type: 'enum', enumName: 'status', enum: ['invited', 'active', 'archived'] })
status: string;
@Column({ type: 'enum', enumName: 'source', enum: ['signup', 'invite'] })
source: string;
@Column({ name: 'organization_id' })
organizationId: string;

View file

@ -20,6 +20,11 @@ export enum SOURCE {
WORKSPACE_SIGNUP = 'workspace_signup',
}
export enum WORKSPACE_USER_SOURCE {
INVITE = 'invite',
SIGNUP = 'signup',
}
export enum USER_STATUS {
INVITED = 'invited',
VERIFIED = 'verified',

View file

@ -10,7 +10,7 @@ import { OrganizationUsersService } from '@services/organization_users.service';
import { OrganizationsService } from '@services/organizations.service';
import { Organization } from 'src/entities/organization.entity';
import { SSOConfigs } from 'src/entities/sso_config.entity';
import { getUserErrorMessages, USER_STATUS } from 'src/helpers/user_lifecycle';
import { getUserErrorMessages, USER_STATUS, WORKSPACE_USER_SOURCE } from 'src/helpers/user_lifecycle';
/*
This guard will check all possible cases to reject an invalid invitation session request
@ -106,11 +106,12 @@ export class InvitedUserSessionAuthGuard extends AuthGuard('jwt') {
}
async onInvalidSession(invitedUser: any) {
const { invitationToken, status } = invitedUser;
const { invitationToken, status, organizationUserSource } = invitedUser;
if (invitationToken && [USER_STATUS.INVITED, USER_STATUS.VERIFIED].includes(status as USER_STATUS)) {
/* User doesn't have a valid session & User didn't activate account yet */
return invitedUser;
} else {
if (organizationUserSource === WORKSPACE_USER_SOURCE.SIGNUP) return invitedUser;
/* User doesn't have a session. Next?: login again and accept invite */
const organization = await this.organizationService.fetchOrganization(invitedUser.invitedOrganizationId);
const errorResponse = {

View file

@ -1,13 +1,19 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { OrganizationUsersService } from '@services/organization_users.service';
import { WORKSPACE_USER_SOURCE } from 'src/helpers/user_lifecycle';
@Injectable()
export class OrganizationInviteAuthGuard extends AuthGuard('jwt') {
constructor(private organizationUsersService: OrganizationUsersService) {
super();
}
async canActivate(context: ExecutionContext): Promise<any> {
let user;
let user: any;
const request = context.switchToHttp().getRequest();
request.isInviteSession = true;
request.isUserNotMandatory = true;
if (request?.cookies['tj_auth_token']) {
try {
user = await super.canActivate(context);
@ -16,6 +22,12 @@ export class OrganizationInviteAuthGuard extends AuthGuard('jwt') {
}
return user;
}
const organizationUser = await this.organizationUsersService.getUser(request.body.token);
if (organizationUser.source === WORKSPACE_USER_SOURCE.SIGNUP) {
return true;
}
return false;
}
}

View file

@ -39,6 +39,7 @@ import {
SOURCE,
URL_SSO_SOURCE,
WORKSPACE_USER_STATUS,
WORKSPACE_USER_SOURCE,
} from 'src/helpers/user_lifecycle';
import { MetadataService } from './metadata.service';
import { CookieOptions, Response } from 'express';
@ -50,6 +51,7 @@ import { AppAuthenticationDto, AppSignupDto } from '@dto/app-authentication.dto'
import { SIGNUP_ERRORS } from 'src/helpers/errors.constants';
const bcrypt = require('bcrypt');
const uuid = require('uuid');
import { ResendInviteDto } from '@dto/resend-invite.dto';
@Injectable()
export class AuthService {
@ -65,7 +67,9 @@ export class AuthService {
private emailService: EmailService,
private metadataService: MetadataService,
private configService: ConfigService,
private sessionService: SessionService
private sessionService: SessionService,
@InjectRepository(Organization)
private organizationsRepository: Repository<Organization>
) {}
verifyToken(token: string) {
@ -240,15 +244,72 @@ export class AuthService {
});
}
async resendEmail(email: string) {
async resendEmail(body: ResendInviteDto) {
const { email, organizationId, redirectTo } = body;
if (!email) {
throw new BadRequestException();
}
const existingUser = await this.usersService.findByEmail(email);
if (existingUser?.organizationUsers?.some((ou) => ou.status === WORKSPACE_USER_STATUS.ACTIVE)) {
if (existingUser?.status === USER_STATUS.ARCHIVED) {
throw new NotAcceptableException('User has been archived, please contact the administrator');
}
if (!organizationId && existingUser?.organizationUsers?.some((ou) => ou.status === WORKSPACE_USER_STATUS.ACTIVE)) {
throw new NotAcceptableException('Email already exists');
}
let organizationUser: OrganizationUser;
if (organizationId) {
/* Workspace signup invitation email */
organizationUser = existingUser.organizationUsers.find(
(organizationUser) => organizationUser.organizationId === organizationId
);
if (organizationUser.status === WORKSPACE_USER_STATUS.ACTIVE) {
throw new NotAcceptableException('User already exists in the workspace.');
}
if (organizationUser.status === WORKSPACE_USER_STATUS.ARCHIVED) {
throw new NotAcceptableException('User has been archived, please contact the administrator');
}
}
if (organizationUser) {
const invitedOrganization = await this.organizationsRepository.findOne({
where: { id: organizationUser.organizationId },
select: ['name', 'id'],
});
if (existingUser.invitationToken) {
/* Not activated. */
this.emailService
.sendWelcomeEmail(
existingUser.email,
existingUser.firstName,
existingUser.invitationToken,
organizationUser.invitationToken,
organizationUser.organizationId,
invitedOrganization.name,
null,
redirectTo
)
.catch((err) => console.error('Error while sending welcome mail', err));
return;
} else {
/* Already activated */
this.emailService
.sendOrganizationUserWelcomeEmail(
existingUser.email,
existingUser.firstName,
null,
organizationUser.invitationToken,
invitedOrganization.name,
organizationUser.organizationId,
redirectTo
)
.catch((err) => console.error(err));
return;
}
}
if (existingUser?.invitationToken) {
this.emailService
.sendWelcomeEmail(existingUser.email, existingUser.firstName, existingUser.invitationToken)
@ -351,7 +412,13 @@ export class AuthService {
await this.organizationUsersService.create(user, personalWorkspace, true, manager);
if (signingUpOrganization) {
/* Attach the user and user groups to the organization */
const organizationUser = await this.organizationUsersService.create(user, signingUpOrganization, true, manager);
const organizationUser = await this.organizationUsersService.create(
user,
signingUpOrganization,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
await this.usersService.attachUserGroup(['all_users'], signingUpOrganization.id, user.id, manager);
this.emailService
@ -561,7 +628,13 @@ export class AuthService {
async addUserToTheWorkspace(existingUser: User, signingUpOrganization: Organization, manager: EntityManager) {
await this.usersService.attachUserGroup(['all_users'], signingUpOrganization.id, existingUser.id, manager);
return this.organizationUsersService.create(existingUser, signingUpOrganization, true, manager);
return this.organizationUsersService.create(
existingUser,
signingUpOrganization,
true,
manager,
WORKSPACE_USER_SOURCE.SIGNUP
);
}
async activateAccountWithToken(activateAccountWithToken: ActivateAccountWithTokenDto, response: any) {
@ -846,10 +919,28 @@ export class AuthService {
});
await this.organizationUsersService.activateOrganization(organizationUser, manager);
}
return this.generateLoginResultPayload(response, user, organization, null, null, loggedInUser, manager);
const isWorkspaceSignup = organizationUser.source === WORKSPACE_USER_SOURCE.SIGNUP;
return this.generateLoginResultPayload(
response,
user,
organization,
null,
isWorkspaceSignup,
loggedInUser,
manager
);
});
}
async getInviteeDetails(token: string) {
const organizationUser: OrganizationUser = await this.organizationUsersRepository.findOneOrFail({
where: { invitationToken: token },
select: ['id', 'user'],
relations: ['user'],
});
return { email: organizationUser.user.email };
}
async verifyInviteToken(token: string, organizationToken?: string) {
const user: User = await this.usersRepository.findOne({ where: { invitationToken: token } });
let organizationUser: OrganizationUser;
@ -924,20 +1015,22 @@ export class AuthService {
: user?.organizationIds?.includes(user?.defaultOrganizationId)
? user.defaultOrganizationId
: user?.organizationIds?.[0];
const organizationDetails = currentOrganization
const organizationDetails = currentOrganizationId
? currentOrganization
: await manager.findOneOrFail(Organization, {
where: { id: currentOrganizationId },
select: ['slug', 'name', 'id'],
});
? currentOrganization
: await manager.findOneOrFail(Organization, {
where: { id: currentOrganizationId },
select: ['slug', 'name', 'id'],
})
: null;
return decamelizeKeys({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
currentOrganizationSlug: organizationDetails.slug,
currentOrganizationName: organizationDetails.name,
currentOrganizationSlug: organizationDetails?.slug,
currentOrganizationName: organizationDetails?.name,
currentOrganizationId,
});
});
@ -1019,6 +1112,7 @@ export class AuthService {
lastName,
status: invitedUserStatus,
organizationStatus,
organizationUserSource,
invitedOrganizationId,
source,
} = invitedUser;
@ -1055,6 +1149,18 @@ export class AuthService {
throw new NotAcceptableException(errorResponse);
}
const isWorkspaceSignup =
organizationStatus === WORKSPACE_USER_STATUS.INVITED &&
!!organizationToken &&
invitedUserStatus === USER_STATUS.ACTIVE &&
organizationUserSource === WORKSPACE_USER_SOURCE.SIGNUP;
if (isWorkspaceSignup) {
/* Active user & Organization invite */
const responseObj = {
organizationUserSource,
};
return decamelizeKeys(responseObj);
}
/* Send back the organization invite url if the user has old workspace + account invitation URL */
const doesUserHaveWorkspaceAndAccountInvite =
organizationAndAccountInvite &&

Some files were not shown because too many files have changed in this diff Show more